diff --git a/src/acme2.js b/src/acme2.js index badb0537e..e8ca40acd 100644 --- a/src/acme2.js +++ b/src/acme2.js @@ -13,6 +13,7 @@ const assert = require('assert'), BoxError = require('./boxerror.js'), crypto = require('crypto'), debug = require('debug')('box:cert/acme2'), + dns = require('./dns.js'), domains = require('./domains.js'), fs = require('fs'), os = require('os'), @@ -400,10 +401,10 @@ Acme2.prototype.prepareDnsChallenge = async function (hostname, domain, authoriz debug(`prepareDnsChallenge: update ${challengeSubdomain} with ${txtValue}`); return new Promise((resolve, reject) => { - domains.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) { + dns.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) { if (error) return reject(error); - domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { times: 200 }, function (error) { + dns.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { times: 200 }, function (error) { if (error) return reject(error); resolve(challenge); @@ -427,7 +428,7 @@ Acme2.prototype.cleanupDnsChallenge = async function (hostname, domain, challeng debug(`cleanupDnsChallenge: remove ${challengeSubdomain} with ${txtValue}`); return new Promise((resolve, reject) => { - domains.removeDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) { + dns.removeDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) { if (error) return reject(error); resolve(null); @@ -522,7 +523,7 @@ Acme2.prototype.getCertificate = async function (vhost, domain, paths) { debug(`getCertificate: start acme flow for ${vhost} from ${this.caDirectory}`); if (vhost !== domain && this.wildcard) { // bare domain is not part of wildcard SAN - vhost = domains.makeWildcard(vhost); + vhost = dns.makeWildcard(vhost); debug(`getCertificate: will get wildcard cert for ${vhost}`); } diff --git a/src/apps.js b/src/apps.js index 2a2fc46ea..883aaa7c1 100644 --- a/src/apps.js +++ b/src/apps.js @@ -122,8 +122,8 @@ const appdb = require('./appdb.js'), constants = require('./constants.js'), database = require('./database.js'), debug = require('debug')('box:apps'), + dns = require('./dns.js'), docker = require('./docker.js'), - domaindb = require('./domaindb.js'), domains = require('./domains.js'), eventlog = require('./eventlog.js'), fs = require('fs'), @@ -150,6 +150,7 @@ const appdb = require('./appdb.js'), _ = require('underscore'); const NOOP_CALLBACK = function (error) { if (error) debug(error); }; +const domainsList = util.callbackify(domains.list); // validate the port bindings function validatePortBindings(portBindings, manifest) { @@ -390,7 +391,7 @@ function getDuplicateErrorDetails(errorMessage, locations, domainObjectMap, port const { subdomain, domain } = locations[i]; if (match[1] !== `${subdomain}-${domain}`) continue; - return new BoxError(BoxError.ALREADY_EXISTS, `Domain '${domains.fqdn(subdomain, domainObjectMap[domain])}' is in use`, { subdomain, domain }); + return new BoxError(BoxError.ALREADY_EXISTS, `Domain '${dns.fqdn(subdomain, domainObjectMap[domain])}' is in use`, { subdomain, domain }); } } @@ -465,9 +466,9 @@ function postProcess(app, domainObjectMap) { } app.portBindings = result; app.iconUrl = app.hasIcon || app.hasAppStoreIcon ? `/api/v1/apps/${app.id}/icon` : null; - app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]); - app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); - app.aliasDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); + app.fqdn = dns.fqdn(app.location, domainObjectMap[app.domain]); + app.alternateDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); + app.aliasDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); } function hasAccessTo(app, user, callback) { @@ -492,7 +493,7 @@ function hasAccessTo(app, user, callback) { function getDomainObjectMap(callback) { assert.strictEqual(typeof callback, 'function'); - domaindb.getAll(function (error, domainObjects) { + domainsList(function (error, domainObjects) { if (error) return callback(error); let domainObjectMap = {}; @@ -713,7 +714,7 @@ function validateLocations(locations, callback) { subdomain = subdomain.replace(/^\*\./, ''); // remove *. } - error = domains.validateHostname(subdomain, domainObjectMap[location.domain]); + error = dns.validateHostname(subdomain, domainObjectMap[location.domain]); if (error) return callback(new BoxError(BoxError.BAD_FIELD, 'Bad location: ' + error.message, { field: 'location', domain: location.domain, subdomain: location.subdomain })); } @@ -841,9 +842,9 @@ function install(data, auditSource, callback) { if (error) return callback(error); const newApp = _.extend({}, _.omit(data, 'icon'), { appStoreId, manifest, location, domain, portBindings }); - newApp.fqdn = domains.fqdn(newApp.location, domainObjectMap[newApp.domain]); - newApp.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); - newApp.aliasDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); + newApp.fqdn = dns.fqdn(newApp.location, domainObjectMap[newApp.domain]); + newApp.alternateDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); + newApp.aliasDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId, app: newApp, taskId: result.taskId }); @@ -1250,9 +1251,9 @@ function setLocation(app, data, auditSource, callback) { if (error && error.reason === BoxError.ALREADY_EXISTS) error = getDuplicateErrorDetails(error.message, locations, domainObjectMap, data.portBindings); if (error) return callback(error); - values.fqdn = domains.fqdn(values.location, domainObjectMap[values.domain]); - values.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); - values.aliasDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); + values.fqdn = dns.fqdn(values.location, domainObjectMap[values.domain]); + values.alternateDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); + values.aliasDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, _.extend({ appId, app, taskId: result.taskId }, values)); @@ -1723,9 +1724,9 @@ function clone(app, data, user, auditSource, callback) { if (error) return callback(error); const newApp = _.extend({}, _.omit(data, 'icon'), { appStoreId, manifest, location, domain, portBindings }); - newApp.fqdn = domains.fqdn(newApp.location, domainObjectMap[newApp.domain]); - newApp.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); - newApp.aliasDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); + newApp.fqdn = dns.fqdn(newApp.location, domainObjectMap[newApp.domain]); + newApp.alternateDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); + newApp.aliasDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, oldApp: app, newApp: newApp, taskId: result.taskId }); diff --git a/src/apptask.js b/src/apptask.js index 47e7f6100..6d980cdae 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -22,6 +22,7 @@ const appdb = require('./appdb.js'), constants = require('./constants.js'), debug = require('debug')('box:apptask'), df = require('@sindresorhus/df'), + dns = require('./dns.js'), docker = require('./docker.js'), domains = require('./domains.js'), ejs = require('ejs'), @@ -319,12 +320,12 @@ function waitForDnsPropagation(app, callback) { sysinfo.getServerIp(function (error, ip) { if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Error getting public IP: ${error.message}`)); - domains.waitForDnsRecord(app.location, app.domain, 'A', ip, { times: 240 }, function (error) { + dns.waitForDnsRecord(app.location, app.domain, 'A', ip, { times: 240 }, function (error) { if (error) return callback(new BoxError(BoxError.DNS_ERROR, `DNS Record is not synced yet: ${error.message}`, { ip: ip, subdomain: app.location, domain: app.domain })); // now wait for alternateDomains and aliasDomains, if any async.eachSeries(app.alternateDomains.concat(app.aliasDomains), function (domain, iteratorCallback) { - domains.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ip, { times: 240 }, function (error) { + dns.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ip, { times: 240 }, function (error) { if (error) return callback(new BoxError(BoxError.DNS_ERROR, `DNS Record is not synced yet: ${error.message}`, { ip: ip, subdomain: domain.subdomain, domain: domain.domain })); iteratorCallback(); @@ -438,7 +439,7 @@ function install(app, args, progressCallback, callback) { async.series([ progressCallback.bind(null, { percent: 30, message: 'Registering subdomains' }), - domains.registerLocations.bind(null, [ { subdomain: app.location, domain: app.domain }].concat(app.alternateDomains).concat(app.aliasDomains), { overwriteDns }, progressCallback) + dns.registerLocations.bind(null, [ { subdomain: app.location, domain: app.domain }].concat(app.alternateDomains).concat(app.aliasDomains), { overwriteDns }, progressCallback) ], done); }, @@ -589,7 +590,7 @@ function changeLocation(app, args, progressCallback, callback) { if (obsoleteDomains.length === 0) return next(); - domains.unregisterLocations(obsoleteDomains, progressCallback, next); + dns.unregisterLocations(obsoleteDomains, progressCallback, next); }, function setupDnsIfNeeded(done) { @@ -597,7 +598,7 @@ function changeLocation(app, args, progressCallback, callback) { async.series([ progressCallback.bind(null, { percent: 30, message: 'Registering subdomains' }), - domains.registerLocations.bind(null, [ { subdomain: app.location, domain: app.domain }].concat(app.alternateDomains).concat(app.aliasDomains), { overwriteDns }, progressCallback) + dns.registerLocations.bind(null, [ { subdomain: app.location, domain: app.domain }].concat(app.alternateDomains).concat(app.aliasDomains), { overwriteDns }, progressCallback) ], done); }, @@ -931,7 +932,7 @@ function uninstall(app, args, progressCallback, callback) { docker.deleteImage.bind(null, app.manifest), progressCallback.bind(null, { percent: 70, message: 'Unregistering domains' }), - domains.unregisterLocations.bind(null, [ { subdomain: app.location, domain: app.domain } ].concat(app.alternateDomains).concat(app.aliasDomains), progressCallback), + dns.unregisterLocations.bind(null, [ { subdomain: app.location, domain: app.domain } ].concat(app.alternateDomains).concat(app.aliasDomains), progressCallback), progressCallback.bind(null, { percent: 90, message: 'Cleanup logs' }), cleanupLogs.bind(null, app), diff --git a/src/cloudron.js b/src/cloudron.js index 20c187562..153bd5282 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -33,6 +33,7 @@ const apps = require('./apps.js'), constants = require('./constants.js'), cron = require('./cron.js'), debug = require('debug')('box:cloudron'), + dns = require('./dns.js'), domains = require('./domains.js'), eventlog = require('./eventlog.js'), fs = require('fs'), @@ -55,6 +56,8 @@ const apps = require('./apps.js'), const REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'); +const domainsGet = util.callbackify(domains.get); + const NOOP_CALLBACK = function (error) { if (error) debug(error); }; async function initialize() { @@ -282,10 +285,10 @@ function prepareDashboardDomain(domain, auditSource, callback) { if (settings.isDemo()) return callback(new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode')); - domains.get(domain, function (error, domainObject) { + domainsGet(domain, function (error, domainObject) { if (error) return callback(error); - const fqdn = domains.fqdn(constants.DASHBOARD_LOCATION, domainObject); + const fqdn = dns.fqdn(constants.DASHBOARD_LOCATION, domainObject); apps.getAll(async function (error, result) { if (error) return callback(error); @@ -311,13 +314,13 @@ function setDashboardDomain(domain, auditSource, callback) { debug(`setDashboardDomain: ${domain}`); - domains.get(domain, function (error, domainObject) { + domainsGet(domain, function (error, domainObject) { if (error) return callback(error); reverseProxy.writeDashboardConfig(domain, function (error) { if (error) return callback(error); - const fqdn = domains.fqdn(constants.DASHBOARD_LOCATION, domainObject); + const fqdn = dns.fqdn(constants.DASHBOARD_LOCATION, domainObject); settings.setDashboardLocation(domain, fqdn, function (error) { if (error) return callback(error); @@ -367,21 +370,21 @@ function setupDnsAndCert(subdomain, domain, auditSource, progressCallback, callb assert.strictEqual(typeof progressCallback, 'function'); assert.strictEqual(typeof callback, 'function'); - domains.get(domain, function (error, domainObject) { + domainsGet(domain, function (error, domainObject) { if (error) return callback(error); - const dashboardFqdn = domains.fqdn(subdomain, domainObject); + const dashboardFqdn = dns.fqdn(subdomain, domainObject); sysinfo.getServerIp(function (error, ip) { if (error) return callback(error); async.series([ (done) => { progressCallback({ message: `Updating DNS of ${dashboardFqdn}` }); done(); }, - domains.upsertDnsRecords.bind(null, subdomain, domain, 'A', [ ip ]), + dns.upsertDnsRecords.bind(null, subdomain, domain, 'A', [ ip ]), (done) => { progressCallback({ message: `Waiting for DNS of ${dashboardFqdn}` }); done(); }, - domains.waitForDnsRecord.bind(null, subdomain, domain, 'A', ip, { interval: 30000, times: 50000 }), + dns.waitForDnsRecord.bind(null, subdomain, domain, 'A', ip, { interval: 30000, times: 50000 }), (done) => { progressCallback({ message: `Getting certificate of ${dashboardFqdn}` }); done(); }, - reverseProxy.ensureCertificate.bind(null, domains.fqdn(subdomain, domainObject), domain, auditSource) + reverseProxy.ensureCertificate.bind(null, dns.fqdn(subdomain, domainObject), domain, auditSource) ], function (error) { if (error) return callback(error); diff --git a/src/dns.js b/src/dns.js new file mode 100644 index 000000000..630aed0f6 --- /dev/null +++ b/src/dns.js @@ -0,0 +1,375 @@ +'use strict'; + +module.exports = exports = { + fqdn, + getName, + + getDnsRecords, + upsertDnsRecords, + removeDnsRecords, + + waitForDnsRecord, + + validateHostname, + + makeWildcard, + + registerLocations, + unregisterLocations, + + checkDnsRecords, + syncDnsRecords, + + resolve +}; + +const apps = require('./apps.js'), + assert = require('assert'), + async = require('async'), + BoxError = require('./boxerror.js'), + constants = require('./constants.js'), + debug = require('debug')('box:domains'), + dns = require('dns'), + domains = require('./domains.js'), + mail = require('./mail.js'), + settings = require('./settings.js'), + sysinfo = require('./sysinfo.js'), + tld = require('tldjs'), + util = require('util'), + _ = require('underscore'); + +const domainsGet = util.callbackify(domains.get), + domainsList = util.callbackify(domains.list); + +// choose which subdomain backend we use for test purpose we use route53 +function api(provider) { + assert.strictEqual(typeof provider, 'string'); + + switch (provider) { + case 'cloudflare': return require('./dns/cloudflare.js'); + case 'route53': return require('./dns/route53.js'); + case 'gcdns': return require('./dns/gcdns.js'); + case 'digitalocean': return require('./dns/digitalocean.js'); + case 'gandi': return require('./dns/gandi.js'); + case 'godaddy': return require('./dns/godaddy.js'); + case 'linode': return require('./dns/linode.js'); + case 'vultr': return require('./dns/vultr.js'); + case 'namecom': return require('./dns/namecom.js'); + case 'namecheap': return require('./dns/namecheap.js'); + case 'netcup': return require('./dns/netcup.js'); + case 'noop': return require('./dns/noop.js'); + case 'manual': return require('./dns/manual.js'); + case 'wildcard': return require('./dns/wildcard.js'); + default: return null; + } +} + +function fqdn(location, domainObject) { + assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof domainObject, 'object'); + + return location + (location ? '.' : '') + domainObject.domain; +} + +// Hostname validation comes from RFC 1123 (section 2.1) +// Domain name validation comes from RFC 2181 (Name syntax) +// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names +// We are validating the validity of the location-fqdn as host name (and not dns name) +function validateHostname(location, domainObject) { + assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof domainObject, 'object'); + + const hostname = fqdn(location, domainObject); + + const RESERVED_LOCATIONS = [ + constants.SMTP_LOCATION, + constants.IMAP_LOCATION + ]; + if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new BoxError(BoxError.BAD_FIELD, location + ' is reserved', { field: 'location' }); + + if (hostname === settings.dashboardFqdn()) return new BoxError(BoxError.BAD_FIELD, location + ' is reserved', { field: 'location' }); + + // workaround https://github.com/oncletom/tld.js/issues/73 + var tmp = hostname.replace('_', '-'); + if (!tld.isValid(tmp)) return new BoxError(BoxError.BAD_FIELD, 'Hostname is not a valid domain name', { field: 'location' }); + + if (hostname.length > 253) return new BoxError(BoxError.BAD_FIELD, 'Hostname length exceeds 253 characters', { field: 'location' }); + + if (location) { + // label validation + if (location.split('.').some(function (p) { return p.length > 63 || p.length < 1; })) return new BoxError(BoxError.BAD_FIELD, 'Invalid subdomain length', { field: 'location' }); + if (location.match(/^[A-Za-z0-9-.]+$/) === null) return new BoxError(BoxError.BAD_FIELD, 'Subdomain can only contain alphanumeric, hyphen and dot', { field: 'location' }); + if (/^[-.]/.test(location)) return new BoxError(BoxError.BAD_FIELD, 'Subdomain cannot start or end with hyphen or dot', { field: 'location' }); + } + + return null; +} + +// returns the 'name' that needs to be inserted into zone +// eslint-disable-next-line no-unused-vars +function getName(domain, location, type) { + const part = domain.domain.slice(0, -domain.zoneName.length - 1); + + if (location === '') return part; + + return part ? `${location}.${part}` : location; +} + +function getDnsRecords(location, domain, type, callback) { + assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof domain, 'string'); + assert.strictEqual(typeof type, 'string'); + assert.strictEqual(typeof callback, 'function'); + + domainsGet(domain, function (error, domainObject) { + if (error) return callback(error); + + api(domainObject.provider).get(domainObject, location, type, function (error, values) { + if (error) return callback(error); + + callback(null, values); + }); + }); +} + +function checkDnsRecords(location, domain, callback) { + assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof domain, 'string'); + assert.strictEqual(typeof callback, 'function'); + + getDnsRecords(location, domain, 'A', function (error, values) { + if (error) return callback(error); + + sysinfo.getServerIp(function (error, ip) { + if (error) return callback(error); + + if (values.length === 0) return callback(null, { needsOverwrite: false }); // does not exist + if (values[0] === ip) return callback(null, { needsOverwrite: false }); // exists but in sync + + callback(null, { needsOverwrite: true }); + }); + }); +} + +// note: for TXT records the values must be quoted +function upsertDnsRecords(location, domain, type, values, callback) { + assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof domain, 'string'); + assert.strictEqual(typeof type, 'string'); + assert(Array.isArray(values)); + assert.strictEqual(typeof callback, 'function'); + + debug('upsertDNSRecord: %s on %s type %s values', location, domain, type, values); + + domainsGet(domain, function (error, domainObject) { + if (error) return callback(error); + + api(domainObject.provider).upsert(domainObject, location, type, values, function (error) { + if (error) return callback(error); + + callback(null); + }); + }); +} + +function removeDnsRecords(location, domain, type, values, callback) { + assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof domain, 'string'); + assert.strictEqual(typeof type, 'string'); + assert(Array.isArray(values)); + assert.strictEqual(typeof callback, 'function'); + + debug('removeDNSRecord: %s on %s type %s values', location, domain, type, values); + + domainsGet(domain, function (error, domainObject) { + if (error) return callback(error); + + api(domainObject.provider).del(domainObject, location, type, values, function (error) { + if (error && error.reason !== BoxError.NOT_FOUND) return callback(error); + + callback(null); + }); + }); +} + +function waitForDnsRecord(location, domain, type, value, options, callback) { + assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof domain, 'string'); + assert(type === 'A' || type === 'TXT'); + assert.strictEqual(typeof value, 'string'); + assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } + assert.strictEqual(typeof callback, 'function'); + + domainsGet(domain, function (error, domainObject) { + if (error) return callback(error); + + // linode DNS takes ~15mins + if (!options.interval) options.interval = domainObject.provider === 'linode' ? 20000 : 5000; + + api(domainObject.provider).wait(domainObject, location, type, value, options, callback); + }); +} + +function makeWildcard(vhost) { + assert.strictEqual(typeof vhost, 'string'); + + // if the vhost is like *.example.com, this function will do nothing + let parts = vhost.split('.'); + parts[0] = '*'; + return parts.join('.'); +} + +function registerLocations(locations, options, progressCallback, callback) { + assert(Array.isArray(locations)); + assert.strictEqual(typeof options, 'object'); + assert.strictEqual(typeof progressCallback, 'function'); + assert.strictEqual(typeof callback, 'function'); + + debug(`registerLocations: Will register ${JSON.stringify(locations)} with options ${JSON.stringify(options)}`); + + const overwriteDns = options.overwriteDns || false; + + sysinfo.getServerIp(function (error, ip) { + if (error) return callback(error); + + async.eachSeries(locations, function (location, iteratorDone) { + async.retry({ times: 200, interval: 5000 }, function (retryCallback) { + progressCallback({ message: `Registering location: ${location.subdomain ? (location.subdomain + '.') : ''}${location.domain}` }); + + // get the current record before updating it + getDnsRecords(location.subdomain, location.domain, 'A', function (error, values) { + if (error && error.reason === BoxError.EXTERNAL_ERROR) return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain: location })); // try again + if (error && error.reason === BoxError.ACCESS_DENIED) return retryCallback(null, new BoxError(BoxError.ACCESS_DENIED, error.message, { domain: location })); + if (error && error.reason === BoxError.NOT_FOUND) return retryCallback(null, new BoxError(BoxError.NOT_FOUND, error.message, { domain: location })); + if (error) return retryCallback(null, new BoxError(BoxError.EXTERNAL_ERROR, error.message, location)); // give up for other errors + + if (values.length !== 0 && values[0] === ip) return retryCallback(null); // up-to-date + + // refuse to update any existing DNS record for custom domains that we did not create + if (values.length !== 0 && !overwriteDns) return retryCallback(null, new BoxError(BoxError.ALREADY_EXISTS, 'DNS Record already exists', { domain: location })); + + upsertDnsRecords(location.subdomain, location.domain, 'A', [ ip ], function (error) { + if (error && (error.reason === BoxError.BUSY || error.reason === BoxError.EXTERNAL_ERROR)) { + progressCallback({ message: `registerSubdomains: Upsert error. Will retry. ${error.message}` }); + return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain: location })); // try again + } + + retryCallback(null, error ? new BoxError(BoxError.EXTERNAL_ERROR, error.message, location) : null); + }); + }); + }, function (error, result) { + if (error || result) return iteratorDone(error || result); + + iteratorDone(null); + }); + }, callback); + }); +} + +function unregisterLocations(locations, progressCallback, callback) { + assert(Array.isArray(locations)); + assert.strictEqual(typeof progressCallback, 'function'); + assert.strictEqual(typeof callback, 'function'); + + sysinfo.getServerIp(function (error, ip) { + if (error) return callback(error); + + async.eachSeries(locations, function (location, iteratorDone) { + async.retry({ times: 30, interval: 5000 }, function (retryCallback) { + progressCallback({ message: `Unregistering location: ${location.subdomain ? (location.subdomain + '.') : ''}${location.domain}` }); + + removeDnsRecords(location.subdomain, location.domain, 'A', [ ip ], function (error) { + if (error && error.reason === BoxError.NOT_FOUND) return retryCallback(null, null); + if (error && (error.reason === BoxError.SBUSY || error.reason === BoxError.EXTERNAL_ERROR)) { + progressCallback({ message: `Error unregistering location. Will retry. ${error.message}`}); + return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain: location })); // try again + } + + retryCallback(null, error ? new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain: location }) : null); + }); + }, function (error, result) { + if (error || result) return iteratorDone(error || result); + + iteratorDone(); + }); + }, callback); + }); +} + +function syncDnsRecords(options, progressCallback, callback) { + assert.strictEqual(typeof options, 'object'); + assert.strictEqual(typeof progressCallback, 'function'); + assert.strictEqual(typeof callback, 'function'); + + if (options.domain && options.type === 'mail') return mail.setDnsRecords(options.domain, callback); + + domainsList(function (error, domains) { + if (error) return callback(error); + + if (options.domain) domains = domains.filter(d => d.domain === options.domain); + + const mailSubdomain = settings.mailFqdn().substr(0, settings.mailFqdn().length - settings.mailDomain().length - 1); + + apps.getAll(function (error, allApps) { + if (error) return callback(error); + + let progress = 1, errors = []; + + // we sync by domain only to get some nice progress + async.eachSeries(domains, function (domain, iteratorDone) { + progressCallback({ percent: progress, message: `Updating DNS of ${domain.domain}`}); + progress += Math.round(100/(1+domains.length)); + + let locations = []; + if (domain.domain === settings.dashboardDomain()) locations.push({ subdomain: constants.DASHBOARD_LOCATION, domain: settings.dashboardDomain() }); + if (domain.domain === settings.mailDomain() && settings.mailFqdn() !== settings.dashboardFqdn()) locations.push({ subdomain: mailSubdomain, domain: settings.mailDomain() }); + + allApps.forEach(function (app) { + const appLocations = [{ subdomain: app.location, domain: app.domain }].concat(app.alternateDomains).concat(app.aliasDomains); + locations = locations.concat(appLocations.filter(al => al.domain === domain.domain)); + }); + + async.series([ + registerLocations.bind(null, locations, { overwriteDns: true }, progressCallback), + progressCallback.bind(null, { message: `Updating mail DNS of ${domain.domain}`}), + mail.setDnsRecords.bind(null, domain.domain) + ], function (error) { + if (error) errors.push({ domain: domain.domain, message: error.message }); + iteratorDone(); + }); + }, () => callback(null, { errors })); + }); + }); +} + +// a note on TXT records. It doesn't have quotes ("") at the DNS level. Those quotes +// are added for DNS server software to enclose spaces. Such quotes may also be returned +// by the DNS REST API of some providers +function resolve(hostname, rrtype, options, callback) { + assert.strictEqual(typeof hostname, 'string'); + assert.strictEqual(typeof rrtype, 'string'); + assert(options && typeof options === 'object'); + assert.strictEqual(typeof callback, 'function'); + + const defaultOptions = { server: '127.0.0.1', timeout: 5000 }; // unbound runs on 127.0.0.1 + const resolver = new dns.Resolver(); + options = _.extend({ }, defaultOptions, options); + + // Only use unbound on a Cloudron + if (constants.CLOUDRON) resolver.setServers([ options.server ]); + + // should callback with ECANCELLED but looks like we might hit https://github.com/nodejs/node/issues/14814 + const timerId = setTimeout(resolver.cancel.bind(resolver), options.timeout || 5000); + + resolver.resolve(hostname, rrtype, function (error, result) { + clearTimeout(timerId); + + if (error && error.code === 'ECANCELLED') error.code = 'TIMEOUT'; + + // result is an empty array if there was no error but there is no record. when you query a random + // domain, it errors with ENOTFOUND. But if you query an existing domain (A record) but with different + // type (CNAME) it is not an error and empty array + // for TXT records, result is 2d array of strings + callback(error, result); + }); +} diff --git a/src/dns/cloudflare.js b/src/dns/cloudflare.js index 36dabfb30..90c52fda1 100644 --- a/src/dns/cloudflare.js +++ b/src/dns/cloudflare.js @@ -1,22 +1,21 @@ 'use strict'; exports = module.exports = { - removePrivateFields: removePrivateFields, - injectPrivateFields: injectPrivateFields, - upsert: upsert, - get: get, - del: del, - wait: wait, - verifyDnsConfig: verifyDnsConfig + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDnsConfig }; -var assert = require('assert'), +const assert = require('assert'), async = require('async'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), debug = require('debug')('box:dns/cloudflare'), - dns = require('../native-dns.js'), - domains = require('../domains.js'), + dns = require('../dns.js'), superagent = require('superagent'), util = require('util'), waitForDns = require('./waitfordns.js'), @@ -115,7 +114,7 @@ function upsert(domainObject, location, type, values, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - fqdn = domains.fqdn(location, domainObject); + fqdn = dns.fqdn(location, domainObject); debug('upsert: %s for zone %s of type %s with values %j', fqdn, zoneName, type, values); @@ -186,7 +185,7 @@ function get(domainObject, location, type, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - fqdn = domains.fqdn(location, domainObject); + fqdn = dns.fqdn(location, domainObject); getZoneByName(dnsConfig, zoneName, function(error, zone) { if (error) return callback(error); @@ -211,7 +210,7 @@ function del(domainObject, location, type, values, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - fqdn = domains.fqdn(location, domainObject); + fqdn = dns.fqdn(location, domainObject); getZoneByName(dnsConfig, zoneName, function(error, zone) { if (error) return callback(error); @@ -256,7 +255,7 @@ function wait(domainObject, location, type, value, options, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - fqdn = domains.fqdn(location, domainObject); + fqdn = dns.fqdn(location, domainObject); debug('wait: %s for zone %s of type %s', fqdn, zoneName, type); diff --git a/src/dns/digitalocean.js b/src/dns/digitalocean.js index 5829574f2..e6c6799f2 100644 --- a/src/dns/digitalocean.js +++ b/src/dns/digitalocean.js @@ -15,8 +15,7 @@ const assert = require('assert'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), debug = require('debug')('box:dns/digitalocean'), - dns = require('../native-dns.js'), - domains = require('../domains.js'), + dns = require('../dns.js'), safe = require('safetydance'), superagent = require('superagent'), util = require('util'), @@ -87,7 +86,7 @@ function upsert(domainObject, location, type, values, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - name = domains.getName(domainObject, location, type) || '@'; + name = dns.getName(domainObject, location, type) || '@'; debug('upsert: %s for zone %s of type %s with values %j', name, zoneName, type, values); @@ -167,7 +166,7 @@ function get(domainObject, location, type, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - name = domains.getName(domainObject, location, type) || '@'; + name = dns.getName(domainObject, location, type) || '@'; getInternal(dnsConfig, zoneName, name, type, function (error, result) { if (error) return callback(error); @@ -190,7 +189,7 @@ function del(domainObject, location, type, values, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - name = domains.getName(domainObject, location, type) || '@'; + name = dns.getName(domainObject, location, type) || '@'; getInternal(dnsConfig, zoneName, name, type, function (error, result) { if (error) return callback(error); @@ -230,7 +229,7 @@ function wait(domainObject, location, type, value, options, callback) { assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } assert.strictEqual(typeof callback, 'function'); - const fqdn = domains.fqdn(location, domainObject); + const fqdn = dns.fqdn(location, domainObject); waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); } diff --git a/src/dns/gandi.js b/src/dns/gandi.js index ea6281dac..2912e2cf6 100644 --- a/src/dns/gandi.js +++ b/src/dns/gandi.js @@ -1,26 +1,25 @@ 'use strict'; exports = module.exports = { - removePrivateFields: removePrivateFields, - injectPrivateFields: injectPrivateFields, - upsert: upsert, - get: get, - del: del, - wait: wait, - verifyDnsConfig: verifyDnsConfig + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDnsConfig }; -var assert = require('assert'), +const assert = require('assert'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), debug = require('debug')('box:dns/gandi'), - dns = require('../native-dns.js'), - domains = require('../domains.js'), + dns = require('../dns.js'), superagent = require('superagent'), util = require('util'), waitForDns = require('./waitfordns.js'); -var GANDI_API = 'https://dns.api.gandi.net/api/v5'; +const GANDI_API = 'https://dns.api.gandi.net/api/v5'; function formatError(response) { return util.format(`Gandi DNS error [${response.statusCode}] ${response.body.message}`); @@ -44,7 +43,7 @@ function upsert(domainObject, location, type, values, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - name = domains.getName(domainObject, location, type) || '@'; + name = dns.getName(domainObject, location, type) || '@'; debug(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`); @@ -75,7 +74,7 @@ function get(domainObject, location, type, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - name = domains.getName(domainObject, location, type) || '@'; + name = dns.getName(domainObject, location, type) || '@'; debug(`get: ${name} in zone ${zoneName} of type ${type}`); @@ -103,7 +102,7 @@ function del(domainObject, location, type, values, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - name = domains.getName(domainObject, location, type) || '@'; + name = dns.getName(domainObject, location, type) || '@'; debug(`del: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`); @@ -130,7 +129,7 @@ function wait(domainObject, location, type, value, options, callback) { assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } assert.strictEqual(typeof callback, 'function'); - const fqdn = domains.fqdn(location, domainObject); + const fqdn = dns.fqdn(location, domainObject); waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); } diff --git a/src/dns/gcdns.js b/src/dns/gcdns.js index 109962fe6..2304c7187 100644 --- a/src/dns/gcdns.js +++ b/src/dns/gcdns.js @@ -1,23 +1,21 @@ 'use strict'; exports = module.exports = { - removePrivateFields: removePrivateFields, - injectPrivateFields: injectPrivateFields, - upsert: upsert, - get: get, - del: del, - wait: wait, - verifyDnsConfig: verifyDnsConfig + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDnsConfig }; -var assert = require('assert'), +const assert = require('assert'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), debug = require('debug')('box:dns/gcdns'), - dns = require('../native-dns.js'), - domains = require('../domains.js'), + dns = require('../dns.js'), GCDNS = require('@google-cloud/dns').DNS, - util = require('util'), waitForDns = require('./waitfordns.js'), _ = require('underscore'); @@ -78,7 +76,7 @@ function upsert(domainObject, location, type, values, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - fqdn = domains.fqdn(location, domainObject); + fqdn = dns.fqdn(location, domainObject); debug('add: %s for zone %s of type %s with values %j', fqdn, zoneName, type, values); @@ -120,7 +118,7 @@ function get(domainObject, location, type, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - fqdn = domains.fqdn(location, domainObject); + fqdn = dns.fqdn(location, domainObject); getZoneByName(getDnsCredentials(dnsConfig), zoneName, function (error, zone) { if (error) return callback(error); @@ -149,7 +147,7 @@ function del(domainObject, location, type, values, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - fqdn = domains.fqdn(location, domainObject); + fqdn = dns.fqdn(location, domainObject); getZoneByName(getDnsCredentials(dnsConfig), zoneName, function (error, zone) { if (error) return callback(error); @@ -183,7 +181,7 @@ function wait(domainObject, location, type, value, options, callback) { assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } assert.strictEqual(typeof callback, 'function'); - const fqdn = domains.fqdn(location, domainObject); + const fqdn = dns.fqdn(location, domainObject); waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); } diff --git a/src/dns/godaddy.js b/src/dns/godaddy.js index 3e339d344..fe5be0855 100644 --- a/src/dns/godaddy.js +++ b/src/dns/godaddy.js @@ -1,21 +1,20 @@ 'use strict'; exports = module.exports = { - removePrivateFields: removePrivateFields, - injectPrivateFields: injectPrivateFields, - upsert: upsert, - get: get, - del: del, - wait: wait, - verifyDnsConfig: verifyDnsConfig + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDnsConfig }; -var assert = require('assert'), +const assert = require('assert'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), debug = require('debug')('box:dns/godaddy'), - dns = require('../native-dns.js'), - domains = require('../domains.js'), + dns = require('../dns.js'), superagent = require('superagent'), util = require('util'), waitForDns = require('./waitfordns.js'); @@ -50,7 +49,7 @@ function upsert(domainObject, location, type, values, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - name = domains.getName(domainObject, location, type) || '@'; + name = dns.getName(domainObject, location, type) || '@'; debug(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`); @@ -91,7 +90,7 @@ function get(domainObject, location, type, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - name = domains.getName(domainObject, location, type) || '@'; + name = dns.getName(domainObject, location, type) || '@'; debug(`get: ${name} in zone ${zoneName} of type ${type}`); @@ -123,7 +122,7 @@ function del(domainObject, location, type, values, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - name = domains.getName(domainObject, location, type) || '@'; + name = dns.getName(domainObject, location, type) || '@'; debug(`get: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`); @@ -165,7 +164,7 @@ function wait(domainObject, location, type, value, options, callback) { assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } assert.strictEqual(typeof callback, 'function'); - const fqdn = domains.fqdn(location, domainObject); + const fqdn = dns.fqdn(location, domainObject); waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); } diff --git a/src/dns/interface.js b/src/dns/interface.js index 963fc523d..a6e7bf820 100644 --- a/src/dns/interface.js +++ b/src/dns/interface.js @@ -7,18 +7,17 @@ // ------------------------------------------- exports = module.exports = { - removePrivateFields: removePrivateFields, - injectPrivateFields: injectPrivateFields, - upsert: upsert, - get: get, - del: del, - wait: wait, - verifyDnsConfig: verifyDnsConfig + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDnsConfig }; -var assert = require('assert'), - BoxError = require('../boxerror.js'), - util = require('util'); +const assert = require('assert'), + BoxError = require('../boxerror.js'); function removePrivateFields(domainObject) { // in-place removal of tokens and api keys with constants.SECRET_PLACEHOLDER diff --git a/src/dns/linode.js b/src/dns/linode.js index f0299f153..65c0ecc5d 100644 --- a/src/dns/linode.js +++ b/src/dns/linode.js @@ -10,13 +10,12 @@ exports = module.exports = { verifyDnsConfig }; -let async = require('async'), +const async = require('async'), assert = require('assert'), constants = require('../constants.js'), BoxError = require('../boxerror.js'), debug = require('debug')('box:dns/linode'), - dns = require('../native-dns.js'), - domains = require('../domains.js'), + dns = require('../dns.js'), superagent = require('superagent'), util = require('util'), waitForDns = require('./waitfordns.js'); @@ -117,7 +116,7 @@ function get(domainObject, location, type, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - name = domains.getName(domainObject, location, type) || ''; + name = dns.getName(domainObject, location, type) || ''; getZoneRecords(dnsConfig, zoneName, name, type, function (error, result) { if (error) return callback(error); @@ -140,7 +139,7 @@ function upsert(domainObject, location, type, values, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - name = domains.getName(domainObject, location, type) || ''; + name = dns.getName(domainObject, location, type) || ''; debug('upsert: %s for zone %s of type %s with values %j', name, zoneName, type, values); @@ -222,7 +221,7 @@ function del(domainObject, location, type, values, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - name = domains.getName(domainObject, location, type) || ''; + name = dns.getName(domainObject, location, type) || ''; getZoneRecords(dnsConfig, zoneName, name, type, function (error, result) { if (error) return callback(error); @@ -263,7 +262,7 @@ function wait(domainObject, location, type, value, options, callback) { assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } assert.strictEqual(typeof callback, 'function'); - const fqdn = domains.fqdn(location, domainObject); + const fqdn = dns.fqdn(location, domainObject); waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); } diff --git a/src/dns/manual.js b/src/dns/manual.js index 0398521c6..9e605cb73 100644 --- a/src/dns/manual.js +++ b/src/dns/manual.js @@ -10,12 +10,10 @@ exports = module.exports = { verifyDnsConfig: verifyDnsConfig }; -var assert = require('assert'), +const assert = require('assert'), BoxError = require('../boxerror.js'), debug = require('debug')('box:dns/manual'), - dns = require('../native-dns.js'), - domains = require('../domains.js'), - util = require('util'), + dns = require('../dns.js'), waitForDns = require('./waitfordns.js'); function removePrivateFields(domainObject) { @@ -66,7 +64,7 @@ function wait(domainObject, location, type, value, options, callback) { assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } assert.strictEqual(typeof callback, 'function'); - const fqdn = domains.fqdn(location, domainObject); + const fqdn = dns.fqdn(location, domainObject); waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); } diff --git a/src/dns/namecheap.js b/src/dns/namecheap.js index 1e79c6c5d..1df9e60c1 100644 --- a/src/dns/namecheap.js +++ b/src/dns/namecheap.js @@ -1,21 +1,20 @@ 'use strict'; exports = module.exports = { - removePrivateFields: removePrivateFields, - injectPrivateFields: injectPrivateFields, - upsert: upsert, - get: get, - del: del, - verifyDnsConfig: verifyDnsConfig, - wait: wait + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + verifyDnsConfig, + wait }; -var assert = require('assert'), +const assert = require('assert'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), debug = require('debug')('box:dns/namecheap'), - dns = require('../native-dns.js'), - domains = require('../domains.js'), + dns = require('../dns.js'), querystring = require('querystring'), safe = require('safetydance'), superagent = require('superagent'), @@ -151,7 +150,7 @@ function upsert(domainObject, subdomain, type, values, callback) { const dnsConfig = domainObject.config; const zoneName = domainObject.zoneName; - subdomain = domains.getName(domainObject, subdomain, type) || '@'; + subdomain = dns.getName(domainObject, subdomain, type) || '@'; debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values); @@ -214,7 +213,7 @@ function get(domainObject, subdomain, type, callback) { const dnsConfig = domainObject.config; const zoneName = domainObject.zoneName; - subdomain = domains.getName(domainObject, subdomain, type) || '@'; + subdomain = dns.getName(domainObject, subdomain, type) || '@'; getZone(dnsConfig, zoneName, function (error, result) { if (error) return callback(error); @@ -241,7 +240,7 @@ function del(domainObject, subdomain, type, values, callback) { const dnsConfig = domainObject.config; const zoneName = domainObject.zoneName; - subdomain = domains.getName(domainObject, subdomain, type) || '@'; + subdomain = dns.getName(domainObject, subdomain, type) || '@'; debug('del: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values); @@ -316,7 +315,7 @@ function wait(domainObject, subdomain, type, value, options, callback) { assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } assert.strictEqual(typeof callback, 'function'); - const fqdn = domains.fqdn(subdomain, domainObject); + const fqdn = dns.fqdn(subdomain, domainObject); waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); } diff --git a/src/dns/namecom.js b/src/dns/namecom.js index 6a1298e5a..1049f6cd7 100644 --- a/src/dns/namecom.js +++ b/src/dns/namecom.js @@ -1,24 +1,22 @@ 'use strict'; exports = module.exports = { - removePrivateFields: removePrivateFields, - injectPrivateFields: injectPrivateFields, - upsert: upsert, - get: get, - del: del, - wait: wait, - verifyDnsConfig: verifyDnsConfig + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDnsConfig }; -var assert = require('assert'), +const assert = require('assert'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), debug = require('debug')('box:dns/namecom'), - dns = require('../native-dns.js'), - domains = require('../domains.js'), + dns = require('../dns.js'), safe = require('safetydance'), superagent = require('superagent'), - util = require('util'), waitForDns = require('./waitfordns.js'); const NAMECOM_API = 'https://api.name.com/v4'; @@ -162,7 +160,7 @@ function upsert(domainObject, location, type, values, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - name = domains.getName(domainObject, location, type) || ''; + name = dns.getName(domainObject, location, type) || ''; debug(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`); @@ -183,7 +181,7 @@ function get(domainObject, location, type, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - name = domains.getName(domainObject, location, type) || ''; + name = dns.getName(domainObject, location, type) || ''; getInternal(dnsConfig, zoneName, name, type, function (error, result) { if (error) return callback(error); @@ -205,7 +203,7 @@ function del(domainObject, location, type, values, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - name = domains.getName(domainObject, location, type) || ''; + name = dns.getName(domainObject, location, type) || ''; debug(`del: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`); @@ -235,7 +233,7 @@ function wait(domainObject, location, type, value, options, callback) { assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } assert.strictEqual(typeof callback, 'function'); - const fqdn = domains.fqdn(location, domainObject); + const fqdn = dns.fqdn(location, domainObject); waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); } diff --git a/src/dns/netcup.js b/src/dns/netcup.js index adc59b186..3cebcdf76 100644 --- a/src/dns/netcup.js +++ b/src/dns/netcup.js @@ -1,26 +1,25 @@ 'use strict'; exports = module.exports = { - removePrivateFields: removePrivateFields, - injectPrivateFields: injectPrivateFields, - upsert: upsert, - get: get, - del: del, - wait: wait, - verifyDnsConfig: verifyDnsConfig + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDnsConfig }; -var assert = require('assert'), +const assert = require('assert'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), debug = require('debug')('box:dns/netcup'), - dns = require('../native-dns.js'), - domains = require('../domains.js'), + dns = require('../dns.js'), superagent = require('superagent'), util = require('util'), waitForDns = require('./waitfordns.js'); -var API_ENDPOINT = 'https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON'; +const API_ENDPOINT = 'https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON'; function formatError(response) { if (response.body) return util.format('Netcup DNS error [%s] %s', response.body.statuscode, response.body.longmessage); @@ -95,7 +94,7 @@ function upsert(domainObject, location, type, values, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - name = domains.getName(domainObject, location, type) || '@'; + name = dns.getName(domainObject, location, type) || '@'; debug('upsert: %s for zone %s of type %s with values %j', name, zoneName, type, values); @@ -163,7 +162,7 @@ function get(domainObject, location, type, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - name = domains.getName(domainObject, location, type) || '@'; + name = dns.getName(domainObject, location, type) || '@'; debug('get: %s for zone %s of type %s', name, zoneName, type); @@ -188,7 +187,7 @@ function del(domainObject, location, type, values, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - name = domains.getName(domainObject, location, type) || '@'; + name = dns.getName(domainObject, location, type) || '@'; debug('del: %s for zone %s of type %s with values %j', name, zoneName, type, values); @@ -249,7 +248,7 @@ function wait(domainObject, location, type, value, options, callback) { assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } assert.strictEqual(typeof callback, 'function'); - const fqdn = domains.fqdn(location, domainObject); + const fqdn = dns.fqdn(location, domainObject); waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); } diff --git a/src/dns/noop.js b/src/dns/noop.js index ed6efe119..3c0124baf 100644 --- a/src/dns/noop.js +++ b/src/dns/noop.js @@ -1,18 +1,17 @@ 'use strict'; exports = module.exports = { - removePrivateFields: removePrivateFields, - injectPrivateFields: injectPrivateFields, - upsert: upsert, - get: get, - del: del, - wait: wait, - verifyDnsConfig: verifyDnsConfig + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDnsConfig }; -var assert = require('assert'), - debug = require('debug')('box:dns/noop'), - util = require('util'); +const assert = require('assert'), + debug = require('debug')('box:dns/noop'); function removePrivateFields(domainObject) { return domainObject; diff --git a/src/dns/route53.js b/src/dns/route53.js index 413edd58e..4fe6fac74 100644 --- a/src/dns/route53.js +++ b/src/dns/route53.js @@ -1,23 +1,21 @@ 'use strict'; exports = module.exports = { - removePrivateFields: removePrivateFields, - injectPrivateFields: injectPrivateFields, - upsert: upsert, - get: get, - del: del, - wait: wait, - verifyDnsConfig: verifyDnsConfig + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDnsConfig }; -var assert = require('assert'), +const assert = require('assert'), AWS = require('aws-sdk'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), debug = require('debug')('box:dns/route53'), - dns = require('../native-dns.js'), - domains = require('../domains.js'), - util = require('util'), + dns = require('../dns.js'), waitForDns = require('./waitfordns.js'), _ = require('underscore'); @@ -102,7 +100,7 @@ function upsert(domainObject, location, type, values, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - fqdn = domains.fqdn(location, domainObject); + fqdn = dns.fqdn(location, domainObject); debug('add: %s for zone %s of type %s with values %j', fqdn, zoneName, type, values); @@ -147,7 +145,7 @@ function get(domainObject, location, type, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - fqdn = domains.fqdn(location, domainObject); + fqdn = dns.fqdn(location, domainObject); getZoneByName(dnsConfig, zoneName, function (error, zone) { if (error) return callback(error); @@ -183,7 +181,7 @@ function del(domainObject, location, type, values, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - fqdn = domains.fqdn(location, domainObject); + fqdn = dns.fqdn(location, domainObject); getZoneByName(dnsConfig, zoneName, function (error, zone) { if (error) return callback(error); @@ -241,7 +239,7 @@ function wait(domainObject, location, type, value, options, callback) { assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } assert.strictEqual(typeof callback, 'function'); - const fqdn = domains.fqdn(location, domainObject); + const fqdn = dns.fqdn(location, domainObject); waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); } diff --git a/src/dns/vultr.js b/src/dns/vultr.js index 29d7a1584..f7e0187c6 100644 --- a/src/dns/vultr.js +++ b/src/dns/vultr.js @@ -15,8 +15,7 @@ const async = require('async'), constants = require('../constants.js'), BoxError = require('../boxerror.js'), debug = require('debug')('box:dns/vultr'), - dns = require('../native-dns.js'), - domains = require('../domains.js'), + dns = require('../dns.js'), safe = require('safetydance'), superagent = require('superagent'), util = require('util'), @@ -87,7 +86,7 @@ function get(domainObject, location, type, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - name = domains.getName(domainObject, location, type) || ''; + name = dns.getName(domainObject, location, type) || ''; getZoneRecords(dnsConfig, zoneName, name, type, function (error, records) { if (error) return callback(error); @@ -109,7 +108,7 @@ function upsert(domainObject, location, type, values, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - name = domains.getName(domainObject, location, type) || ''; + name = dns.getName(domainObject, location, type) || ''; debug('upsert: %s for zone %s of type %s with values %j', name, zoneName, type, values); @@ -190,7 +189,7 @@ function del(domainObject, location, type, values, callback) { const dnsConfig = domainObject.config, zoneName = domainObject.zoneName, - name = domains.getName(domainObject, location, type) || ''; + name = dns.getName(domainObject, location, type) || ''; getZoneRecords(dnsConfig, zoneName, name, type, function (error, records) { if (error) return callback(error); @@ -230,7 +229,7 @@ function wait(domainObject, location, type, value, options, callback) { assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } assert.strictEqual(typeof callback, 'function'); - const fqdn = domains.fqdn(location, domainObject); + const fqdn = dns.fqdn(location, domainObject); waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); } diff --git a/src/dns/waitfordns.js b/src/dns/waitfordns.js index 916a12ba0..a93ca3324 100644 --- a/src/dns/waitfordns.js +++ b/src/dns/waitfordns.js @@ -2,11 +2,11 @@ exports = module.exports = waitForDns; -var assert = require('assert'), +const assert = require('assert'), async = require('async'), BoxError = require('../boxerror.js'), debug = require('debug')('box:dns/waitfordns'), - dns = require('../native-dns.js'); + dns = require('../dns.js'); function resolveIp(hostname, options, callback) { assert.strictEqual(typeof hostname, 'string'); diff --git a/src/dns/wildcard.js b/src/dns/wildcard.js index 7930c676e..949e98109 100644 --- a/src/dns/wildcard.js +++ b/src/dns/wildcard.js @@ -1,22 +1,20 @@ 'use strict'; exports = module.exports = { - removePrivateFields: removePrivateFields, - injectPrivateFields: injectPrivateFields, - upsert: upsert, - get: get, - del: del, - wait: wait, - verifyDnsConfig: verifyDnsConfig + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDnsConfig }; -var assert = require('assert'), +const assert = require('assert'), BoxError = require('../boxerror.js'), debug = require('debug')('box:dns/manual'), - dns = require('../native-dns.js'), - domains = require('../domains.js'), + dns = require('../dns.js'), sysinfo = require('../sysinfo.js'), - util = require('util'), waitForDns = require('./waitfordns.js'); function removePrivateFields(domainObject) { @@ -66,7 +64,7 @@ function wait(domainObject, location, type, value, options, callback) { assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } assert.strictEqual(typeof callback, 'function'); - const fqdn = domains.fqdn(location, domainObject); + const fqdn = dns.fqdn(location, domainObject); waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); } @@ -83,7 +81,7 @@ function verifyDnsConfig(domainObject, callback) { if (error || !nameservers) return callback(new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' })); const location = 'cloudrontestdns'; - const fqdn = domains.fqdn(location, domainObject); + const fqdn = dns.fqdn(location, domainObject); dns.resolve(fqdn, 'A', { server: '127.0.0.1', timeout: 5000 }, function (error, result) { if (error && error.code === 'ENOTFOUND') return callback(new BoxError(BoxError.BAD_FIELD, `Unable to resolve ${fqdn}`, { field: 'nameservers' })); diff --git a/src/domaindb.js b/src/domaindb.js deleted file mode 100644 index c003d1f7c..000000000 --- a/src/domaindb.js +++ /dev/null @@ -1,141 +0,0 @@ -/* jslint node:true */ - -'use strict'; - -exports = module.exports = { - add, - get, - getAll, - update, - del, - clear -}; - -const assert = require('assert'), - BoxError = require('./boxerror.js'), - database = require('./database.js'), - safe = require('safetydance'); - -const DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson', 'wellKnownJson', 'fallbackCertificateJson' ].join(','); - -function postProcess(data) { - data.config = safe.JSON.parse(data.configJson); - delete data.configJson; - - data.tlsConfig = safe.JSON.parse(data.tlsConfigJson); - delete data.tlsConfigJson; - - data.wellKnown = safe.JSON.parse(data.wellKnownJson); - delete data.wellKnownJson; - - data.fallbackCertificate = safe.JSON.parse(data.fallbackCertificateJson); - delete data.fallbackCertificateJson; - - return data; -} - -function get(domain, callback) { - assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query(`SELECT ${DOMAINS_FIELDS} FROM domains WHERE domain=?`, [ domain ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Domain not found')); - - postProcess(result[0]); - - callback(null, result[0]); - }); -} - -function getAll(callback) { - database.query(`SELECT ${DOMAINS_FIELDS} FROM domains ORDER BY domain`, function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - results.forEach(postProcess); - - callback(null, results); - }); -} - -function add(name, data, callback) { - assert.strictEqual(typeof name, 'string'); - assert.strictEqual(typeof data, 'object'); - assert.strictEqual(typeof data.zoneName, 'string'); - assert.strictEqual(typeof data.provider, 'string'); - assert.strictEqual(typeof data.config, 'object'); - assert.strictEqual(typeof data.tlsConfig, 'object'); - assert.strictEqual(typeof data.fallbackCertificate, 'object'); - assert.strictEqual(typeof callback, 'function'); - - let queries = [ - { query: 'INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson, fallbackCertificateJson) VALUES (?, ?, ?, ?, ?, ?)', - args: [ name, data.zoneName, data.provider, JSON.stringify(data.config), JSON.stringify(data.tlsConfig), JSON.stringify(data.fallbackCertificate) ] }, - { query: 'INSERT INTO mail (domain, dkimSelector) VALUES (?, ?)', args: [ name, data.dkimSelector || 'cloudron' ] }, - ]; - - database.transaction(queries, function (error) { - if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'Domain already exists')); - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null); - }); -} - -function update(name, domain, callback) { - assert.strictEqual(typeof name, 'string'); - assert.strictEqual(typeof domain, 'object'); - assert.strictEqual(typeof callback, 'function'); - - var args = [ ], fields = [ ]; - for (var k in domain) { - if (k === 'config' || k === 'tlsConfig' || k === 'wellKnown' || k === 'fallbackCertificate') { // json fields - fields.push(`${k}Json = ?`); - args.push(JSON.stringify(domain[k])); - } else { - fields.push(k + ' = ?'); - args.push(domain[k]); - } - } - args.push(name); - - database.query('UPDATE domains SET ' + fields.join(', ') + ' WHERE domain=?', args, function (error) { - if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.NOT_FOUND, 'Domain not found')); - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null); - }); -} - -function del(domain, callback) { - assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof callback, 'function'); - - let queries = [ - { query: 'DELETE FROM mail WHERE domain = ?', args: [ domain ] }, - { query: 'DELETE FROM domains WHERE domain = ?', args: [ domain ] }, - ]; - - database.transaction(queries, function (error, results) { - if (error && error.code === 'ER_ROW_IS_REFERENCED_2') { - if (error.message.indexOf('apps_mailDomain_constraint') !== -1) return callback(new BoxError(BoxError.CONFLICT, 'Domain is in use by an app or the mailbox of an app. Check the domains of apps and the Email section of each app.')); - if (error.message.indexOf('subdomains') !== -1) return callback(new BoxError(BoxError.CONFLICT, 'Domain is in use by one or more app(s).')); - if (error.message.indexOf('mail') !== -1) return callback(new BoxError(BoxError.CONFLICT, 'Domain is in use by one or more mailboxes. Delete them first in the Email view.')); - - return callback(new BoxError(BoxError.CONFLICT, error.message)); - } - - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (results[1].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Domain not found')); - - callback(null); - }); -} - -function clear(callback) { - database.query('DELETE FROM domains', function (error) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(error); - }); -} diff --git a/src/domains.js b/src/domains.js index ff15a0b69..db45007b8 100644 --- a/src/domains.js +++ b/src/domains.js @@ -3,54 +3,49 @@ module.exports = exports = { add, get, - getAll, + list, update, del, clear, - fqdn, - getName, - - getDnsRecords, - upsertDnsRecords, - removeDnsRecords, - - waitForDnsRecord, - removePrivateFields, removeRestrictedFields, - - validateHostname, - - makeWildcard, - - parentDomain, - - registerLocations, - unregisterLocations, - - checkDnsRecords, - syncDnsRecords }; -const apps = require('./apps.js'), - assert = require('assert'), - async = require('async'), +const assert = require('assert'), BoxError = require('./boxerror.js'), - constants = require('./constants.js'), crypto = require('crypto'), + database = require('./database.js'), debug = require('debug')('box:domains'), - domaindb = require('./domaindb.js'), eventlog = require('./eventlog.js'), mail = require('./mail.js'), reverseProxy = require('./reverseproxy.js'), + safe = require('safetydance'), settings = require('./settings.js'), - sysinfo = require('./sysinfo.js'), tld = require('tldjs'), + util = require('util'), _ = require('underscore'); const NOOP_CALLBACK = function (error) { if (error) debug(error); }; +const DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson', 'wellKnownJson', 'fallbackCertificateJson' ].join(','); + +function postProcess(data) { + data.config = safe.JSON.parse(data.configJson); + delete data.configJson; + + data.tlsConfig = safe.JSON.parse(data.tlsConfigJson); + delete data.tlsConfigJson; + + data.wellKnown = safe.JSON.parse(data.wellKnownJson); + delete data.wellKnownJson; + + data.fallbackCertificate = safe.JSON.parse(data.fallbackCertificateJson); + delete data.fallbackCertificateJson; + + return data; +} + // choose which subdomain backend we use for test purpose we use route53 function api(provider) { assert.strictEqual(typeof provider, 'string'); @@ -74,11 +69,6 @@ function api(provider) { } } -function parentDomain(domain) { - assert.strictEqual(typeof domain, 'string'); - return domain.replace(/^\S+?\./, ''); // +? means non-greedy -} - function verifyDnsConfig(dnsConfig, domain, zoneName, provider, callback) { assert(dnsConfig && typeof dnsConfig === 'object'); // the dns config to test with assert.strictEqual(typeof domain, 'string'); @@ -100,44 +90,6 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, provider, callback) { }); } -function fqdn(location, domainObject) { - return location + (location ? '.' : '') + domainObject.domain; -} - -// Hostname validation comes from RFC 1123 (section 2.1) -// Domain name validation comes from RFC 2181 (Name syntax) -// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names -// We are validating the validity of the location-fqdn as host name (and not dns name) -function validateHostname(location, domainObject) { - assert.strictEqual(typeof location, 'string'); - assert.strictEqual(typeof domainObject, 'object'); - - const hostname = fqdn(location, domainObject); - - const RESERVED_LOCATIONS = [ - constants.SMTP_LOCATION, - constants.IMAP_LOCATION - ]; - if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new BoxError(BoxError.BAD_FIELD, location + ' is reserved', { field: 'location' }); - - if (hostname === settings.dashboardFqdn()) return new BoxError(BoxError.BAD_FIELD, location + ' is reserved', { field: 'location' }); - - // workaround https://github.com/oncletom/tld.js/issues/73 - var tmp = hostname.replace('_', '-'); - if (!tld.isValid(tmp)) return new BoxError(BoxError.BAD_FIELD, 'Hostname is not a valid domain name', { field: 'location' }); - - if (hostname.length > 253) return new BoxError(BoxError.BAD_FIELD, 'Hostname length exceeds 253 characters', { field: 'location' }); - - if (location) { - // label validation - if (location.split('.').some(function (p) { return p.length > 63 || p.length < 1; })) return new BoxError(BoxError.BAD_FIELD, 'Invalid subdomain length', { field: 'location' }); - if (location.match(/^[A-Za-z0-9-.]+$/) === null) return new BoxError(BoxError.BAD_FIELD, 'Subdomain can only contain alphanumeric, hyphen and dot', { field: 'location' }); - if (/^[-.]/.test(location)) return new BoxError(BoxError.BAD_FIELD, 'Subdomain cannot start or end with hyphen or dot', { field: 'location' }); - } - - return null; -} - function validateTlsConfig(tlsConfig, dnsProvider) { assert.strictEqual(typeof tlsConfig, 'object'); assert.strictEqual(typeof dnsProvider, 'string'); @@ -165,37 +117,36 @@ function validateWellKnown(wellKnown) { return null; } -function add(domain, data, auditSource, callback) { +async function add(domain, data, auditSource) { assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof data.zoneName, 'string'); assert.strictEqual(typeof data.provider, 'string'); assert.strictEqual(typeof data.config, 'object'); assert.strictEqual(typeof data.fallbackCertificate, 'object'); assert.strictEqual(typeof data.tlsConfig, 'object'); - assert.strictEqual(typeof callback, 'function'); let { zoneName, provider, config, fallbackCertificate, tlsConfig, dkimSelector } = data; - if (!tld.isValid(domain)) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid domain', { field: 'domain' })); - if (domain.endsWith('.')) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid domain', { field: 'domain' })); + if (!tld.isValid(domain)) throw new BoxError(BoxError.BAD_FIELD, 'Invalid domain', { field: 'domain' }); + if (domain.endsWith('.')) throw new BoxError(BoxError.BAD_FIELD, 'Invalid domain', { field: 'domain' }); if (zoneName) { - if (!tld.isValid(zoneName)) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid zoneName', { field: 'zoneName' })); - if (zoneName.endsWith('.')) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid zoneName', { field: 'zoneName' })); + if (!tld.isValid(zoneName)) throw new BoxError(BoxError.BAD_FIELD, 'Invalid zoneName', { field: 'zoneName' }); + if (zoneName.endsWith('.')) throw new BoxError(BoxError.BAD_FIELD, 'Invalid zoneName', { field: 'zoneName' }); } else { zoneName = tld.getDomain(domain) || domain; } if (fallbackCertificate) { let error = reverseProxy.validateCertificate('test', { domain, config }, fallbackCertificate); - if (error) return callback(error); + if (error) throw error; } else { fallbackCertificate = reverseProxy.generateFallbackCertificateSync(domain); - if (fallbackCertificate.error) return callback(fallbackCertificate.error); + if (fallbackCertificate.error) throw fallbackCertificate.error; } let error = validateTlsConfig(tlsConfig, provider); - if (error) return callback(error); + if (error) throw error; if (!dkimSelector) { // create a unique suffix. this lets one add this domain can be added in another cloudron instance and not have their dkim selector conflict @@ -203,47 +154,42 @@ function add(domain, data, auditSource, callback) { dkimSelector = `cloudron-${suffix}`; } - verifyDnsConfig(config, domain, zoneName, provider, function (error, sanitizedConfig) { - if (error) return callback(error); + const verifyDnsConfigAsync = util.promisify(verifyDnsConfig); - domaindb.add(domain, { zoneName, provider, config: sanitizedConfig, tlsConfig, dkimSelector, fallbackCertificate }, function (error) { - if (error) return callback(error); + const sanitizedConfig = await verifyDnsConfigAsync(config, domain, zoneName, provider); - reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) { - if (error) return callback(error); + let queries = [ + { query: 'INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson, fallbackCertificateJson) VALUES (?, ?, ?, ?, ?, ?)', + args: [ domain, zoneName, provider, JSON.stringify(sanitizedConfig), JSON.stringify(tlsConfig), JSON.stringify(fallbackCertificate) ] }, + { query: 'INSERT INTO mail (domain, dkimSelector) VALUES (?, ?)', args: [ domain, dkimSelector || 'cloudron' ] }, + ]; - eventlog.add(eventlog.ACTION_DOMAIN_ADD, auditSource, { domain, zoneName, provider }); + [error] = await safe(database.transaction(queries)); + if (error && error.code === 'ER_DUP_ENTRY') throw new BoxError(BoxError.ALREADY_EXISTS, 'Domain already exists'); + if (error) throw new BoxError(BoxError.DATABASE_ERROR, error); - mail.onDomainAdded(domain, NOOP_CALLBACK); + await reverseProxy.setFallbackCertificate(domain, fallbackCertificate); - callback(); - }); - }); - }); + eventlog.add(eventlog.ACTION_DOMAIN_ADD, auditSource, { domain, zoneName, provider }); + + mail.onDomainAdded(domain, NOOP_CALLBACK); } -function get(domain, callback) { +async function get(domain) { assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof callback, 'function'); - domaindb.get(domain, function (error, result) { - if (error) return callback(error); - - return callback(null, result); - }); + const result = await database.query(`SELECT ${DOMAINS_FIELDS} FROM domains WHERE domain=?`, [ domain ]); + if (result.length === 0) return null; + return postProcess(result[0]); } -function getAll(callback) { - assert.strictEqual(typeof callback, 'function'); - - domaindb.getAll(function (error, result) { - if (error) return callback(error); - - return callback(null, result); - }); +async function list() { + const results = await database.query(`SELECT ${DOMAINS_FIELDS} FROM domains ORDER BY domain`); + results.forEach(postProcess); + return results; } -function update(domain, data, auditSource, callback) { +async function update(domain, data, auditSource) { assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof data.zoneName, 'string'); assert.strictEqual(typeof data.provider, 'string'); @@ -251,196 +197,98 @@ function update(domain, data, auditSource, callback) { assert.strictEqual(typeof data.fallbackCertificate, 'object'); assert.strictEqual(typeof data.tlsConfig, 'object'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); let { zoneName, provider, config, fallbackCertificate, tlsConfig, wellKnown } = data; + let error; - if (settings.isDemo() && (domain === settings.dashboardDomain())) return callback(new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode')); + if (settings.isDemo() && (domain === settings.dashboardDomain())) throw new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode'); - domaindb.get(domain, function (error, domainObject) { - if (error) return callback(error); + const domainObject = await get(domain); + if (zoneName) { + if (!tld.isValid(zoneName)) throw new BoxError(BoxError.BAD_FIELD, 'Invalid zoneName', { field: 'zoneName' }); + } else { + zoneName = domainObject.zoneName; + } - if (zoneName) { - if (!tld.isValid(zoneName)) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid zoneName', { field: 'zoneName' })); + if (fallbackCertificate) { + let error = reverseProxy.validateCertificate('test', domainObject, fallbackCertificate); + if (error) throw error; + } + + error = validateTlsConfig(tlsConfig, provider); + if (error) throw error; + + error = validateWellKnown(wellKnown, provider); + if (error) throw error; + + if (provider === domainObject.provider) api(provider).injectPrivateFields(config, domainObject.config); + + const verifyDnsConfigAsync = util.promisify(verifyDnsConfig); + + const sanitizedConfig = await verifyDnsConfigAsync(config, domain, zoneName, provider); + + let newData = { + config: sanitizedConfig, + zoneName, + provider, + tlsConfig, + wellKnown, + }; + + if (fallbackCertificate) newData.fallbackCertificate = fallbackCertificate; + + let args = [ ], fields = [ ]; + for (const k in newData) { + if (k === 'config' || k === 'tlsConfig' || k === 'wellKnown' || k === 'fallbackCertificate') { // json fields + fields.push(`${k}Json = ?`); + args.push(JSON.stringify(newData[k])); } else { - zoneName = domainObject.zoneName; + fields.push(k + ' = ?'); + args.push(newData[k]); } + } + args.push(domain); - if (fallbackCertificate) { - let error = reverseProxy.validateCertificate('test', domainObject, fallbackCertificate); - if (error) return callback(error); - } + [error] = await safe(database.query('UPDATE domains SET ' + fields.join(', ') + ' WHERE domain=?', args)); + if (error && error.reason === BoxError.NOT_FOUND) throw new BoxError(BoxError.NOT_FOUND, 'Domain not found'); + if (error) throw new BoxError(BoxError.DATABASE_ERROR, error); - error = validateTlsConfig(tlsConfig, provider); - if (error) return callback(error); + if (!fallbackCertificate) return; - error = validateWellKnown(wellKnown, provider); - if (error) return callback(error); + await reverseProxy.setFallbackCertificate(domain, fallbackCertificate); - if (provider === domainObject.provider) api(provider).injectPrivateFields(config, domainObject.config); - - verifyDnsConfig(config, domain, zoneName, provider, function (error, sanitizedConfig) { - if (error) return callback(error); - - let newData = { - config: sanitizedConfig, - zoneName, - provider, - tlsConfig, - wellKnown, - }; - - if (fallbackCertificate) newData.fallbackCertificate = fallbackCertificate; - - domaindb.update(domain, newData, function (error) { - if (error) return callback(error); - - if (!fallbackCertificate) return callback(); - - reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) { - if (error) return callback(error); - - eventlog.add(eventlog.ACTION_DOMAIN_UPDATE, auditSource, { domain, zoneName, provider }); - - callback(); - }); - }); - }); - }); + eventlog.add(eventlog.ACTION_DOMAIN_UPDATE, auditSource, { domain, zoneName, provider }); } -function del(domain, auditSource, callback) { +async function del(domain, auditSource) { assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); - if (domain === settings.dashboardDomain()) return callback(new BoxError(BoxError.CONFLICT, 'Cannot remove admin domain')); - if (domain === settings.mailDomain()) return callback(new BoxError(BoxError.CONFLICT, 'Cannot remove mail domain. Change the mail server location first')); + if (domain === settings.dashboardDomain()) throw new BoxError(BoxError.CONFLICT, 'Cannot remove admin domain'); + if (domain === settings.mailDomain()) throw new BoxError(BoxError.CONFLICT, 'Cannot remove mail domain. Change the mail server location first'); - domaindb.del(domain, function (error) { - if (error) return callback(error); + let queries = [ + { query: 'DELETE FROM mail WHERE domain = ?', args: [ domain ] }, + { query: 'DELETE FROM domains WHERE domain = ?', args: [ domain ] }, + ]; - eventlog.add(eventlog.ACTION_DOMAIN_REMOVE, auditSource, { domain }); + const [error, results] = await safe(database.transaction(queries)); + if (error && error.code === 'ER_ROW_IS_REFERENCED_2') { + if (error.message.indexOf('apps_mailDomain_constraint') !== -1) throw new BoxError(BoxError.CONFLICT, 'Domain is in use by an app or the mailbox of an app. Check the domains of apps and the Email section of each app.'); + if (error.message.indexOf('subdomains') !== -1) throw new BoxError(BoxError.CONFLICT, 'Domain is in use by one or more app(s).'); + if (error.message.indexOf('mail') !== -1) throw new BoxError(BoxError.CONFLICT, 'Domain is in use by one or more mailboxes. Delete them first in the Email view.'); + throw new BoxError(BoxError.CONFLICT, error.message); + } + if (error) throw error; + if (results[1].affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Domain not found'); - mail.onDomainRemoved(domain, NOOP_CALLBACK); + eventlog.add(eventlog.ACTION_DOMAIN_REMOVE, auditSource, { domain }); - return callback(null); - }); + mail.onDomainRemoved(domain, NOOP_CALLBACK); } -function clear(callback) { - assert.strictEqual(typeof callback, 'function'); - - domaindb.clear(function (error) { - if (error) return callback(error); - - return callback(null); - }); -} - -// returns the 'name' that needs to be inserted into zone -// eslint-disable-next-line no-unused-vars -function getName(domain, location, type) { - const part = domain.domain.slice(0, -domain.zoneName.length - 1); - - if (location === '') return part; - - return part ? `${location}.${part}` : location; -} - -function getDnsRecords(location, domain, type, callback) { - assert.strictEqual(typeof location, 'string'); - assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof type, 'string'); - assert.strictEqual(typeof callback, 'function'); - - get(domain, function (error, domainObject) { - if (error) return callback(error); - - api(domainObject.provider).get(domainObject, location, type, function (error, values) { - if (error) return callback(error); - - callback(null, values); - }); - }); -} - -function checkDnsRecords(location, domain, callback) { - assert.strictEqual(typeof location, 'string'); - assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof callback, 'function'); - - getDnsRecords(location, domain, 'A', function (error, values) { - if (error) return callback(error); - - sysinfo.getServerIp(function (error, ip) { - if (error) return callback(error); - - if (values.length === 0) return callback(null, { needsOverwrite: false }); // does not exist - if (values[0] === ip) return callback(null, { needsOverwrite: false }); // exists but in sync - - callback(null, { needsOverwrite: true }); - }); - }); -} - -// note: for TXT records the values must be quoted -function upsertDnsRecords(location, domain, type, values, callback) { - assert.strictEqual(typeof location, 'string'); - assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof type, 'string'); - assert(Array.isArray(values)); - assert.strictEqual(typeof callback, 'function'); - - debug('upsertDNSRecord: %s on %s type %s values', location, domain, type, values); - - get(domain, function (error, domainObject) { - if (error) return callback(error); - - api(domainObject.provider).upsert(domainObject, location, type, values, function (error) { - if (error) return callback(error); - - callback(null); - }); - }); -} - -function removeDnsRecords(location, domain, type, values, callback) { - assert.strictEqual(typeof location, 'string'); - assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof type, 'string'); - assert(Array.isArray(values)); - assert.strictEqual(typeof callback, 'function'); - - debug('removeDNSRecord: %s on %s type %s values', location, domain, type, values); - - get(domain, function (error, domainObject) { - if (error) return callback(error); - - api(domainObject.provider).del(domainObject, location, type, values, function (error) { - if (error && error.reason !== BoxError.NOT_FOUND) return callback(error); - - callback(null); - }); - }); -} - -function waitForDnsRecord(location, domain, type, value, options, callback) { - assert.strictEqual(typeof location, 'string'); - assert.strictEqual(typeof domain, 'string'); - assert(type === 'A' || type === 'TXT'); - assert.strictEqual(typeof value, 'string'); - assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } - assert.strictEqual(typeof callback, 'function'); - - get(domain, function (error, domainObject) { - if (error) return callback(error); - - // linode DNS takes ~15mins - if (!options.interval) options.interval = domainObject.provider === 'linode' ? 20000 : 5000; - - api(domainObject.provider).wait(domainObject, location, type, value, options, callback); - }); +async function clear() { + await database.query('DELETE FROM domains'); } // removes all fields that are strictly private and should never be returned by API calls @@ -457,135 +305,3 @@ function removeRestrictedFields(domain) { return result; } - -function makeWildcard(vhost) { - assert.strictEqual(typeof vhost, 'string'); - - // if the vhost is like *.example.com, this function will do nothing - let parts = vhost.split('.'); - parts[0] = '*'; - return parts.join('.'); -} - -function registerLocations(locations, options, progressCallback, callback) { - assert(Array.isArray(locations)); - assert.strictEqual(typeof options, 'object'); - assert.strictEqual(typeof progressCallback, 'function'); - assert.strictEqual(typeof callback, 'function'); - - debug(`registerLocations: Will register ${JSON.stringify(locations)} with options ${JSON.stringify(options)}`); - - const overwriteDns = options.overwriteDns || false; - - sysinfo.getServerIp(function (error, ip) { - if (error) return callback(error); - - async.eachSeries(locations, function (location, iteratorDone) { - async.retry({ times: 200, interval: 5000 }, function (retryCallback) { - progressCallback({ message: `Registering location: ${location.subdomain ? (location.subdomain + '.') : ''}${location.domain}` }); - - // get the current record before updating it - getDnsRecords(location.subdomain, location.domain, 'A', function (error, values) { - if (error && error.reason === BoxError.EXTERNAL_ERROR) return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain: location })); // try again - if (error && error.reason === BoxError.ACCESS_DENIED) return retryCallback(null, new BoxError(BoxError.ACCESS_DENIED, error.message, { domain: location })); - if (error && error.reason === BoxError.NOT_FOUND) return retryCallback(null, new BoxError(BoxError.NOT_FOUND, error.message, { domain: location })); - if (error) return retryCallback(null, new BoxError(BoxError.EXTERNAL_ERROR, error.message, location)); // give up for other errors - - if (values.length !== 0 && values[0] === ip) return retryCallback(null); // up-to-date - - // refuse to update any existing DNS record for custom domains that we did not create - if (values.length !== 0 && !overwriteDns) return retryCallback(null, new BoxError(BoxError.ALREADY_EXISTS, 'DNS Record already exists', { domain: location })); - - upsertDnsRecords(location.subdomain, location.domain, 'A', [ ip ], function (error) { - if (error && (error.reason === BoxError.BUSY || error.reason === BoxError.EXTERNAL_ERROR)) { - progressCallback({ message: `registerSubdomains: Upsert error. Will retry. ${error.message}` }); - return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain: location })); // try again - } - - retryCallback(null, error ? new BoxError(BoxError.EXTERNAL_ERROR, error.message, location) : null); - }); - }); - }, function (error, result) { - if (error || result) return iteratorDone(error || result); - - iteratorDone(null); - }); - }, callback); - }); -} - -function unregisterLocations(locations, progressCallback, callback) { - assert(Array.isArray(locations)); - assert.strictEqual(typeof progressCallback, 'function'); - assert.strictEqual(typeof callback, 'function'); - - sysinfo.getServerIp(function (error, ip) { - if (error) return callback(error); - - async.eachSeries(locations, function (location, iteratorDone) { - async.retry({ times: 30, interval: 5000 }, function (retryCallback) { - progressCallback({ message: `Unregistering location: ${location.subdomain ? (location.subdomain + '.') : ''}${location.domain}` }); - - removeDnsRecords(location.subdomain, location.domain, 'A', [ ip ], function (error) { - if (error && error.reason === BoxError.NOT_FOUND) return retryCallback(null, null); - if (error && (error.reason === BoxError.SBUSY || error.reason === BoxError.EXTERNAL_ERROR)) { - progressCallback({ message: `Error unregistering location. Will retry. ${error.message}`}); - return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain: location })); // try again - } - - retryCallback(null, error ? new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain: location }) : null); - }); - }, function (error, result) { - if (error || result) return iteratorDone(error || result); - - iteratorDone(); - }); - }, callback); - }); -} - -function syncDnsRecords(options, progressCallback, callback) { - assert.strictEqual(typeof options, 'object'); - assert.strictEqual(typeof progressCallback, 'function'); - assert.strictEqual(typeof callback, 'function'); - - if (options.domain && options.type === 'mail') return mail.setDnsRecords(options.domain, callback); - - getAll(function (error, domains) { - if (error) return callback(error); - - if (options.domain) domains = domains.filter(d => d.domain === options.domain); - - const mailSubdomain = settings.mailFqdn().substr(0, settings.mailFqdn().length - settings.mailDomain().length - 1); - - apps.getAll(function (error, allApps) { - if (error) return callback(error); - - let progress = 1, errors = []; - - // we sync by domain only to get some nice progress - async.eachSeries(domains, function (domain, iteratorDone) { - progressCallback({ percent: progress, message: `Updating DNS of ${domain.domain}`}); - progress += Math.round(100/(1+domains.length)); - - let locations = []; - if (domain.domain === settings.dashboardDomain()) locations.push({ subdomain: constants.DASHBOARD_LOCATION, domain: settings.dashboardDomain() }); - if (domain.domain === settings.mailDomain() && settings.mailFqdn() !== settings.dashboardFqdn()) locations.push({ subdomain: mailSubdomain, domain: settings.mailDomain() }); - - allApps.forEach(function (app) { - const appLocations = [{ subdomain: app.location, domain: app.domain }].concat(app.alternateDomains).concat(app.aliasDomains); - locations = locations.concat(appLocations.filter(al => al.domain === domain.domain)); - }); - - async.series([ - registerLocations.bind(null, locations, { overwriteDns: true }, progressCallback), - progressCallback.bind(null, { message: `Updating mail DNS of ${domain.domain}`}), - mail.setDnsRecords.bind(null, domain.domain) - ], function (error) { - if (error) errors.push({ domain: domain.domain, message: error.message }); - iteratorDone(); - }); - }, () => callback(null, { errors })); - }); - }); -} diff --git a/src/dyndns.js b/src/dyndns.js index 5885b4e55..a686181ad 100644 --- a/src/dyndns.js +++ b/src/dyndns.js @@ -9,7 +9,7 @@ let apps = require('./apps.js'), async = require('async'), constants = require('./constants.js'), debug = require('debug')('box:dyndns'), - domains = require('./domains.js'), + dns = require('./dns.js'), eventlog = require('./eventlog.js'), paths = require('./paths.js'), safe = require('safetydance'), @@ -32,7 +32,7 @@ function sync(auditSource, callback) { debug(`refreshDNS: updating ip from ${info.ip} to ${ip}`); - domains.upsertDnsRecords(constants.DASHBOARD_LOCATION, settings.dashboardDomain(), 'A', [ ip ], function (error) { + dns.upsertDnsRecords(constants.DASHBOARD_LOCATION, settings.dashboardDomain(), 'A', [ ip ], function (error) { if (error) return callback(error); debug('refreshDNS: updated admin location'); @@ -44,7 +44,7 @@ function sync(auditSource, callback) { // do not change state of installing apps since apptask will error if dns record already exists if (app.installationState !== apps.ISTATE_INSTALLED) return callback(); - domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], callback); + dns.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], callback); }, function (error) { if (error) return callback(error); diff --git a/src/mail.js b/src/mail.js index 6d4a23331..a7d07ec41 100644 --- a/src/mail.js +++ b/src/mail.js @@ -69,7 +69,7 @@ const assert = require('assert'), constants = require('./constants.js'), database = require('./database.js'), debug = require('debug')('box:mail'), - dns = require('./native-dns.js'), + dns = require('./dns.js'), docker = require('./docker.js'), domains = require('./domains.js'), eventlog = require('./eventlog.js'), @@ -102,6 +102,9 @@ const REMOVE_MAILBOX = path.join(__dirname, 'scripts/rmmailbox.sh'); const MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson', 'dkimSelector', 'bannerJson' ].join(','); +const domainsGet = util.callbackify(domains.get), + domainsList = util.callbackify(domains.list); + function postProcess(data) { data.enabled = !!data.enabled; // int to boolean data.mailFromValidation = !!data.mailFromValidation; // int to boolean @@ -544,7 +547,7 @@ function checkConfiguration(callback) { let messages = {}; - domains.getAll(function (error, allDomains) { + domainsList(function (error, allDomains) { if (error) return callback(error); async.eachSeries(allDomains, function (domainObject, iteratorCallback) { @@ -810,7 +813,7 @@ function txtRecordsWithSpf(domain, mailFqdn, callback) { assert.strictEqual(typeof mailFqdn, 'string'); assert.strictEqual(typeof callback, 'function'); - domains.getDnsRecords('', domain, 'TXT', function (error, txtRecords) { + dns.getDnsRecords('', domain, 'TXT', function (error, txtRecords) { if (error) return callback(error); debug('txtRecordsWithSpf: current txt records - %j', txtRecords); @@ -927,7 +930,7 @@ function upsertDnsRecords(domain, mailFqdn, callback) { if (txtRecords) records.push({ subdomain: '', domain: domain, type: 'TXT', values: txtRecords }); - domains.getDnsRecords('_dmarc', domain, 'TXT', function (error, dmarcRecords) { // only update dmarc if absent. this allows user to set email for reporting + dns.getDnsRecords('_dmarc', domain, 'TXT', function (error, dmarcRecords) { // only update dmarc if absent. this allows user to set email for reporting if (error) return callback(error); if (dmarcRecords.length === 0) records.push({ subdomain: '_dmarc', domain: domain, type: 'TXT', values: [ '"v=DMARC1; p=reject; pct=100"' ] }); @@ -935,7 +938,7 @@ function upsertDnsRecords(domain, mailFqdn, callback) { debug('upsertDnsRecords: will update %j', records); async.mapSeries(records, function (record, iteratorCallback) { - domains.upsertDnsRecords(record.subdomain, record.domain, record.type, record.values, iteratorCallback); + dns.upsertDnsRecords(record.subdomain, record.domain, record.type, record.values, iteratorCallback); }, function (error, changeIds) { if (error) { debug(`upsertDnsRecords: failed to update: ${error}`); @@ -981,7 +984,7 @@ function changeLocation(auditSource, progressCallback, callback) { cloudron.setupDnsAndCert(subdomain, domain, auditSource, progressCallback, function (error) { if (error) return callback(error); - domains.getAll(function (error, allDomains) { + domainsList(function (error, allDomains) { if (error) return callback(error); async.eachOfSeries(allDomains, function (domainObject, idx, iteratorDone) { @@ -1010,10 +1013,10 @@ function setLocation(subdomain, domain, auditSource, callback) { assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); - domains.get(domain, function (error, domainObject) { + domainsGet(domain, function (error, domainObject) { if (error) return callback(error); - const fqdn = domains.fqdn(subdomain, domainObject); + const fqdn = dns.fqdn(subdomain, domainObject); settings.setMailLocation(domain, fqdn, async function (error) { if (error) return callback(error); diff --git a/src/native-dns.js b/src/native-dns.js deleted file mode 100644 index 2ae7464b3..000000000 --- a/src/native-dns.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -exports = module.exports = { - resolve -}; - -var assert = require('assert'), - constants = require('./constants.js'), - dns = require('dns'), - _ = require('underscore'); - -const DEFAULT_OPTIONS = { server: '127.0.0.1', timeout: 5000 }; // unbound runs on 127.0.0.1 - -// a note on TXT records. It doesn't have quotes ("") at the DNS level. Those quotes -// are added for DNS server software to enclose spaces. Such quotes may also be returned -// by the DNS REST API of some providers -function resolve(hostname, rrtype, options, callback) { - assert.strictEqual(typeof hostname, 'string'); - assert.strictEqual(typeof rrtype, 'string'); - assert(options && typeof options === 'object'); - assert.strictEqual(typeof callback, 'function'); - - const resolver = new dns.Resolver(); - options = _.extend({ }, DEFAULT_OPTIONS, options); - - // Only use unbound on a Cloudron - if (constants.CLOUDRON) resolver.setServers([ options.server ]); - - // should callback with ECANCELLED but looks like we might hit https://github.com/nodejs/node/issues/14814 - const timerId = setTimeout(resolver.cancel.bind(resolver), options.timeout || 5000); - - resolver.resolve(hostname, rrtype, function (error, result) { - clearTimeout(timerId); - - if (error && error.code === 'ECANCELLED') error.code = 'TIMEOUT'; - - // result is an empty array if there was no error but there is no record. when you query a random - // domain, it errors with ENOTFOUND. But if you query an existing domain (A record) but with different - // type (CNAME) it is not an error and empty array - // for TXT records, result is 2d array of strings - callback(error, result); - }); -} diff --git a/src/reverseproxy.js b/src/reverseproxy.js index 8c97d444e..c645d2bad 100644 --- a/src/reverseproxy.js +++ b/src/reverseproxy.js @@ -38,6 +38,7 @@ const acme2 = require('./acme2.js'), constants = require('./constants.js'), crypto = require('crypto'), debug = require('debug')('box:reverseproxy'), + dns = require('./dns.js'), domains = require('./domains.js'), ejs = require('ejs'), eventlog = require('./eventlog.js'), @@ -57,6 +58,8 @@ const acme2 = require('./acme2.js'), const NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/nginxconfig.ejs', { encoding: 'utf8' }); const RESTART_SERVICE_CMD = path.join(__dirname, 'scripts/restartservice.sh'); +const domainsGet = util.callbackify(domains.get), + domainsList = util.callbackify(domains.list); function nginxLocation(s) { if (!s.startsWith('!')) return s; @@ -159,7 +162,7 @@ function validateCertificate(location, domainObject, certificate) { if (cert && !key) return new BoxError(BoxError.BAD_FIELD, 'missing key', { field: 'key' }); // -checkhost checks for SAN or CN exclusively. SAN takes precedence and if present, ignores the CN. - const fqdn = domains.fqdn(location, domainObject); + const fqdn = dns.fqdn(location, domainObject); let result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${fqdn}"`, { encoding: 'utf8', input: cert }); if (result === null) return new BoxError(BoxError.BAD_FIELD, 'Unable to get certificate subject:' + safe.error.message, { field: 'cert' }); @@ -224,24 +227,23 @@ function generateFallbackCertificateSync(domain) { return { cert: cert, key: key, error: null }; } -function setFallbackCertificate(domain, fallback, callback) { +async function setFallbackCertificate(domain, fallback) { assert.strictEqual(typeof domain, 'string'); assert(fallback && typeof fallback === 'object'); assert.strictEqual(typeof fallback, 'object'); - assert.strictEqual(typeof callback, 'function'); debug(`setFallbackCertificate: setting certs for domain ${domain}`); - if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message)); - if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`), fallback.key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message)); + if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`), fallback.cert)) throw new BoxError(BoxError.FS_ERROR, safe.error.message); + if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`), fallback.key)) throw new BoxError(BoxError.FS_ERROR, safe.error.message); // TODO: maybe the cert is being used by the mail container - reload(callback); + await util.promisify(reload)(); } function restoreFallbackCertificates(callback) { assert.strictEqual(typeof callback, 'function'); - domains.getAll(function (error, result) { + domainsList(function (error, result) { if (error) return callback(error); result.forEach(function (domain) { @@ -278,7 +280,7 @@ function getAcmeCertificatePathSync(vhost, domainObject) { let certName, certFilePath, keyFilePath, csrFilePath, acmeChallengesDir = paths.ACME_CHALLENGES_DIR; if (vhost !== domainObject.domain && domainObject.tlsConfig.wildcard) { // bare domain is not part of wildcard SAN - certName = domains.makeWildcard(vhost).replace('*.', '_.'); + certName = dns.makeWildcard(vhost).replace('*.', '_.'); certFilePath = path.join(paths.NGINX_CERT_DIR, `${certName}.cert`); keyFilePath = path.join(paths.NGINX_CERT_DIR, `${certName}.key`); csrFilePath = path.join(paths.NGINX_CERT_DIR, `${certName}.csr`); @@ -298,7 +300,7 @@ function setAppCertificate(location, domainObject, certificate, callback) { assert.strictEqual(typeof certificate, 'object'); assert.strictEqual(typeof callback, 'function'); - const fqdn = domains.fqdn(location, domainObject); + const fqdn = dns.fqdn(location, domainObject); const { certFilePath, keyFilePath } = getAppCertificatePathSync(fqdn); if (certificate.cert && certificate.key) { @@ -321,7 +323,7 @@ function getCertificatePath(fqdn, domain, callback) { // 2. if using fallback provider, return that cert // 3. look for LE certs - domains.get(domain, function (error, domainObject) { + domainsGet(domain, function (error, domainObject) { if (error) return callback(error); const appCertPath = getAppCertificatePathSync(fqdn); // user cert always wins @@ -398,7 +400,7 @@ function ensureCertificate(vhost, domain, auditSource, callback) { assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); - domains.get(domain, async function (error, domainObject) { + domainsGet(domain, async function (error, domainObject) { if (error) return callback(error); let bundle; @@ -482,10 +484,10 @@ function writeDashboardConfig(domain, callback) { debug(`writeDashboardConfig: writing admin config for ${domain}`); - domains.get(domain, function (error, domainObject) { + domainsGet(domain, function (error, domainObject) { if (error) return callback(error); - const dashboardFqdn = domains.fqdn(constants.DASHBOARD_LOCATION, domainObject); + const dashboardFqdn = dns.fqdn(constants.DASHBOARD_LOCATION, domainObject); getCertificatePath(dashboardFqdn, domainObject.domain, function (error, bundle) { if (error) return callback(error); diff --git a/src/routes/domains.js b/src/routes/domains.js index 7f669b049..c1052edbe 100644 --- a/src/routes/domains.js +++ b/src/routes/domains.js @@ -3,7 +3,7 @@ exports = module.exports = { add, get, - getAll, + list, update, del, @@ -13,11 +13,13 @@ exports = module.exports = { const assert = require('assert'), auditSource = require('../auditsource.js'), BoxError = require('../boxerror.js'), + dns = require('../dns.js'), domains = require('../domains.js'), HttpError = require('connect-lastmile').HttpError, - HttpSuccess = require('connect-lastmile').HttpSuccess; + HttpSuccess = require('connect-lastmile').HttpSuccess, + safe = require('safetydance'); -function add(req, res, next) { +async function add(req, res, next) { assert.strictEqual(typeof req.body, 'object'); if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string')); @@ -50,34 +52,31 @@ function add(req, res, next) { tlsConfig: req.body.tlsConfig || { provider: 'letsencrypt-prod' } }; - domains.add(req.body.domain, data, auditSource.fromRequest(req), function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(domains.add(req.body.domain, data, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(201, {})); - }); + next(new HttpSuccess(201, {})); } -function get(req, res, next) { +async function get(req, res, next) { assert.strictEqual(typeof req.params.domain, 'string'); - domains.get(req.params.domain, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(domains.get(req.params.domain)); + if (error) return next(BoxError.toHttpError(error)); + if (!result) return next(new HttpError(404, 'Domain not found')); - next(new HttpSuccess(200, domains.removePrivateFields(result))); - }); + next(new HttpSuccess(200, domains.removePrivateFields(result))); } -function getAll(req, res, next) { - domains.getAll(function (error, result) { - if (error) return next(new HttpError(500, error)); +async function list(req, res, next) { + let [error, results] = await safe(domains.list()); + if (error) return next(new HttpError(500, error)); - result = result.map(domains.removeRestrictedFields); - - next(new HttpSuccess(200, { domains: result })); - }); + results = results.map(domains.removeRestrictedFields); + next(new HttpSuccess(200, { domains: results })); } -function update(req, res, next) { +async function update(req, res, next) { assert.strictEqual(typeof req.params.domain, 'string'); assert.strictEqual(typeof req.body, 'object'); @@ -118,21 +117,19 @@ function update(req, res, next) { wellKnown: req.body.wellKnown || null }; - domains.update(req.params.domain, data, auditSource.fromRequest(req), function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(domains.update(req.params.domain, data, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(204, {})); - }); + next(new HttpSuccess(204, {})); } -function del(req, res, next) { +async function del(req, res, next) { assert.strictEqual(typeof req.params.domain, 'string'); - domains.del(req.params.domain, auditSource.fromRequest(req), function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(domains.del(req.params.domain, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(204)); - }); + next(new HttpSuccess(204)); } function checkDnsRecords(req, res, next) { @@ -143,7 +140,7 @@ function checkDnsRecords(req, res, next) { // some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26) req.clearTimeout(); - domains.checkDnsRecords(req.query.subdomain, req.params.domain, function (error, result) { + dns.checkDnsRecords(req.query.subdomain, req.params.domain, function (error, result) { if (error && error.reason === BoxError.ACCESS_DENIED) return next(new HttpSuccess(200, { error: { reason: error.reason, message: error.message }})); if (error) return next(BoxError.toHttpError(error)); diff --git a/src/routes/test/mail-test.js b/src/routes/test/mail-test.js index 7822ac4cc..b53be74ed 100644 --- a/src/routes/test/mail-test.js +++ b/src/routes/test/mail-test.js @@ -48,7 +48,7 @@ describe('Mail API', function () { let dkimDomain, spfDomain, mxDomain, dmarcDomain; before(function (done) { - const dns = require('../../native-dns.js'); + const dns = require('../../dns.js'); // replace dns resolveTxt() resolve = dns.resolve; @@ -79,7 +79,7 @@ describe('Mail API', function () { }); after(function (done) { - var dns = require('../../native-dns.js'); + const dns = require('../../dns.js'); dns.resolve = resolve; diff --git a/src/routes/test/tasks-test.js b/src/routes/test/tasks-test.js index f3a02dfda..0b944706c 100644 --- a/src/routes/test/tasks-test.js +++ b/src/routes/test/tasks-test.js @@ -1,6 +1,5 @@ 'use strict'; -const { resolve } = require('../../native-dns.js'); /* global it:false */ /* global describe:false */ /* global before:false */ diff --git a/src/server.js b/src/server.js index ca32efdca..00747b689 100644 --- a/src/server.js +++ b/src/server.js @@ -297,7 +297,7 @@ function initializeExpressSync() { // domain routes router.post('/api/v1/domains', json, token, authorizeAdmin, routes.domains.add); - router.get ('/api/v1/domains', token, routes.domains.getAll); + router.get ('/api/v1/domains', token, routes.domains.list); router.get ('/api/v1/domains/:domain', token, authorizeAdmin, routes.domains.get); // this is manage scope because it returns non-restricted fields router.put ('/api/v1/domains/:domain', json, token, authorizeAdmin, routes.domains.update); router.del ('/api/v1/domains/:domain', token, authorizeAdmin, routes.domains.del); diff --git a/src/taskworker.js b/src/taskworker.js index d151fba60..0e2ef7b85 100755 --- a/src/taskworker.js +++ b/src/taskworker.js @@ -8,7 +8,7 @@ const apptask = require('./apptask.js'), backuptask = require('./backuptask.js'), cloudron = require('./cloudron.js'), database = require('./database.js'), - domains = require('./domains.js'), + dns = require('./dns.js'), externalLdap = require('./externalldap.js'), fs = require('fs'), mail = require('./mail.js'), @@ -27,7 +27,7 @@ const TASKS = { // indexed by task type cleanBackups: backupCleaner.run, syncExternalLdap: externalLdap.sync, changeMailLocation: mail.changeLocation, - syncDnsRecords: domains.syncDnsRecords, + syncDnsRecords: dns.syncDnsRecords, _identity: (arg, progressCallback, callback) => callback(null, arg), _error: (arg, progressCallback, callback) => callback(new Error(`Failed for arg: ${arg}`)), diff --git a/src/test/apptask-test.js b/src/test/apptask-test.js index 79db9997f..dcdb5c800 100644 --- a/src/test/apptask-test.js +++ b/src/test/apptask-test.js @@ -12,40 +12,40 @@ const apptask = require('../apptask.js'), paths = require('../paths.js'), _ = require('underscore'); -const { APP } = common; - describe('apptask', function () { - before(common.setup); - after(common.cleanup); + const { setup, cleanup, app } = common; + + before(setup); + after(cleanup); it('create volume', function (done) { - apptask._createAppDir(APP, function (error) { - expect(fs.existsSync(paths.APPS_DATA_DIR + '/' + APP.id)).to.be(true); - expect(fs.existsSync(paths.APPS_DATA_DIR + '/' + APP.id + '/data')).to.be(false); + apptask._createAppDir(app, function (error) { + expect(fs.existsSync(paths.APPS_DATA_DIR + '/' + app.id)).to.be(true); + expect(fs.existsSync(paths.APPS_DATA_DIR + '/' + app.id + '/data')).to.be(false); expect(error).to.be(null); done(); }); }); it('delete volume - removeDirectory (false) ', function (done) { - apptask._deleteAppDir(APP, { removeDirectory: false }, function (error) { - expect(fs.existsSync(paths.APPS_DATA_DIR + '/' + APP.id)).to.be(true); - expect(fs.readdirSync(paths.APPS_DATA_DIR + '/' + APP.id).length).to.be(0); // empty + apptask._deleteAppDir(app, { removeDirectory: false }, function (error) { + expect(fs.existsSync(paths.APPS_DATA_DIR + '/' + app.id)).to.be(true); + expect(fs.readdirSync(paths.APPS_DATA_DIR + '/' + app.id).length).to.be(0); // empty expect(error).to.be(null); done(); }); }); it('delete volume - removeDirectory (true) ', function (done) { - apptask._deleteAppDir(APP, { removeDirectory: true }, function (error) { - expect(!fs.existsSync(paths.APPS_DATA_DIR + '/' + APP.id)).to.be(true); + apptask._deleteAppDir(app, { removeDirectory: true }, function (error) { + expect(!fs.existsSync(paths.APPS_DATA_DIR + '/' + app.id)).to.be(true); expect(error).to.be(null); done(); }); }); it('barfs on empty manifest', function (done) { - var badApp = _.extend({ }, APP); + var badApp = _.extend({ }, app); badApp.manifest = { }; apptask._verifyManifest(badApp.manifest, function (error) { @@ -55,8 +55,8 @@ describe('apptask', function () { }); it('fails on bad manifest', function (done) { - var badApp = _.extend({ }, APP); - badApp.manifest = _.extend({ }, APP.manifest); + var badApp = _.extend({ }, app); + badApp.manifest = _.extend({ }, app.manifest); delete badApp.manifest.httpPort; apptask._verifyManifest(badApp.manifest, function (error) { @@ -66,8 +66,8 @@ describe('apptask', function () { }); it('barfs on incompatible manifest', function (done) { - var badApp = _.extend({ }, APP); - badApp.manifest = _.extend({ }, APP.manifest); + var badApp = _.extend({ }, app); + badApp.manifest = _.extend({ }, app.manifest); badApp.manifest.maxBoxVersion = '0.0.0'; // max box version is too small apptask._verifyManifest(badApp.manifest, function (error) { @@ -77,7 +77,7 @@ describe('apptask', function () { }); it('verifies manifest', function (done) { - var goodApp = _.extend({ }, APP); + var goodApp = _.extend({ }, app); apptask._verifyManifest(goodApp.manifest, function (error) { expect(error).to.be(null); diff --git a/src/test/common.js b/src/test/common.js index 434a2a36a..79141b842 100644 --- a/src/test/common.js +++ b/src/test/common.js @@ -116,19 +116,20 @@ const app = { exports = module.exports = { createTree, domainSetup, + databaseSetup, setup, cleanup, checkMails, clearMailQueue, mockApiServerOrigin: 'http://localhost:6060', - dashboardDomain: 'test.example.com', - dashboardFqdn: 'my.test.example.com', + dashboardDomain: domain.domain, + dashboardFqdn: `my.${domain.domain}`, app, admin, auditSource, - domain, + domain, // the domain object manifest, user, appstoreToken: 'atoken', @@ -161,7 +162,7 @@ function createTree(root, obj) { createSubTree(obj, root); } -function domainSetup(done) { +function databaseSetup(done) { nock.cleanAll(); async.series([ @@ -171,6 +172,14 @@ function domainSetup(done) { settings.setDashboardLocation.bind(null, exports.dashboardDomain, exports.dashboardFqdn), settings.initCache, blobs.initSecrets, + ], done); +} + +function domainSetup(done) { + nock.cleanAll(); + + async.series([ + databaseSetup, domains.add.bind(null, domain.domain, domain, auditSource), ], done); } diff --git a/src/test/database-test.js b/src/test/database-test.js index 8c661fa73..a8782121b 100644 --- a/src/test/database-test.js +++ b/src/test/database-test.js @@ -10,7 +10,7 @@ const appdb = require('../appdb.js'), async = require('async'), BoxError = require('../boxerror.js'), database = require('../database'), - domaindb = require('../domaindb'), + domains = require('../domains.js'), expect = require('expect.js'), mailboxdb = require('../mailboxdb.js'), reverseProxy = require('../reverseproxy.js'), @@ -27,6 +27,8 @@ const DOMAIN_0 = { }; DOMAIN_0.fallbackCertificate = reverseProxy.generateFallbackCertificateSync(DOMAIN_0.domain); +const auditSource = { ip: '1.2.3.4' }; + const DOMAIN_1 = { domain: 'foo.cloudron.io', zoneName: 'cloudron.io', @@ -52,158 +54,6 @@ describe('database', function () { ], done); }); - describe('domains', function () { - after(function (done) { - database._clear(done); - }); - - it('can add domain', function (done) { - domaindb.add(DOMAIN_0.domain, DOMAIN_0, done); - }); - - it('can add another domain', function (done) { - domaindb.add(DOMAIN_1.domain, DOMAIN_1, done); - }); - - it('cannot add same domain twice', function (done) { - domaindb.add(DOMAIN_0.domain, DOMAIN_0, function (error) { - expect(error).to.be.ok(); - expect(error.reason).to.be(BoxError.ALREADY_EXISTS); - done(); - }); - }); - - it('can get domain', function (done) { - domaindb.get(DOMAIN_0.domain, function (error, result) { - expect(error).to.equal(null); - expect(result).to.be.an('object'); - expect(result.domain).to.equal(DOMAIN_0.domain); - expect(result.zoneName).to.equal(DOMAIN_0.zoneName); - expect(result.config).to.eql(DOMAIN_0.config); - - done(); - }); - }); - - it('can update domain', function (done) { - const newConfig = { provider: 'manual' }; - const newTlsConfig = { provider: 'foobar' }; - - domaindb.update(DOMAIN_1.domain, { provider: DOMAIN_1.provider, config: newConfig, tlsConfig: newTlsConfig }, function (error) { - expect(error).to.equal(null); - - domaindb.get(DOMAIN_1.domain, function (error, result) { - expect(error).to.equal(null); - expect(result).to.be.an('object'); - expect(result.domain).to.equal(DOMAIN_1.domain); - expect(result.zoneName).to.equal(DOMAIN_1.zoneName); - expect(result.provider).to.equal(DOMAIN_1.provider); - expect(result.config).to.eql(newConfig); - expect(result.tlsConfig).to.eql(newTlsConfig); - - DOMAIN_1.config = newConfig; - DOMAIN_1.tlsConfig = newTlsConfig; - - done(); - }); - }); - }); - - it('can get all domains', function (done) { - domaindb.getAll(function (error, result) { - expect(error).to.equal(null); - expect(result).to.be.an('array'); - expect(result.length).to.equal(2); - - // sorted by domain - expect(result[0].domain).to.equal(DOMAIN_1.domain); - expect(result[0].zoneName).to.equal(DOMAIN_1.zoneName); - expect(result[0].provider).to.equal(DOMAIN_1.provider); - expect(result[0].config).to.eql(DOMAIN_1.config); - expect(result[0].tlsConfig).to.eql(DOMAIN_1.tlsConfig); - - expect(result[1].domain).to.equal(DOMAIN_0.domain); - expect(result[1].zoneName).to.equal(DOMAIN_0.zoneName); - expect(result[1].provider).to.equal(DOMAIN_0.provider); - expect(result[1].config).to.eql(DOMAIN_0.config); - expect(result[1].tlsConfig).to.eql(DOMAIN_0.tlsConfig); - - done(); - }); - }); - - it('cannot delete non-existing domain', function (done) { - domaindb.del('not.exists', function (error) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.equal(BoxError.NOT_FOUND); - - done(); - }); - }); - - var APP_0 = { - id: 'appid-0', - appStoreId: 'appStoreId-0', - installationState: apps.ISTATE_PENDING_INSTALL, - error: null, - runState: 'running', - location: 'some-location-0', - domain: DOMAIN_0.domain, - manifest: { version: '0.1', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0' }, - containerId: null, - containerIp: null, - portBindings: { port: { hostPort: 5678, type: 'tcp' } }, - health: null, - accessRestriction: null, - lastBackupId: null, - memoryLimit: 4294967296, - cpuShares: 1024, - sso: true, - debugMode: null, - reverseProxyConfig: {}, - enableBackup: true, - env: {}, - mailboxName: 'talktome', - mailboxDomain: DOMAIN_0.domain, - enableAutomaticUpdate: true, - dataDir: null, - tags: [], - label: null, - taskId: null, - mounts: [], - proxyAuth: false, - servicesConfig: {}, - hasIcon: false, - hasAppStoreIcon: false - }; - - it('cannot delete referenced domain', function (done) { - appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.portBindings, APP_0, function (error) { - expect(error).to.be(null); - - domaindb.del(DOMAIN_0.domain, function (error) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.equal(BoxError.CONFLICT); - - appdb.del(APP_0.id, done); - }); - }); - }); - - it('can delete existing domain', function (done) { - domaindb.del(DOMAIN_0.domain, function (error) { - expect(error).to.be(null); - - domaindb.get(DOMAIN_0.domain, function (error) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.equal(BoxError.NOT_FOUND); - - done(); - }); - }); - }); - }); - describe('apps', function () { var APP_0 = { id: 'appid-0', @@ -286,7 +136,7 @@ describe('database', function () { before(function (done) { async.series([ database._clear, - domaindb.add.bind(null, DOMAIN_0.domain, DOMAIN_0) + domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, auditSource) ], done); }); @@ -626,7 +476,7 @@ describe('database', function () { describe('mailboxes', function () { before(function (done) { async.series([ - domaindb.add.bind(null, DOMAIN_0.domain, DOMAIN_0), + domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0), ], done); }); diff --git a/src/test/dns-providers-test.js b/src/test/dns-providers-test.js new file mode 100644 index 000000000..377216e15 --- /dev/null +++ b/src/test/dns-providers-test.js @@ -0,0 +1,1269 @@ +/* jslint node:true */ +/* global it:false */ +/* global describe:false */ +/* global before:false */ +/* global beforeEach:false */ +/* global after:false */ + +'use strict'; + +const AWS = require('aws-sdk'), + common = require('./common.js'), + dns = require('../dns.js'), + domains = require('../domains.js'), + expect = require('expect.js'), + GCDNS = require('@google-cloud/dns').DNS, + nock = require('nock'), + _ = require('underscore'); + +describe('dns provider', function () { + const { setup, cleanup, auditSource, domain } = common; + const domainCopy = Object.assign({}, domain); // make a copy + + before(setup); + after(cleanup); + + describe('noop', function () { + before(async function () { + domainCopy.provider = 'noop'; + domainCopy.config = {}; + + await domains.update(domainCopy.domain, domainCopy, auditSource); + }); + + it('upsert succeeds', function (done) { + dns.upsertDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { + expect(error).to.eql(null); + + done(); + }); + }); + + it('get succeeds', function (done) { + dns.getDnsRecords('test', domainCopy.domain, 'A', function (error, result) { + expect(error).to.eql(null); + expect(result).to.be.an(Array); + expect(result.length).to.eql(0); + + done(); + }); + }); + + it('del succeeds', function (done) { + dns.removeDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { + expect(error).to.eql(null); + + done(); + }); + }); + }); + + describe('digitalocean', function () { + let TOKEN = 'sometoken'; + let DIGITALOCEAN_ENDPOINT = 'https://api.digitalocean.com'; + + before(async function () { + domainCopy.provider = 'digitalocean'; + domainCopy.config = { + token: TOKEN + }; + + await domains.update(domainCopy.domain, domainCopy, auditSource); + }); + + it('upsert non-existing record succeeds', function (done) { + nock.cleanAll(); + + let DOMAIN_RECORD_0 = { + id: 3352892, + type: 'A', + name: '@', + data: '1.2.3.4', + priority: null, + port: null, + weight: null + }; + + let req1 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) + .get('/v2/domains/' + domainCopy.zoneName + '/records') + .reply(200, { domain_records: [] }); + let req2 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) + .post('/v2/domains/' + domainCopy.zoneName + '/records') + .reply(201, { domain_record: DOMAIN_RECORD_0 }); + + dns.upsertDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { + expect(error).to.eql(null); + expect(req1.isDone()).to.be.ok(); + expect(req2.isDone()).to.be.ok(); + + done(); + }); + }); + + it('upsert existing record succeeds', function (done) { + nock.cleanAll(); + + let DOMAIN_RECORD_0 = { + id: 3352892, + type: 'A', + name: '@', + data: '1.2.3.4', + priority: null, + port: null, + weight: null + }; + + let DOMAIN_RECORD_1 = { + id: 3352893, + type: 'A', + name: 'test', + data: '1.2.3.4', + priority: null, + port: null, + weight: null + }; + + let DOMAIN_RECORD_1_NEW = { + id: 3352893, + type: 'A', + name: 'test', + data: '1.2.3.5', + priority: null, + port: null, + weight: null + }; + + let req1 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) + .get('/v2/domains/' + domainCopy.zoneName + '/records') + .reply(200, { domain_records: [DOMAIN_RECORD_0, DOMAIN_RECORD_1] }); + let req2 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) + .put('/v2/domains/' + domainCopy.zoneName + '/records/' + DOMAIN_RECORD_1.id) + .reply(200, { domain_record: DOMAIN_RECORD_1_NEW }); + + dns.upsertDnsRecords('test', domainCopy.domain, 'A', [DOMAIN_RECORD_1_NEW.data], function (error) { + expect(error).to.eql(null); + expect(req1.isDone()).to.be.ok(); + expect(req2.isDone()).to.be.ok(); + + done(); + }); + }); + + it('upsert multiple record succeeds', function (done) { + nock.cleanAll(); + + let DOMAIN_RECORD_0 = { + id: 3352892, + type: 'A', + name: '@', + data: '1.2.3.4', + priority: null, + port: null, + weight: null + }; + + let DOMAIN_RECORD_1 = { + id: 3352893, + type: 'TXT', + name: '@', + data: '1.2.3.4', + priority: null, + port: null, + weight: null + }; + + let DOMAIN_RECORD_1_NEW = { + id: 3352893, + type: 'TXT', + name: '@', + data: 'somethingnew', + priority: null, + port: null, + weight: null + }; + + let DOMAIN_RECORD_2 = { + id: 3352894, + type: 'TXT', + name: '@', + data: 'something', + priority: null, + port: null, + weight: null + }; + + let DOMAIN_RECORD_2_NEW = { + id: 3352894, + type: 'TXT', + name: '@', + data: 'somethingnew', + priority: null, + port: null, + weight: null + }; + + let DOMAIN_RECORD_3_NEW = { + id: 3352895, + type: 'TXT', + name: '@', + data: 'thirdnewone', + priority: null, + port: null, + weight: null + }; + + let req1 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) + .get('/v2/domains/' + domainCopy.zoneName + '/records') + .reply(200, { domain_records: [DOMAIN_RECORD_0, DOMAIN_RECORD_1, DOMAIN_RECORD_2] }); + let req2 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) + .put('/v2/domains/' + domainCopy.zoneName + '/records/' + DOMAIN_RECORD_1.id) + .reply(200, { domain_record: DOMAIN_RECORD_1_NEW }); + let req3 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) + .put('/v2/domains/' + domainCopy.zoneName + '/records/' + DOMAIN_RECORD_2.id) + .reply(200, { domain_record: DOMAIN_RECORD_2_NEW }); + let req4 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) + .post('/v2/domains/' + domainCopy.zoneName + '/records') + .reply(201, { domain_record: DOMAIN_RECORD_2_NEW }); + + dns.upsertDnsRecords('', domainCopy.domain, 'TXT', [DOMAIN_RECORD_2_NEW.data, DOMAIN_RECORD_1_NEW.data, DOMAIN_RECORD_3_NEW.data], function (error) { + expect(error).to.eql(null); + expect(req1.isDone()).to.be.ok(); + expect(req2.isDone()).to.be.ok(); + expect(req3.isDone()).to.be.ok(); + expect(req4.isDone()).to.be.ok(); + + done(); + }); + }); + + it('get succeeds', function (done) { + nock.cleanAll(); + + let DOMAIN_RECORD_0 = { + id: 3352892, + type: 'A', + name: '@', + data: '1.2.3.4', + priority: null, + port: null, + weight: null + }; + + let DOMAIN_RECORD_1 = { + id: 3352893, + type: 'A', + name: 'test', + data: '1.2.3.4', + priority: null, + port: null, + weight: null + }; + + let req1 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) + .get('/v2/domains/' + domainCopy.zoneName + '/records') + .reply(200, { domain_records: [DOMAIN_RECORD_0, DOMAIN_RECORD_1] }); + + dns.getDnsRecords('test', domainCopy.domain, 'A', function (error, result) { + expect(error).to.eql(null); + expect(result).to.be.an(Array); + expect(result.length).to.eql(1); + expect(result[0]).to.eql(DOMAIN_RECORD_1.data); + expect(req1.isDone()).to.be.ok(); + + done(); + }); + }); + + it('del succeeds', function (done) { + nock.cleanAll(); + + let DOMAIN_RECORD_0 = { + id: 3352892, + type: 'A', + name: '@', + data: '1.2.3.4', + priority: null, + port: null, + weight: null + }; + + let DOMAIN_RECORD_1 = { + id: 3352893, + type: 'A', + name: 'test', + data: '1.2.3.4', + priority: null, + port: null, + weight: null + }; + + let req1 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) + .get('/v2/domains/' + domainCopy.zoneName + '/records') + .reply(200, { domain_records: [DOMAIN_RECORD_0, DOMAIN_RECORD_1] }); + let req2 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) + .delete('/v2/domains/' + domainCopy.zoneName + '/records/' + DOMAIN_RECORD_1.id) + .reply(204, {}); + + dns.removeDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { + expect(error).to.eql(null); + expect(req1.isDone()).to.be.ok(); + expect(req2.isDone()).to.be.ok(); + + done(); + }); + }); + }); + + describe('godaddy', function () { + let KEY = 'somekey', SECRET = 'somesecret'; + let GODADDY_API = 'https://api.godaddy.com/v1/domains'; + + before(async function () { + domainCopy.provider = 'godaddy'; + domainCopy.config = { + apiKey: KEY, + apiSecret: SECRET + }; + + await domains.update(domainCopy.domain, domainCopy, auditSource); + }); + + it('upsert record succeeds', function (done) { + nock.cleanAll(); + + let DOMAIN_RECORD_0 = [{ + ttl: 600, + data: '1.2.3.4' + }]; + + let req1 = nock(GODADDY_API) + .put('/' + domainCopy.zoneName + '/records/A/test', DOMAIN_RECORD_0) + .reply(200, {}); + + dns.upsertDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { + expect(error).to.eql(null); + expect(req1.isDone()).to.be.ok(); + + done(); + }); + }); + + it('get succeeds', function (done) { + nock.cleanAll(); + + let DOMAIN_RECORD_0 = [{ + ttl: 600, + data: '1.2.3.4' + }]; + + let req1 = nock(GODADDY_API) + .get('/' + domainCopy.zoneName + '/records/A/test') + .reply(200, DOMAIN_RECORD_0); + + dns.getDnsRecords('test', domainCopy.domain, 'A', function (error, result) { + expect(error).to.eql(null); + expect(result).to.be.an(Array); + expect(result.length).to.eql(1); + expect(result[0]).to.eql(DOMAIN_RECORD_0[0].data); + expect(req1.isDone()).to.be.ok(); + + done(); + }); + }); + + it('del succeeds', function (done) { + nock.cleanAll(); + + let DOMAIN_RECORD_0 = [{ // existing + ttl: 600, + data: '1.2.3.4' + }]; + + let DOMAIN_RECORD_1 = [{ // replaced + ttl: 600, + data: '0.0.0.0' + }]; + + let req1 = nock(GODADDY_API) + .get('/' + domainCopy.zoneName + '/records/A/test') + .reply(200, DOMAIN_RECORD_0); + + let req2 = nock(GODADDY_API) + .put('/' + domainCopy.zoneName + '/records/A/test', DOMAIN_RECORD_1) + .reply(200, {}); + + dns.removeDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { + expect(error).to.eql(null); + expect(req1.isDone()).to.be.ok(); + expect(req2.isDone()).to.be.ok(); + + done(); + }); + }); + }); + + describe('gandi', function () { + let TOKEN = 'sometoken'; + let GANDI_API = 'https://dns.api.gandi.net/api/v5'; + + before(async function () { + domainCopy.provider = 'gandi'; + domainCopy.config = { + token: TOKEN + }; + + await domains.update(domainCopy.domain, domainCopy, auditSource); + }); + + it('upsert record succeeds', function (done) { + nock.cleanAll(); + + let DOMAIN_RECORD_0 = { + 'rrset_ttl': 300, + 'rrset_values': ['1.2.3.4'] + }; + + let req1 = nock(GANDI_API) + .put('/domains/' + domainCopy.zoneName + '/records/test/A', DOMAIN_RECORD_0) + .reply(201, { message: 'Zone Record Created' }); + + dns.upsertDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { + expect(error).to.eql(null); + expect(req1.isDone()).to.be.ok(); + + done(); + }); + }); + + it('get succeeds', function (done) { + nock.cleanAll(); + + let DOMAIN_RECORD_0 = { + 'rrset_type': 'A', + 'rrset_ttl': 600, + 'rrset_name': 'test', + 'rrset_values': ['1.2.3.4'] + }; + + let req1 = nock(GANDI_API) + .get('/domains/' + domainCopy.zoneName + '/records/test/A') + .reply(200, DOMAIN_RECORD_0); + + dns.getDnsRecords('test', domainCopy.domain, 'A', function (error, result) { + expect(error).to.eql(null); + expect(result).to.be.an(Array); + expect(result.length).to.eql(1); + expect(result[0]).to.eql(DOMAIN_RECORD_0.rrset_values[0]); + expect(req1.isDone()).to.be.ok(); + + done(); + }); + }); + + it('del succeeds', function (done) { + nock.cleanAll(); + + let req2 = nock(GANDI_API) + .delete('/domains/' + domainCopy.zoneName + '/records/test/A') + .reply(204, {}); + + dns.removeDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { + expect(error).to.eql(null); + expect(req2.isDone()).to.be.ok(); + + done(); + }); + }); + }); + + describe('name.com', function () { + const TOKEN = 'sometoken'; + const NAMECOM_API = 'https://api.name.com/v4'; + + before(async function () { + domainCopy.provider = 'namecom'; + domainCopy.config = { + username: 'fake', + token: TOKEN + }; + + await domains.update(domainCopy.domain, domainCopy, auditSource); + }); + + it('upsert record succeeds', function (done) { + nock.cleanAll(); + + let DOMAIN_RECORD_0 = { + host: 'test', + type: 'A', + answer: '1.2.3.4', + ttl: 300 + }; + + let req1 = nock(NAMECOM_API) + .get(`/domains/${domainCopy.zoneName}/records`) + .reply(200, { records: [] }); + + let req2 = nock(NAMECOM_API) + .post(`/domains/${domainCopy.zoneName}/records`, DOMAIN_RECORD_0) + .reply(200, {}); + + dns.upsertDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { + expect(error).to.eql(null); + expect(req1.isDone()).to.be.ok(); + expect(req2.isDone()).to.be.ok(); + + done(); + }); + }); + + it('get succeeds', function (done) { + nock.cleanAll(); + + let DOMAIN_RECORD_0 = { + host: 'test', + type: 'A', + answer: '1.2.3.4', + ttl: 300 + }; + + let req1 = nock(NAMECOM_API) + .get(`/domains/${domainCopy.zoneName}/records`) + .reply(200, { records: [DOMAIN_RECORD_0] }); + + dns.getDnsRecords('test', domainCopy.domain, 'A', function (error, result) { + expect(error).to.eql(null); + expect(result).to.be.an(Array); + expect(result.length).to.eql(1); + expect(result[0]).to.eql(DOMAIN_RECORD_0.answer); + expect(req1.isDone()).to.be.ok(); + + done(); + }); + }); + + it('del succeeds', function (done) { + nock.cleanAll(); + + let DOMAIN_RECORD_0 = { + id: 'someid', + host: 'test', + type: 'A', + answer: '1.2.3.4', + ttl: 300 + }; + + let req1 = nock(NAMECOM_API) + .get(`/domains/${domainCopy.zoneName}/records`) + .reply(200, { records: [DOMAIN_RECORD_0] }); + + let req2 = nock(NAMECOM_API) + .delete(`/domains/${domainCopy.zoneName}/records/${DOMAIN_RECORD_0.id}`) + .reply(200, {}); + + dns.removeDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { + expect(error).to.eql(null); + expect(req1.isDone()).to.be.ok(); + expect(req2.isDone()).to.be.ok(); + + done(); + }); + }); + }); + + describe('namecheap', function () { + const NAMECHEAP_ENDPOINT = 'https://api.namecheap.com'; + const username = 'namecheapuser'; + const token = 'namecheaptoken'; + + // the success answer is always the same + const SET_HOSTS_RETURN = ` + + + + namecheap.domains.dns.sethosts + + + + + + PHX01APIEXT03 + --4:00 + 0.408 + `; + + before(async function () { + domainCopy.provider = 'namecheap'; + domainCopy.config = { + username: username, + token: token + }; + + await domains.update(domainCopy.domain, domainCopy, auditSource); + }); + + beforeEach(function () { + nock.cleanAll(); + }); + + it('upsert non-existing record succeeds', function (done) { + const GET_HOSTS_RETURN = ` + + + + namecheap.domains.dns.gethosts + + + + + + + PHX01APIEXT04 + --4:00 + 0.16 + `; + + let req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response') + .query({ + ApiUser: username, + ApiKey: token, + UserName: username, + ClientIp: '127.0.0.1', + Command: 'namecheap.domains.dns.getHosts', + SLD: domainCopy.zoneName.split('.')[0], + TLD: domainCopy.zoneName.split('.')[1] + }) + .reply(200, GET_HOSTS_RETURN); + + let req2 = nock(NAMECHEAP_ENDPOINT).post('/xml.response', (body) => { + const expected = { + ApiUser: username, + ApiKey: token, + UserName: username, + ClientIp: '127.0.0.1', + Command: 'namecheap.domains.dns.setHosts', + SLD: domainCopy.zoneName.split('.')[0], + TLD: domainCopy.zoneName.split('.')[1], + + TTL1: '300', + HostName1: '@', + RecordType1: 'MX', + Address1: 'my.nebulon.space.', + EmailType1: 'MX', + MXPref1: '10', + + TTL2: '300', + HostName2: '@', + RecordType2: 'TXT', + Address2: 'v=spf1 a:my.nebulon.space ~all', + + TTL3: '300', + HostName3: 'test', + RecordType3: 'A', + Address3: '1.2.3.4', + }; + return _.isEqual(body, expected); + }) + .reply(200, SET_HOSTS_RETURN); + + dns.upsertDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { + expect(error).to.eql(null); + expect(req1.isDone()).to.be.ok(); + expect(req2.isDone()).to.be.ok(); + + done(); + }); + }); + + it('upsert multiple non-existing records succeeds', function (done) { + const GET_HOSTS_RETURN = ` + + + + namecheap.domains.dns.gethosts + + + + + + + PHX01APIEXT04 + --4:00 + 0.16 + `; + + let req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response') + .query({ + ApiUser: username, + ApiKey: token, + UserName: username, + ClientIp: '127.0.0.1', + Command: 'namecheap.domains.dns.getHosts', + SLD: domainCopy.zoneName.split('.')[0], + TLD: domainCopy.zoneName.split('.')[1] + }) + .reply(200, GET_HOSTS_RETURN); + + let req2 = nock(NAMECHEAP_ENDPOINT).post('/xml.response', (body) => { + const expected = { + ApiUser: username, + ApiKey: token, + UserName: username, + ClientIp: '127.0.0.1', + Command: 'namecheap.domains.dns.setHosts', + SLD: domainCopy.zoneName.split('.')[0], + TLD: domainCopy.zoneName.split('.')[1], + + TTL1: '300', + HostName1: '@', + RecordType1: 'MX', + Address1: 'my.nebulon.space.', + EmailType1: 'MX', + MXPref1: '10', + + TTL2: '300', + HostName2: '@', + RecordType2: 'TXT', + Address2: 'v=spf1 a:my.nebulon.space ~all', + + TTL3: '300', + HostName3: 'test', + RecordType3: 'TXT', + Address3: '1.2.3.4', + + TTL4: '300', + HostName4: 'test', + RecordType4: 'TXT', + Address4: '2.3.4.5', + + TTL5: '300', + HostName5: 'test', + RecordType5: 'TXT', + Address5: '3.4.5.6', + }; + return _.isEqual(body, expected); + }) + .reply(200, SET_HOSTS_RETURN); + + dns.upsertDnsRecords('test', domainCopy.domain, 'TXT', ['1.2.3.4', '2.3.4.5', '3.4.5.6'], function (error) { + expect(error).to.eql(null); + expect(req1.isDone()).to.be.ok(); + expect(req2.isDone()).to.be.ok(); + + done(); + }); + }); + + it('upsert existing record succeeds', function (done) { + const GET_HOSTS_RETURN = ` + + + + namecheap.domains.dns.gethosts + + + + + + + PHX01APIEXT04 + --4:00 + 0.16 + `; + + let req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response') + .query({ + ApiUser: username, + ApiKey: token, + UserName: username, + ClientIp: '127.0.0.1', + Command: 'namecheap.domains.dns.getHosts', + SLD: domainCopy.zoneName.split('.')[0], + TLD: domainCopy.zoneName.split('.')[1] + }) + .reply(200, GET_HOSTS_RETURN); + + let req2 = nock(NAMECHEAP_ENDPOINT).post('/xml.response', (body) => { + const expected = { + ApiUser: username, + ApiKey: token, + UserName: username, + ClientIp: '127.0.0.1', + Command: 'namecheap.domains.dns.setHosts', + SLD: domainCopy.zoneName.split('.')[0], + TLD: domainCopy.zoneName.split('.')[1], + + TTL1: '300', + HostName1: '@', + RecordType1: 'MX', + Address1: 'my.nebulon.space.', + EmailType1: 'MX', + MXPref1: '10', + + TTL2: '300', + HostName2: 'www', + RecordType2: 'CNAME', + Address2: '1.2.3.4' + }; + return _.isEqual(body, expected); + }) + .reply(200, SET_HOSTS_RETURN); + + dns.upsertDnsRecords('www', domainCopy.domain, 'CNAME', ['1.2.3.4'], function (error) { + expect(error).to.eql(null); + expect(req1.isDone()).to.be.ok(); + expect(req2.isDone()).to.be.ok(); + + done(); + }); + }); + + it('get succeeds', function(done) { + const GET_HOSTS_RETURN = ` + + + + namecheap.domains.dns.gethosts + + + + + + + + PHX01APIEXT04 + --4:00 + 0.16 + `; + + let req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response') + .query({ + ApiUser: username, + ApiKey: token, + UserName: username, + ClientIp: '127.0.0.1', + Command: 'namecheap.domains.dns.getHosts', + SLD: domainCopy.zoneName.split('.')[0], + TLD: domainCopy.zoneName.split('.')[1] + }) + .reply(200, GET_HOSTS_RETURN); + + dns.getDnsRecords('test', domainCopy.domain, 'A', function (error, result) { + expect(error).to.eql(null); + expect(req1.isDone()).to.be.ok(); + expect(result).to.be.an(Array); + expect(result.length).to.eql(2); + expect(result).to.eql(['1.2.3.4', '2.3.4.5']); + + done(); + }); + }); + + it('del succeeds', function (done) { + const GET_HOSTS_RETURN = ` + + + + namecheap.domains.dns.gethosts + + + + + + + PHX01APIEXT04 + --4:00 + 0.16 + `; + + let req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response') + .query({ + ApiUser: username, + ApiKey: token, + UserName: username, + ClientIp: '127.0.0.1', + Command: 'namecheap.domains.dns.getHosts', + SLD: domainCopy.zoneName.split('.')[0], + TLD: domainCopy.zoneName.split('.')[1] + }) + .reply(200, GET_HOSTS_RETURN); + + let req2 = nock(NAMECHEAP_ENDPOINT).post('/xml.response', (body) => { + const expected = { + ApiUser: username, + ApiKey: token, + UserName: username, + ClientIp: '127.0.0.1', + Command: 'namecheap.domains.dns.setHosts', + SLD: domainCopy.zoneName.split('.')[0], + TLD: domainCopy.zoneName.split('.')[1], + + TTL1: '300', + HostName1: '@', + RecordType1: 'MX', + Address1: 'my.nebulon.space.', + EmailType1: 'MX', + MXPref1: '10', + }; + return _.isEqual(body, expected); + }) + .reply(200, SET_HOSTS_RETURN); + + dns.removeDnsRecords('www', domainCopy.domain, 'CNAME', ['1.2.3.4'], function (error) { + expect(error).to.eql(null); + expect(req1.isDone()).to.be.ok(); + expect(req2.isDone()).to.be.ok(); + + done(); + }); + }); + + it('del succeeds with non-existing domain', function (done) { + const GET_HOSTS_RETURN = ` + + + + namecheap.domains.dns.gethosts + + + + + + + PHX01APIEXT04 + --4:00 + 0.16 + `; + + let req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response') + .query({ + ApiUser: username, + ApiKey: token, + UserName: username, + ClientIp: '127.0.0.1', + Command: 'namecheap.domains.dns.getHosts', + SLD: domainCopy.zoneName.split('.')[0], + TLD: domainCopy.zoneName.split('.')[1] + }) + .reply(200, GET_HOSTS_RETURN); + + dns.removeDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { + expect(error).to.eql(null); + expect(req1.isDone()).to.be.ok(); + + done(); + }); + }); + + }); + + describe('route53', function () { + // do not clear this with [] but .length = 0 so we don't loose the reference in mockery + let awsAnswerQueue = []; + + let AWS_HOSTED_ZONES = null; + + before(async function () { + domainCopy.provider = 'route53'; + domainCopy.config = { + accessKeyId: 'unused', + secretAccessKey: 'unused' + }; + + AWS_HOSTED_ZONES = { + HostedZones: [{ + Id: '/hostedzone/Z34G16B38TNZ9L', + Name: domainCopy.zoneName + '.', + CallerReference: '305AFD59-9D73-4502-B020-F4E6F889CB30', + ResourceRecordSetCount: 2, + ChangeInfo: { + Id: '/change/CKRTFJA0ANHXB', + Status: 'INSYNC' + } + }, { + Id: '/hostedzone/Z3OFC3B6E8YTA7', + Name: 'cloudron.us.', + CallerReference: '0B37F2DE-21A4-E678-BA32-3FC8AF0CF635', + Config: {}, + ResourceRecordSetCount: 2, + ChangeInfo: { + Id: '/change/C2682N5HXP0BZ5', + Status: 'INSYNC' + } + }], + IsTruncated: false, + MaxItems: '100' + }; + + function mockery(queue) { + return function (options, callback) { + expect(options).to.be.an(Object); + + let elem = queue.shift(); + if (!Array.isArray(elem)) throw (new Error('Mock answer required')); + + // if no callback passed, return a req object with send(); + if (typeof callback !== 'function') { + return { + httpRequest: { headers: {} }, + send: function (callback) { + expect(callback).to.be.a(Function); + callback(elem[0], elem[1]); + } + }; + } else { + callback(elem[0], elem[1]); + } + }; + } + + function Route53Mock(cfg) { + expect(cfg).to.eql({ + accessKeyId: domainCopy.config.accessKeyId, + secretAccessKey: domainCopy.config.secretAccessKey, + region: 'us-east-1' + }); + } + Route53Mock.prototype.getHostedZone = mockery(awsAnswerQueue); + Route53Mock.prototype.getChange = mockery(awsAnswerQueue); + Route53Mock.prototype.changeResourceRecordSets = mockery(awsAnswerQueue); + Route53Mock.prototype.listResourceRecordSets = mockery(awsAnswerQueue); + Route53Mock.prototype.listHostedZonesByName = mockery(awsAnswerQueue); + + // override route53 in AWS + // Comment this out and replace the config with real tokens to test against AWS proper + AWS._originalRoute53 = AWS.Route53; + AWS.Route53 = Route53Mock; + + await domains.update(domainCopy.domain, domainCopy, auditSource); + }); + + after(function () { + AWS.Route53 = AWS._originalRoute53; + delete AWS._originalRoute53; + }); + + it('upsert non-existing record succeeds', function (done) { + awsAnswerQueue.push([null, AWS_HOSTED_ZONES]); + awsAnswerQueue.push([null, { + ChangeInfo: { + Id: '/change/C2QLKQIWEI0BZF', + Status: 'PENDING', + SubmittedAt: 'Mon Aug 04 2014 17: 44: 49 GMT - 0700(PDT)' + } + }]); + + dns.upsertDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { + expect(error).to.eql(null); + expect(awsAnswerQueue.length).to.eql(0); + + done(); + }); + }); + + it('upsert existing record succeeds', function (done) { + awsAnswerQueue.push([null, AWS_HOSTED_ZONES]); + awsAnswerQueue.push([null, { + ChangeInfo: { + Id: '/change/C2QLKQIWEI0BZF', + Status: 'PENDING', + SubmittedAt: 'Mon Aug 04 2014 17: 44: 49 GMT - 0700(PDT)' + } + }]); + + dns.upsertDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { + expect(error).to.eql(null); + expect(awsAnswerQueue.length).to.eql(0); + + done(); + }); + }); + + it('upsert multiple record succeeds', function (done) { + awsAnswerQueue.push([null, AWS_HOSTED_ZONES]); + awsAnswerQueue.push([null, { + ChangeInfo: { + Id: '/change/C2QLKQIWEI0BZF', + Status: 'PENDING', + SubmittedAt: 'Mon Aug 04 2014 17: 44: 49 GMT - 0700(PDT)' + } + }]); + + dns.upsertDnsRecords('', domainCopy.domain, 'TXT', ['first', 'second', 'third'], function (error) { + expect(error).to.eql(null); + expect(awsAnswerQueue.length).to.eql(0); + + done(); + }); + }); + + it('get succeeds', function (done) { + awsAnswerQueue.push([null, AWS_HOSTED_ZONES]); + awsAnswerQueue.push([null, { + ResourceRecordSets: [{ + Name: 'test.' + domainCopy.zoneName + '.', + Type: 'A', + ResourceRecords: [{ + Value: '1.2.3.4' + }] + }] + }]); + + dns.getDnsRecords('test', domainCopy.domain, 'A', function (error, result) { + expect(error).to.eql(null); + expect(result).to.be.an(Array); + expect(result.length).to.eql(1); + expect(result[0]).to.eql('1.2.3.4'); + expect(awsAnswerQueue.length).to.eql(0); + + done(); + }); + }); + + it('del succeeds', function (done) { + awsAnswerQueue.push([null, AWS_HOSTED_ZONES]); + awsAnswerQueue.push([null, { + ChangeInfo: { + Id: '/change/C2QLKQIWEI0BZF', + Status: 'PENDING', + SubmittedAt: 'Mon Aug 04 2014 17: 44: 49 GMT - 0700(PDT)' + } + }]); + + dns.removeDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { + expect(error).to.eql(null); + expect(awsAnswerQueue.length).to.eql(0); + + done(); + }); + }); + }); + + describe('gcdns', function () { + let HOSTED_ZONES = []; + let zoneQueue = []; + let _OriginalGCDNS; + + before(async function () { + domainCopy.provider = 'gcdns'; + domainCopy.config = { + projectId: 'my-dns-proj', + credentials: { + 'client_email': '123456789349-compute@developer.gserviceaccount.com', + 'private_key': 'privatehushhush' + } + }; + + function mockery(queue) { + return function () { + let callback = arguments[--arguments.length]; + + let elem = queue.shift(); + if (!Array.isArray(elem)) throw (new Error('Mock answer required')); + + // if no callback passed, return a req object with send(); + if (typeof callback !== 'function') { + return { + httpRequest: { headers: {} }, + send: function (callback) { + expect(callback).to.be.a(Function); + callback.apply(callback, elem); + } + }; + } else { + callback.apply(callback, elem); + } + }; + } + + function fakeZone(name, ns, recordQueue) { + let zone = new GCDNS().zone(name.replace('.', '-')); + zone.metadata.dnsName = name + '.'; + zone.metadata.nameServers = ns || ['8.8.8.8', '8.8.4.4']; + zone.getRecords = mockery(recordQueue || zoneQueue); + zone.createChange = mockery(recordQueue || zoneQueue); + zone.replaceRecords = mockery(recordQueue || zoneQueue); + zone.deleteRecords = mockery(recordQueue || zoneQueue); + return zone; + } + HOSTED_ZONES = [fakeZone(domainCopy.domain), fakeZone('cloudron.us')]; + + _OriginalGCDNS = GCDNS.prototype.getZones; + GCDNS.prototype.getZones = mockery(zoneQueue); + + await domains.update(domainCopy.domain, domainCopy, auditSource); + }); + + after(function () { + GCDNS.prototype.getZones = _OriginalGCDNS; + _OriginalGCDNS = null; + }); + + it('upsert non-existing record succeeds', function (done) { + zoneQueue.push([null, HOSTED_ZONES]); // getZone + zoneQueue.push([null, []]); // getRecords + zoneQueue.push([null, { id: '1' }]); + + dns.upsertDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { + expect(error).to.eql(null); + expect(zoneQueue.length).to.eql(0); + + done(); + }); + }); + + it('upsert existing record succeeds', function (done) { + zoneQueue.push([null, HOSTED_ZONES]); + zoneQueue.push([null, [new GCDNS().zone('test').record('A', { 'name': 'test', data: ['5.6.7.8'], ttl: 1 })]]); + zoneQueue.push([null, { id: '2' }]); + + dns.upsertDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { + expect(error).to.eql(null); + expect(zoneQueue.length).to.eql(0); + + done(); + }); + }); + + it('upsert multiple record succeeds', function (done) { + zoneQueue.push([null, HOSTED_ZONES]); + zoneQueue.push([null, []]); // getRecords + zoneQueue.push([null, { id: '3' }]); + + dns.upsertDnsRecords('', domainCopy.domain, 'TXT', ['first', 'second', 'third'], function (error) { + expect(error).to.eql(null); + expect(zoneQueue.length).to.eql(0); + + done(); + }); + }); + + it('get succeeds', function (done) { + zoneQueue.push([null, HOSTED_ZONES]); + zoneQueue.push([null, [new GCDNS().zone('test').record('A', { 'name': 'test', data: ['1.2.3.4', '5.6.7.8'], ttl: 1 })]]); + + dns.getDnsRecords('test', domainCopy.domain, 'A', function (error, result) { + expect(error).to.eql(null); + expect(result).to.be.an(Array); + expect(result.length).to.eql(2); + expect(result).to.eql(['1.2.3.4', '5.6.7.8']); + expect(zoneQueue.length).to.eql(0); + + done(); + }); + }); + + it('del succeeds', function (done) { + zoneQueue.push([null, HOSTED_ZONES]); + zoneQueue.push([null, [new GCDNS().zone('test').record('A', { 'name': 'test', data: ['5.6.7.8'], ttl: 1 })]]); + zoneQueue.push([null, { id: '5' }]); + + dns.removeDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { + expect(error).to.eql(null); + expect(zoneQueue.length).to.eql(0); + + done(); + }); + }); + }); +}); diff --git a/src/test/dns-test.js b/src/test/dns-test.js index 4ef65d8d7..abae0c4db 100644 --- a/src/test/dns-test.js +++ b/src/test/dns-test.js @@ -1,1266 +1,108 @@ -/* jslint node:true */ /* global it:false */ /* global describe:false */ /* global before:false */ -/* global beforeEach:false */ /* global after:false */ 'use strict'; -const AWS = require('aws-sdk'), - common = require('./common.js'), - domains = require('../domains.js'), - expect = require('expect.js'), - GCDNS = require('@google-cloud/dns').DNS, - nock = require('nock'), - _ = require('underscore'); +const common = require('./common.js'), + dns = require('../dns.js'), + expect = require('expect.js'); -describe('dns provider', function () { - const { setup, cleanup, auditSource, domain } = common; - const domainCopy = Object.assign({}, domain); // make a copy +describe('DNS', function () { + const { setup, cleanup, app, domain } = common; before(setup); after(cleanup); - describe('noop', function () { - before(function (done) { - domainCopy.provider = 'noop'; - domainCopy.config = {}; - - domains.update(domainCopy.domain, domainCopy, auditSource, done); + describe('validateHostname', function () { + it('does not allow admin subdomain', function () { + expect(dns.validateHostname('my', domain)).to.be.an(Error); }); - it('upsert succeeds', function (done) { - domains.upsertDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { - expect(error).to.eql(null); - - done(); - }); + it('cannot have >63 length subdomains', function () { + var s = Array(64).fill('s').join(''); + expect(dns.validateHostname(s, domain)).to.be.an(Error); + domain.zoneName = `dev.${s}.example.com`; + expect(dns.validateHostname(`dev.${s}`, domain)).to.be.an(Error); }); - it('get succeeds', function (done) { - domains.getDnsRecords('test', domainCopy.domain, 'A', function (error, result) { - expect(error).to.eql(null); - expect(result).to.be.an(Array); - expect(result.length).to.eql(0); - - done(); - }); + it('allows only alphanumerics and hypen', function () { + expect(dns.validateHostname('#2r', domain)).to.be.an(Error); + expect(dns.validateHostname('a%b', domain)).to.be.an(Error); + expect(dns.validateHostname('ab_', domain)).to.be.an(Error); + expect(dns.validateHostname('ab.', domain)).to.be.an(Error); + expect(dns.validateHostname('ab..c', domain)).to.be.an(Error); + expect(dns.validateHostname('.ab', domain)).to.be.an(Error); + expect(dns.validateHostname('-ab', domain)).to.be.an(Error); + expect(dns.validateHostname('ab-', domain)).to.be.an(Error); }); - it('del succeeds', function (done) { - domains.removeDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { - expect(error).to.eql(null); + it('total length cannot exceed 255', function () { + var s = ''; + for (var i = 0; i < (255 - 'example.com'.length); i++) s += 's'; - done(); - }); + expect(dns.validateHostname(s, domain)).to.be.an(Error); + }); + + it('allow valid domains', function () { + expect(dns.validateHostname('a', domain)).to.be(null); + expect(dns.validateHostname('a0-x', domain)).to.be(null); + expect(dns.validateHostname('a0.x', domain)).to.be(null); + expect(dns.validateHostname('a0.x.y', domain)).to.be(null); + expect(dns.validateHostname('01', domain)).to.be(null); }); }); - describe('digitalocean', function () { - var TOKEN = 'sometoken'; - var DIGITALOCEAN_ENDPOINT = 'https://api.digitalocean.com'; - - before(function (done) { - domainCopy.provider = 'digitalocean'; - domainCopy.config = { - token: TOKEN + describe('getName', function () { + it('works with zoneName==domain', function () { + const d = { + domain: 'example.com', + zoneName: 'example.com', + config: {} }; - domains.update(domainCopy.domain, domainCopy, auditSource, done); + expect(dns.getName(d, '', 'A')).to.be(''); + expect(dns.getName(d, 'www', 'A')).to.be('www'); + expect(dns.getName(d, 'www.dev', 'A')).to.be('www.dev'); + + expect(dns.getName(d, '', 'MX')).to.be(''); + + expect(dns.getName(d, '', 'TXT')).to.be(''); + expect(dns.getName(d, 'www', 'TXT')).to.be('www'); + expect(dns.getName(d, 'www.dev', 'TXT')).to.be('www.dev'); }); - it('upsert non-existing record succeeds', function (done) { - nock.cleanAll(); - - var DOMAIN_RECORD_0 = { - id: 3352892, - type: 'A', - name: '@', - data: '1.2.3.4', - priority: null, - port: null, - weight: null + it('works when zoneName!=domain', function () { + const d = { + domain: 'dev.example.com', + zoneName: 'example.com', + config: {} }; - var req1 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) - .get('/v2/domains/' + domainCopy.zoneName + '/records') - .reply(200, { domain_records: [] }); - var req2 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) - .post('/v2/domains/' + domainCopy.zoneName + '/records') - .reply(201, { domain_record: DOMAIN_RECORD_0 }); + expect(dns.getName(d, '', 'A')).to.be('dev'); + expect(dns.getName(d, 'www', 'A')).to.be('www.dev'); + expect(dns.getName(d, 'www.dev', 'A')).to.be('www.dev.dev'); - domains.upsertDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { - expect(error).to.eql(null); - expect(req1.isDone()).to.be.ok(); - expect(req2.isDone()).to.be.ok(); + expect(dns.getName(d, '', 'MX')).to.be('dev'); - done(); - }); - }); - - it('upsert existing record succeeds', function (done) { - nock.cleanAll(); - - var DOMAIN_RECORD_0 = { - id: 3352892, - type: 'A', - name: '@', - data: '1.2.3.4', - priority: null, - port: null, - weight: null - }; - - var DOMAIN_RECORD_1 = { - id: 3352893, - type: 'A', - name: 'test', - data: '1.2.3.4', - priority: null, - port: null, - weight: null - }; - - var DOMAIN_RECORD_1_NEW = { - id: 3352893, - type: 'A', - name: 'test', - data: '1.2.3.5', - priority: null, - port: null, - weight: null - }; - - var req1 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) - .get('/v2/domains/' + domainCopy.zoneName + '/records') - .reply(200, { domain_records: [DOMAIN_RECORD_0, DOMAIN_RECORD_1] }); - var req2 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) - .put('/v2/domains/' + domainCopy.zoneName + '/records/' + DOMAIN_RECORD_1.id) - .reply(200, { domain_record: DOMAIN_RECORD_1_NEW }); - - domains.upsertDnsRecords('test', domainCopy.domain, 'A', [DOMAIN_RECORD_1_NEW.data], function (error) { - expect(error).to.eql(null); - expect(req1.isDone()).to.be.ok(); - expect(req2.isDone()).to.be.ok(); - - done(); - }); - }); - - it('upsert multiple record succeeds', function (done) { - nock.cleanAll(); - - var DOMAIN_RECORD_0 = { - id: 3352892, - type: 'A', - name: '@', - data: '1.2.3.4', - priority: null, - port: null, - weight: null - }; - - var DOMAIN_RECORD_1 = { - id: 3352893, - type: 'TXT', - name: '@', - data: '1.2.3.4', - priority: null, - port: null, - weight: null - }; - - var DOMAIN_RECORD_1_NEW = { - id: 3352893, - type: 'TXT', - name: '@', - data: 'somethingnew', - priority: null, - port: null, - weight: null - }; - - var DOMAIN_RECORD_2 = { - id: 3352894, - type: 'TXT', - name: '@', - data: 'something', - priority: null, - port: null, - weight: null - }; - - var DOMAIN_RECORD_2_NEW = { - id: 3352894, - type: 'TXT', - name: '@', - data: 'somethingnew', - priority: null, - port: null, - weight: null - }; - - var DOMAIN_RECORD_3_NEW = { - id: 3352895, - type: 'TXT', - name: '@', - data: 'thirdnewone', - priority: null, - port: null, - weight: null - }; - - var req1 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) - .get('/v2/domains/' + domainCopy.zoneName + '/records') - .reply(200, { domain_records: [DOMAIN_RECORD_0, DOMAIN_RECORD_1, DOMAIN_RECORD_2] }); - var req2 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) - .put('/v2/domains/' + domainCopy.zoneName + '/records/' + DOMAIN_RECORD_1.id) - .reply(200, { domain_record: DOMAIN_RECORD_1_NEW }); - var req3 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) - .put('/v2/domains/' + domainCopy.zoneName + '/records/' + DOMAIN_RECORD_2.id) - .reply(200, { domain_record: DOMAIN_RECORD_2_NEW }); - var req4 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) - .post('/v2/domains/' + domainCopy.zoneName + '/records') - .reply(201, { domain_record: DOMAIN_RECORD_2_NEW }); - - domains.upsertDnsRecords('', domainCopy.domain, 'TXT', [DOMAIN_RECORD_2_NEW.data, DOMAIN_RECORD_1_NEW.data, DOMAIN_RECORD_3_NEW.data], function (error) { - expect(error).to.eql(null); - expect(req1.isDone()).to.be.ok(); - expect(req2.isDone()).to.be.ok(); - expect(req3.isDone()).to.be.ok(); - expect(req4.isDone()).to.be.ok(); - - done(); - }); - }); - - it('get succeeds', function (done) { - nock.cleanAll(); - - var DOMAIN_RECORD_0 = { - id: 3352892, - type: 'A', - name: '@', - data: '1.2.3.4', - priority: null, - port: null, - weight: null - }; - - var DOMAIN_RECORD_1 = { - id: 3352893, - type: 'A', - name: 'test', - data: '1.2.3.4', - priority: null, - port: null, - weight: null - }; - - var req1 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) - .get('/v2/domains/' + domainCopy.zoneName + '/records') - .reply(200, { domain_records: [DOMAIN_RECORD_0, DOMAIN_RECORD_1] }); - - domains.getDnsRecords('test', domainCopy.domain, 'A', function (error, result) { - expect(error).to.eql(null); - expect(result).to.be.an(Array); - expect(result.length).to.eql(1); - expect(result[0]).to.eql(DOMAIN_RECORD_1.data); - expect(req1.isDone()).to.be.ok(); - - done(); - }); - }); - - it('del succeeds', function (done) { - nock.cleanAll(); - - var DOMAIN_RECORD_0 = { - id: 3352892, - type: 'A', - name: '@', - data: '1.2.3.4', - priority: null, - port: null, - weight: null - }; - - var DOMAIN_RECORD_1 = { - id: 3352893, - type: 'A', - name: 'test', - data: '1.2.3.4', - priority: null, - port: null, - weight: null - }; - - var req1 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) - .get('/v2/domains/' + domainCopy.zoneName + '/records') - .reply(200, { domain_records: [DOMAIN_RECORD_0, DOMAIN_RECORD_1] }); - var req2 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) - .delete('/v2/domains/' + domainCopy.zoneName + '/records/' + DOMAIN_RECORD_1.id) - .reply(204, {}); - - domains.removeDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { - expect(error).to.eql(null); - expect(req1.isDone()).to.be.ok(); - expect(req2.isDone()).to.be.ok(); - - done(); - }); + expect(dns.getName(d, '', 'TXT')).to.be('dev'); + expect(dns.getName(d, 'www', 'TXT')).to.be('www.dev'); + expect(dns.getName(d, 'www.dev', 'TXT')).to.be('www.dev.dev'); }); }); - describe('godaddy', function () { - var KEY = 'somekey', SECRET = 'somesecret'; - var GODADDY_API = 'https://api.godaddy.com/v1/domains'; - - before(function (done) { - domainCopy.provider = 'godaddy'; - domainCopy.config = { - apiKey: KEY, - apiSecret: SECRET - }; - - domains.update(domainCopy.domain, domainCopy, auditSource, done); - }); - - it('upsert record succeeds', function (done) { - nock.cleanAll(); - - var DOMAIN_RECORD_0 = [{ - ttl: 600, - data: '1.2.3.4' - }]; - - var req1 = nock(GODADDY_API) - .put('/' + domainCopy.zoneName + '/records/A/test', DOMAIN_RECORD_0) - .reply(200, {}); - - domains.upsertDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { - expect(error).to.eql(null); - expect(req1.isDone()).to.be.ok(); - + describe('register', function () { + it('registers subdomain', function (done) { + dns.registerLocations([ { subdomain: app.location, domain: app.domain } ], { overwriteDns: true }, (/*progress*/) => {}, function (error) { + expect(error).to.be(null); done(); }); }); - it('get succeeds', function (done) { - nock.cleanAll(); - - var DOMAIN_RECORD_0 = [{ - ttl: 600, - data: '1.2.3.4' - }]; - - var req1 = nock(GODADDY_API) - .get('/' + domainCopy.zoneName + '/records/A/test') - .reply(200, DOMAIN_RECORD_0); - - domains.getDnsRecords('test', domainCopy.domain, 'A', function (error, result) { - expect(error).to.eql(null); - expect(result).to.be.an(Array); - expect(result.length).to.eql(1); - expect(result[0]).to.eql(DOMAIN_RECORD_0[0].data); - expect(req1.isDone()).to.be.ok(); - - done(); - }); - }); - - it('del succeeds', function (done) { - nock.cleanAll(); - - var DOMAIN_RECORD_0 = [{ // existing - ttl: 600, - data: '1.2.3.4' - }]; - - var DOMAIN_RECORD_1 = [{ // replaced - ttl: 600, - data: '0.0.0.0' - }]; - - var req1 = nock(GODADDY_API) - .get('/' + domainCopy.zoneName + '/records/A/test') - .reply(200, DOMAIN_RECORD_0); - - var req2 = nock(GODADDY_API) - .put('/' + domainCopy.zoneName + '/records/A/test', DOMAIN_RECORD_1) - .reply(200, {}); - - domains.removeDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { - expect(error).to.eql(null); - expect(req1.isDone()).to.be.ok(); - expect(req2.isDone()).to.be.ok(); - - done(); - }); - }); - }); - - describe('gandi', function () { - var TOKEN = 'sometoken'; - var GANDI_API = 'https://dns.api.gandi.net/api/v5'; - - before(function (done) { - domainCopy.provider = 'gandi'; - domainCopy.config = { - token: TOKEN - }; - - domains.update(domainCopy.domain, domainCopy, auditSource, done); - }); - - it('upsert record succeeds', function (done) { - nock.cleanAll(); - - var DOMAIN_RECORD_0 = { - 'rrset_ttl': 300, - 'rrset_values': ['1.2.3.4'] - }; - - var req1 = nock(GANDI_API) - .put('/domains/' + domainCopy.zoneName + '/records/test/A', DOMAIN_RECORD_0) - .reply(201, { message: 'Zone Record Created' }); - - domains.upsertDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { - expect(error).to.eql(null); - expect(req1.isDone()).to.be.ok(); - - done(); - }); - }); - - it('get succeeds', function (done) { - nock.cleanAll(); - - var DOMAIN_RECORD_0 = { - 'rrset_type': 'A', - 'rrset_ttl': 600, - 'rrset_name': 'test', - 'rrset_values': ['1.2.3.4'] - }; - - var req1 = nock(GANDI_API) - .get('/domains/' + domainCopy.zoneName + '/records/test/A') - .reply(200, DOMAIN_RECORD_0); - - domains.getDnsRecords('test', domainCopy.domain, 'A', function (error, result) { - expect(error).to.eql(null); - expect(result).to.be.an(Array); - expect(result.length).to.eql(1); - expect(result[0]).to.eql(DOMAIN_RECORD_0.rrset_values[0]); - expect(req1.isDone()).to.be.ok(); - - done(); - }); - }); - - it('del succeeds', function (done) { - nock.cleanAll(); - - var req2 = nock(GANDI_API) - .delete('/domains/' + domainCopy.zoneName + '/records/test/A') - .reply(204, {}); - - domains.removeDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { - expect(error).to.eql(null); - expect(req2.isDone()).to.be.ok(); - - done(); - }); - }); - }); - - describe('name.com', function () { - const TOKEN = 'sometoken'; - const NAMECOM_API = 'https://api.name.com/v4'; - - before(function (done) { - domainCopy.provider = 'namecom'; - domainCopy.config = { - username: 'fake', - token: TOKEN - }; - - domains.update(domainCopy.domain, domainCopy, auditSource, done); - }); - - it('upsert record succeeds', function (done) { - nock.cleanAll(); - - var DOMAIN_RECORD_0 = { - host: 'test', - type: 'A', - answer: '1.2.3.4', - ttl: 300 - }; - - var req1 = nock(NAMECOM_API) - .get(`/domains/${domainCopy.zoneName}/records`) - .reply(200, { records: [] }); - - var req2 = nock(NAMECOM_API) - .post(`/domains/${domainCopy.zoneName}/records`, DOMAIN_RECORD_0) - .reply(200, {}); - - domains.upsertDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { - expect(error).to.eql(null); - expect(req1.isDone()).to.be.ok(); - expect(req2.isDone()).to.be.ok(); - - done(); - }); - }); - - it('get succeeds', function (done) { - nock.cleanAll(); - - var DOMAIN_RECORD_0 = { - host: 'test', - type: 'A', - answer: '1.2.3.4', - ttl: 300 - }; - - var req1 = nock(NAMECOM_API) - .get(`/domains/${domainCopy.zoneName}/records`) - .reply(200, { records: [DOMAIN_RECORD_0] }); - - domains.getDnsRecords('test', domainCopy.domain, 'A', function (error, result) { - expect(error).to.eql(null); - expect(result).to.be.an(Array); - expect(result.length).to.eql(1); - expect(result[0]).to.eql(DOMAIN_RECORD_0.answer); - expect(req1.isDone()).to.be.ok(); - - done(); - }); - }); - - it('del succeeds', function (done) { - nock.cleanAll(); - - var DOMAIN_RECORD_0 = { - id: 'someid', - host: 'test', - type: 'A', - answer: '1.2.3.4', - ttl: 300 - }; - - var req1 = nock(NAMECOM_API) - .get(`/domains/${domainCopy.zoneName}/records`) - .reply(200, { records: [DOMAIN_RECORD_0] }); - - var req2 = nock(NAMECOM_API) - .delete(`/domains/${domainCopy.zoneName}/records/${DOMAIN_RECORD_0.id}`) - .reply(200, {}); - - domains.removeDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { - expect(error).to.eql(null); - expect(req1.isDone()).to.be.ok(); - expect(req2.isDone()).to.be.ok(); - - done(); - }); - }); - }); - - describe('namecheap', function () { - const NAMECHEAP_ENDPOINT = 'https://api.namecheap.com'; - const username = 'namecheapuser'; - const token = 'namecheaptoken'; - - // the success answer is always the same - const SET_HOSTS_RETURN = ` - - - - namecheap.domains.dns.sethosts - - - - - - PHX01APIEXT03 - --4:00 - 0.408 - `; - - before(function (done) { - domainCopy.provider = 'namecheap'; - domainCopy.config = { - username: username, - token: token - }; - - domains.update(domainCopy.domain, domainCopy, auditSource, done); - }); - - beforeEach(function () { - nock.cleanAll(); - }); - - it('upsert non-existing record succeeds', function (done) { - const GET_HOSTS_RETURN = ` - - - - namecheap.domains.dns.gethosts - - - - - - - PHX01APIEXT04 - --4:00 - 0.16 - `; - - var req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response') - .query({ - ApiUser: username, - ApiKey: token, - UserName: username, - ClientIp: '127.0.0.1', - Command: 'namecheap.domains.dns.getHosts', - SLD: domainCopy.zoneName.split('.')[0], - TLD: domainCopy.zoneName.split('.')[1] - }) - .reply(200, GET_HOSTS_RETURN); - - var req2 = nock(NAMECHEAP_ENDPOINT).post('/xml.response', (body) => { - const expected = { - ApiUser: username, - ApiKey: token, - UserName: username, - ClientIp: '127.0.0.1', - Command: 'namecheap.domains.dns.setHosts', - SLD: domainCopy.zoneName.split('.')[0], - TLD: domainCopy.zoneName.split('.')[1], - - TTL1: '300', - HostName1: '@', - RecordType1: 'MX', - Address1: 'my.nebulon.space.', - EmailType1: 'MX', - MXPref1: '10', - - TTL2: '300', - HostName2: '@', - RecordType2: 'TXT', - Address2: 'v=spf1 a:my.nebulon.space ~all', - - TTL3: '300', - HostName3: 'test', - RecordType3: 'A', - Address3: '1.2.3.4', - }; - return _.isEqual(body, expected); - }) - .reply(200, SET_HOSTS_RETURN); - - domains.upsertDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { - expect(error).to.eql(null); - expect(req1.isDone()).to.be.ok(); - expect(req2.isDone()).to.be.ok(); - - done(); - }); - }); - - it('upsert multiple non-existing records succeeds', function (done) { - const GET_HOSTS_RETURN = ` - - - - namecheap.domains.dns.gethosts - - - - - - - PHX01APIEXT04 - --4:00 - 0.16 - `; - - var req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response') - .query({ - ApiUser: username, - ApiKey: token, - UserName: username, - ClientIp: '127.0.0.1', - Command: 'namecheap.domains.dns.getHosts', - SLD: domainCopy.zoneName.split('.')[0], - TLD: domainCopy.zoneName.split('.')[1] - }) - .reply(200, GET_HOSTS_RETURN); - - var req2 = nock(NAMECHEAP_ENDPOINT).post('/xml.response', (body) => { - const expected = { - ApiUser: username, - ApiKey: token, - UserName: username, - ClientIp: '127.0.0.1', - Command: 'namecheap.domains.dns.setHosts', - SLD: domainCopy.zoneName.split('.')[0], - TLD: domainCopy.zoneName.split('.')[1], - - TTL1: '300', - HostName1: '@', - RecordType1: 'MX', - Address1: 'my.nebulon.space.', - EmailType1: 'MX', - MXPref1: '10', - - TTL2: '300', - HostName2: '@', - RecordType2: 'TXT', - Address2: 'v=spf1 a:my.nebulon.space ~all', - - TTL3: '300', - HostName3: 'test', - RecordType3: 'TXT', - Address3: '1.2.3.4', - - TTL4: '300', - HostName4: 'test', - RecordType4: 'TXT', - Address4: '2.3.4.5', - - TTL5: '300', - HostName5: 'test', - RecordType5: 'TXT', - Address5: '3.4.5.6', - }; - return _.isEqual(body, expected); - }) - .reply(200, SET_HOSTS_RETURN); - - domains.upsertDnsRecords('test', domainCopy.domain, 'TXT', ['1.2.3.4', '2.3.4.5', '3.4.5.6'], function (error) { - expect(error).to.eql(null); - expect(req1.isDone()).to.be.ok(); - expect(req2.isDone()).to.be.ok(); - - done(); - }); - }); - - it('upsert existing record succeeds', function (done) { - const GET_HOSTS_RETURN = ` - - - - namecheap.domains.dns.gethosts - - - - - - - PHX01APIEXT04 - --4:00 - 0.16 - `; - - var req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response') - .query({ - ApiUser: username, - ApiKey: token, - UserName: username, - ClientIp: '127.0.0.1', - Command: 'namecheap.domains.dns.getHosts', - SLD: domainCopy.zoneName.split('.')[0], - TLD: domainCopy.zoneName.split('.')[1] - }) - .reply(200, GET_HOSTS_RETURN); - - var req2 = nock(NAMECHEAP_ENDPOINT).post('/xml.response', (body) => { - const expected = { - ApiUser: username, - ApiKey: token, - UserName: username, - ClientIp: '127.0.0.1', - Command: 'namecheap.domains.dns.setHosts', - SLD: domainCopy.zoneName.split('.')[0], - TLD: domainCopy.zoneName.split('.')[1], - - TTL1: '300', - HostName1: '@', - RecordType1: 'MX', - Address1: 'my.nebulon.space.', - EmailType1: 'MX', - MXPref1: '10', - - TTL2: '300', - HostName2: 'www', - RecordType2: 'CNAME', - Address2: '1.2.3.4' - }; - return _.isEqual(body, expected); - }) - .reply(200, SET_HOSTS_RETURN); - - domains.upsertDnsRecords('www', domainCopy.domain, 'CNAME', ['1.2.3.4'], function (error) { - expect(error).to.eql(null); - expect(req1.isDone()).to.be.ok(); - expect(req2.isDone()).to.be.ok(); - - done(); - }); - }); - - it('get succeeds', function(done) { - const GET_HOSTS_RETURN = ` - - - - namecheap.domains.dns.gethosts - - - - - - - - PHX01APIEXT04 - --4:00 - 0.16 - `; - - var req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response') - .query({ - ApiUser: username, - ApiKey: token, - UserName: username, - ClientIp: '127.0.0.1', - Command: 'namecheap.domains.dns.getHosts', - SLD: domainCopy.zoneName.split('.')[0], - TLD: domainCopy.zoneName.split('.')[1] - }) - .reply(200, GET_HOSTS_RETURN); - - domains.getDnsRecords('test', domainCopy.domain, 'A', function (error, result) { - expect(error).to.eql(null); - expect(req1.isDone()).to.be.ok(); - expect(result).to.be.an(Array); - expect(result.length).to.eql(2); - expect(result).to.eql(['1.2.3.4', '2.3.4.5']); - - done(); - }); - }); - - it('del succeeds', function (done) { - const GET_HOSTS_RETURN = ` - - - - namecheap.domains.dns.gethosts - - - - - - - PHX01APIEXT04 - --4:00 - 0.16 - `; - - var req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response') - .query({ - ApiUser: username, - ApiKey: token, - UserName: username, - ClientIp: '127.0.0.1', - Command: 'namecheap.domains.dns.getHosts', - SLD: domainCopy.zoneName.split('.')[0], - TLD: domainCopy.zoneName.split('.')[1] - }) - .reply(200, GET_HOSTS_RETURN); - - var req2 = nock(NAMECHEAP_ENDPOINT).post('/xml.response', (body) => { - const expected = { - ApiUser: username, - ApiKey: token, - UserName: username, - ClientIp: '127.0.0.1', - Command: 'namecheap.domains.dns.setHosts', - SLD: domainCopy.zoneName.split('.')[0], - TLD: domainCopy.zoneName.split('.')[1], - - TTL1: '300', - HostName1: '@', - RecordType1: 'MX', - Address1: 'my.nebulon.space.', - EmailType1: 'MX', - MXPref1: '10', - }; - return _.isEqual(body, expected); - }) - .reply(200, SET_HOSTS_RETURN); - - domains.removeDnsRecords('www', domainCopy.domain, 'CNAME', ['1.2.3.4'], function (error) { - expect(error).to.eql(null); - expect(req1.isDone()).to.be.ok(); - expect(req2.isDone()).to.be.ok(); - - done(); - }); - }); - - it('del succeeds with non-existing domain', function (done) { - const GET_HOSTS_RETURN = ` - - - - namecheap.domains.dns.gethosts - - - - - - - PHX01APIEXT04 - --4:00 - 0.16 - `; - - var req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response') - .query({ - ApiUser: username, - ApiKey: token, - UserName: username, - ClientIp: '127.0.0.1', - Command: 'namecheap.domains.dns.getHosts', - SLD: domainCopy.zoneName.split('.')[0], - TLD: domainCopy.zoneName.split('.')[1] - }) - .reply(200, GET_HOSTS_RETURN); - - domains.removeDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { - expect(error).to.eql(null); - expect(req1.isDone()).to.be.ok(); - - done(); - }); - }); - - }); - - describe('route53', function () { - // do not clear this with [] but .length = 0 so we don't loose the reference in mockery - var awsAnswerQueue = []; - - var AWS_HOSTED_ZONES = null; - - before(function (done) { - domainCopy.provider = 'route53'; - domainCopy.config = { - accessKeyId: 'unused', - secretAccessKey: 'unused' - }; - - AWS_HOSTED_ZONES = { - HostedZones: [{ - Id: '/hostedzone/Z34G16B38TNZ9L', - Name: domainCopy.zoneName + '.', - CallerReference: '305AFD59-9D73-4502-B020-F4E6F889CB30', - ResourceRecordSetCount: 2, - ChangeInfo: { - Id: '/change/CKRTFJA0ANHXB', - Status: 'INSYNC' - } - }, { - Id: '/hostedzone/Z3OFC3B6E8YTA7', - Name: 'cloudron.us.', - CallerReference: '0B37F2DE-21A4-E678-BA32-3FC8AF0CF635', - Config: {}, - ResourceRecordSetCount: 2, - ChangeInfo: { - Id: '/change/C2682N5HXP0BZ5', - Status: 'INSYNC' - } - }], - IsTruncated: false, - MaxItems: '100' - }; - - function mockery(queue) { - return function (options, callback) { - expect(options).to.be.an(Object); - - var elem = queue.shift(); - if (!Array.isArray(elem)) throw (new Error('Mock answer required')); - - // if no callback passed, return a req object with send(); - if (typeof callback !== 'function') { - return { - httpRequest: { headers: {} }, - send: function (callback) { - expect(callback).to.be.a(Function); - callback(elem[0], elem[1]); - } - }; - } else { - callback(elem[0], elem[1]); - } - }; - } - - function Route53Mock(cfg) { - expect(cfg).to.eql({ - accessKeyId: domainCopy.config.accessKeyId, - secretAccessKey: domainCopy.config.secretAccessKey, - region: 'us-east-1' - }); - } - Route53Mock.prototype.getHostedZone = mockery(awsAnswerQueue); - Route53Mock.prototype.getChange = mockery(awsAnswerQueue); - Route53Mock.prototype.changeResourceRecordSets = mockery(awsAnswerQueue); - Route53Mock.prototype.listResourceRecordSets = mockery(awsAnswerQueue); - Route53Mock.prototype.listHostedZonesByName = mockery(awsAnswerQueue); - - // override route53 in AWS - // Comment this out and replace the config with real tokens to test against AWS proper - AWS._originalRoute53 = AWS.Route53; - AWS.Route53 = Route53Mock; - - domains.update(domainCopy.domain, domainCopy, auditSource, done); - }); - - after(function () { - AWS.Route53 = AWS._originalRoute53; - delete AWS._originalRoute53; - }); - - it('upsert non-existing record succeeds', function (done) { - awsAnswerQueue.push([null, AWS_HOSTED_ZONES]); - awsAnswerQueue.push([null, { - ChangeInfo: { - Id: '/change/C2QLKQIWEI0BZF', - Status: 'PENDING', - SubmittedAt: 'Mon Aug 04 2014 17: 44: 49 GMT - 0700(PDT)' - } - }]); - - domains.upsertDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { - expect(error).to.eql(null); - expect(awsAnswerQueue.length).to.eql(0); - - done(); - }); - }); - - it('upsert existing record succeeds', function (done) { - awsAnswerQueue.push([null, AWS_HOSTED_ZONES]); - awsAnswerQueue.push([null, { - ChangeInfo: { - Id: '/change/C2QLKQIWEI0BZF', - Status: 'PENDING', - SubmittedAt: 'Mon Aug 04 2014 17: 44: 49 GMT - 0700(PDT)' - } - }]); - - domains.upsertDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { - expect(error).to.eql(null); - expect(awsAnswerQueue.length).to.eql(0); - - done(); - }); - }); - - it('upsert multiple record succeeds', function (done) { - awsAnswerQueue.push([null, AWS_HOSTED_ZONES]); - awsAnswerQueue.push([null, { - ChangeInfo: { - Id: '/change/C2QLKQIWEI0BZF', - Status: 'PENDING', - SubmittedAt: 'Mon Aug 04 2014 17: 44: 49 GMT - 0700(PDT)' - } - }]); - - domains.upsertDnsRecords('', domainCopy.domain, 'TXT', ['first', 'second', 'third'], function (error) { - expect(error).to.eql(null); - expect(awsAnswerQueue.length).to.eql(0); - - done(); - }); - }); - - it('get succeeds', function (done) { - awsAnswerQueue.push([null, AWS_HOSTED_ZONES]); - awsAnswerQueue.push([null, { - ResourceRecordSets: [{ - Name: 'test.' + domainCopy.zoneName + '.', - Type: 'A', - ResourceRecords: [{ - Value: '1.2.3.4' - }] - }] - }]); - - domains.getDnsRecords('test', domainCopy.domain, 'A', function (error, result) { - expect(error).to.eql(null); - expect(result).to.be.an(Array); - expect(result.length).to.eql(1); - expect(result[0]).to.eql('1.2.3.4'); - expect(awsAnswerQueue.length).to.eql(0); - - done(); - }); - }); - - it('del succeeds', function (done) { - awsAnswerQueue.push([null, AWS_HOSTED_ZONES]); - awsAnswerQueue.push([null, { - ChangeInfo: { - Id: '/change/C2QLKQIWEI0BZF', - Status: 'PENDING', - SubmittedAt: 'Mon Aug 04 2014 17: 44: 49 GMT - 0700(PDT)' - } - }]); - - domains.removeDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { - expect(error).to.eql(null); - expect(awsAnswerQueue.length).to.eql(0); - - done(); - }); - }); - }); - - describe('gcdns', function () { - var HOSTED_ZONES = []; - var zoneQueue = []; - var _OriginalGCDNS; - - before(function (done) { - domainCopy.provider = 'gcdns'; - domainCopy.config = { - projectId: 'my-dns-proj', - credentials: { - 'client_email': '123456789349-compute@developer.gserviceaccount.com', - 'private_key': 'privatehushhush' - } - }; - - function mockery(queue) { - return function () { - var callback = arguments[--arguments.length]; - - var elem = queue.shift(); - if (!Array.isArray(elem)) throw (new Error('Mock answer required')); - - // if no callback passed, return a req object with send(); - if (typeof callback !== 'function') { - return { - httpRequest: { headers: {} }, - send: function (callback) { - expect(callback).to.be.a(Function); - callback.apply(callback, elem); - } - }; - } else { - callback.apply(callback, elem); - } - }; - } - - function fakeZone(name, ns, recordQueue) { - var zone = new GCDNS().zone(name.replace('.', '-')); - zone.metadata.dnsName = name + '.'; - zone.metadata.nameServers = ns || ['8.8.8.8', '8.8.4.4']; - zone.getRecords = mockery(recordQueue || zoneQueue); - zone.createChange = mockery(recordQueue || zoneQueue); - zone.replaceRecords = mockery(recordQueue || zoneQueue); - zone.deleteRecords = mockery(recordQueue || zoneQueue); - return zone; - } - HOSTED_ZONES = [fakeZone(domainCopy.domain), fakeZone('cloudron.us')]; - - _OriginalGCDNS = GCDNS.prototype.getZones; - GCDNS.prototype.getZones = mockery(zoneQueue); - - domains.update(domainCopy.domain, domainCopy, auditSource, done); - }); - - after(function () { - GCDNS.prototype.getZones = _OriginalGCDNS; - _OriginalGCDNS = null; - }); - - it('upsert non-existing record succeeds', function (done) { - zoneQueue.push([null, HOSTED_ZONES]); // getZone - zoneQueue.push([null, []]); // getRecords - zoneQueue.push([null, { id: '1' }]); - - domains.upsertDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { - expect(error).to.eql(null); - expect(zoneQueue.length).to.eql(0); - - done(); - }); - }); - - it('upsert existing record succeeds', function (done) { - zoneQueue.push([null, HOSTED_ZONES]); - zoneQueue.push([null, [new GCDNS().zone('test').record('A', { 'name': 'test', data: ['5.6.7.8'], ttl: 1 })]]); - zoneQueue.push([null, { id: '2' }]); - - domains.upsertDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { - expect(error).to.eql(null); - expect(zoneQueue.length).to.eql(0); - - done(); - }); - }); - - it('upsert multiple record succeeds', function (done) { - zoneQueue.push([null, HOSTED_ZONES]); - zoneQueue.push([null, []]); // getRecords - zoneQueue.push([null, { id: '3' }]); - - domains.upsertDnsRecords('', domainCopy.domain, 'TXT', ['first', 'second', 'third'], function (error) { - expect(error).to.eql(null); - expect(zoneQueue.length).to.eql(0); - - done(); - }); - }); - - it('get succeeds', function (done) { - zoneQueue.push([null, HOSTED_ZONES]); - zoneQueue.push([null, [new GCDNS().zone('test').record('A', { 'name': 'test', data: ['1.2.3.4', '5.6.7.8'], ttl: 1 })]]); - - domains.getDnsRecords('test', domainCopy.domain, 'A', function (error, result) { - expect(error).to.eql(null); - expect(result).to.be.an(Array); - expect(result.length).to.eql(2); - expect(result).to.eql(['1.2.3.4', '5.6.7.8']); - expect(zoneQueue.length).to.eql(0); - - done(); - }); - }); - - it('del succeeds', function (done) { - zoneQueue.push([null, HOSTED_ZONES]); - zoneQueue.push([null, [new GCDNS().zone('test').record('A', { 'name': 'test', data: ['5.6.7.8'], ttl: 1 })]]); - zoneQueue.push([null, { id: '5' }]); - - domains.removeDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4'], function (error) { - expect(error).to.eql(null); - expect(zoneQueue.length).to.eql(0); - + it('unregisters subdomain', function (done) { + dns.unregisterLocations([ { subdomain: app.location, domain: app.domain } ], (/*progress*/) => {}, function (error) { + expect(error).to.be(null); done(); }); }); diff --git a/src/test/domains-test.js b/src/test/domains-test.js index e266ec0d6..3e75af166 100644 --- a/src/test/domains-test.js +++ b/src/test/domains-test.js @@ -1,3 +1,4 @@ +/* jslint node:true */ /* global it:false */ /* global describe:false */ /* global before:false */ @@ -5,146 +6,118 @@ 'use strict'; -const common = require('./common.js'), +const appdb = require('../appdb.js'), + BoxError = require('../boxerror.js'), + common = require('./common.js'), domains = require('../domains.js'), expect = require('expect.js'), - js2xml = require('js2xmlparser').parse, - nock = require('nock'); + safe = require('safetydance'), + util = require('util'); describe('Domains', function () { - const { setup, cleanup, app, domain } = common; + const { setup, cleanup, domain, app, auditSource } = common; before(setup); after(cleanup); - describe('validateHostname', function () { - it('does not allow admin subdomain', function () { - expect(domains.validateHostname('my', domain)).to.be.an(Error); - }); + const DOMAIN_0 = { + domain: 'z0.com', + zoneName: 'z0.com', + provider: 'noop', + config: { }, + fallbackCertificate: null, + tlsConfig: { + provider: 'fallback' + }, + wellKnown: null + }; - it('cannot have >63 length subdomains', function () { - var s = Array(64).fill('s').join(''); - expect(domains.validateHostname(s, domain)).to.be.an(Error); - domain.zoneName = `dev.${s}.example.com`; - expect(domains.validateHostname(`dev.${s}`, domain)).to.be.an(Error); - }); - - it('allows only alphanumerics and hypen', function () { - expect(domains.validateHostname('#2r', domain)).to.be.an(Error); - expect(domains.validateHostname('a%b', domain)).to.be.an(Error); - expect(domains.validateHostname('ab_', domain)).to.be.an(Error); - expect(domains.validateHostname('ab.', domain)).to.be.an(Error); - expect(domains.validateHostname('ab..c', domain)).to.be.an(Error); - expect(domains.validateHostname('.ab', domain)).to.be.an(Error); - expect(domains.validateHostname('-ab', domain)).to.be.an(Error); - expect(domains.validateHostname('ab-', domain)).to.be.an(Error); - }); - - it('total length cannot exceed 255', function () { - var s = ''; - for (var i = 0; i < (255 - 'example.com'.length); i++) s += 's'; - - expect(domains.validateHostname(s, domain)).to.be.an(Error); - }); - - it('allow valid domains', function () { - expect(domains.validateHostname('a', domain)).to.be(null); - expect(domains.validateHostname('a0-x', domain)).to.be(null); - expect(domains.validateHostname('a0.x', domain)).to.be(null); - expect(domains.validateHostname('a0.x.y', domain)).to.be(null); - expect(domains.validateHostname('01', domain)).to.be(null); - }); + it('can add domain', async function () { + await domains.add(DOMAIN_0.domain, DOMAIN_0, auditSource); }); - describe('getName', function () { - it('works with zoneName==domain', function () { - const d = { - domain: 'example.com', - zoneName: 'example.com', - config: {} - }; - - expect(domains.getName(d, '', 'A')).to.be(''); - expect(domains.getName(d, 'www', 'A')).to.be('www'); - expect(domains.getName(d, 'www.dev', 'A')).to.be('www.dev'); - - expect(domains.getName(d, '', 'MX')).to.be(''); - - expect(domains.getName(d, '', 'TXT')).to.be(''); - expect(domains.getName(d, 'www', 'TXT')).to.be('www'); - expect(domains.getName(d, 'www.dev', 'TXT')).to.be('www.dev'); - }); - - it('works when zoneName!=domain', function () { - const d = { - domain: 'dev.example.com', - zoneName: 'example.com', - config: {} - }; - - expect(domains.getName(d, '', 'A')).to.be('dev'); - expect(domains.getName(d, 'www', 'A')).to.be('www.dev'); - expect(domains.getName(d, 'www.dev', 'A')).to.be('www.dev.dev'); - - expect(domains.getName(d, '', 'MX')).to.be('dev'); - - expect(domains.getName(d, '', 'TXT')).to.be('dev'); - expect(domains.getName(d, 'www', 'TXT')).to.be('www.dev'); - expect(domains.getName(d, 'www.dev', 'TXT')).to.be('www.dev.dev'); - }); + it('cannot add same domain twice', async function () { + const [error] = await safe(domains.add(DOMAIN_0.domain, DOMAIN_0, auditSource)); + expect(error.reason).to.be(BoxError.ALREADY_EXISTS); }); - describe('register', function () { - let awsHostedZones; + it('can get domain', async function () { + const result = await domains.get(DOMAIN_0.domain); + expect(result.domain).to.equal(DOMAIN_0.domain); + expect(result.zoneName).to.equal(DOMAIN_0.zoneName); + expect(result.config).to.eql(DOMAIN_0.config); + }); - it('registers subdomain', function (done) { - awsHostedZones = { - HostedZones: [{ - Id: '/hostedzone/ZONEID', - Name: `${domain.domain}.`, - CallerReference: '305AFD59-9D73-4502-B020-F4E6F889CB30', - ResourceRecordSetCount: 2, - ChangeInfo: { - Id: '/change/CKRTFJA0ANHXB', - Status: 'INSYNC' - } - }], - IsTruncated: false, - MaxItems: '100' - }; + it('cannot get non-existent domain', async function () { + const result = await domains.get('random'); + expect(result).to.be(null); + }); - nock.cleanAll(); + it('can update domain', async function () { + const newConfig = {}; + const newTlsConfig = { provider: 'letsencrypt-staging' }; + const newDomain = Object.assign({}, DOMAIN_0, { config: newConfig, tlsConfig: newTlsConfig }); - let awsScope = nock('http://localhost:5353') - .get('/2013-04-01/hostedzonesbyname?dnsname=example.com.&maxitems=1') - .times(2) - .reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { wrapHandlers: { HostedZones: () => 'HostedZone'} })) - .get('/2013-04-01/hostedzone/ZONEID/rrset?maxitems=1&name=applocation.' + domain.domain + '.&type=A') - .reply(200, js2xml('ListResourceRecordSetsResponse', { ResourceRecordSets: [ ] }, { 'Content-Type': 'application/xml' })) - .post('/2013-04-01/hostedzone/ZONEID/rrset/') - .reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'RRID', Status: 'INSYNC' } })); + await domains.update(DOMAIN_0.domain, newDomain, auditSource); - domains.registerLocations([ { subdomain: app.location, domain: app.domain } ], { overwriteDns: true }, (/*progress*/) => {}, function (error) { - expect(error).to.be(null); - expect(awsScope.isDone()).to.be.ok(); - done(); - }); - }); + const result = await domains.get(DOMAIN_0.domain); + expect(result.domain).to.equal(DOMAIN_0.domain); + expect(result.zoneName).to.equal(DOMAIN_0.zoneName); + expect(result.provider).to.equal(DOMAIN_0.provider); + expect(result.config).to.eql(newConfig); + expect(result.tlsConfig).to.eql(newTlsConfig); - it('unregisters subdomain', function (done) { - nock.cleanAll(); + DOMAIN_0.config = newConfig; + DOMAIN_0.tlsConfig = newTlsConfig; + }); - let awsScope = nock('http://localhost:5353') - .get('/2013-04-01/hostedzonesbyname?dnsname=example.com.&maxitems=1') - .reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { wrapHandlers: { HostedZones: () => 'HostedZone'} })) - .post('/2013-04-01/hostedzone/ZONEID/rrset/') - .reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'RRID', Status: 'INSYNC' } })); + it('can get all domains', async function () { + const result = await domains.list(); + expect(result.length).to.equal(2); - domains.unregisterLocations([ { subdomain: app.location, domain: app.domain } ], (/*progress*/) => {}, function (error) { - expect(error).to.be(null); - expect(awsScope.isDone()).to.be.ok(); - done(); - }); - }); + // sorted by domain + expect(result[0].domain).to.equal(domain.domain); + expect(result[0].zoneName).to.equal(domain.zoneName); + expect(result[0].provider).to.equal(domain.provider); + expect(result[0].config).to.eql(domain.config); + expect(result[0].tlsConfig).to.eql(domain.tlsConfig); + + expect(result[1].domain).to.equal(DOMAIN_0.domain); + expect(result[1].zoneName).to.equal(DOMAIN_0.zoneName); + expect(result[1].provider).to.equal(DOMAIN_0.provider); + expect(result[1].config).to.eql(DOMAIN_0.config); + expect(result[1].tlsConfig).to.eql(DOMAIN_0.tlsConfig); + }); + + it('cannot delete non-existing domain', async function () { + const [error] = await safe(domains.del('not.exists', auditSource)); + expect(error).to.be.a(BoxError); + expect(error.reason).to.equal(BoxError.NOT_FOUND); + }); + + it('cannot delete dashboard domain', async function () { + const [error] = await safe(domains.del(domain.domain, auditSource)); + expect(error).to.be.a(BoxError); + expect(error.reason).to.equal(BoxError.CONFLICT); + expect(error.message).to.equal('Cannot remove admin domain'); + }); + + it('cannot delete referenced domain', async function () { + const appCopy = Object.assign({}, app, { id: 'into', location: 'xx', domain: DOMAIN_0.domain, portBindings: {} }); + + await util.promisify(appdb.add)(appCopy.id, appCopy.appStoreId, appCopy.manifest, appCopy.location, appCopy.domain, appCopy.portBindings, appCopy); + + const [error] = await safe(domains.del(DOMAIN_0.domain, auditSource)); + expect(error.reason).to.equal(BoxError.CONFLICT); + expect(error.message).to.contain('Domain is in use by one or more app'); + + await util.promisify(appdb.del)(appCopy.id); + }); + + it('can delete existing domain', async function () { + await domains.del(DOMAIN_0.domain, auditSource); + + const result = await domains.get(DOMAIN_0.domain); + expect(result).to.be(null); }); }); diff --git a/src/test/reverseproxy-test.js b/src/test/reverseproxy-test.js index e84759288..47ee349ab 100644 --- a/src/test/reverseproxy-test.js +++ b/src/test/reverseproxy-test.js @@ -12,7 +12,7 @@ const common = require('./common.js'), paths = require('../paths.js'), reverseProxy = require('../reverseproxy.js'); -describe('Certificates', function () { +describe('Reverse Proxy', function () { const { setup, cleanup, domain, auditSource, app } = common; const domainCopy = Object.assign({}, domain); @@ -141,10 +141,10 @@ describe('Certificates', function () { }); describe('getApi - letsencrypt-prod', function () { - before(function (done) { + before(async function () { domainCopy.tlsConfig = { provider: 'letsencrypt-prod' }; - domains.update(domainCopy.domain, domainCopy, auditSource, done); + await domains.update(domainCopy.domain, domainCopy, auditSource); }); it('returns prod acme in prod cloudron', async function () { @@ -155,10 +155,10 @@ describe('Certificates', function () { }); describe('getApi - letsencrypt-staging', function () { - before(function (done) { + before(async function () { domainCopy.tlsConfig = { provider: 'letsencrypt-staging' }; - domains.update(domainCopy.domain, domainCopy, auditSource, done); + await domains.update(domainCopy.domain, domainCopy, auditSource); }); it('returns staging acme in prod cloudron', async function () { @@ -169,10 +169,10 @@ describe('Certificates', function () { }); describe('configureApp', function () { - before(function (done) { + before(async function () { domainCopy.tlsConfig = { provider: 'fallback' }; - domains.update(domainCopy.domain, domainCopy, auditSource, done); + await domains.update(domainCopy.domain, domainCopy, auditSource); }); it('configure nginx correctly', function (done) { diff --git a/src/wellknown.js b/src/wellknown.js index fadf9304c..e311ba139 100644 --- a/src/wellknown.js +++ b/src/wellknown.js @@ -14,6 +14,7 @@ const assert = require('assert'), util = require('util'); const MAIL_AUTOCONFIG_EJS = fs.readFileSync(__dirname + '/autoconfig.xml.ejs', { encoding: 'utf8' }); +const domainsGet = util.callbackify(domains.get); function get(domain, location, callback) { assert.strictEqual(typeof domain, 'string'); @@ -34,7 +35,7 @@ function get(domain, location, callback) { } else if (location === 'host-meta' || location === 'matrix/server' || location === 'matrix/client') { const type = location === 'host-meta' ? 'text/xml' : 'application/json'; - domains.get(domain, function (error, domainObject) { + domainsGet(domain, function (error, domainObject) { if (error) return callback(error); if (!domainObject.wellKnown || !(location in domainObject.wellKnown)) return callback(new BoxError(BoxError.NOT_FOUND, 'No custom well-known config'));