diff --git a/src/acme2.js b/src/acme2.js index d30a1b3e5..f5107936e 100644 --- a/src/acme2.js +++ b/src/acme2.js @@ -20,7 +20,7 @@ const assert = require('assert'), paths = require('./paths.js'), promiseRetry = require('./promise-retry.js'), safe = require('safetydance'), - shell = require('./shell.js'), + shell = require('./shell.js')('acme2'), superagent = require('superagent'), users = require('./users.js'); @@ -74,7 +74,7 @@ function b64(str) { async function getModulus(pem) { assert.strictEqual(typeof pem, 'string'); - const stdout = await shell.exec('getModulus', 'openssl rsa -modulus -noout', { input: pem }); + const stdout = await shell.exec('openssl rsa -modulus -noout', { input: pem }); const match = stdout.match(/Modulus=([0-9a-fA-F]+)$/m); if (!match) throw new BoxError(BoxError.OPENSSL_ERROR, 'Could not get modulus'); return Buffer.from(match[1], 'hex'); @@ -153,7 +153,7 @@ Acme2.prototype.updateContact = async function (registrationUri) { }; async function generateAccountKey() { - const acmeAccountKey = await shell.exec('generateAccountKey', 'openssl genrsa 4096', {}); + const acmeAccountKey = await shell.exec('openssl genrsa 4096', {}); return acmeAccountKey; } @@ -294,7 +294,7 @@ Acme2.prototype.signCertificate = async function (finalizationUrl, csrPem) { assert.strictEqual(typeof finalizationUrl, 'string'); assert.strictEqual(typeof csrPem, 'string'); - const csrDer = await shell.exec('signCertificate', 'openssl req -inform pem -outform der', { input: csrPem, encoding: 'buffer' }); + const csrDer = await shell.exec('openssl req -inform pem -outform der', { input: csrPem, encoding: 'buffer' }); const payload = { csr: b64(csrDer) @@ -316,7 +316,7 @@ Acme2.prototype.ensureKey = async function () { debug(`ensureKey: generating new key for ${this.cn}`); // same as prime256v1. openssl ecparam -list_curves. we used to use secp384r1 but it doesn't seem to be accepted by few mail servers - const newKey = await shell.exec('ensureKey', 'openssl ecparam -genkey -name secp256r1', {}); + const newKey = await shell.exec('openssl ecparam -genkey -name secp256r1', {}); return newKey; }; @@ -344,7 +344,7 @@ Acme2.prototype.createCsr = async function (key) { if (!safe.fs.writeFileSync(opensslConfigFile, conf)) throw new BoxError(BoxError.FS_ERROR, `Failed to write openssl config: ${safe.error.message}`); // while we pass the CN anyways, subjectAltName takes precedence - const csrPem = await shell.exec('createCsr', `openssl req -new -key ${keyFilePath} -outform PEM -subj /CN=${this.cn} -config ${opensslConfigFile}`, {}); + const csrPem = await shell.exec(`openssl req -new -key ${keyFilePath} -outform PEM -subj /CN=${this.cn} -config ${opensslConfigFile}`, {}); await safe(fs.promises.rm(tmpdir, { recursive: true, force: true })); debug(`createCsr: csr file created for ${this.cn}`); return csrPem; // inspect with openssl req -text -noout -in hostname.csr -inform pem @@ -393,7 +393,7 @@ function getChallengeSubdomain(cn, domain) { if (cn === domain) { challengeSubdomain = '_acme-challenge'; } else if (cn.includes('*')) { // wildcard - let subdomain = cn.slice(0, -domain.length - 1); + const subdomain = cn.slice(0, -domain.length - 1); challengeSubdomain = subdomain ? subdomain.replace('*', '_acme-challenge') : '_acme-challenge'; } else { challengeSubdomain = '_acme-challenge.' + cn.slice(0, -domain.length - 1); diff --git a/src/apps.js b/src/apps.js index 995c94c91..82495a34c 100644 --- a/src/apps.js +++ b/src/apps.js @@ -170,7 +170,7 @@ const appstore = require('./appstore.js'), safe = require('safetydance'), semver = require('semver'), services = require('./services.js'), - shell = require('./shell.js'), + shell = require('./shell.js')('apps'), storage = require('./storage.js'), tasks = require('./tasks.js'), tgz = require('./backupformat/tgz.js'), @@ -518,7 +518,7 @@ async function checkStorage(app, volumeId, prefix) { const rel = path.relative(sourceDir, targetDir); if (!rel.startsWith('../') && rel.split('/').length > 1) throw new BoxError(BoxError.BAD_FIELD, 'Only one level subdirectory moves are supported'); - const [error] = await safe(shell.promises.sudo('checkStorage', [ CHECKVOLUME_CMD, targetDir, sourceDir ], {})); + const [error] = await safe(shell.promises.sudo([ CHECKVOLUME_CMD, targetDir, sourceDir ], {})); if (error && error.code === 2) throw new BoxError(BoxError.BAD_FIELD, `Target directory ${targetDir} is not empty`); if (error && error.code === 3) throw new BoxError(BoxError.BAD_FIELD, `Target directory ${targetDir} does not support chown`); @@ -2896,7 +2896,7 @@ async function uploadFile(app, sourceFilePath, destFilePath) { // the built-in bash printf understands "%q" but not /usr/bin/printf. // ' gets replaced with '\'' . the first closes the quote and last one starts a new one - const escapedDestFilePath = await shell.exec('uploadFile', `printf %q '${destFilePath.replace(/'/g, '\'\\\'\'')}'`, { shell: '/bin/bash' }); + const escapedDestFilePath = await shell.exec(`printf %q '${destFilePath.replace(/'/g, '\'\\\'\'')}'`, { shell: '/bin/bash' }); debug(`uploadFile: ${sourceFilePath} -> ${escapedDestFilePath}`); const execId = await createExec(app, { cmd: [ 'bash', '-c', `cat - > ${escapedDestFilePath}` ], tty: false }); diff --git a/src/appstore.js b/src/appstore.js index e751cfe24..908afe4f1 100644 --- a/src/appstore.js +++ b/src/appstore.js @@ -48,7 +48,7 @@ const apps = require('./apps.js'), safe = require('safetydance'), semver = require('semver'), settings = require('./settings.js'), - shell = require('./shell.js'), + shell = require('./shell.js')('appstore'), superagent = require('superagent'), support = require('./support.js'); @@ -394,7 +394,7 @@ async function createTicket(info, auditSource) { const logPaths = await apps.getLogPaths(info.app); for (const logPath of logPaths) { - const [error, logs] = await safe(shell.exec('createTicket', `tail --lines=1000 ${logPath}`, {})); + const [error, logs] = await safe(shell.exec(`tail --lines=1000 ${logPath}`, {})); if (!error && logs) request.attach(path.basename(logPath), logs, path.basename(logPath)); } } else { diff --git a/src/apptask.js b/src/apptask.js index 8673992ef..2c0e1fc31 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -35,7 +35,7 @@ const apps = require('./apps.js'), reverseProxy = require('./reverseproxy.js'), safe = require('safetydance'), services = require('./services.js'), - shell = require('./shell.js'), + shell = require('./shell.js')('apptask'), _ = require('underscore'); const MV_VOLUME_CMD = path.join(__dirname, 'scripts/mvvolume.sh'), @@ -168,14 +168,14 @@ async function addLogrotateConfig(app) { safe.fs.writeFileSync(tmpFilePath, logrotateConf); if (safe.error) throw new BoxError(BoxError.FS_ERROR, `Error writing logrotate config: ${safe.error.message}`); - const [error] = await safe(shell.promises.sudo('addLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'add', app.id, tmpFilePath ], {})); + const [error] = await safe(shell.promises.sudo([ CONFIGURE_LOGROTATE_CMD, 'add', app.id, tmpFilePath ], {})); if (error) throw new BoxError(BoxError.LOGROTATE_ERROR, `Error adding logrotate config: ${error.message}`); } async function removeLogrotateConfig(app) { assert.strictEqual(typeof app, 'object'); - const [error] = await safe(shell.promises.sudo('removeLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'remove', app.id ], {})); + const [error] = await safe(shell.promises.sudo([ CONFIGURE_LOGROTATE_CMD, 'remove', app.id ], {})); if (error) throw new BoxError(BoxError.LOGROTATE_ERROR, `Error removing logrotate config: ${error.message}`); } @@ -221,7 +221,7 @@ async function moveDataDir(app, targetVolumeId, targetVolumePrefix) { debug(`moveDataDir: migrating data from ${resolvedSourceDir} to ${resolvedTargetDir}`); if (resolvedSourceDir !== resolvedTargetDir) { - const [error] = await safe(shell.promises.sudo('moveDataDir', [ MV_VOLUME_CMD, resolvedSourceDir, resolvedTargetDir ], {})); + const [error] = await safe(shell.promises.sudo([ MV_VOLUME_CMD, resolvedSourceDir, resolvedTargetDir ], {})); if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, `Error migrating data directory: ${error.message}`); } diff --git a/src/backupformat/rsync.js b/src/backupformat/rsync.js index d285b700a..e4b2551fd 100644 --- a/src/backupformat/rsync.js +++ b/src/backupformat/rsync.js @@ -22,7 +22,7 @@ const assert = require('assert'), ProgressStream = require('../progress-stream.js'), promiseRetry = require('../promise-retry.js'), safe = require('safetydance'), - shell = require('../shell.js'), + shell = require('../shell.js')('backupformat/rsync'), storage = require('../storage.js'), stream = require('stream/promises'), syncer = require('../syncer.js'), @@ -125,13 +125,13 @@ async function saveFsMetadata(dataLayout, metadataFile) { // we assume small number of files. spawnSync will raise a ENOBUFS error after maxBuffer for (const lp of dataLayout.localPaths()) { - const emptyDirs = await shell.exec('saveFsMetadata', `find ${lp} -type d -empty`, { maxBuffer: 1024 * 1024 * 80 }); + const emptyDirs = await shell.exec(`find ${lp} -type d -empty`, { maxBuffer: 1024 * 1024 * 80 }); if (emptyDirs.length) metadata.emptyDirs = metadata.emptyDirs.concat(emptyDirs.trim().split('\n').map((ed) => dataLayout.toRemotePath(ed))); - const execFiles = await shell.exec('saveFsMetadata', `find ${lp} -type f -executable`, { maxBuffer: 1024 * 1024 * 80 }); + const execFiles = await shell.exec(`find ${lp} -type f -executable`, { maxBuffer: 1024 * 1024 * 80 }); if (execFiles.length) metadata.execFiles = metadata.execFiles.concat(execFiles.trim().split('\n').map((ef) => dataLayout.toRemotePath(ef))); - const symlinkFiles = await shell.exec('safeFsMetadata', `find ${lp} -type l`, { maxBuffer: 1024 * 1024 * 30 }); + const symlinkFiles = await shell.exec(`find ${lp} -type l`, { maxBuffer: 1024 * 1024 * 30 }); if (symlinkFiles.length) metadata.symlinks = metadata.symlinks.concat(symlinkFiles.trim().split('\n').map((sl) => { const target = safe.fs.readlinkSync(sl); return { path: dataLayout.toRemotePath(sl), target }; diff --git a/src/backuptask.js b/src/backuptask.js index 2c655ade7..416ff11b4 100644 --- a/src/backuptask.js +++ b/src/backuptask.js @@ -28,7 +28,7 @@ const apps = require('./apps.js'), paths = require('./paths.js'), safe = require('safetydance'), services = require('./services.js'), - shell = require('./shell.js'), + shell = require('./shell.js')('backuptask'), storage = require('./storage.js'); const BACKUP_UPLOAD_CMD = path.join(__dirname, 'scripts/backupupload.js'); @@ -61,7 +61,7 @@ async function checkPreconditions(backupConfig, dataLayout) { let used = 0; for (const localPath of dataLayout.localPaths()) { debug(`checkPreconditions: getting disk usage of ${localPath}`); - const result = await shell.execArgs('checkPreconditions', 'du', [ '-Dsb', '--exclude=*.lock', '--exclude=dovecot.list.index.log.*', localPath], {}); + const result = await shell.execArgs('du', [ '-Dsb', '--exclude=*.lock', '--exclude=dovecot.list.index.log.*', localPath], {}); used += parseInt(result, 10); } @@ -159,7 +159,7 @@ async function runBackupUpload(uploadConfig, progressCallback) { result = progress.result; } - const [error] = await safe(shell.promises.sudo(`backup-${remotePath}`, [ BACKUP_UPLOAD_CMD, remotePath, backupConfig.format, dataLayout.toString() ], { env: envCopy, preserveEnv: true, ipc: true, onMessage, outputHasTimestamps: true })); + const [error] = await safe(shell.promises.sudo([ BACKUP_UPLOAD_CMD, remotePath, backupConfig.format, dataLayout.toString() ], { env: envCopy, preserveEnv: true, ipc: true, onMessage, outputHasTimestamps: true })); if (error && (error.code === null /* signal */ || (error.code !== 0 && error.code !== 50))) { // backuptask crashed debug(`runBackupUpload: backuptask crashed`, error); throw new BoxError(BoxError.INTERNAL_ERROR, 'Backuptask crashed'); diff --git a/src/database.js b/src/database.js index 2bea7df39..1d73acd5a 100644 --- a/src/database.js +++ b/src/database.js @@ -20,7 +20,7 @@ const assert = require('assert'), fs = require('fs'), mysql = require('mysql'), safe = require('safetydance'), - shell = require('./shell.js'); + shell = require('./shell.js')('database'); let gConnectionPool = null; @@ -80,14 +80,14 @@ async function clear() { await fs.promises.writeFile('/tmp/extra.cnf', `[client]\nhost=${gDatabase.hostname}\nuser=${gDatabase.username}\npassword=${gDatabase.password}\ndatabase=${gDatabase.name}`, 'utf8'); const cmd = 'mysql --defaults-extra-file=/tmp/extra.cnf -Nse "SHOW TABLES" | grep -v "^migrations$" | while read table; do mysql --defaults-extra-file=/tmp/extra.cnf -e "SET FOREIGN_KEY_CHECKS = 0; TRUNCATE TABLE $table"; done'; - await shell.exec('clear_database', cmd, { shell: '/bin/bash' }); + await shell.exec(cmd, { shell: '/bin/bash' }); } async function query() { assert.notStrictEqual(gConnectionPool, null); return new Promise((resolve, reject) => { - let args = Array.prototype.slice.call(arguments); + const args = Array.prototype.slice.call(arguments); args.push(function queryCallback(error, result) { if (error) return reject(new BoxError(BoxError.DATABASE_ERROR, error, { code: error.code, sqlMessage: error.sqlMessage || null })); @@ -138,7 +138,7 @@ async function importFromFile(file) { const cmd = `/usr/bin/mysql -h "${gDatabase.hostname}" -u ${gDatabase.username} -p${gDatabase.password} ${gDatabase.name} < ${file}`; await query('CREATE DATABASE IF NOT EXISTS box'); - const [error] = await safe(shell.exec('importFromFile', cmd, { shell: '/bin/bash' })); + const [error] = await safe(shell.exec(cmd, { shell: '/bin/bash' })); if (error) throw new BoxError(BoxError.DATABASE_ERROR, error); } @@ -146,12 +146,12 @@ async function exportToFile(file) { assert.strictEqual(typeof file, 'string'); // latest mysqldump enables column stats by default which is not present in 5.7 util - const mysqlDumpHelp = await shell.exec('exportToFile', '/usr/bin/mysqldump --help', {}); + const mysqlDumpHelp = await shell.exec('/usr/bin/mysqldump --help', {}); const hasColStats = mysqlDumpHelp.includes('column-statistics'); const colStats = hasColStats ? '--column-statistics=0' : ''; const cmd = `/usr/bin/mysqldump -h "${gDatabase.hostname}" -u root -p${gDatabase.password} ${colStats} --single-transaction --routines --triggers ${gDatabase.name} > "${file}"`; - const [error] = await safe(shell.exec('exportToFile', cmd, { shell: '/bin/bash' })); + const [error] = await safe(shell.exec(cmd, { shell: '/bin/bash' })); if (error) throw new BoxError(BoxError.DATABASE_ERROR, error); } diff --git a/src/df.js b/src/df.js index 341de5446..f64ef5523 100644 --- a/src/df.js +++ b/src/df.js @@ -10,7 +10,7 @@ const assert = require('assert'), BoxError = require('./boxerror.js'), debug = require('debug')('box:df'), safe = require('safetydance'), - shell = require('./shell.js'); + shell = require('./shell.js')('df'); // binary units (non SI) 1024 based function prettyBytes(bytes) { @@ -37,7 +37,7 @@ function parseLine(line) { } async function disks() { - const [error, output] = await safe(shell.exec('disks', 'df -B1 --output=source,fstype,size,used,avail,pcent,target', { timeout: 5000 })); + const [error, output] = await safe(shell.exec('df -B1 --output=source,fstype,size,used,avail,pcent,target', { timeout: 5000 })); if (error) { debug(`disks: df command failed. error: ${error}\n stdout: ${error.stdout}\n stderr: ${error.stderr}`); throw new BoxError(BoxError.FS_ERROR, `Error running df: ${error.message}`); @@ -54,7 +54,7 @@ async function disks() { async function file(filename) { assert.strictEqual(typeof filename, 'string'); - const [error, output] = await safe(shell.exec('file', `df -B1 --output=source,fstype,size,used,avail,pcent,target ${filename}`, { timeout: 5000 })); + const [error, output] = await safe(shell.exec(`df -B1 --output=source,fstype,size,used,avail,pcent,target ${filename}`, { timeout: 5000 })); if (error) { debug(`file: df command failed. error: ${error}\n stdout: ${error.stdout}\n stderr: ${error.stderr}`); throw new BoxError(BoxError.FS_ERROR, `Error running df: ${error.message}`); diff --git a/src/directoryserver.js b/src/directoryserver.js index 4502eff45..54d696ac6 100644 --- a/src/directoryserver.js +++ b/src/directoryserver.js @@ -23,7 +23,7 @@ const assert = require('assert'), reverseProxy = require('./reverseproxy.js'), safe = require('safetydance'), settings = require('./settings.js'), - shell = require('./shell.js'), + shell = require('./shell.js')('directoryserver'), users = require('./users.js'), util = require('util'), validator = require('validator'); @@ -75,7 +75,7 @@ async function applyConfig(config) { safe.fs.unlinkSync(paths.LDAP_ALLOWLIST_FILE); } - const [error] = await safe(shell.promises.sudo('setLdapAllowlist', [ SET_LDAP_ALLOWLIST_CMD ], {})); + const [error] = await safe(shell.promises.sudo([ SET_LDAP_ALLOWLIST_CMD ], {})); if (error) throw new BoxError(BoxError.IPTABLES_ERROR, `Error setting ldap allowlist: ${error.message}`); if (!config.enabled) { @@ -109,8 +109,7 @@ async function setConfig(directoryServerConfig, auditSource) { // helper function to deal with pagination function finalSend(results, req, res, next) { - let min = 0; - let max = results.length; + const min = 0, max = results.length; let cookie = null; let pageSize = 0; @@ -208,7 +207,7 @@ async function userSearch(req, res, next) { const [groupsError, allGroups] = await safe(groups.listWithMembers()); if (groupsError) return next(new ldap.OperationsError(groupsError.message)); - let results = []; + const results = []; // send user objects for (const user of allUsers) { @@ -263,7 +262,7 @@ async function groupSearch(req, res, next) { const results = []; - let [errorGroups, allGroups] = await safe(groups.listWithMembers()); + const [errorGroups, allGroups] = await safe(groups.listWithMembers()); if (errorGroups) return next(new ldap.OperationsError(errorGroups.message)); for (const group of allGroups) { diff --git a/src/docker.js b/src/docker.js index 2d0fd49b1..6ca5a3746 100644 --- a/src/docker.js +++ b/src/docker.js @@ -48,7 +48,7 @@ const apps = require('./apps.js'), services = require('./services.js'), settings = require('./settings.js'), semver = require('semver'), - shell = require('./shell.js'), + shell = require('./shell.js')('docker'), safe = require('safetydance'), timers = require('timers/promises'), volumes = require('./volumes.js'); @@ -245,7 +245,7 @@ async function getAddressesForPort53() { const addresses = []; for (const phy of physicalDevices) { - const [error, output] = await safe(shell.exec('getAddressesForPort53', `ip -f inet -j addr show dev ${phy.name} scope global`, {})); + const [error, output] = await safe(shell.exec(`ip -f inet -j addr show dev ${phy.name} scope global`, {})); if (error) continue; const inet = safe.JSON.parse(output) || []; for (const r of inet) { @@ -650,7 +650,7 @@ async function update(name, memory) { // scale back db containers, if possible. this is retried because updating memory constraints can fail // with failed to write to memory.memsw.limit_in_bytes: write /sys/fs/cgroup/memory/docker/xx/memory.memsw.limit_in_bytes: device or resource busy for (let times = 0; times < 10; times++) { - const [error] = await safe(shell.exec(`update(${name})`, `docker update --memory ${memory} --memory-swap -1 ${name}`, {})); + const [error] = await safe(shell.exec(`docker update --memory ${memory} --memory-swap -1 ${name}`, {})); if (!error) return; await timers.setTimeout(60 * 1000); } diff --git a/src/logs.js b/src/logs.js index da8b2e63b..9b9093c35 100644 --- a/src/logs.js +++ b/src/logs.js @@ -2,7 +2,7 @@ const assert = require('assert'), path = require('path'), - shell = require('./shell.js'), + shell = require('./shell.js')('logs'), spawn = require('child_process').spawn, stream = require('stream'), { StringDecoder } = require('string_decoder'), @@ -69,7 +69,7 @@ function tail(filePaths, options) { if (options.follow) args.push('--follow'); if (options.sudo) { - return shell.sudo('tail', args.concat(filePaths), { quiet: true }, () => {}); + return shell.sudo(args.concat(filePaths), { quiet: true }, () => {}); } else { const cp = spawn('/usr/bin/tail', args.concat(filePaths)); cp.terminate = () => cp.kill('SIGKILL'); diff --git a/src/mail.js b/src/mail.js index 876263d3f..79b7d2f8d 100644 --- a/src/mail.js +++ b/src/mail.js @@ -77,7 +77,7 @@ const assert = require('assert'), path = require('path'), safe = require('safetydance'), services = require('./services.js'), - shell = require('./shell.js'), + shell = require('./shell.js')('mail'), superagent = require('superagent'), validator = require('validator'), _ = require('underscore'); @@ -975,7 +975,7 @@ async function delMailbox(name, domain, options, auditSource) { const mailbox =`${name}@${domain}`; if (options.deleteMails) { - const [error] = await safe(shell.promises.sudo('removeMailbox', [ REMOVE_MAILBOX_CMD, mailbox ], {})); + const [error] = await safe(shell.promises.sudo([ REMOVE_MAILBOX_CMD, mailbox ], {})); if (error) throw new BoxError(BoxError.FS_ERROR, `Error removing mailbox: ${error.message}`); } diff --git a/src/mailserver.js b/src/mailserver.js index c0b00a051..7e6596cd3 100644 --- a/src/mailserver.js +++ b/src/mailserver.js @@ -44,7 +44,7 @@ const assert = require('assert'), safe = require('safetydance'), services = require('./services.js'), settings = require('./settings.js'), - shell = require('./shell.js'), + shell = require('./shell.js')('mailserver'), tasks = require('./tasks.js'), users = require('./users.js'); @@ -53,8 +53,8 @@ async function generateDkimKey() { const privateKeyFilePath = path.join(os.tmpdir(), `dkim-${crypto.randomBytes(4).readUInt32LE(0)}.private`); // https://www.unlocktheinbox.com/dkim-key-length-statistics/ and https://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-email-authentication-dkim-easy.html for key size - await shell.exec('generateDkimKey', `openssl genrsa -out ${privateKeyFilePath} 1024`, {}); - await shell.exec('generateDkimKey', `openssl rsa -in ${privateKeyFilePath} -out ${publicKeyFilePath} -pubout -outform PEM`, {}); + await shell.exec(`openssl genrsa -out ${privateKeyFilePath} 1024`, {}); + await shell.exec(`openssl rsa -in ${privateKeyFilePath} -out ${publicKeyFilePath} -pubout -outform PEM`, {}); const publicKey = safe.fs.readFileSync(publicKeyFilePath, 'utf8'); if (!publicKey) throw new BoxError(BoxError.FS_ERROR, safe.error.message); @@ -194,7 +194,7 @@ async function configureMail(mailFqdn, mailDomain, serviceConfig) { ${readOnly} -v /run -v /tmp ${image} ${cmd}`; debug('configureMail: starting mail container'); - await shell.exec('configureMail', runCmd, { shell: '/bin/bash' }); + await shell.exec(runCmd, { shell: '/bin/bash' }); } async function restart() { diff --git a/src/mounts.js b/src/mounts.js index faaa3c885..f1c3ca119 100644 --- a/src/mounts.js +++ b/src/mounts.js @@ -18,7 +18,7 @@ const assert = require('assert'), path = require('path'), paths = require('./paths.js'), safe = require('safetydance'), - shell = require('./shell.js'); + shell = require('./shell.js')('mounts'); const ADD_MOUNT_CMD = path.join(__dirname, 'scripts/addmount.sh'); const RM_MOUNT_CMD = path.join(__dirname, 'scripts/rmmount.sh'); @@ -80,7 +80,7 @@ async function renderMountFile(mount) { let options, what, type; switch (mountType) { case 'cifs': { - const out = await shell.execArgs('renderMountFile', 'systemd-escape', [ '-p', hostPath ], {}); // this ensures uniqueness of creds file + const out = await shell.execArgs('systemd-escape', [ '-p', hostPath ], {}); // this ensures uniqueness of creds file const credentialsFilePath = path.join(paths.CIFS_CREDENTIALS_DIR, `${out.trim()}.cred`); if (!safe.fs.writeFileSync(credentialsFilePath, `username=${mountOptions.username}\npassword=${mountOptions.password}\n`, { mode: 0o600 })) throw new BoxError(BoxError.FS_ERROR, `Could not write credentials file: ${safe.error.message}`); @@ -133,13 +133,13 @@ async function removeMount(mount) { if (constants.TEST) return; - await safe(shell.promises.sudo('removeMount', [ RM_MOUNT_CMD, hostPath ], {}), { debug }); // ignore any error + await safe(shell.promises.sudo([ RM_MOUNT_CMD, hostPath ], {}), { debug }); // ignore any error if (mountType === 'sshfs') { const keyFilePath = path.join(paths.SSHFS_KEYS_DIR, `id_rsa_${mountOptions.host}`); safe.fs.unlinkSync(keyFilePath); } else if (mountType === 'cifs') { - const out = await shell.execArgs('removeMount', 'systemd-escape', [ '-p', hostPath ], {}); + const out = await shell.execArgs('systemd-escape', [ '-p', hostPath ], {}); const credentialsFilePath = path.join(paths.CIFS_CREDENTIALS_DIR, `${out.trim()}.cred`); safe.fs.unlinkSync(credentialsFilePath); } @@ -151,7 +151,7 @@ async function getStatus(mountType, hostPath) { if (mountType === 'filesystem') return { state: 'active', message: 'Mounted' }; - const [error] = await safe(shell.execArgs('getStatus', 'mountpoint', [ '-q', '--', hostPath ], { timeout: 5000 })); + const [error] = await safe(shell.execArgs('mountpoint', [ '-q', '--', hostPath ], { timeout: 5000 })); const state = error ? 'inactive' : 'active'; if (mountType === 'mountpoint') return { state, message: state === 'active' ? 'Mounted' : 'Not mounted' }; @@ -160,7 +160,7 @@ async function getStatus(mountType, hostPath) { let message; if (state !== 'active') { // find why it failed - const logsJson = await shell.exec('getStatus', `journalctl -u $(systemd-escape -p --suffix=mount ${hostPath}) -n 10 --no-pager -o json`, { shell: '/bin/bash' }); + const logsJson = await shell.exec(`journalctl -u $(systemd-escape -p --suffix=mount ${hostPath}) -n 10 --no-pager -o json`, { shell: '/bin/bash' }); if (logsJson) { const lines = logsJson.trim().split('\n').map(l => JSON.parse(l)); // array of json @@ -196,7 +196,7 @@ async function tryAddMount(mount, options) { if (constants.TEST) return; const mountFileContents = await renderMountFile(mount); - const [error] = await safe(shell.promises.sudo('addMount', [ ADD_MOUNT_CMD, mountFileContents, options.timeout ], {})); + const [error] = await safe(shell.promises.sudo([ ADD_MOUNT_CMD, mountFileContents, options.timeout ], {})); if (error && error.code === 2) throw new BoxError(BoxError.MOUNT_ERROR, 'Failed to unmount existing mount'); // at this point, the old mount config is still there if (options.skipCleanup) return; @@ -215,6 +215,6 @@ async function remount(mount) { if (constants.TEST) return; - const [error] = await safe(shell.promises.sudo('remountMount', [ REMOUNT_MOUNT_CMD, mount.hostPath ], {})); + const [error] = await safe(shell.promises.sudo([ REMOUNT_MOUNT_CMD, mount.hostPath ], {})); if (error && error.code === 2) throw new BoxError(BoxError.MOUNT_ERROR, 'Failed to remount existing mount'); // at this point, the old mount config is still there } diff --git a/src/network.js b/src/network.js index a63eac78c..0846dae6c 100644 --- a/src/network.js +++ b/src/network.js @@ -32,7 +32,7 @@ const assert = require('assert'), paths = require('./paths.js'), safe = require('safetydance'), settings = require('./settings.js'), - shell = require('./shell.js'), + shell = require('./shell.js')('network'), validator = require('validator'); const SET_BLOCKLIST_CMD = path.join(__dirname, 'scripts/setblocklist.sh'); @@ -102,7 +102,7 @@ async function setBlocklist(blocklist, auditSource) { // this is done only because it's easier for the shell script and the firewall service to get the value if (!safe.fs.writeFileSync(paths.FIREWALL_BLOCKLIST_FILE, blocklist + '\n', 'utf8')) throw new BoxError(BoxError.FS_ERROR, safe.error.message); - const [error] = await safe(shell.promises.sudo('setBlocklist', [ SET_BLOCKLIST_CMD ], {})); + const [error] = await safe(shell.promises.sudo([ SET_BLOCKLIST_CMD ], {})); if (error) throw new BoxError(BoxError.IPTABLES_ERROR, `Error setting blocklist: ${error.message}`); } diff --git a/src/platform.js b/src/platform.js index e3b14de69..85101a8e6 100644 --- a/src/platform.js +++ b/src/platform.js @@ -31,7 +31,7 @@ const apps = require('./apps.js'), reverseProxy = require('./reverseproxy.js'), safe = require('safetydance'), services = require('./services.js'), - shell = require('./shell.js'), + shell = require('./shell.js')('platform'), tasks = require('./tasks.js'), timers = require('timers/promises'), updater = require('./updater.js'), @@ -50,7 +50,7 @@ async function pruneInfraImages() { // cannot blindly remove all unused images since redis image may not be used const imageNames = Object.keys(infra.images).map(addon => infra.images[addon]); - const [error, output] = await safe(shell.exec('pruneInfraImages', 'docker images --digests --format "{{.ID}} {{.Repository}} {{.Tag}} {{.Digest}}"', { shell: '/bin/bash' })); + const [error, output] = await safe(shell.exec('docker images --digests --format "{{.ID}} {{.Repository}} {{.Tag}} {{.Digest}}"', { shell: '/bin/bash' })); if (error) { debug(`Failed to list images ${error.message}`); throw error; @@ -70,7 +70,7 @@ async function pruneInfraImages() { const imageIdToPrune = tag === '' ? `${repo}@${digest}` : `${repo}:${tag}`; // untagged, use digest console.log(`pruneInfraImages: removing unused image of ${imageName}: ${imageIdToPrune}`); - const [error] = await safe(shell.execArgs('pruneInfraImages', 'docker', [ 'rmi', imageIdToPrune ], {})); + const [error] = await safe(shell.execArgs('docker', [ 'rmi', imageIdToPrune ], {})); if (error) console.log(`Error removing image ${imageIdToPrune}: ${error.mesage}`); } } @@ -79,23 +79,23 @@ async function pruneInfraImages() { async function pruneVolumes() { debug('pruneVolumes: remove all unused local volumes'); - const [error] = await safe(shell.execArgs('pruneVolumes', 'docker', [ 'volume', 'prune', '--all', '--force' ], {})); + const [error] = await safe(shell.execArgs('docker', [ 'volume', 'prune', '--all', '--force' ], {})); if (error) console.log(`Error pruning volumes: ${error.mesage}`); } async function createDockerNetwork() { debug('createDockerNetwork: recreating docker network'); - await shell.exec('createDockerNetwork', 'docker network rm -f cloudron', {}); + await shell.exec('docker network rm -f cloudron', {}); // the --ipv6 option will work even in ipv6 is disabled. fd00 is IPv6 ULA - await shell.exec('createDockerNetwork', `docker network create --subnet=${constants.DOCKER_IPv4_SUBNET} --ip-range=${constants.DOCKER_IPv4_RANGE} --gateway ${constants.DOCKER_IPv4_GATEWAY} --ipv6 --subnet=fd00:c107:d509::/64 cloudron`, {}); + await shell.exec(`docker network create --subnet=${constants.DOCKER_IPv4_SUBNET} --ip-range=${constants.DOCKER_IPv4_RANGE} --gateway ${constants.DOCKER_IPv4_GATEWAY} --ipv6 --subnet=fd00:c107:d509::/64 cloudron`, {}); } async function removeAllContainers() { debug('removeAllContainers: removing all containers for infra upgrade'); - await shell.exec('removeAllContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker stop', { shell: '/bin/bash' }); - await shell.exec('removeAllContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker rm -f', { shell: '/bin/bash' }); + await shell.exec('docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker stop', { shell: '/bin/bash' }); + await shell.exec('docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker rm -f', { shell: '/bin/bash' }); } async function markApps(existingInfra, restoreOptions) { diff --git a/src/provision.js b/src/provision.js index 6960e51da..79499bc0d 100644 --- a/src/provision.js +++ b/src/provision.js @@ -24,7 +24,7 @@ const assert = require('assert'), platform = require('./platform.js'), reverseProxy = require('./reverseproxy.js'), safe = require('safetydance'), - shell = require('./shell.js'), + shell = require('./shell.js')('provision'), semver = require('semver'), paths = require('./paths.js'), system = require('./system.js'), @@ -60,7 +60,7 @@ function setProgress(task, message) { async function ensureDhparams() { if (fs.existsSync(paths.DHPARAMS_FILE)) return; debug('ensureDhparams: generating dhparams'); - const dhparams = await shell.exec('ensureDhParams', 'openssl dhparam -dsaparam 2048', {}); + const dhparams = await shell.exec('openssl dhparam -dsaparam 2048', {}); if (!safe.fs.writeFileSync(paths.DHPARAMS_FILE, dhparams)) throw new BoxError(BoxError.FS_ERROR, `Could not save dhparams.pem: ${safe.error.message}`); } diff --git a/src/reverseproxy.js b/src/reverseproxy.js index dcba6dd38..f5b94ae14 100644 --- a/src/reverseproxy.js +++ b/src/reverseproxy.js @@ -58,7 +58,7 @@ const acme2 = require('./acme2.js'), paths = require('./paths.js'), safe = require('safetydance'), settings = require('./settings.js'), - shell = require('./shell.js'), + shell = require('./shell.js')('reverseproxy'), tasks = require('./tasks.js'), validator = require('validator'); @@ -75,7 +75,7 @@ function nginxLocation(s) { async function getCertificateDates(cert) { assert.strictEqual(typeof cert, 'string'); - const [error, result] = await safe(shell.exec('getCertificateDates', 'openssl x509 -startdate -enddate -subject -noout', { input: cert })); + const [error, result] = await safe(shell.exec('openssl x509 -startdate -enddate -subject -noout', { input: cert })); if (error) return { startDate: null, endDate: null } ; // some error const lines = result.trim().split('\n'); @@ -103,7 +103,7 @@ async function isOcspEnabled(certFilePath) { // We used to check for the must-staple in the cert using openssl x509 -text -noout -in ${certFilePath} | grep -q status_request // however, we cannot set the must-staple because first request to nginx fails because of it's OCSP caching behavior - const [error, result] = await safe(shell.exec('isOscpEnabled', `openssl x509 -in ${certFilePath} -noout -ocsp_uri`, {})); + const [error, result] = await safe(shell.exec(`openssl x509 -in ${certFilePath} -noout -ocsp_uri`, {})); return !error && result.length > 0; // no error and has uri } @@ -112,7 +112,7 @@ async function providerMatches(domainObject, cert) { assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof cert, 'string'); - const [error, subjectAndIssuer] = await safe(shell.exec('providerMatches', 'openssl x509 -noout -subject -issuer', { input: cert })); + const [error, subjectAndIssuer] = await safe(shell.exec('openssl x509 -noout -subject -issuer', { input: cert })); if (error) return false; // something bad happenned const subject = subjectAndIssuer.match(/^subject=(.*)$/m)[1]; @@ -153,20 +153,20 @@ async function validateCertificate(subdomain, domain, certificate) { // -checkhost checks for SAN or CN exclusively. SAN takes precedence and if present, ignores the CN. const fqdn = dns.fqdn(subdomain, domain); - const [checkHostError, checkHostOutput] = await safe(shell.exec('validateCertificate', `openssl x509 -noout -checkhost ${fqdn}`, { input: cert })); + const [checkHostError, checkHostOutput] = await safe(shell.exec(`openssl x509 -noout -checkhost ${fqdn}`, { input: cert })); if (checkHostError) throw new BoxError(BoxError.BAD_FIELD, 'Could not validate certificate'); if (checkHostOutput.indexOf('does match certificate') === -1) throw new BoxError(BoxError.BAD_FIELD, `Certificate is not valid for this domain. Expecting ${fqdn}`); // check if public key in the cert and private key matches. pkey below works for RSA and ECDSA keys - const [pubKeyError1, pubKeyFromCert] = await safe(shell.exec('validateCertificate', 'openssl x509 -noout -pubkey', { input: cert })); + const [pubKeyError1, pubKeyFromCert] = await safe(shell.exec('openssl x509 -noout -pubkey', { input: cert })); if (pubKeyError1) throw new BoxError(BoxError.BAD_FIELD, 'Could not get public key from cert'); - const [pubKeyError2, pubKeyFromKey] = await safe(shell.exec('validateCertificate', 'openssl pkey -pubout', { input: key })); + const [pubKeyError2, pubKeyFromKey] = await safe(shell.exec('openssl pkey -pubout', { input: key })); if (pubKeyError2) throw new BoxError(BoxError.BAD_FIELD, 'Could not get public key from private key'); if (pubKeyFromCert !== pubKeyFromKey) throw new BoxError(BoxError.BAD_FIELD, 'Public key does not match the certificate.'); // check expiration - const [error] = await safe(shell.exec('validateCertificate', 'openssl x509 -checkend 0', { input: cert })); + const [error] = await safe(shell.exec('openssl x509 -checkend 0', { input: cert })); if (error) throw new BoxError(BoxError.BAD_FIELD, 'Certificate has expired'); return null; @@ -174,7 +174,7 @@ async function validateCertificate(subdomain, domain, certificate) { async function notifyCertChange() { await mailServer.checkCertificate(); - await shell.promises.sudo('notifyCertChange', [ RESTART_SERVICE_CMD, 'box' ], {}); // directory server + await shell.promises.sudo([ RESTART_SERVICE_CMD, 'box' ], {}); // directory server const allApps = (await apps.list()).filter(app => app.runState !== apps.RSTATE_STOPPED); for (const app of allApps) { if (app.manifest.addons?.tls) await setupTlsAddon(app); @@ -184,7 +184,7 @@ async function notifyCertChange() { async function reload() { if (constants.TEST) return; - const [error] = await safe(shell.promises.sudo('reload', [ RESTART_SERVICE_CMD, 'nginx' ], {})); + const [error] = await safe(shell.promises.sudo([ RESTART_SERVICE_CMD, 'nginx' ], {})); if (error) throw new BoxError(BoxError.NGINX_ERROR, `Error reloading nginx: ${error.message}`); } @@ -206,7 +206,7 @@ async function generateFallbackCertificate(domain) { safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8'); // the days field is chosen to be less than 825 days per apple requirement (https://support.apple.com/en-us/HT210176) const certCommand = `openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 800 -subj /CN=*.${cn} -extensions SAN -config ${configFile} -nodes`; - await shell.exec('generateFallbackCertificate', certCommand, {}); + await shell.exec(certCommand, {}); safe.fs.unlinkSync(configFile); const cert = safe.fs.readFileSync(certFilePath, 'utf8'); @@ -740,7 +740,7 @@ async function writeDefaultConfig(options) { const cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy // the days field is chosen to be less than 825 days per apple requirement (https://support.apple.com/en-us/HT210176) - await shell.exec('writeDefaultConfig', `openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 800 -subj /CN=${cn} -nodes`, {}); + await shell.exec(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 800 -subj /CN=${cn} -nodes`, {}); } const data = { diff --git a/src/services.js b/src/services.js index ddc605120..9dd8c8572 100644 --- a/src/services.js +++ b/src/services.js @@ -65,7 +65,7 @@ const addonConfigs = require('./addonconfigs.js'), safe = require('safetydance'), settings = require('./settings.js'), sftp = require('./sftp.js'), - shell = require('./shell.js'), + shell = require('./shell.js')('services'), superagent = require('superagent'); const NOOP = async function (/*app, options*/) {}; @@ -283,7 +283,7 @@ function requiresUpgrade(existingObjOrImageName, currentImageName) { async function hasAVX() { // mongodb 5 and above requires AVX - const [error] = await safe(shell.exec('hasAVX', 'grep -q avx /proc/cpuinfo', {})); + const [error] = await safe(shell.exec('grep -q avx /proc/cpuinfo', {})); return !error; } @@ -523,7 +523,7 @@ async function rebuildService(id, auditSource) { await mailServer.start({ version: 'none' }); break; case 'redis': { - await safe(shell.exec('removeRedis', `docker rm -f redis-${instance}`, {})); // ignore error + await safe(shell.exec(`docker rm -f redis-${instance}`, {})); // ignore error const app = await apps.get(instance); if (app) await setupRedis(app, app.manifest.addons.redis); // starts the container break; @@ -752,8 +752,8 @@ async function exportDatabase(addon) { safe.fs.writeFileSync(path.join(paths.ADDON_CONFIG_DIR, `exported-${addon}`), '', 'utf8'); if (safe.error) throw BoxError(BoxError.FS_ERROR, 'Error writing export checkpoint file'); // note: after this point, we are restart safe. it's ok if the box code crashes at this point - await shell.exec(`exportDatabase - remove${addon}`, `docker rm -f ${addon}`, {}); // what if db writes something when quitting ... - await shell.promises.sudo(`exportDatabase - removeAddonDir${addon}`, [ RMADDONDIR_CMD, addon ], {}); // ready to start afresh + await shell.exec(`docker rm -f ${addon}`, {}); // what if db writes something when quitting ... + await shell.promises.sudo([ RMADDONDIR_CMD, addon ], {}); // ready to start afresh } async function applyMemoryLimit(id) { @@ -876,7 +876,7 @@ async function setupLocalStorage(app, options) { const volumeDataDir = await apps.getStorageDir(app); - const [error] = await safe(shell.promises.sudo('createVolume', [ MKDIRVOLUME_CMD, volumeDataDir ], {})); + const [error] = await safe(shell.promises.sudo([ MKDIRVOLUME_CMD, volumeDataDir ], {})); if (error) throw new BoxError(BoxError.FS_ERROR, `Error creating app storage data dir: ${error.message}`); } @@ -887,7 +887,7 @@ async function clearLocalStorage(app, options) { debug('clearLocalStorage'); const volumeDataDir = await apps.getStorageDir(app); - const [error] = await safe(shell.promises.sudo('clearVolume', [ CLEARVOLUME_CMD, 'clear', volumeDataDir ], {})); + const [error] = await safe(shell.promises.sudo([ CLEARVOLUME_CMD, 'clear', volumeDataDir ], {})); if (error) throw new BoxError(BoxError.FS_ERROR, error); } @@ -898,7 +898,7 @@ async function teardownLocalStorage(app, options) { debug('teardownLocalStorage'); const volumeDataDir = await apps.getStorageDir(app); - const [error] = await safe(shell.promises.sudo('clearVolume', [ CLEARVOLUME_CMD, 'rmdir', volumeDataDir ], {})); + const [error] = await safe(shell.promises.sudo([ CLEARVOLUME_CMD, 'rmdir', volumeDataDir ], {})); if (error) throw new BoxError(BoxError.FS_ERROR, error); } @@ -969,7 +969,7 @@ async function startTurn(existingInfra) { await docker.deleteContainer('turn'); debug('startTurn: starting turn container'); - await shell.exec('startTurn', runCmd, { shell: '/bin/bash' }); + await shell.exec(runCmd, { shell: '/bin/bash' }); } async function teardownTurn(app, options) { @@ -1177,7 +1177,7 @@ async function startMysql(existingInfra) { await docker.deleteContainer('mysql'); debug('startMysql: starting mysql container'); - await shell.exec('startMysql', runCmd, { shell: '/bin/bash' }); + await shell.exec(runCmd, { shell: '/bin/bash' }); if (!serviceConfig.recoveryMode) { await waitForContainer('mysql', 'CLOUDRON_MYSQL_TOKEN'); @@ -1395,7 +1395,7 @@ async function startPostgresql(existingInfra) { await docker.deleteContainer('postgresql'); debug('startPostgresql: starting postgresql container'); - await shell.exec('startPostgresql', runCmd, { shell: '/bin/bash' }); + await shell.exec(runCmd, { shell: '/bin/bash' }); if (!serviceConfig.recoveryMode) { await waitForContainer('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN'); @@ -1544,7 +1544,7 @@ async function startMongodb(existingInfra) { } debug('startMongodb: starting mongodb container'); - await shell.exec('startMongodb', runCmd, { shell: '/bin/bash' }); + await shell.exec(runCmd, { shell: '/bin/bash' }); if (!serviceConfig.recoveryMode) { await waitForContainer('mongodb', 'CLOUDRON_MONGODB_TOKEN'); @@ -1712,13 +1712,13 @@ async function startGraphite(existingInfra) { await docker.stopContainer('graphite'); await docker.deleteContainer('graphite'); - if (upgrading) await shell.promises.sudo('removeGraphiteDir', [ RMADDONDIR_CMD, 'graphite' ], {}); + if (upgrading) await shell.promises.sudo([ RMADDONDIR_CMD, 'graphite' ], {}); debug('startGraphite: starting graphite container'); - await shell.exec('startGraphite', runCmd, { shell: '/bin/bash' }); + await shell.exec(runCmd, { shell: '/bin/bash' }); // restart collectd to get the disk stats after graphite starts. currently, there is no way to do graphite health check - setTimeout(async () => await safe(shell.promises.sudo('restartcollectd', [ RESTART_SERVICE_CMD, 'collectd' ], {})), 60000); + setTimeout(async () => await safe(shell.promises.sudo([ RESTART_SERVICE_CMD, 'collectd' ], {})), 60000); } async function setupProxyAuth(app, options) { @@ -1860,7 +1860,7 @@ async function setupRedis(app, options) { const [inspectError, result] = await safe(docker.inspect(redisName)); if (inspectError) { - await shell.exec('startRedis', runCmd, { shell: '/bin/bash' }); + await shell.exec(runCmd, { shell: '/bin/bash' }); } else { // fast path debug(`Re-using existing redis container with state: ${JSON.stringify(result.State)}`); } @@ -1895,7 +1895,7 @@ async function teardownRedis(app, options) { await docker.deleteContainer(`redis-${app.id}`); - const [error] = await safe(shell.promises.sudo('removeVolume', [ RMADDONDIR_CMD, 'redis', app.id ], {})); + const [error] = await safe(shell.promises.sudo([ RMADDONDIR_CMD, 'redis', app.id ], {})); if (error) throw new BoxError(BoxError.FS_ERROR, `Error removing redis data: ${error.message}`); safe.fs.rmSync(path.join(paths.LOG_DIR, `redis-${app.id}`), { recursive: true, force: true }); @@ -1972,12 +1972,12 @@ async function statusDocker() { } async function restartDocker() { - const [error] = await safe(shell.promises.sudo('restartdocker', [ RESTART_SERVICE_CMD, 'docker' ], {})); + const [error] = await safe(shell.promises.sudo([ RESTART_SERVICE_CMD, 'docker' ], {})); if (error) debug(`restartDocker: error restarting docker. ${error.message}`); } async function statusUnbound() { - const [error] = await safe(shell.exec('statusUnbound', 'systemctl is-active unbound', {})); + const [error] = await safe(shell.exec('systemctl is-active unbound', {})); if (error) return { status: exports.SERVICE_STATUS_STOPPED }; const [digError, digResult] = await safe(dig.resolve('ipv4.api.cloudron.io', 'A', { timeout: 10000 })); @@ -1988,17 +1988,17 @@ async function statusUnbound() { } async function restartUnbound() { - const [error] = await safe(shell.promises.sudo('restartunbound', [ RESTART_SERVICE_CMD, 'unbound' ], {})); + const [error] = await safe(shell.promises.sudo([ RESTART_SERVICE_CMD, 'unbound' ], {})); if (error) debug(`restartDocker: error restarting unbound. ${error.message}`); } async function statusNginx() { - const [error] = await safe(shell.exec('statusNginx', 'systemctl is-active nginx', {})); + const [error] = await safe(shell.exec('systemctl is-active nginx', {})); return { status: error ? exports.SERVICE_STATUS_STOPPED : exports.SERVICE_STATUS_ACTIVE }; } async function restartNginx() { - const [error] = await safe(shell.promises.sudo('restartnginx', [ RESTART_SERVICE_CMD, 'nginx' ], {})); + const [error] = await safe(shell.promises.sudo([ RESTART_SERVICE_CMD, 'nginx' ], {})); if (error) debug(`restartNginx: error restarting unbound. ${error.message}`); } @@ -2031,7 +2031,7 @@ async function restartGraphite() { await docker.restartContainer('graphite'); setTimeout(async () => { - const [error] = await safe(shell.promises.sudo('restartcollectd', [ RESTART_SERVICE_CMD, 'collectd' ], {})); + const [error] = await safe(shell.promises.sudo([ RESTART_SERVICE_CMD, 'collectd' ], {})); if (error) debug(`restartGraphite: error restarting collected. ${error.message}`); }, 60000); } diff --git a/src/sftp.js b/src/sftp.js index 65f699f06..6ac9f10ad 100644 --- a/src/sftp.js +++ b/src/sftp.js @@ -20,7 +20,7 @@ const apps = require('./apps.js'), paths = require('./paths.js'), safe = require('safetydance'), services = require('./services.js'), - shell = require('./shell.js'), + shell = require('./shell.js')('sftp'), volumes = require('./volumes.js'); async function ensureKeys() { @@ -34,7 +34,7 @@ async function ensureKeys() { debug(`ensureSecrets: generating new sftp keys of type ${keyType}`); safe.fs.unlinkSync(publicKeyFile); safe.fs.unlinkSync(privateKeyFile); - const [error] = await safe(shell.exec('ensureKeys', `ssh-keygen -m PEM -t ${keyType} -f ${paths.SFTP_KEYS_DIR}/ssh_host_${keyType}_key -q -N ""`, { shell: '/bin/bash' })); + const [error] = await safe(shell.exec(`ssh-keygen -m PEM -t ${keyType} -f ${paths.SFTP_KEYS_DIR}/ssh_host_${keyType}_key -q -N ""`, { shell: '/bin/bash' })); if (error) throw new BoxError(BoxError.OPENSSL_ERROR, `Could not generate sftp ${keyType} keys: ${error.message}`); const newPublicKey = safe.fs.readFileSync(publicKeyFile); await blobs.set(`sftp_${keyType}_public_key`, newPublicKey); @@ -124,7 +124,7 @@ async function start(existingInfra) { await docker.deleteContainer('sftp'); debug('startSftp: starting sftp container'); - await shell.exec('startSftp', runCmd, { shell: '/bin/bash' }); + await shell.exec(runCmd, { shell: '/bin/bash' }); } async function status() { diff --git a/src/shell.js b/src/shell.js index 6f45757cc..a7db0b72c 100644 --- a/src/shell.js +++ b/src/shell.js @@ -7,17 +7,6 @@ const assert = require('assert'), once = require('./once.js'), util = require('util'); -exports = module.exports = { - exec, - execArgs, - - sudo, - - promises: { - sudo: util.promisify(sudo) - } -}; - const SUDO = '/usr/bin/sudo'; // default encoding utf8, no shell, handles input, separate args, wait for process to finish @@ -156,3 +145,21 @@ function sudo(tag, args, options, callback) { return cp; } + +function shell(tag) { + assert.strictEqual(typeof tag, 'string'); + + return { + exec: exec.bind(null, tag), + execArgs: execArgs.bind(null, tag), + sudo: sudo.bind(null, tag), + promises: { sudo: util.promisify(sudo.bind(null, tag)) } + }; +} + +shell.exec = exec; +shell.execArgs = execArgs; +shell.sudo = sudo; +shell.promises = { sudo: util.promisify(sudo) }; + +exports = module.exports = shell; diff --git a/src/storage/filesystem.js b/src/storage/filesystem.js index 68bafe7fb..798d6c4e3 100644 --- a/src/storage/filesystem.js +++ b/src/storage/filesystem.js @@ -38,7 +38,7 @@ const assert = require('assert'), paths = require('../paths.js'), readdirp = require('readdirp'), safe = require('safetydance'), - shell = require('../shell.js'); + shell = require('../shell.js')('filesystem'); // the du call in the function below requires root async function getAvailableSize(apiConfig) { @@ -163,13 +163,13 @@ async function copy(apiConfig, oldFilePath, newFilePath, progressCallback) { const sshOptions = [ '-o', '"StrictHostKeyChecking no"', '-i', identityFilePath, '-p', apiConfig.mountOptions.port, `${apiConfig.mountOptions.user}@${apiConfig.mountOptions.host}` ]; const sshArgs = sshOptions.concat([ 'cp', cpOptions, oldFilePath.replace('/mnt/cloudronbackup/', ''), newFilePath.replace('/mnt/cloudronbackup/', '') ]); - const [remoteCopyError] = await safe(shell.execArgs('copy', 'ssh', sshArgs, { shell: true })); + const [remoteCopyError] = await safe(shell.execArgs('ssh', sshArgs, { shell: true })); if (!remoteCopyError) return; if (remoteCopyError.code === 255) throw new BoxError(BoxError.EXTERNAL_ERROR, `SSH connection error: ${remoteCopyError.message}`); // do not attempt fallback copy for ssh errors debug('SSH remote copy failed, trying ssfs copy'); // this can happen for sshfs mounted windows server } - const [copyError] = await safe(shell.execArgs('copy', 'cp', [ cpOptions, oldFilePath, newFilePath ], {})); + const [copyError] = await safe(shell.execArgs('cp', [ cpOptions, oldFilePath, newFilePath ], {})); if (copyError) throw new BoxError(BoxError.EXTERNAL_ERROR, copyError.message); } @@ -194,7 +194,7 @@ async function removeDir(apiConfig, pathPrefix, progressCallback) { progressCallback({ message: `Removing directory ${pathPrefix}` }); - const [error] = await safe(shell.execArgs('removeDir', 'rm', [ '-rf', pathPrefix ], {})); + const [error] = await safe(shell.execArgs('rm', [ '-rf', pathPrefix ], {})); if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, error.message); } @@ -232,7 +232,7 @@ async function testConfig(apiConfig) { const error = validateBackupTarget(apiConfig.mountPoint); if (error) throw error; - const [mountError] = await safe(shell.exec('testStorageConfig', `mountpoint -q -- ${apiConfig.mountPoint}`, { timeout: 5000 })); + const [mountError] = await safe(shell.exec(`mountpoint -q -- ${apiConfig.mountPoint}`, { timeout: 5000 })); if (mountError) throw new BoxError(BoxError.BAD_FIELD, `${apiConfig.mountPoint} is not mounted: ${mountError.message}`); } diff --git a/src/support.js b/src/support.js index 289ed6ec0..aecb9ae07 100644 --- a/src/support.js +++ b/src/support.js @@ -14,7 +14,7 @@ const assert = require('assert'), path = require('path'), paths = require('./paths.js'), safe = require('safetydance'), - shell = require('./shell.js'); + shell = require('./shell.js')('support'); // the logic here is also used in the cloudron-support tool const AUTHORIZED_KEYS_CMD = path.join(__dirname, 'scripts/remotesupport.sh'); @@ -34,7 +34,7 @@ function sshInfo() { } async function getRemoteSupport() { - const [error, stdoutResult] = await safe(shell.promises.sudo('support', [ AUTHORIZED_KEYS_CMD, 'is-enabled', sshInfo().filePath ], { captureStdout: true })); + const [error, stdoutResult] = await safe(shell.promises.sudo([ AUTHORIZED_KEYS_CMD, 'is-enabled', sshInfo().filePath ], { captureStdout: true })); if (error) throw new BoxError(BoxError.FS_ERROR, error); return stdoutResult.trim() === 'true'; @@ -45,7 +45,7 @@ async function enableRemoteSupport(enable, auditSource) { assert.strictEqual(typeof auditSource, 'object'); const si = sshInfo(); - const [error] = await safe(shell.promises.sudo('support', [ AUTHORIZED_KEYS_CMD, enable ? 'enable' : 'disable', si.filePath, si.user ], {})); + const [error] = await safe(shell.promises.sudo([ AUTHORIZED_KEYS_CMD, enable ? 'enable' : 'disable', si.filePath, si.user ], {})); if (error) throw new BoxError(BoxError.FS_ERROR, error); await eventlog.add(eventlog.ACTION_SUPPORT_SSH, auditSource, { enable }); diff --git a/src/system.js b/src/system.js index 921862dc8..98c698fa6 100644 --- a/src/system.js +++ b/src/system.js @@ -33,7 +33,7 @@ const apps = require('./apps.js'), path = require('path'), paths = require('./paths.js'), safe = require('safetydance'), - shell = require('./shell.js'), + shell = require('./shell.js')('system'), tasks = require('./tasks.js'), volumes = require('./volumes.js'); @@ -44,7 +44,7 @@ const REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'); async function du(file) { assert.strictEqual(typeof file, 'string'); - const [error, stdoutResult] = await safe(shell.promises.sudo('system', [ DU_CMD, file ], { captureStdout: true })); + const [error, stdoutResult] = await safe(shell.promises.sudo([ DU_CMD, file ], { captureStdout: true })); if (error) throw new BoxError(BoxError.FS_ERROR, error); return parseInt(stdoutResult.trim(), 10); @@ -53,7 +53,7 @@ async function du(file) { async function hdparm(file) { assert.strictEqual(typeof file, 'string'); - const [error, stdoutResult] = await safe(shell.promises.sudo('system', [ HDPARM_CMD, file ], { captureStdout: true })); + const [error, stdoutResult] = await safe(shell.promises.sudo([ HDPARM_CMD, file ], { captureStdout: true })); if (error) throw new BoxError(BoxError.FS_ERROR, error); const lines = stdoutResult.split('\n'); @@ -67,7 +67,7 @@ async function hdparm(file) { } async function getSwaps() { - const [error, stdout] = await safe(shell.exec('getSwaps', 'swapon --noheadings --raw --bytes --show=type,size,used,name', {})); + const [error, stdout] = await safe(shell.exec('swapon --noheadings --raw --bytes --show=type,size,used,name', {})); if (error) return {}; const output = stdout.trim(); if (!output) return {}; // no swaps @@ -266,7 +266,7 @@ async function updateDiskUsage(progressCallback) { async function reboot() { await notifications.clearAlert(notifications.ALERT_REBOOT, 'Reboot Required'); - const [error] = await safe(shell.promises.sudo('reboot', [ REBOOT_CMD ], {})); + const [error] = await safe(shell.promises.sudo([ REBOOT_CMD ], {})); if (error) debug('reboot: could not reboot. %o', error); } @@ -314,7 +314,7 @@ async function getLogs(unit, options) { } async function getBlockDevices() { - const result = await shell.exec('getBlockDevices', 'lsblk --paths --json --list --fs --output +rota', {}); + const result = await shell.exec('lsblk --paths --json --list --fs --output +rota', {}); const info = safe.JSON.parse(result); if (!info) throw new BoxError(BoxError.INTERNAL_ERROR, `failed to parse lsblk: ${safe.error.message}`); diff --git a/src/tasks.js b/src/tasks.js index 1f8e3d058..773e3698d 100644 --- a/src/tasks.js +++ b/src/tasks.js @@ -53,7 +53,7 @@ const assert = require('assert'), path = require('path'), paths = require('./paths.js'), safe = require('safetydance'), - shell = require('./shell.js'), + shell = require('./shell.js')('tasks'), _ = require('underscore'); let gTasks = {}; // indexed by task id @@ -170,7 +170,7 @@ function startTask(id, options, onTaskFinished) { const sudoOptions = { preserveEnv: true, logStream: null }; if (constants.TEST) sudoOptions.logStream = fs.createWriteStream('/dev/null'); // without this output is messed up, not sure why - gTasks[id] = shell.sudo('startTask', [ START_TASK_CMD, id, logFile, options.nice || 0, options.memoryLimit || 400, options.oomScoreAdjust || 0 ], sudoOptions, async function (sudoError) { + gTasks[id] = shell.sudo([ START_TASK_CMD, id, logFile, options.nice || 0, options.memoryLimit || 400, options.oomScoreAdjust || 0 ], sudoOptions, async function (sudoError) { if (!gTasks[id]) return; // ignore task exit since we are shutting down. see stopAllTasks const code = sudoError ? sudoError.code : 0; @@ -227,14 +227,14 @@ async function stopTask(id) { debug(`stopTask: stopping task ${id}`); - await shell.promises.sudo('stopTask', [ STOP_TASK_CMD, id, ], {}); + await shell.promises.sudo([ STOP_TASK_CMD, id, ], {}); } async function stopAllTasks() { debug('stopAllTasks: stopping all tasks'); gTasks = {}; // this signals startTask() to not set completion status as "crashed" - const [error] = await safe(shell.promises.sudo('stopTask', [ STOP_TASK_CMD, 'all' ], { cwd: paths.baseDir() })); + const [error] = await safe(shell.promises.sudo([ STOP_TASK_CMD, 'all' ], { cwd: paths.baseDir() })); if (error) debug(`stopAllTasks: error stopping stasks: ${error.message}`); } diff --git a/src/test/shell-test.js b/src/test/shell-test.js index af75fdb69..fa7fa8138 100644 --- a/src/test/shell-test.js +++ b/src/test/shell-test.js @@ -8,70 +8,70 @@ const BoxError = require('../boxerror.js'), expect = require('expect.js'), path = require('path'), safe = require('safetydance'), - shell = require('../shell.js'); + shell = require('../shell.js')('test'); describe('shell', function () { describe('execArgs', function () { it('can run valid program', async function () { - await shell.execArgs('test', 'ls', [ '-l' ], {}); + await shell.execArgs('ls', [ '-l' ], {}); }); it('fails on invalid program', async function () { - const [error] = await safe(shell.execArgs('test', 'randomprogram', [ ], {})); + const [error] = await safe(shell.execArgs('randomprogram', [ ], {})); expect(error.reason).to.be(BoxError.SHELL_ERROR); }); it('fails on failing program', async function () { - const [error] = await safe(shell.execArgs('test', '/usr/bin/false', [ ], {})); + const [error] = await safe(shell.execArgs('/usr/bin/false', [ ], {})); expect(error.reason).to.be(BoxError.SHELL_ERROR); }); }); describe('sudo', function () { it('cannot sudo invalid program', function (done) { - shell.sudo('test', [ 'randomprogram' ], {}, function (error) { + shell.sudo([ 'randomprogram' ], {}, function (error) { expect(error).to.be.ok(); done(); }); }); it('can sudo valid program', function (done) { - let RELOAD_NGINX_CMD = path.join(__dirname, '../src/scripts/restartservice.sh'); - shell.sudo('test', [ RELOAD_NGINX_CMD, 'nginx' ], {}, function (error) { + const RELOAD_NGINX_CMD = path.join(__dirname, '../src/scripts/restartservice.sh'); + shell.sudo([ RELOAD_NGINX_CMD, 'nginx' ], {}, function (error) { expect(error).to.be.ok(); done(); }); }); it('can run valid program (promises)', async function () { - let RELOAD_NGINX_CMD = path.join(__dirname, '../src/scripts/restartservice.sh'); - await safe(shell.promises.sudo('test', [ RELOAD_NGINX_CMD, 'nginx' ], {})); + const RELOAD_NGINX_CMD = path.join(__dirname, '../src/scripts/restartservice.sh'); + await safe(shell.promises.sudo([ RELOAD_NGINX_CMD, 'nginx' ], {})); }); }); describe('exec', function () { it('exec throws for invalid program', async function () { - const [error] = await safe(shell.exec('test', 'cannotexist', {})); + const [error] = await safe(shell.exec('cannotexist', {})); expect(error.reason).to.be(BoxError.SHELL_ERROR); }); it('exec throws for failed program', async function () { - const [error] = await safe(shell.exec('test', 'false', {})); + const [error] = await safe(shell.exec('false', {})); expect(error.reason).to.be(BoxError.SHELL_ERROR); }); it('exec times out properly', async function () { - const [error] = await safe(shell.exec('sleeping', 'sleep 20', { timeout: 1000 })); + const [error] = await safe(shell.exec('sleep 20', { timeout: 1000 })); expect(error.reason).to.be(BoxError.SHELL_ERROR); }); it('cannot exec a shell program by default', async function () { - const [error] = await safe(shell.exec('test', 'ls -l | wc -c', {})); + const [error] = await safe(shell.exec('ls -l | wc -c', {})); expect(error.reason).to.be(BoxError.SHELL_ERROR); }); it('cannot exec a shell program b', async function () { - await shell.exec('test', 'ls -l | wc -c', { shell: '/bin/bash' }); + await shell.exec('ls -l | wc -c', { shell: '/bin/bash' }); }); }); }); diff --git a/src/updater.js b/src/updater.js index 3aa7a0852..81529e43e 100644 --- a/src/updater.js +++ b/src/updater.js @@ -32,7 +32,7 @@ const apps = require('./apps.js'), safe = require('safetydance'), semver = require('semver'), settings = require('./settings.js'), - shell = require('./shell.js'), + shell = require('./shell.js')('updater'), tasks = require('./tasks.js'), updateChecker = require('./updatechecker.js'); @@ -66,7 +66,7 @@ async function downloadUrl(url, file) { await promiseRetry({ times: 10, interval: 5000, debug }, async function () { debug(`downloadUrl: downloading ${url} to ${file}`); - const [error] = await safe(shell.exec('downloadUrl', `curl -s --fail ${url} -o ${file}`, {})); + const [error] = await safe(shell.exec(`curl -s --fail ${url} -o ${file}`, {})); if (error) throw new BoxError(BoxError.NETWORK_ERROR, `Failed to download ${url}: ${error.message}`); debug('downloadUrl: done'); }); @@ -80,7 +80,7 @@ async function gpgVerify(file, sig) { debug(`gpgVerify: ${cmd}`); - const [error, stdout] = await safe(shell.exec('gpgVerify', cmd, {})); + const [error, stdout] = await safe(shell.exec(cmd, {})); if (error) { debug(`gpgVerify: command failed. error: ${error}\n stdout: ${error.stdout}\n stderr: ${error.stderr}`); throw new BoxError(BoxError.NOT_SIGNED, `The signature in ${path.basename(sig)} could not be verified (command failed)`); @@ -99,7 +99,7 @@ async function extractTarball(tarball, dir) { debug(`extractTarball: extracting ${tarball} to ${dir}`); - const [error] = await safe(shell.exec('extractTarball', `tar -zxf ${tarball} -C ${dir}`, {})); + const [error] = await safe(shell.exec(`tar -zxf ${tarball} -C ${dir}`, {})); if (error) throw new BoxError(BoxError.FS_ERROR, `Failed to extract release package: ${error.message}`); safe.fs.unlinkSync(tarball); @@ -121,7 +121,7 @@ async function verifyUpdateInfo(versionsFile, updateInfo) { async function downloadAndVerifyRelease(updateInfo) { assert.strictEqual(typeof updateInfo, 'object'); - await safe(shell.exec('cleanupOldArtifacts', `rm -rf ${path.join(os.tmpdir(), 'box-*')}`, { shell: '/bin/bash' }), { debug }); // remove any old artifacts + await safe(shell.exec(`rm -rf ${path.join(os.tmpdir(), 'box-*')}`, { shell: '/bin/bash' }), { debug }); // remove any old artifacts await downloadUrl(updateInfo.boxVersionsUrl, `${paths.UPDATE_DIR}/versions.json`); await downloadUrl(updateInfo.boxVersionsSigUrl, `${paths.UPDATE_DIR}/versions.json.sig`); @@ -174,7 +174,7 @@ async function update(boxUpdateInfo, options, progressCallback) { progressCallback({ percent: 70, message: 'Installing update' }); - await shell.promises.sudo('update', [ UPDATE_CMD, packageInfo.file, process.stdout.logFile ], {}); // run installer.sh from new box code as a separate service + await shell.promises.sudo([ UPDATE_CMD, packageInfo.file, process.stdout.logFile ], {}); // run installer.sh from new box code as a separate service // Do not add any code here. The installer script will stop the box code any instant }