diff --git a/src/cloudron.js b/src/cloudron.js index e535962e4..b71995c03 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -16,15 +16,10 @@ exports = module.exports = { restore: restore, reboot: reboot, - checkDiskSpace: checkDiskSpace, - - readDkimPublicKeySync: readDkimPublicKeySync, - refreshDNS: refreshDNS + checkDiskSpace: checkDiskSpace }; -var appdb = require('./appdb.js'), - apps = require('./apps.js'), - assert = require('assert'), +var assert = require('assert'), async = require('async'), backups = require('./backups.js'), BackupsError = require('./backups.js').BackupsError, @@ -296,12 +291,29 @@ function configureWebadmin(callback) { }); } + function addWebadminDnsRecord(ip, domain, callback) { + assert.strictEqual(typeof ip, 'string'); + assert.strictEqual(typeof domain, 'string'); + assert.strictEqual(typeof callback, 'function'); + + if (process.env.BOX_ENV === 'test') return callback(); + + async.retry({ times: 10, interval: 20000 }, function (retryCallback) { + domains.upsertDNSRecords(config.adminLocation(), domain, 'A', [ ip ], retryCallback); + }, function (error) { + if (error) debug('addWebadminDnsRecord: done updating records with error:', error); + else debug('addWebadminDnsRecord: done'); + + callback(error); + }); + } + // update the DNS. configure nginx regardless of whether it succeeded so that // box is accessible even if dns creds are invalid sysinfo.getPublicIp(function (error, ip) { if (error) return configureNginx(error); - addDnsRecords(ip, config.fqdn(), function (error) { + addWebadminDnsRecord(ip, config.fqdn(), function (error) { if (error) return configureNginx(error); domains.waitForDNSRecord(config.adminFqdn(), config.fqdn(), ip, 'A', { interval: 30000, times: 50000 }, function (error) { @@ -474,108 +486,6 @@ function getConfig(callback) { }); } -function readDkimPublicKeySync() { - if (!config.fqdn()) { - debug('Cannot read dkim public key without a domain.', safe.error); - return null; - } - - var dkimPath = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn()); - var dkimPublicKeyFile = path.join(dkimPath, 'public'); - - var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8'); - - if (publicKey === null) { - debug('Error reading dkim public key.', safe.error); - return null; - } - - // remove header, footer and new lines - publicKey = publicKey.split('\n').slice(1, -2).join(''); - - return publicKey; -} - -// NOTE: if you change the SPF record here, be sure the wait check in mailer.js -// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be- -function txtRecordsWithSpf(callback) { - assert.strictEqual(typeof callback, 'function'); - - domains.getDNSRecords('', config.fqdn(), 'TXT', function (error, txtRecords) { - if (error) return callback(error); - - debug('txtRecordsWithSpf: current txt records - %j', txtRecords); - - var i, matches, validSpf; - - for (i = 0; i < txtRecords.length; i++) { - matches = txtRecords[i].match(/^("?v=spf1) /); // DO backend may return without quotes - if (matches === null) continue; - - // this won't work if the entry is arbitrarily "split" across quoted strings - validSpf = txtRecords[i].indexOf('a:' + config.adminFqdn()) !== -1; - break; // there can only be one SPF record - } - - if (validSpf) return callback(null, null); - - if (!matches) { // no spf record was found, create one - txtRecords.push('"v=spf1 a:' + config.adminFqdn() + ' ~all"'); - debug('txtRecordsWithSpf: adding txt record'); - } else { // just add ourself - txtRecords[i] = matches[1] + ' a:' + config.adminFqdn() + txtRecords[i].slice(matches[1].length); - debug('txtRecordsWithSpf: inserting txt record'); - } - - return callback(null, txtRecords); - }); -} - -function addDnsRecords(ip, domain, callback) { - assert.strictEqual(typeof ip, 'string'); - assert.strictEqual(typeof domain, 'string'); - callback = callback || NOOP_CALLBACK; - - if (process.env.BOX_ENV === 'test') return callback(); - - var dkimKey = readDkimPublicKeySync(); - if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('Failed to read dkim public key'))); - - var webadminRecord = { subdomain: config.adminLocation(), domain: domain, type: 'A', values: [ ip ] }; - // t=s limits the domainkey to this domain and not it's subdomains - var dkimRecord = { subdomain: config.dkimSelector() + '._domainkey', domain: domain, type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] }; - - var records = [ ]; - records.push(webadminRecord); - records.push(dkimRecord); - - debug('addDnsRecords: %j', records); - - async.retry({ times: 10, interval: 20000 }, function (retryCallback) { - txtRecordsWithSpf(function (error, txtRecords) { - if (error) return retryCallback(error); - - if (txtRecords) records.push({ subdomain: '', domain: domain, type: 'TXT', values: txtRecords }); - - debug('addDnsRecords: will update %j', records); - - async.mapSeries(records, function (record, iteratorCallback) { - domains.upsertDNSRecords(record.subdomain, record.domain, record.type, record.values, iteratorCallback); - }, function (error, changeIds) { - if (error) debug('addDnsRecords: failed to update : %s. will retry', error); - else debug('addDnsRecords: records %j added with changeIds %j', records, changeIds); - - retryCallback(error); - }); - }); - }, function (error) { - if (error) debug('addDnsRecords: done updating records with error:', error); - else debug('addDnsRecords: done'); - - callback(error); - }); -} - function restore(backupConfig, backupId, version, callback) { assert.strictEqual(typeof backupConfig, 'object'); assert.strictEqual(typeof backupId, 'string'); @@ -774,40 +684,6 @@ function checkDiskSpace(callback) { }); } -// called for dynamic dns setups where we have to update the IP -function refreshDNS(callback) { - callback = callback || NOOP_CALLBACK; - - sysinfo.getPublicIp(function (error, ip) { - if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)); - - debug('refreshDNS: current ip %s', ip); - - addDnsRecords(ip, config.fqdn(), function (error) { - if (error) return callback(error); - - debug('refreshDNS: done for system records'); - - apps.getAll(function (error, result) { - if (error) return callback(error); - - async.each(result, function (app, callback) { - // do not change state of installing apps since apptask will error if dns record already exists - if (app.installationState !== appdb.ISTATE_INSTALLED) return callback(); - - domains.upsertDNSRecords(app.location, app.domain, 'A', [ ip ], callback); - }, function (error) { - if (error) return callback(error); - - debug('refreshDNS: done for apps'); - - callback(); - }); - }); - }); - }); -} - function getLogs(options, callback) { assert(options && typeof options === 'object'); assert.strictEqual(typeof callback, 'function'); diff --git a/src/cron.js b/src/cron.js index 6d0fcbfaf..36760238d 100644 --- a/src/cron.js +++ b/src/cron.js @@ -17,6 +17,7 @@ var apps = require('./apps.js'), CronJob = require('cron').CronJob, debug = require('debug')('box:cron'), digest = require('./digest.js'), + dyndns = require('./dyndns.js'), eventlog = require('./eventlog.js'), janitor = require('./janitor.js'), scheduler = require('./scheduler.js'), @@ -78,14 +79,14 @@ function initialize(callback) { settings.events.on(settings.TIME_ZONE_KEY, recreateJobs); settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged); - settings.events.on(settings.DYNAMIC_DNS_KEY, dynamicDNSChanged); + settings.events.on(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged); settings.getAll(function (error, allSettings) { if (error) return callback(error); recreateJobs(allSettings[settings.TIME_ZONE_KEY]); autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY]); - dynamicDNSChanged(allSettings[settings.DYNAMIC_DNS_KEY]); + dynamicDnsChanged(allSettings[settings.DYNAMIC_DNS_KEY]); callback(); }); @@ -221,7 +222,7 @@ function autoupdatePatternChanged(pattern) { }); } -function dynamicDNSChanged(enabled) { +function dynamicDnsChanged(enabled) { assert.strictEqual(typeof enabled, 'boolean'); assert(gJobs.boxUpdateCheckerJob); @@ -230,7 +231,7 @@ function dynamicDNSChanged(enabled) { if (enabled) { gJobs.dynamicDNS = new CronJob({ cronTime: '00 */10 * * * *', - onTick: cloudron.refreshDNS, + onTick: dyndns.sync, start: true, timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack }); @@ -245,7 +246,7 @@ function uninitialize(callback) { settings.events.removeListener(settings.TIME_ZONE_KEY, recreateJobs); settings.events.removeListener(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged); - settings.events.removeListener(settings.DYNAMIC_DNS_KEY, dynamicDNSChanged); + settings.events.removeListener(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged); for (var job in gJobs) { if (!gJobs[job]) continue; diff --git a/src/dyndns.js b/src/dyndns.js new file mode 100644 index 000000000..88d289d4a --- /dev/null +++ b/src/dyndns.js @@ -0,0 +1,49 @@ +'use strict'; + +exports = module.exports = { + sync: sync +}; + +var appdb = require('./appdb.js'), + apps = require('./apps.js'), + async = require('async'), + config = require('./config.js'), + debug = require('debug')('box:dyndns'), + domains = require('./domains.js'), + sysinfo = require('./sysinfo.js'); + +var NOOP_CALLBACK = function (error) { if (error) debug(error); }; + +// called for dynamic dns setups where we have to update the IP +function sync(callback) { + callback = callback || NOOP_CALLBACK; + + sysinfo.getPublicIp(function (error, ip) { + if (error) return callback(error); + + debug('refreshDNS: current ip %s', ip); + + domains.upsertDNSRecords(config.adminLocation(), config.fqdn(), 'A', [ ip ], function (error) { + if (error) return callback(error); + + debug('refreshDNS: done for admin location'); + + apps.getAll(function (error, result) { + if (error) return callback(error); + + async.each(result, function (app, callback) { + // do not change state of installing apps since apptask will error if dns record already exists + if (app.installationState !== appdb.ISTATE_INSTALLED) return callback(); + + domains.upsertDNSRecords(app.location, app.domain, 'A', [ ip ], callback); + }, function (error) { + if (error) return callback(error); + + debug('refreshDNS: done for apps'); + + callback(); + }); + }); + }); + }); +} diff --git a/src/mail.js b/src/mail.js index c2613a80c..95dc4eb02 100644 --- a/src/mail.js +++ b/src/mail.js @@ -656,7 +656,6 @@ function txtRecordsWithSpf(callback) { } function addDnsRecords(domain, callback) { - assert.strictEqual(typeof ip, 'string'); assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof callback, 'function');