diff --git a/migrations/20210429194328-users-add-avatar.js b/migrations/20210429194328-users-add-avatar.js index 585764ce8..3ee11a84c 100644 --- a/migrations/20210429194328-users-add-avatar.js +++ b/migrations/20210429194328-users-add-avatar.js @@ -19,7 +19,11 @@ exports.up = function(db, callback) { const userId = filename; db.runSql('UPDATE users SET avatar=? WHERE id=?', [ avatar, userId ], iteratorCallback); - }, callback); + }, function (error) { + if (error) return callback(error); + + fs.rmdir(AVATAR_DIR, { recursive: true }, callback); + }); }); }); }; diff --git a/migrations/20210430200947-apps-add-icon.js b/migrations/20210430200947-apps-add-icon.js index 623add1db..124e4fc31 100644 --- a/migrations/20210430200947-apps-add-icon.js +++ b/migrations/20210430200947-apps-add-icon.js @@ -24,7 +24,11 @@ exports.up = function(db, callback) { } else { db.runSql('UPDATE apps SET appStoreIcon=? WHERE id=?', [ icon, appId ], iteratorCallback); } - }, next); + }, function (error) { + if (error) return next(error); + + fs.rmdir(APPICONS_DIR, { recursive: true }, next); + }); }); } ], callback); diff --git a/migrations/20210503182308-blobs-migrate-secrets.js b/migrations/20210503182308-blobs-migrate-secrets.js new file mode 100644 index 000000000..6c2295b87 --- /dev/null +++ b/migrations/20210503182308-blobs-migrate-secrets.js @@ -0,0 +1,48 @@ +'use strict'; + +const async = require('async'), + fs = require('fs'), + safe = require('safetydance'); + +const BOX_DATA_DIR = '/home/yellowtent/boxdata'; +const PLATFORM_DATA_DIR = '/home/yellowtent/platformdata'; + +exports.up = function (db, callback) { + let funcs = []; + + const acmeKey = safe.fs.readFileSync(`${BOX_DATA_DIR}/acme/acme.key`); + if (acmeKey) { + funcs.push(db.runSql.bind(db, 'INSERT INTO blobs (id, value) VALUES (?, ?)', [ 'acme_account_key', acmeKey ])); + funcs.push(fs.unlink.bind(fs.fsyncSync, `${BOX_DATA_DIR}/acme`)); + } + const dhparams = safe.fs.readFileSync(`${BOX_DATA_DIR}/dhparams.pem`); + if (dhparams) { + safe.fs.writeFileSync(`${PLATFORM_DATA_DIR}/dhparams.pem`, dhparams); + funcs.push(db.runSql.bind(db, 'INSERT INTO blobs (id, value) VALUES (?, ?)', [ 'dhparams', dhparams ])); + // leave the dhparms here for the moment because startup code regenerates box config and reloads nginx. at that point, + // nginx config of apps has not been removed yet and the reload fails. + // funcs.push(fs.unlink.bind(fs, `${BOX_DATA_DIR}/dhparams.pem`)); + } + const turnSecret = safe.fs.readFileSync(`${BOX_DATA_DIR}/addon-turn-secret`); + if (turnSecret) { + funcs.push(db.runSql.bind(db, 'INSERT INTO blobs (id, value) VALUES (?, ?)', [ 'addon_turn_secret', turnSecret ])); + funcs.push(fs.unlink.bind(fs, `${BOX_DATA_DIR}/addon-turn-secret`)); + } + + // sftp keys get moved to platformdata in start.sh + const sftpPublicKey = safe.fs.readFileSync(`${BOX_DATA_DIR}/sftp/ssh/ssh_host_rsa_key.pub`); + const sftpPrivateKey = safe.fs.readFileSync(`${BOX_DATA_DIR}/sftp/ssh/ssh_host_rsa_key`); + if (sftpPublicKey) { + safe.fs.writeFileSync(`${PLATFORM_DATA_DIR}/sftp/ssh/ssh_host_rsa_key.pub`, sftpPublicKey); + safe.fs.writeFileSync(`${PLATFORM_DATA_DIR}/sftp/ssh/ssh_host_rsa_key`, sftpPrivateKey); + funcs.push(db.runSql.bind(db, 'INSERT INTO blobs (id, value) VALUES (?, ?)', [ 'sftp_public_key', sftpPublicKey ])); + funcs.push(db.runSql.bind(db, 'INSERT INTO blobs (id, value) VALUES (?, ?)', [ 'sftp_private_key', sftpPrivateKey ])); + funcs.push(fs.rmdir.bind(fs, `${BOX_DATA_DIR}/sftp`, { recursive: true })); + } + + async.series(funcs, callback); +}; + +exports.down = function(db, callback) { + callback(); +}; diff --git a/package-lock.json b/package-lock.json index 2f038210a..fa6cb4456 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3919,9 +3919,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "safetydance": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/safetydance/-/safetydance-1.2.0.tgz", - "integrity": "sha512-JGmrvRO3PMhYawoQeKsbR1lXRz8PVp9Fq0NIZtRjY/lkVbf0Fyz/TGlkYztFi1JjWdbv7YE5z1W8HFaelCZDWQ==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/safetydance/-/safetydance-1.2.1.tgz", + "integrity": "sha512-Y0hli/NfjYsmpLMHr7dBhBFQ9Isn7cWZPRnL5/FyEifq5ZwYdu6Xgur//vRyRkyd3M7Jq1cE0QhgCa9EyQm79g==" }, "sass-graph": { "version": "2.2.5", diff --git a/package.json b/package.json index 1bbac3790..463527649 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "request": "^2.88.2", "rimraf": "^3.0.2", "s3-block-read-stream": "^0.5.0", - "safetydance": "^1.2.0", + "safetydance": "^1.2.1", "semver": "^7.3.5", "showdown": "^1.9.1", "speakeasy": "^2.0.0", diff --git a/runTests b/runTests index 7b07fed74..8b86a1675 100755 --- a/runTests +++ b/runTests @@ -22,8 +22,8 @@ fi mkdir -p ${DATA_DIR} cd ${DATA_DIR} mkdir -p appsdata -mkdir -p boxdata/appicons boxdata/mail boxdata/certs boxdata/mail/dkim/localhost boxdata/mail/dkim/foobar.com boxdata/sftp/ssh boxdata/firewall -mkdir -p platformdata/addons/mail/banner platformdata/nginx/cert platformdata/nginx/applications platformdata/collectd/collectd.conf.d platformdata/addons platformdata/logrotate.d platformdata/backup platformdata/logs/tasks +mkdir -p boxdata/mail boxdata/certs boxdata/mail/dkim/localhost boxdata/mail/dkim/foobar.com boxdata/firewall +mkdir -p platformdata/addons/mail/banner platformdata/nginx/cert platformdata/nginx/applications platformdata/collectd/collectd.conf.d platformdata/addons platformdata/logrotate.d platformdata/backup platformdata/logs/tasks platformdata/sftp/ssh sudo mkdir -p /mnt/cloudron-test-music /media/cloudron-test-music # volume test # translations @@ -34,9 +34,6 @@ cp -r ${source_dir}/../dashboard/dist/translation/* box/dashboard/dist/translati echo "=> Generating a localhost selfsigned cert" openssl req -x509 -newkey rsa:2048 -keyout platformdata/nginx/cert/host.key -out platformdata/nginx/cert/host.cert -days 3650 -subj '/CN=localhost' -nodes -config <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.localhost")) -# generate legacy key format for sftp -ssh-keygen -m PEM -t rsa -f boxdata/sftp/ssh/ssh_host_rsa_key -q -N "" - # clear out any containers if FAST is unset if [[ -z ${FAST+x} ]]; then echo "=> Delete all docker containers first" diff --git a/setup/start.sh b/setup/start.sh index 75c11a8fa..5360ddfc4 100755 --- a/setup/start.sh +++ b/setup/start.sh @@ -61,13 +61,11 @@ mkdir -p "${PLATFORM_DATA_DIR}/logs/backup" \ "${PLATFORM_DATA_DIR}/logs/crash" \ "${PLATFORM_DATA_DIR}/logs/collectd" mkdir -p "${PLATFORM_DATA_DIR}/update" +mkdir -p "${PLATFORM_DATA_DIR}/sftp/ssh" # sftp keys -mkdir -p "${BOX_DATA_DIR}/appicons" mkdir -p "${BOX_DATA_DIR}/firewall" mkdir -p "${BOX_DATA_DIR}/certs" -mkdir -p "${BOX_DATA_DIR}/acme" # acme keys mkdir -p "${BOX_DATA_DIR}/mail/dkim" -mkdir -p "${BOX_DATA_DIR}/sftp/ssh" # sftp keys # ensure backups folder exists and is writeable mkdir -p /var/backups @@ -224,24 +222,6 @@ fi rm -f /etc/cloudron/cloudron.conf -if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then - log "Generating dhparams (takes forever)" - openssl dhparam -out "${BOX_DATA_DIR}/dhparams.pem" 2048 - cp "${BOX_DATA_DIR}/dhparams.pem" "${PLATFORM_DATA_DIR}/addons/mail/dhparams.pem" -else - cp "${BOX_DATA_DIR}/dhparams.pem" "${PLATFORM_DATA_DIR}/addons/mail/dhparams.pem" -fi - -if [[ ! -f "${BOX_DATA_DIR}/sftp/ssh/ssh_host_rsa_key" ]]; then - # the key format in Ubuntu 20 changed, so we create keys in legacy format. for older ubuntu, just re-use the host keys - # see https://github.com/proftpd/proftpd/issues/793 - if [[ "${ubuntu_version}" == "20.04" ]]; then - ssh-keygen -m PEM -t rsa -f "${BOX_DATA_DIR}/sftp/ssh/ssh_host_rsa_key" -q -N "" - else - cp /etc/ssh/ssh_host_rsa_key* ${BOX_DATA_DIR}/sftp/ssh - fi -fi - log "Changing ownership" # be careful of what is chown'ed here. subdirs like mysql,redis etc are owned by the containers and will stop working if perms change chown -R "${USER}" /etc/cloudron diff --git a/src/blobs.js b/src/blobs.js index 18751e0b1..4d5fafd25 100644 --- a/src/blobs.js +++ b/src/blobs.js @@ -5,11 +5,26 @@ exports = module.exports = { get, set, + + initSecrets, + + ACME_ACCOUNT_KEY: 'acme_account_key', + ADDON_TURN_SECRET: 'addon_turn_secret', + DHPARAMS: 'dhparams', + SFTP_PUBLIC_KEY: 'sftp_public_key', + SFTP_PRIVATE_KEY: 'sftp_private_key', + _clear: clear }; const assert = require('assert'), - database = require('./database.js'); + BoxError = require('./boxerror.js'), + constants = require('./constants.js'), + crypto = require('crypto'), + database = require('./database.js'), + debug = require('debug')('box:blobs'), + paths = require('./paths.js'), + safe = require('safetydance'); const BLOBS_FIELDS = [ 'id', 'value' ].join(','); @@ -31,3 +46,41 @@ async function set(id, value) { async function clear() { await database.query('DELETE FROM blobs'); } + +async function initSecrets() { + let value = await get(exports.ACME_ACCOUNT_KEY); + if (!value) { + const accountKeyPem = safe.child_process.execSync('openssl genrsa 4096'); + if (!accountKeyPem) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error); + await set(exports.ACME_ACCOUNT_KEY, accountKeyPem); + } + + value = await get(exports.ADDON_TURN_SECRET); + if (!value) { + const secret = 'a' + crypto.randomBytes(15).toString('hex'); // prefix with a to ensure string starts with a letter + await set(exports.ADDON_TURN_SECRET, Buffer.from(secret)); + } + + value = await get(exports.DHPARAMS); + if (!constants.TEST && !value) { + debug('initSecrets: generating dhparams.pem. this takes forever'); + const dhparams = safe.child_process.execSync('openssl dhparam 2048'); + if (!dhparams) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error); + if (!safe.fs.writeFileSync(paths.DHPARAMS_FILE, dhparams)) throw new BoxError(BoxError.FS_ERROR, `Could not save dhparams.pem: ${safe.error.message}`); + await set(exports.DHPARAMS, dhparams); + } + + value = await get(exports.SFTP_PRIVATE_KEY); + if (!value) { + debug('initSecrets: generate sftp keys'); + if (constants.TEST) { + safe.fs.unlinkSync(`${paths.SFTP_KEYS_DIR}/ssh_host_rsa_key.pub`); + safe.fs.unlinkSync(`${paths.SFTP_KEYS_DIR}/ssh_host_rsa_key`); + } + if (!safe.child_process.execSync(`ssh-keygen -m PEM -t rsa -f "${paths.SFTP_KEYS_DIR}/ssh_host_rsa_key" -q -N ""`)) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error); + const publicKey = safe.fs.readFileSync(`${paths.SFTP_KEYS_DIR}/ssh_host_rsa_key.pub`); + await set(exports.SFTP_PUBLIC_KEY, publicKey); + const privateKey = safe.fs.readFileSync(`${paths.SFTP_KEYS_DIR}/ssh_host_rsa_key`); + await set(exports.SFTP_PRIVATE_KEY, privateKey); + } +} diff --git a/src/cert/acme2.js b/src/cert/acme2.js index 27f3a0d1a..5a598862d 100644 --- a/src/cert/acme2.js +++ b/src/cert/acme2.js @@ -11,7 +11,6 @@ var assert = require('assert'), paths = require('../paths.js'), request = require('request'), safe = require('safetydance'), - util = require('util'), _ = require('underscore'); const CA_PROD_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory', @@ -32,7 +31,7 @@ exports = module.exports = { function Acme2(options) { assert.strictEqual(typeof options, 'object'); - this.accountKeyPem = null; // Buffer + this.accountKeyPem = options.accountKeyPem; // Buffer this.email = options.email; this.keyId = null; this.caDirectory = options.prod ? CA_PROD_DIRECTORY_URL : CA_STAGING_DIRECTORY_URL; @@ -531,17 +530,6 @@ Acme2.prototype.acmeFlow = function (hostname, domain, callback) { assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof callback, 'function'); - if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) { - debug('acmeFlow: generating acme account key on first run'); - this.accountKeyPem = safe.child_process.execSync('openssl genrsa 4096'); - if (!this.accountKeyPem) return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error)); - - safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, this.accountKeyPem); - } else { - debug('acmeFlow: using existing acme account key'); - this.accountKeyPem = fs.readFileSync(paths.ACME_ACCOUNT_KEY_FILE); - } - var that = this; this.registerUser(function (error) { if (error) return callback(error); diff --git a/src/cloudron.js b/src/cloudron.js index 884bb008f..c82e907fe 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -77,6 +77,8 @@ function onActivated(options, callback) { assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); + debug('onActivated: running post activation tasks'); + // Starting the platform after a user is available means: // 1. mail bounces can now be sent to the cloudron owner // 2. the restore code path can run without sudo (since mail/ is non-root) diff --git a/src/infra_version.js b/src/infra_version.js index 6d6c07fcd..0a5acd5e8 100644 --- a/src/infra_version.js +++ b/src/infra_version.js @@ -6,7 +6,7 @@ exports = module.exports = { // a version change recreates all containers with latest docker config - 'version': '48.19.0', + 'version': '48.20.0', 'baseImages': [ { repo: 'cloudron/base', tag: 'cloudron/base:3.0.0@sha256:455c70428723e3a823198c57472785437eb6eab082e79b3ff04ea584faf46e92' } diff --git a/src/mail.js b/src/mail.js index e9f855fc4..5f0d2de72 100644 --- a/src/mail.js +++ b/src/mail.js @@ -646,10 +646,11 @@ function configureMail(mailFqdn, mailDomain, serviceConfig, callback) { reverseProxy.getCertificate(mailFqdn, mailDomain, function (error, bundle) { if (error) return callback(error); - // the setup script copies dhparams.pem to /addons/mail + const dhparamsFilePath = path.join(paths.ADDON_CONFIG_DIR, 'mail/dhparams.pem'); const mailCertFilePath = path.join(paths.ADDON_CONFIG_DIR, 'mail/tls_cert.pem'); const mailKeyFilePath = path.join(paths.ADDON_CONFIG_DIR, 'mail/tls_key.pem'); + if (!safe.child_process.execSync(`cp ${paths.DHPARAMS_FILE} ${dhparamsFilePath}`)) return callback(new BoxError(BoxError.FS_ERROR, 'Could not copy dhparams:' + safe.error.message)); if (!safe.child_process.execSync(`cp ${bundle.certFilePath} ${mailCertFilePath}`)) return callback(new BoxError(BoxError.FS_ERROR, 'Could not create cert file:' + safe.error.message)); if (!safe.child_process.execSync(`cp ${bundle.keyFilePath} ${mailKeyFilePath}`)) return callback(new BoxError(BoxError.FS_ERROR, 'Could not create key file:' + safe.error.message)); diff --git a/src/nginxconfig.ejs b/src/nginxconfig.ejs index 0eadf3cc8..eb2958e7f 100644 --- a/src/nginxconfig.ejs +++ b/src/nginxconfig.ejs @@ -87,7 +87,7 @@ server { ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256; ssl_prefer_server_ciphers off; - ssl_dhparam /home/yellowtent/boxdata/dhparams.pem; + ssl_dhparam /home/yellowtent/platformdata/dhparams.pem; add_header Strict-Transport-Security "max-age=63072000"; <% if ( ocsp ) { -%> diff --git a/src/paths.js b/src/paths.js index fcd3d4c0e..56a7dc6d7 100644 --- a/src/paths.js +++ b/src/paths.js @@ -37,16 +37,15 @@ exports = module.exports = { UPDATE_CHECKER_FILE: path.join(baseDir(), 'platformdata/update/updatechecker.json'), SNAPSHOT_INFO_FILE: path.join(baseDir(), 'platformdata/backup/snapshot-info.json'), DYNDNS_INFO_FILE: path.join(baseDir(), 'platformdata/dyndns-info.json'), + DHPARAMS_FILE: path.join(baseDir(), 'platformdata/dhparams.pem'), FEATURES_INFO_FILE: path.join(baseDir(), 'platformdata/features-info.json'), PROXY_AUTH_TOKEN_SECRET_FILE: path.join(baseDir(), 'platformdata/proxy-auth-token-secret'), VERSION_FILE: path.join(baseDir(), 'platformdata/VERSION'), + SFTP_KEYS_DIR: path.join(baseDir(), 'platformdata/sftp/ssh'), // this is not part of appdata because an icon may be set before install MAIL_DATA_DIR: path.join(baseDir(), 'boxdata/mail'), - SFTP_KEYS_DIR: path.join(baseDir(), 'boxdata/sftp/ssh'), - ACME_ACCOUNT_KEY_FILE: path.join(baseDir(), 'boxdata/acme/acme.key'), APP_CERTS_DIR: path.join(baseDir(), 'boxdata/certs'), - ADDON_TURN_SECRET_FILE: path.join(baseDir(), 'boxdata/addon-turn-secret'), FIREWALL_BLOCKLIST_FILE: path.join(baseDir(), 'boxdata/firewall/blocklist.txt'), LOG_DIR: path.join(baseDir(), 'platformdata/logs'), diff --git a/src/reverseproxy.js b/src/reverseproxy.js index fe323754d..04fa9cd35 100644 --- a/src/reverseproxy.js +++ b/src/reverseproxy.js @@ -30,10 +30,11 @@ exports = module.exports = { _getAcmeApi: getAcmeApi }; -var acme2 = require('./cert/acme2.js'), +const acme2 = require('./cert/acme2.js'), apps = require('./apps.js'), assert = require('assert'), async = require('async'), + blobs = require('./blobs.js'), BoxError = require('./boxerror.js'), constants = require('./constants.js'), crypto = require('crypto'), @@ -52,7 +53,8 @@ var acme2 = require('./cert/acme2.js'), shell = require('./shell.js'), sysinfo = require('./sysinfo.js'), users = require('./users.js'), - util = require('util'); + util = require('util'), + _ = require('underscore'); const NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/nginxconfig.ejs', { encoding: 'utf8' }); const RESTART_SERVICE_CMD = path.join(__dirname, 'scripts/restartservice.sh'); @@ -83,7 +85,15 @@ function getAcmeApi(domainObject, callback) { users.getOwner(function (error, owner) { options.email = error ? 'webmaster@cloudron.io' : owner.email; // can error if not activated yet - callback(null, api, options); + const blobGet = util.callbackify(blobs.get); + blobGet(blobs.ACME_ACCOUNT_KEY, function (error, accountKeyPem) { + if (error) return callback(error); + if (!accountKeyPem) return callback(new BoxError(BoxError.NOT_FOUND, 'acme account key not found')); + + options.accountKeyPem = accountKeyPem; + + callback(null, api, options); + }); }); } @@ -356,7 +366,7 @@ function ensureCertificate(vhost, domain, auditSource, callback) { debug(`ensureCertificate: ${vhost} cert does not exist`); } - debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions); + debug('ensureCertificate: getting certificate for %s with options %j', vhost, _.omit(apiOptions, 'accountKeyPem')); acmeApi.getCertificate(vhost, domain, apiOptions, function (error, certFilePath, keyFilePath) { debug(`ensureCertificate: error: ${error ? error.message : 'null'} cert: ${certFilePath || 'null'}`); diff --git a/src/server.js b/src/server.js index 1132da2ef..abf9bd1b0 100644 --- a/src/server.js +++ b/src/server.js @@ -7,6 +7,7 @@ exports = module.exports = { let assert = require('assert'), async = require('async'), + blobs = require('./blobs.js'), cloudron = require('./cloudron.js'), constants = require('./constants.js'), database = require('./database.js'), @@ -18,6 +19,7 @@ let assert = require('assert'), routes = require('./routes/index.js'), settings = require('./settings.js'), users = require('./users.js'), + util = require('util'), ws = require('ws'); let gHttpServer = null; @@ -367,6 +369,7 @@ function start(callback) { async.series([ database.initialize, settings.initCache, // pre-load very often used settings + blobs.initSecrets, cloudron.initialize, gHttpServer.listen.bind(gHttpServer, constants.PORT, '127.0.0.1'), eventlog.add.bind(null, eventlog.ACTION_START, { userId: null, username: 'boot' }, { version: constants.VERSION }) diff --git a/src/services.js b/src/services.js index 97acdfde9..bc07d9d0b 100644 --- a/src/services.js +++ b/src/services.js @@ -31,10 +31,11 @@ exports = module.exports = { SERVICE_STATUS_STOPPED: 'stopped' }; -var appdb = require('./appdb.js'), +const appdb = require('./appdb.js'), apps = require('./apps.js'), assert = require('assert'), async = require('async'), + blobs = require('./blobs.js'), BoxError = require('./boxerror.js'), constants = require('./constants.js'), crypto = require('crypto'), @@ -958,22 +959,25 @@ function setupTurn(app, options, callback) { assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); - var turnSecret = safe.fs.readFileSync(paths.ADDON_TURN_SECRET_FILE, 'utf8'); - if (!turnSecret) debug('setupTurn: no turn secret set. Will leave emtpy, but this is a problem!'); + const blobGet = util.callbackify(blobs.get); + blobGet(blobs.ADDON_TURN_SECRET, function (error, turnSecret) { + if (error) return callback(error); + if (!turnSecret) return callback(new BoxError(BoxError.ADDONS_ERROR, 'Turn secret is missing')); - const env = [ - { name: 'CLOUDRON_STUN_SERVER', value: settings.adminFqdn() }, - { name: 'CLOUDRON_STUN_PORT', value: '3478' }, - { name: 'CLOUDRON_STUN_TLS_PORT', value: '5349' }, - { name: 'CLOUDRON_TURN_SERVER', value: settings.adminFqdn() }, - { name: 'CLOUDRON_TURN_PORT', value: '3478' }, - { name: 'CLOUDRON_TURN_TLS_PORT', value: '5349' }, - { name: 'CLOUDRON_TURN_SECRET', value: turnSecret } - ]; + const env = [ + { name: 'CLOUDRON_STUN_SERVER', value: settings.adminFqdn() }, + { name: 'CLOUDRON_STUN_PORT', value: '3478' }, + { name: 'CLOUDRON_STUN_TLS_PORT', value: '5349' }, + { name: 'CLOUDRON_TURN_SERVER', value: settings.adminFqdn() }, + { name: 'CLOUDRON_TURN_PORT', value: '3478' }, + { name: 'CLOUDRON_TURN_TLS_PORT', value: '5349' }, + { name: 'CLOUDRON_TURN_SECRET', value: turnSecret } + ]; - debugApp(app, 'Setting up TURN'); + debugApp(app, 'Setting up TURN'); - appdb.setAddonConfig(app.id, 'turn', env, callback); + appdb.setAddonConfig(app.id, 'turn', env, callback); + }); } function teardownTurn(app, options, callback) { @@ -1557,40 +1561,39 @@ function startTurn(existingInfra, serviceConfig, callback) { assert.strictEqual(typeof serviceConfig, 'object'); assert.strictEqual(typeof callback, 'function'); - // get and ensure we have a turn secret - var turnSecret = safe.fs.readFileSync(paths.ADDON_TURN_SECRET_FILE, 'utf8'); - if (!turnSecret) { - turnSecret = 'a' + crypto.randomBytes(15).toString('hex'); // prefix with a to ensure string starts with a letter - safe.fs.writeFileSync(paths.ADDON_TURN_SECRET_FILE, turnSecret, 'utf8'); - } - const tag = infra.images.turn.tag; const memoryLimit = serviceConfig.memoryLimit || SERVICES['turn'].defaultMemoryLimit; const memory = system.getMemoryAllocation(memoryLimit); const realm = settings.adminFqdn(); - // this exports 3478/tcp, 5349/tls and 50000-51000/udp. note that this runs on the host network! - const cmd = `docker run --restart=always -d --name="turn" \ - --hostname turn \ - --net host \ - --log-driver syslog \ - --log-opt syslog-address=udp://127.0.0.1:2514 \ - --log-opt syslog-format=rfc5424 \ - --log-opt tag=turn \ - -m ${memory} \ - --memory-swap ${memoryLimit} \ - --dns 172.18.0.1 \ - --dns-search=. \ - -e CLOUDRON_TURN_SECRET="${turnSecret}" \ - -e CLOUDRON_REALM="${realm}" \ - --label isCloudronManaged=true \ - --read-only -v /tmp -v /run "${tag}"`; + const blobGet = util.callbackify(blobs.get); + blobGet(blobs.ADDON_TURN_SECRET, function (error, turnSecret) { + if (error) return callback(error); + if (!turnSecret) return callback(new BoxError(BoxError.ADDONS_ERROR, 'Turn secret is missing')); - async.series([ - shell.exec.bind(null, 'stopTurn', 'docker stop turn || true'), - shell.exec.bind(null, 'removeTurn', 'docker rm -f turn || true'), - shell.exec.bind(null, 'startTurn', cmd) - ], callback); + // this exports 3478/tcp, 5349/tls and 50000-51000/udp. note that this runs on the host network! + const cmd = `docker run --restart=always -d --name="turn" \ + --hostname turn \ + --net host \ + --log-driver syslog \ + --log-opt syslog-address=udp://127.0.0.1:2514 \ + --log-opt syslog-format=rfc5424 \ + --log-opt tag=turn \ + -m ${memory} \ + --memory-swap ${memoryLimit} \ + --dns 172.18.0.1 \ + --dns-search=. \ + -e CLOUDRON_TURN_SECRET="${turnSecret}" \ + -e CLOUDRON_REALM="${realm}" \ + --label isCloudronManaged=true \ + --read-only -v /tmp -v /run "${tag}"`; + + async.series([ + shell.exec.bind(null, 'stopTurn', 'docker stop turn || true'), + shell.exec.bind(null, 'removeTurn', 'docker rm -f turn || true'), + shell.exec.bind(null, 'startTurn', cmd) + ], callback); + }); } function startMongodb(existingInfra, callback) { diff --git a/src/test/apptask-test.js b/src/test/apptask-test.js index a80314506..f0ac6da32 100644 --- a/src/test/apptask-test.js +++ b/src/test/apptask-test.js @@ -9,6 +9,7 @@ var appdb = require('../appdb.js'), apps = require('../apps.js'), apptask = require('../apptask.js'), async = require('async'), + blobs = require('../blobs.js'), database = require('../database.js'), domains = require('../domains.js'), expect = require('expect.js'), @@ -99,6 +100,7 @@ describe('apptask', function () { async.series([ database.initialize, database._clear, + blobs.initSecrets, settings.setAdminLocation.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain), domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE), userdb.add.bind(null, ADMIN.id, ADMIN), diff --git a/src/test/reverseproxy-test.js b/src/test/reverseproxy-test.js index 39d65f433..d3f54a58e 100644 --- a/src/test/reverseproxy-test.js +++ b/src/test/reverseproxy-test.js @@ -5,7 +5,8 @@ 'use strict'; -var async = require('async'), +const async = require('async'), + blobs = require('../blobs.js'), database = require('../database.js'), domains = require('../domains.js'), expect = require('expect.js'), @@ -28,6 +29,7 @@ function setup(done) { async.series([ database.initialize, database._clear, + blobs.initSecrets, settings.setAdminLocation.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain), domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE), settings.initCache