diff --git a/src/apptask.js b/src/apptask.js index d69862630..62278fbaf 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -38,6 +38,7 @@ var addons = require('./addons.js'), DatabaseError = require('./databaseerror.js'), debug = require('debug')('box:apptask'), docker = require('./docker.js'), + domains = require('./domains.js'), ejs = require('ejs'), fs = require('fs'), manifestFormat = require('cloudron-manifestformat'), @@ -49,7 +50,6 @@ var addons = require('./addons.js'), safe = require('safetydance'), shell = require('./shell.js'), SubdomainError = require('./subdomains.js').SubdomainError, - subdomains = require('./subdomains.js'), superagent = require('superagent'), sysinfo = require('./sysinfo.js'), tld = require('tldjs'), @@ -255,14 +255,14 @@ function registerSubdomain(app, overwrite, callback) { debugApp(app, 'Registering subdomain location [%s] overwrite: %s', app.location, overwrite); // get the current record before updating it - subdomains.get(app.location, 'A', function (error, values) { + domains.getDNSRecords(app.location, 'A', function (error, values) { if (error) return retryCallback(error); // refuse to update any existing DNS record for custom domains that we did not create // note that the appstore sets up the naked domain for non-custom domains if (config.isCustomDomain() && values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists')); - subdomains.upsert(app.location, 'A', [ ip ], function (error, changeId) { + domains.upsertDNSRecords(app.location, 'A', [ ip ], function (error, changeId) { if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again retryCallback(null, error || changeId); @@ -299,7 +299,7 @@ function unregisterSubdomain(app, location, callback) { async.retry({ times: 30, interval: 5000 }, function (retryCallback) { debugApp(app, 'Unregistering subdomain: %s', location); - subdomains.remove(location, 'A', [ ip ], function (error) { + domains.removeDNSRecords(location, 'A', [ ip ], function (error) { if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again retryCallback(null, error); @@ -334,7 +334,7 @@ function waitForDnsPropagation(app, callback) { sysinfo.getPublicIp(function (error, ip) { if (error) return callback(error); - subdomains.waitForDns(config.appFqdn(app.location), ip, 'A', { interval: 5000, times: 120 }, callback); + domains.waitForDNSRecord(app.location, ip, 'A', { interval: 5000, times: 120 }, callback); }); } @@ -348,10 +348,10 @@ function waitForAltDomainDnsPropagation(app, callback) { sysinfo.getPublicIp(function (error, ip) { if (error) return callback(error); - subdomains.waitForDns(app.altDomain, ip, 'A', { interval: 10000, times: 60 }, callback); + domains.waitForDNSRecord(app.altDomain, ip, 'A', { interval: 10000, times: 60 }, callback); }); } else { - subdomains.waitForDns(app.altDomain, config.appFqdn(app.location) + '.', 'CNAME', { interval: 10000, times: 60 }, callback); + domains.waitForDNSRecord(app.altDomain, app.location + '.', 'CNAME', { interval: 10000, times: 60 }, callback); } } diff --git a/src/domains.js b/src/domains.js index 113d7c42f..42414097a 100644 --- a/src/domains.js +++ b/src/domains.js @@ -7,14 +7,19 @@ module.exports = exports = { update: update, del: del, + getDNSRecords: getDNSRecords, + upsertDNSRecords: upsertDNSRecords, + removeDNSRecords: removeDNSRecords, + + waitForDNSRecord: waitForDNSRecord, + DomainError: DomainError }; var assert = require('assert'), DatabaseError = require('./databaseerror.js'), domaindb = require('./domaindb.js'), - subdomains = require('./subdomains.js'), - SubdomainError = subdomains.SubdomainError, + SubdomainError = require('./subdomains.js').SubdomainError, sysinfo = require('./sysinfo.js'), tld = require('tldjs'), util = require('util'); @@ -48,6 +53,38 @@ DomainError.INTERNAL_ERROR = 'Internal error'; DomainError.ACCESS_DENIED = 'Access denied'; DomainError.INVALID_PROVIDER = 'provider must be route53, gcdns, digitalocean, cloudflare, noop, manual or caas'; +// choose which subdomain backend we use for test purpose we use route53 +function api(provider) { + assert.strictEqual(typeof provider, 'string'); + + switch (provider) { + case 'caas': return require('./dns/caas.js'); + 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 'noop': return require('./dns/noop.js'); + case 'manual': return require('./dns/manual.js'); + default: return null; + } +} + +// TODO make it return a DomainError instead of SubdomainError +function verifyDnsConfig(config, domain, zoneName, ip, callback) { + assert(config && typeof config === 'object'); // the dns config to test with + assert(typeof config.provider === 'string'); + assert.strictEqual(typeof domain, 'string'); + assert.strictEqual(typeof zoneName, 'string'); + assert.strictEqual(typeof ip, 'string'); + assert.strictEqual(typeof callback, 'function'); + + var backend = api(config.provider); + if (!backend) return callback(new SubdomainError(SubdomainError.INVALID_PROVIDER)); + + api(config.provider).verifyDnsConfig(config, domain, zoneName, ip, callback); +} + + function add(domain, zoneName, config, callback) { assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof zoneName, 'string'); @@ -60,7 +97,7 @@ function add(domain, zoneName, config, callback) { sysinfo.getPublicIp(function (error, ip) { if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, 'Error getting IP:' + error.message)); - subdomains.verifyDnsConfig(config, domain, zoneName, ip, function (error, result) { + verifyDnsConfig(config, domain, zoneName, ip, function (error, result) { if (error && error.reason === SubdomainError.ACCESS_DENIED) return callback(new DomainError(DomainError.BAD_FIELD, 'Error adding A record. Access denied')); if (error && error.reason === SubdomainError.NOT_FOUND) return callback(new DomainError(DomainError.BAD_FIELD, 'Zone not found')); if (error && error.reason === SubdomainError.EXTERNAL_ERROR) return callback(new DomainError(DomainError.BAD_FIELD, 'Error adding A record:' + error.message)); @@ -112,7 +149,7 @@ function update(domain, config, callback) { sysinfo.getPublicIp(function (error, ip) { if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, 'Error getting IP:' + error.message)); - subdomains.verifyDnsConfig(config, domain, result.zoneName, ip, function (error, result) { + verifyDnsConfig(config, domain, result.zoneName, ip, function (error, result) { if (error && error.reason === SubdomainError.ACCESS_DENIED) return callback(new DomainError(DomainError.BAD_FIELD, 'Error adding A record. Access denied')); if (error && error.reason === SubdomainError.NOT_FOUND) return callback(new DomainError(DomainError.BAD_FIELD, 'Zone not found')); if (error && error.reason === SubdomainError.EXTERNAL_ERROR) return callback(new DomainError(DomainError.BAD_FIELD, 'Error adding A record:' + error.message)); @@ -144,3 +181,82 @@ function del(domain, callback) { return callback(null); }); } + +function getDNSRecords(fqdn, type, callback) { + assert.strictEqual(typeof fqdn, 'string'); + assert.strictEqual(typeof type, 'string'); + assert.strictEqual(typeof callback, 'function'); + + const domain = tld.getDomain(fqdn); + const subdomain = tld.getSubdomain(fqdn); + + get(domain, function (error, result) { + if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error)); + + api(result.config.provider).get(result.config, result.zoneName, subdomain, type, function (error, values) { + if (error) return callback(error); + + callback(null, values); + }); + }); +} + +function upsertDNSRecords(fqdn, type, values, callback) { + assert.strictEqual(typeof fqdn, 'string'); + assert.strictEqual(typeof type, 'string'); + assert(util.isArray(values)); + assert.strictEqual(typeof callback, 'function'); + + const domain = tld.getDomain(fqdn); + const subdomain = tld.getSubdomain(fqdn); + + get(domain, function (error, result) { + if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error)); + + api(result.config.provider).upsert(result.config, result.zoneName, subdomain, type, values, function (error, changeId) { + if (error) return callback(error); + + callback(null, changeId); + }); + }); +} + +function removeDNSRecords(fqdn, type, values, callback) { + assert.strictEqual(typeof fqdn, 'string'); + assert.strictEqual(typeof type, 'string'); + assert(util.isArray(values)); + assert.strictEqual(typeof callback, 'function'); + + const domain = tld.getDomain(fqdn); + const subdomain = tld.getSubdomain(fqdn); + + get(domain, function (error, result) { + if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error)); + + api(result.config.provider).del(result.config, result.zoneName, subdomain, type, values, function (error) { + if (error && error.reason !== SubdomainError.NOT_FOUND) return callback(error); + + callback(null); + }); + }); +} + +function waitForDNSRecord(fqdn, value, type, options, callback) { + assert.strictEqual(typeof fqdn, 'string'); + assert(typeof value === 'string' || util.isRegExp(value)); + assert(type === 'A' || type === 'CNAME' || type === 'TXT'); + assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } + assert.strictEqual(typeof callback, 'function'); + + const domain = tld.getDomain(fqdn); + + get(domain, function (error, result) { + if (error && error.reason !== DomainError.NOT_FOUND) return callback(new DomainError(DomainError.INTERNAL_ERROR, error)); + + // if the domain is on another zone in case of external domain, use the correct zone + const zoneName = result ? result.zoneName : tld.getDomain(domain); + const provider = result ? result.config.provider : 'manual'; + + api(provider).waitForDns(fqdn, zoneName, value, type, options, callback); + }); +} \ No newline at end of file