diff --git a/src/acme2.js b/src/acme2.js index eb75518c8..00415cb84 100644 --- a/src/acme2.js +++ b/src/acme2.js @@ -74,7 +74,7 @@ function b64(str) { async function getModulus(pem) { assert.strictEqual(typeof pem, 'string'); - const stdout = await shell.promises.exec('getModulus', 'openssl rsa -modulus -noout', { input: pem }); + const stdout = await shell.exec('getModulus', '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.promises.exec('generateAccountKey', 'openssl genrsa 4096', {}); + const acmeAccountKey = await shell.exec('generateAccountKey', '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.promises.exec('signCertificate', 'openssl req -inform pem -outform der', { input: csrPem }); + const csrDer = await shell.exec('signCertificate', 'openssl req -inform pem -outform der', { input: csrPem }); 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.promises.exec('ensureKey', 'openssl ecparam -genkey -name secp256r1', {}); + const newKey = await shell.exec('ensureKey', '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.promises.exec('createCsr', `openssl req -new -key ${keyFilePath} -outform PEM -subj /CN=${this.cn} -config ${opensslConfigFile}`, {}); + const csrPem = await shell.exec('createCsr', `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 diff --git a/src/apps.js b/src/apps.js index ead364f37..0ce5ee0a4 100644 --- a/src/apps.js +++ b/src/apps.js @@ -2832,7 +2832,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.promises.exec('uploadFile', `printf %q '${destFilePath.replace(/'/g, '\'\\\'\'')}'`, { shell: '/bin/bash' }); + const escapedDestFilePath = await shell.exec('uploadFile', `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 6bfb3d23c..94e9f7929 100644 --- a/src/appstore.js +++ b/src/appstore.js @@ -396,7 +396,7 @@ async function createTicket(info, auditSource) { const logPaths = await apps.getLogPaths(info.app); for (const logPath of logPaths) { - const [error, logs] = await safe(shell.promises.exec('createTicket', `tail --lines=1000 ${logPath}`, {})); + const [error, logs] = await safe(shell.exec('createTicket', `tail --lines=1000 ${logPath}`, {})); if (!error && logs) request.attach(path.basename(logPath), logs, path.basename(logPath)); } } else { diff --git a/src/backupformat/rsync.js b/src/backupformat/rsync.js index 9f4f946cb..4e15ad2d1 100644 --- a/src/backupformat/rsync.js +++ b/src/backupformat/rsync.js @@ -110,13 +110,13 @@ async function saveFsMetadata(dataLayout, metadataFile) { // we assume small number of files. spawnSync will raise a ENOBUFS error after maxBuffer for (let lp of dataLayout.localPaths()) { - const emptyDirs = await shell.promises.exec('saveFsMetadata', `find ${lp} -type d -empty`, { maxBuffer: 1024 * 1024 * 80 }); + const emptyDirs = await shell.exec('saveFsMetadata', `find ${lp} -type d -empty`, { maxBuffer: 1024 * 1024 * 80 }); metadata.emptyDirs = metadata.emptyDirs.concat(emptyDirs.trim().split('\n').map((ed) => dataLayout.toRemotePath(ed))); - const execFiles = await shell.promises.exec('saveFsMetadata', `find ${lp} -type f -executable`, { maxBuffer: 1024 * 1024 * 80 }); + const execFiles = await shell.exec('saveFsMetadata', `find ${lp} -type f -executable`, { maxBuffer: 1024 * 1024 * 80 }); metadata.execFiles = metadata.execFiles.concat(execFiles.trim().split('\n').map((ef) => dataLayout.toRemotePath(ef))); - const symlinks = await shell.promises.exec('safeFsMetadata', `find ${lp} -type l`, { maxBuffer: 1024 * 1024 * 30 }); + const symlinks = await shell.exec('safeFsMetadata', `find ${lp} -type l`, { maxBuffer: 1024 * 1024 * 30 }); metadata.symlinks = metadata.symlinks.concat(symlinks.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 70dd8fc71..bee840107 100644 --- a/src/backuptask.js +++ b/src/backuptask.js @@ -60,7 +60,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.promises.execArgs('checkPreconditions', 'du', [ '-Dsb', '--exclude=*.lock', '--exclude=dovecot.list.index.log.*', localPath], {}); + const result = await shell.execArgs('checkPreconditions', 'du', [ '-Dsb', '--exclude=*.lock', '--exclude=dovecot.list.index.log.*', localPath], {}); used += parseInt(result, 10); } diff --git a/src/database.js b/src/database.js index f1d77d093..0dc80500c 100644 --- a/src/database.js +++ b/src/database.js @@ -78,7 +78,7 @@ 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.promises.exec('clear_database', cmd, { shell: '/bin/bash' }); + await shell.exec('clear_database', cmd, { shell: '/bin/bash' }); } async function query() { @@ -136,7 +136,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.promises.exec('importFromFile', cmd, { shell: '/bin/bash' })); + const [error] = await safe(shell.exec('importFromFile', cmd, { shell: '/bin/bash' })); if (error) throw new BoxError(BoxError.DATABASE_ERROR, error); } @@ -144,12 +144,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.promises.exec('exportToFile', '/usr/bin/mysqldump --help', {}); + const mysqlDumpHelp = await shell.exec('exportToFile', '/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.promises.exec('exportToFile', cmd, { shell: '/bin/bash' })); + const [error] = await safe(shell.exec('exportToFile', cmd, { shell: '/bin/bash' })); if (error) throw new BoxError(BoxError.DATABASE_ERROR, error); } diff --git a/src/df.js b/src/df.js index 233b5590d..341de5446 100644 --- a/src/df.js +++ b/src/df.js @@ -37,7 +37,7 @@ function parseLine(line) { } async function disks() { - const [error, output] = await safe(shell.promises.exec('disks', 'df -B1 --output=source,fstype,size,used,avail,pcent,target', { timeout: 5000 })); + const [error, output] = await safe(shell.exec('disks', '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.promises.exec('file', `df -B1 --output=source,fstype,size,used,avail,pcent,target ${filename}`, { timeout: 5000 })); + const [error, output] = await safe(shell.exec('file', `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/docker.js b/src/docker.js index 6155ddf93..790723bb0 100644 --- a/src/docker.js +++ b/src/docker.js @@ -247,7 +247,7 @@ async function getAddressesForPort53() { const addresses = []; for (const phy of physicalDevices) { - const [error, output] = await safe(shell.promises.exec('getAddressesForPort53', `ip -f inet -j addr show dev ${phy.name} scope global`, {})); + const [error, output] = await safe(shell.exec('getAddressesForPort53', `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) { @@ -651,7 +651,7 @@ async function update(name, memory, memorySwap) { // 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.promises.exec(`update(${name})`, `docker update --memory ${memory} --memory-swap ${memorySwap} ${name}`, {})); + const [error] = await safe(shell.exec(`update(${name})`, `docker update --memory ${memory} --memory-swap ${memorySwap} ${name}`, {})); if (!error) return; await timers.setTimeout(60 * 1000); } diff --git a/src/mailserver.js b/src/mailserver.js index f9a6d4ca1..dc50e8d62 100644 --- a/src/mailserver.js +++ b/src/mailserver.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.promises.exec('generateDkimKey', `openssl genrsa -out ${privateKeyFilePath} 1024`, {}); - await shell.promises.exec('generateDkimKey', `openssl rsa -in ${privateKeyFilePath} -out ${publicKeyFilePath} -pubout -outform PEM`, {}); + await shell.exec('generateDkimKey', `openssl genrsa -out ${privateKeyFilePath} 1024`, {}); + await shell.exec('generateDkimKey', `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); @@ -153,7 +153,7 @@ async function configureMail(mailFqdn, mailDomain, serviceConfig) { const mailCertFilePath = `${paths.MAIL_CONFIG_DIR}/tls_cert.pem`; const mailKeyFilePath = `${paths.MAIL_CONFIG_DIR}/tls_key.pem`; - const [copyError] = await safe(shell.promises.exec('configureMail', `cp ${paths.DHPARAMS_FILE} ${dhparamsFilePath}`, {})); + const [copyError] = await safe(shell.exec('configureMail', `cp ${paths.DHPARAMS_FILE} ${dhparamsFilePath}`, {})); if (copyError) throw new BoxError(BoxError.FS_ERROR, `Could not copy dhparams: ${copyError.message}`); if (!safe.fs.writeFileSync(mailCertFilePath, certificate.cert)) throw new BoxError(BoxError.FS_ERROR, `Could not create cert file: ${safe.error.message}`); if (!safe.fs.writeFileSync(mailKeyFilePath, certificate.key)) throw new BoxError(BoxError.FS_ERROR, `Could not create key file: ${safe.error.message}`); @@ -161,8 +161,8 @@ async function configureMail(mailFqdn, mailDomain, serviceConfig) { // if the 'yellowtent' user of OS and the 'cloudron' user of mail container don't match, the keys become inaccessible by mail code if (!safe.fs.chmodSync(mailKeyFilePath, 0o644)) throw new BoxError(BoxError.FS_ERROR, `Could not chmod key file: ${safe.error.message}`); - await safe(shell.promises.exec('stopMail', 'docker stop mail', {})); // ignore error - await safe(shell.promises.exec('removeMail', 'docker rm -f mail', {})); // ignore error + await safe(shell.exec('stopMail', 'docker stop mail', {})); // ignore error + await safe(shell.exec('removeMail', 'docker rm -f mail', {})); // ignore error const allowInbound = await createMailConfig(mailFqdn); @@ -191,7 +191,7 @@ async function configureMail(mailFqdn, mailDomain, serviceConfig) { --label isCloudronManaged=true \ ${readOnly} -v /run -v /tmp ${image} ${cmd}`; - await shell.promises.exec('startMail', runCmd, {}); + await shell.exec('startMail', runCmd, {}); } async function restart() { diff --git a/src/mounts.js b/src/mounts.js index ba190853a..6539cf5c6 100644 --- a/src/mounts.js +++ b/src/mounts.js @@ -79,7 +79,7 @@ async function renderMountFile(mount) { let options, what, type; switch (mountType) { case 'cifs': { - const out = shell.promises.exec('renderMountFile', `systemd-escape -p '${hostPath}'`, {}); // this ensures uniqueness of creds file + const out = shell.exec('renderMountFile', `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}`); @@ -138,7 +138,7 @@ async function removeMount(mount) { const keyFilePath = path.join(paths.SSHFS_KEYS_DIR, `id_rsa_${mountOptions.host}`); safe.fs.unlinkSync(keyFilePath); } else if (mountType === 'cifs') { - const out = await shell.promises.exec('removeMount', `systemd-escape -p '${hostPath}'`, {}); + const out = await shell.exec('removeMount', `systemd-escape -p '${hostPath}'`, {}); const credentialsFilePath = path.join(paths.CIFS_CREDENTIALS_DIR, `${out.trim()}.cred`); safe.fs.unlinkSync(credentialsFilePath); } @@ -150,7 +150,7 @@ async function getStatus(mountType, hostPath) { if (mountType === 'filesystem') return { state: 'active', message: 'Mounted' }; - const [error] = await safe(shell.promises.exec('getVolumeStatus', `mountpoint -q -- ${hostPath}`, { timeout: 5000 })); + const [error] = await safe(shell.exec('getVolumeStatus', `mountpoint -q -- ${hostPath}`, { timeout: 5000 })); const state = error ? 'inactive' : 'active'; if (mountType === 'mountpoint') return { state, message: state === 'active' ? 'Mounted' : 'Not mounted' }; @@ -159,7 +159,7 @@ async function getStatus(mountType, hostPath) { let message; if (state !== 'active') { // find why it failed - const logsJson = await shell.promises.exec('getStatus', `journalctl -u $(systemd-escape -p --suffix=mount ${hostPath}) -n 10 --no-pager -o json`, {}); + const logsJson = await shell.exec('getStatus', `journalctl -u $(systemd-escape -p --suffix=mount ${hostPath}) -n 10 --no-pager -o json`, {}); if (logsJson) { const lines = logsJson.trim().split('\n').map(l => JSON.parse(l)); // array of json diff --git a/src/platform.js b/src/platform.js index 952fce14e..09ed944df 100644 --- a/src/platform.js +++ b/src/platform.js @@ -49,7 +49,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.promises.exec('pruneInfraImages', 'docker images --digests --format "{{.ID}} {{.Repository}} {{.Tag}} {{.Digest}}"', {})); + const [error, output] = await safe(shell.exec('pruneInfraImages', 'docker images --digests --format "{{.ID}} {{.Repository}} {{.Tag}} {{.Digest}}"', {})); if (error) { debug(`Failed to list images ${error.message}`); throw error; @@ -69,7 +69,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.promises.exec('pruneInfraImages', `docker rmi '${imageIdToPrune}'`, {})); + const [error] = await safe(shell.exec('pruneInfraImages', `docker rmi '${imageIdToPrune}'`, {})); if (error) console.log(`Error removing image ${imageIdToPrune}: ${error.mesage}`); } } @@ -78,16 +78,16 @@ async function pruneInfraImages() { async function createDockerNetwork() { debug('createDockerNetwork: recreating docker network'); - await safe(shell.promises.exec('createDockerNetwork', 'docker network rm cloudron', {})); // ignore error + await safe(shell.exec('createDockerNetwork', 'docker network rm cloudron', {})); // ignore error // the --ipv6 option will work even in ipv6 is disabled. fd00 is IPv6 ULA - await shell.promises.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('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`, {}); } async function removeAllContainers() { debug('removeAllContainers: removing all containers for infra upgrade'); - await shell.promises.exec('removeAllContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker stop', { shell: '/bin/bash' }); - await shell.promises.exec('removeAllContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker rm -f', { shell: '/bin/bash' }); + 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' }); } async function markApps(existingInfra, restoreOptions) { diff --git a/src/provision.js b/src/provision.js index a91b2a874..d40c0643d 100644 --- a/src/provision.js +++ b/src/provision.js @@ -58,7 +58,7 @@ function setProgress(task, message) { async function ensureDhparams() { if (fs.existsSync(paths.DHPARAMS_FILE)) return; debug('ensureDhparams: generating dhparams'); - const dhparams = await shell.promises.exec('ensureDhParams', 'openssl dhparam -dsaparam 2048', {}); + const dhparams = await shell.exec('ensureDhParams', '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 81a133888..3a53c8e82 100644 --- a/src/reverseproxy.js +++ b/src/reverseproxy.js @@ -75,7 +75,7 @@ function nginxLocation(s) { async function getCertificateDates(cert) { assert.strictEqual(typeof cert, 'string'); - const [error, result] = await safe(shell.promises.exec('getCertificateDates', 'openssl x509 -startdate -enddate -subject -noout', { input: cert })); + const [error, result] = await safe(shell.exec('getCertificateDates', '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.promises.exec('isOscpEnabled', `openssl x509 -in ${certFilePath} -noout -ocsp_uri`, {})); + const [error, result] = await safe(shell.exec('isOscpEnabled', `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.promises.exec('providerMatches', 'openssl x509 -noout -subject -issuer', { input: cert })); + const [error, subjectAndIssuer] = await safe(shell.exec('providerMatches', '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.promises.exec('validateCertificate', `openssl x509 -noout -checkhost ${fqdn}`, { input: cert })); + const [checkHostError, checkHostOutput] = await safe(shell.exec('validateCertificate', `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.promises.exec('validateCertificate', 'openssl x509 -noout -pubkey', { input: cert })); + const [pubKeyError1, pubKeyFromCert] = await safe(shell.exec('validateCertificate', '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.promises.exec('validateCertificate', 'openssl pkey -pubout', { input: key })); + const [pubKeyError2, pubKeyFromKey] = await safe(shell.exec('validateCertificate', '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.promises.exec('validateCertificate', 'openssl x509 -checkend 0', { input: cert })); + const [error] = await safe(shell.exec('validateCertificate', 'openssl x509 -checkend 0', { input: cert })); if (error) throw new BoxError(BoxError.BAD_FIELD, 'Certificate has expired'); return null; @@ -207,7 +207,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.promises.exec('generateFallbackCertificate', certCommand, {}); + await shell.exec('generateFallbackCertificate', certCommand, {}); safe.fs.unlinkSync(configFile); const cert = safe.fs.readFileSync(certFilePath, 'utf8'); @@ -735,7 +735,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.promises.exec('writeDefaultConfig', `openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 800 -subj /CN=${cn} -nodes`, {}); + await shell.exec('writeDefaultConfig', `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 20f2a07ac..84c02f398 100644 --- a/src/services.js +++ b/src/services.js @@ -512,7 +512,7 @@ async function rebuildService(id, auditSource) { await mailServer.start({ version: 'none' }); break; case 'redis': { - await safe(shell.promises.exec('removeRedis', `docker rm -f redis-${instance}`, {})); // ignore error + await safe(shell.exec('removeRedis', `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; @@ -741,7 +741,7 @@ 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.promises.exec(`exportDatabase - remove${addon}`, `docker rm -f ${addon}`, {}); // what if db writes something when quitting ... + 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 } @@ -952,9 +952,9 @@ async function startTurn(existingInfra) { --label isCloudronManaged=true \ ${readOnly} -v /tmp -v /run "${image}" ${cmd}`; - await safe(shell.promises.exec('stopTurn', 'docker stop turn', {})); // ignore error - await safe(shell.promises.exec('removeTurn', 'docker rm -f turn', {})); // ignore error - await shell.promises.exec('startTurn', runCmd, {}); + await safe(shell.exec('stopTurn', 'docker stop turn', {})); // ignore error + await safe(shell.exec('removeTurn', 'docker rm -f turn', {})); // ignore error + await shell.exec('startTurn', runCmd, {}); } async function teardownTurn(app, options) { @@ -1159,9 +1159,9 @@ async function startMysql(existingInfra) { --cap-add SYS_NICE \ ${readOnly} -v /tmp -v /run "${image}" ${cmd}`; - await safe(shell.promises.exec('stopMysql', 'docker stop mysql', {})); // ignore error - await safe(shell.promises.exec('removeMysql', 'docker rm -f mysql', {})); // ignore error - await shell.promises.exec('startMysql', runCmd, {}); + await safe(shell.exec('stopMysql', 'docker stop mysql', {})); // ignore error + await safe(shell.exec('removeMysql', 'docker rm -f mysql', {})); // ignore error + await shell.exec('startMysql', runCmd, {}); if (!serviceConfig.recoveryMode) { await waitForContainer('mysql', 'CLOUDRON_MYSQL_TOKEN'); @@ -1378,9 +1378,9 @@ async function startPostgresql(existingInfra) { --label isCloudronManaged=true \ ${readOnly} -v /tmp -v /run "${image}" ${cmd}`; - await safe(shell.promises.exec('stopPostgresql', 'docker stop postgresql', {})); // ignore error - await safe(shell.promises.exec('removePostgresql', 'docker rm -f postgresql', {})); // ignore error - await shell.promises.exec('startPostgresql', runCmd, {}); + await safe(shell.exec('stopPostgresql', 'docker stop postgresql', {})); // ignore error + await safe(shell.exec('removePostgresql', 'docker rm -f postgresql', {})); // ignore error + await shell.exec('startPostgresql', runCmd, {}); if (!serviceConfig.recoveryMode) { await waitForContainer('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN'); @@ -1521,9 +1521,9 @@ async function startMongodb(existingInfra) { --label isCloudronManaged=true \ ${readOnly} -v /tmp -v /run "${image}" ${cmd}`; - await safe(shell.promises.exec('stopMongodb', 'docker stop mongodb', {})); // ignore error - await safe(shell.promises.exec('removeMongodb', 'docker rm -f mongodb', {})); // ignore error - await shell.promises.exec('startMongodb', runCmd, {}); + await safe(shell.exec('stopMongodb', 'docker stop mongodb', {})); // ignore error + await safe(shell.exec('removeMongodb', 'docker rm -f mongodb', {})); // ignore error + await shell.exec('startMongodb', runCmd, {}); if (!serviceConfig.recoveryMode) { await waitForContainer('mongodb', 'CLOUDRON_MONGODB_TOKEN'); @@ -1669,10 +1669,10 @@ async function startGraphite(existingInfra) { --label isCloudronManaged=true \ ${readOnly} -v /tmp -v /run "${image}" ${cmd}`; - await safe(shell.promises.exec('stopGraphite', 'docker stop graphite', {})); // ignore error - await safe(shell.promises.exec('removeGraphite', 'docker rm -f graphite', {})); // ignore error + await safe(shell.exec('stopGraphite', 'docker stop graphite', {})); // ignore error + await safe(shell.exec('removeGraphite', 'docker rm -f graphite', {})); // ignore error if (upgrading) await shell.promises.sudo('removeGraphiteDir', [ RMADDONDIR_CMD, 'graphite' ], {}); - await shell.promises.exec('startGraphite', runCmd, {}); + await shell.exec('startGraphite', runCmd, {}); // 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); @@ -1714,8 +1714,8 @@ async function startRedis(existingInfra) { if (upgrading) await backupRedis(app, {}); - await safe(shell.promises.exec('stopRedis', `docker stop ${redisName}`, {})); // redis will backup as part of signal handling - await safe(shell.promises.exec('removeRedis', `docker rm -f ${redisName}`, {})); // ignore error + await safe(shell.exec('stopRedis', `docker stop ${redisName}`, {})); // redis will backup as part of signal handling + await safe(shell.exec('removeRedis', `docker rm -f ${redisName}`, {})); // ignore error await setupRedis(app, app.manifest.addons.redis); // starts the container } @@ -1776,7 +1776,7 @@ async function setupRedis(app, options) { const [inspectError, result] = await safe(docker.inspect(redisName)); if (inspectError) { - await shell.promises.exec('startRedis', runCmd, {}); + await shell.exec('startRedis', runCmd, {}); } else { // fast path debug(`Re-using existing redis container with state: ${JSON.stringify(result.State)}`); } @@ -1900,7 +1900,7 @@ async function restartDocker() { } async function statusUnbound() { - const [error] = await safe(shell.promises.exec('statusUnbound', 'systemctl is-active unbound', {})); + const [error] = await safe(shell.exec('statusUnbound', 'systemctl is-active unbound', {})); if (error) return { status: exports.SERVICE_STATUS_STOPPED }; const [digError, digResult] = await safe(dig.resolve('ipv4.api.cloudron.io', 'A', { server: '127.0.0.1', timeout: 10000 })); @@ -1916,7 +1916,7 @@ async function restartUnbound() { } async function statusNginx() { - const [error] = await safe(shell.promises.exec('statusNginx', 'systemctl is-active nginx', {})); + const [error] = await safe(shell.exec('statusNginx', 'systemctl is-active nginx', {})); return { status: error ? exports.SERVICE_STATUS_STOPPED : exports.SERVICE_STATUS_ACTIVE }; } diff --git a/src/sftp.js b/src/sftp.js index 8521ff242..5c1caef32 100644 --- a/src/sftp.js +++ b/src/sftp.js @@ -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.promises.exec('ensureKeys', `ssh-keygen -m PEM -t ${keyType} -f "${paths.SFTP_KEYS_DIR}/ssh_host_${keyType}_key" -q -N ""`, {})); + const [error] = await safe(shell.exec('ensureKeys', `ssh-keygen -m PEM -t ${keyType} -f "${paths.SFTP_KEYS_DIR}/ssh_host_${keyType}_key" -q -N ""`, {})); 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); @@ -123,9 +123,9 @@ async function start(existingInfra) { ${readOnly} -v /tmp -v /run "${image}" ${cmd}`; // ignore error if container not found (and fail later) so that this code works across restarts - await safe(shell.promises.exec('stopSftp', 'docker stop sftp', {})); // ignore error - await safe(shell.promises.exec('removeSftp', 'docker rm -f sftp', {})); // ignore error - await shell.promises.exec('startSftp', runCmd, {}); + await safe(shell.exec('stopSftp', 'docker stop sftp', {})); // ignore error + await safe(shell.exec('removeSftp', 'docker rm -f sftp', {})); // ignore error + await shell.exec('startSftp', runCmd, {}); } async function status() { diff --git a/src/shell.js b/src/shell.js index 78e30de52..a31388c02 100644 --- a/src/shell.js +++ b/src/shell.js @@ -8,11 +8,12 @@ const assert = require('assert'), util = require('util'); exports = module.exports = { + exec, + execArgs, + sudo, promises: { - exec: util.promisify(exec), - execArgs: util.promisify(execArgs), sudo: util.promisify(sudo) } }; @@ -20,46 +21,43 @@ exports = module.exports = { const SUDO = '/usr/bin/sudo'; // default encoding utf8, no shell, separate args -function execArgs(tag, file, args, options, callback) { +async function execArgs(tag, file, args, options) { assert.strictEqual(typeof tag, 'string'); assert.strictEqual(typeof file, 'string'); assert(Array.isArray(args)); assert.strictEqual(typeof options, 'object'); - assert.strictEqual(typeof callback, 'function'); debug(`${tag} exec: ${file}`); const execOptions = Object.assign({ encoding: 'utf8', shell: false }, options); - // https://github.com/nodejs/node/issues/25231 - const cp = child_process.execFile(file, args, execOptions, function (error, stdout, stderr) { - let e = null; - if (error) { - e = new BoxError(BoxError.SHELL_ERROR, `${tag} errored with code ${error.code} message ${error.message}`); + return new Promise((resolve, reject) => { + const cp = child_process.execFile(file, args, execOptions, function (error, stdout, stderr) { + if (!error) return resolve(stdout); + + const e = new BoxError(BoxError.SHELL_ERROR, `${tag} errored with code ${error.code} message ${error.message}`); e.stdout = stdout; // when promisified, this is the way to get stdout e.stderr = stderr; // when promisified, this is the way to get stderr - debug(`${tag}: ${file} with args ${args.join(' ')} errored`, error); + reject(e); + }); + + // https://github.com/nodejs/node/issues/25231 + if (options.input) { + cp.stdin.write(options.input); + cp.stdin.end(); } - - callback(e, stdout); }); - - if (options.input) { - cp.stdin.write(options.input); - cp.stdin.end(); - } } // default encoding utf8, shell, handles input, full command -function exec(tag, cmd, options, callback) { +async function exec(tag, cmd, options) { assert.strictEqual(typeof tag, 'string'); assert.strictEqual(typeof cmd, 'string'); assert.strictEqual(typeof options, 'object'); - assert.strictEqual(typeof callback, 'function'); const [file, ...args] = cmd.split(' '); - execArgs(tag, file, args, options, callback); + return await execArgs(tag, file, args, options); } // use this when you are afraid of how arguments will split up diff --git a/src/storage/filesystem.js b/src/storage/filesystem.js index c94cdaac9..f45c4e41c 100644 --- a/src/storage/filesystem.js +++ b/src/storage/filesystem.js @@ -187,7 +187,7 @@ async function copy(apiConfig, oldFilePath, newFilePath, progressCallback) { let cpOptions = ((apiConfig.provider !== PROVIDER_MOUNTPOINT && apiConfig.provider !== PROVIDER_CIFS) || apiConfig.preserveAttributes) ? '-a' : '-dR'; cpOptions += apiConfig.noHardlinks ? '' : 'l'; // this will hardlink backups saving space - const [copyError] = await safe(shell.promises.execArgs('copy', 'cp', [ cpOptions, oldFilePath, newFilePath ], {})); + const [copyError] = await safe(shell.execArgs('copy', 'cp', [ cpOptions, oldFilePath, newFilePath ], {})); if (copyError) throw new BoxError(BoxError.EXTERNAL_ERROR, copyError.message); } @@ -212,7 +212,7 @@ async function removeDir(apiConfig, pathPrefix, progressCallback) { progressCallback({ message: `Removing directory ${pathPrefix}` }); - const [error] = await safe(shell.promises.execArgs('removeDir', 'rm', [ '-rf', pathPrefix ], {})); + const [error] = await safe(shell.execArgs('removeDir', 'rm', [ '-rf', pathPrefix ], {})); if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, error.message); } @@ -257,7 +257,7 @@ async function testConfig(apiConfig) { } if (apiConfig.provider === PROVIDER_MOUNTPOINT) { - const [error] = await safe(shell.promises.exec('testStorageConfig', `mountpoint -q -- ${apiConfig.mountPoint}`, { timeout: 5000 })); + const [error] = await safe(shell.exec('testStorageConfig', `mountpoint -q -- ${apiConfig.mountPoint}`, { timeout: 5000 })); if (error) throw new BoxError(BoxError.BAD_FIELD, `${apiConfig.mountPoint} is not mounted: ${error.message}`); } diff --git a/src/system.js b/src/system.js index 5a402ff7d..99c31727d 100644 --- a/src/system.js +++ b/src/system.js @@ -68,7 +68,7 @@ async function hdparm(file) { } async function getSwaps() { - const [error, stdout] = await safe(shell.promises.exec('getSwaps', 'swapon --noheadings --raw --bytes --show=type,size,used,name', {})); + const [error, stdout] = await safe(shell.exec('getSwaps', 'swapon --noheadings --raw --bytes --show=type,size,used,name', {})); if (error) return {}; const swaps = {}; @@ -329,7 +329,7 @@ async function getLogs(unit, options) { } async function getBlockDevices() { - const result = await shell.promises.exec('getBlockDevices', 'lsblk --paths --json --list --fs', {}); + const result = await shell.exec('getBlockDevices', 'lsblk --paths --json --list --fs', {}); 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/test/shell-test.js b/src/test/shell-test.js index f648cbc17..af75fdb69 100644 --- a/src/test/shell-test.js +++ b/src/test/shell-test.js @@ -13,16 +13,16 @@ const BoxError = require('../boxerror.js'), describe('shell', function () { describe('execArgs', function () { it('can run valid program', async function () { - await shell.promises.execArgs('test', 'ls', [ '-l' ], {}); + await shell.execArgs('test', 'ls', [ '-l' ], {}); }); it('fails on invalid program', async function () { - const [error] = await safe(shell.promises.execArgs('test', 'randomprogram', [ ], {})); + const [error] = await safe(shell.execArgs('test', 'randomprogram', [ ], {})); expect(error.reason).to.be(BoxError.SHELL_ERROR); }); it('fails on failing program', async function () { - const [error] = await safe(shell.promises.execArgs('test', '/usr/bin/false', [ ], {})); + const [error] = await safe(shell.execArgs('test', '/usr/bin/false', [ ], {})); expect(error.reason).to.be(BoxError.SHELL_ERROR); }); }); @@ -51,27 +51,27 @@ describe('shell', function () { describe('exec', function () { it('exec throws for invalid program', async function () { - const [error] = await safe(shell.promises.exec('test', 'cannotexist', {})); + const [error] = await safe(shell.exec('test', 'cannotexist', {})); expect(error.reason).to.be(BoxError.SHELL_ERROR); }); it('exec throws for failed program', async function () { - const [error] = await safe(shell.promises.exec('test', 'false', {})); + const [error] = await safe(shell.exec('test', 'false', {})); expect(error.reason).to.be(BoxError.SHELL_ERROR); }); it('exec times out properly', async function () { - const [error] = await safe(shell.promises.exec('sleeping', 'sleep 20', { timeout: 1000 })); + const [error] = await safe(shell.exec('sleeping', '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.promises.exec('test', 'ls -l | wc -c', {})); + const [error] = await safe(shell.exec('test', 'ls -l | wc -c', {})); expect(error.reason).to.be(BoxError.SHELL_ERROR); }); it('cannot exec a shell program b', async function () { - await shell.promises.exec('test', 'ls -l | wc -c', { shell: '/bin/bash' }); + await shell.exec('test', 'ls -l | wc -c', { shell: '/bin/bash' }); }); }); }); diff --git a/src/updater.js b/src/updater.js index a4dfa1e23..e8dd05429 100644 --- a/src/updater.js +++ b/src/updater.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.promises.exec('downloadUrl', `curl -s --fail ${url} -o ${file}`, {})); + const [error] = await safe(shell.exec('downloadUrl', `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.promises.exec('gpgVerify', cmd, {})); + const [error, stdout] = await safe(shell.exec('gpgVerify', 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.promises.exec('extractTarball', `tar -zxf ${tarball} -C ${dir}`, {})); + const [error] = await safe(shell.exec('extractTarball', `tar -zxf ${tarball} -C ${dir}`, {})); if (error) throw new BoxError(BoxError.FS_ERROR, `Failed to extract release package: ${error.message}`); safe.fs.unlinkSync(tarball);