diff --git a/src/dig.js b/src/dig.js new file mode 100644 index 000000000..3e71b9aff --- /dev/null +++ b/src/dig.js @@ -0,0 +1,41 @@ +'use strict'; + +exports = module.exports = { + resolve, +}; + +const assert = require('assert'), + constants = require('./constants.js'), + dns = require('dns'), + safe = require('safetydance'), + _ = require('underscore'); + +// 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 +async function resolve(hostname, rrtype, options) { + assert.strictEqual(typeof hostname, 'string'); + assert.strictEqual(typeof rrtype, 'string'); + assert(options && typeof options === 'object'); + + 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); + + const [error, result] = safe(resolver.resolve(hostname, rrtype)); + clearTimeout(timerId); + + if (error && error.code === 'ECANCELLED') error.code = 'TIMEOUT'; + if (error) throw error; + + // 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 + return result; +} diff --git a/src/dns.js b/src/dns.js index d7eb0d41b..ae9234481 100644 --- a/src/dns.js +++ b/src/dns.js @@ -19,12 +19,6 @@ module.exports = exports = { checkDnsRecords, syncDnsRecords, - - resolve, - - promises: { - resolve: require('util').promisify(resolve) - } }; const apps = require('./apps.js'), @@ -32,7 +26,6 @@ const apps = require('./apps.js'), BoxError = require('./boxerror.js'), constants = require('./constants.js'), debug = require('debug')('box:dns'), - dns = require('dns'), domains = require('./domains.js'), ipaddr = require('ipaddr.js'), mail = require('./mail.js'), @@ -41,8 +34,7 @@ const apps = require('./apps.js'), settings = require('./settings.js'), sysinfo = require('./sysinfo.js'), tld = require('tldjs'), - util = require('util'), - _ = require('underscore'); + util = require('util'); // choose which subdomain backend we use for test purpose we use route53 function api(provider) { @@ -67,30 +59,30 @@ function api(provider) { } } -function fqdn(location, domainObject) { - assert.strictEqual(typeof location, 'string'); +function fqdn(subdomain, domainObject) { + assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof domainObject, 'object'); - return location + (location ? '.' : '') + domainObject.domain; + return subdomain + (subdomain ? '.' : '') + 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'); +function validateHostname(subdomain, domainObject) { + assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof domainObject, 'object'); - const hostname = fqdn(location, domainObject); + const hostname = fqdn(subdomain, 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 (RESERVED_LOCATIONS.indexOf(subdomain) !== -1) return new BoxError(BoxError.BAD_FIELD, subdomain + ' is reserved', { field: 'location' }); - if (hostname === settings.dashboardFqdn()) return new BoxError(BoxError.BAD_FIELD, location + ' is reserved', { field: 'location' }); + if (hostname === settings.dashboardFqdn()) return new BoxError(BoxError.BAD_FIELD, subdomain + ' is reserved', { field: 'location' }); // workaround https://github.com/oncletom/tld.js/issues/73 var tmp = hostname.replace('_', '-'); @@ -98,11 +90,11 @@ function validateHostname(location, domainObject) { if (hostname.length > 253) return new BoxError(BoxError.BAD_FIELD, 'Hostname length exceeds 253 characters', { field: 'location' }); - if (location) { + if (subdomain) { // 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' }); + if (subdomain.split('.').some(function (p) { return p.length > 63 || p.length < 1; })) return new BoxError(BoxError.BAD_FIELD, 'Invalid subdomain length', { field: 'location' }); + if (subdomain.match(/^[A-Za-z0-9-.]+$/) === null) return new BoxError(BoxError.BAD_FIELD, 'Subdomain can only contain alphanumeric, hyphen and dot', { field: 'location' }); + if (/^[-.]/.test(subdomain)) return new BoxError(BoxError.BAD_FIELD, 'Subdomain cannot start or end with hyphen or dot', { field: 'location' }); } return null; @@ -110,12 +102,12 @@ function validateHostname(location, domainObject) { // returns the 'name' that needs to be inserted into zone // eslint-disable-next-line no-unused-vars -function getName(domain, location, type) { +function getName(domain, subdomain, type) { const part = domain.domain.slice(0, -domain.zoneName.length - 1); - if (location === '') return part; + if (subdomain === '') return part; - return part ? `${location}.${part}` : location; + return part ? `${subdomain}.${part}` : subdomain; } function maybePromisify(func) { @@ -123,20 +115,20 @@ function maybePromisify(func) { return util.promisify(func); } -async function getDnsRecords(location, domain, type) { - assert.strictEqual(typeof location, 'string'); +async function getDnsRecords(subdomain, domain, type) { + assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof type, 'string'); const domainObject = await domains.get(domain); - return await maybePromisify(api(domainObject.provider).get)(domainObject, location, type); + return await maybePromisify(api(domainObject.provider).get)(domainObject, subdomain, type); } -async function checkDnsRecords(location, domain) { - assert.strictEqual(typeof location, 'string'); +async function checkDnsRecords(subdomain, domain) { + assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof domain, 'string'); - const ipv4Records = await getDnsRecords(location, domain, 'A'); + const ipv4Records = await getDnsRecords(subdomain, domain, 'A'); const ipv4 = await sysinfo.getServerIPv4(); // if empty OR exactly one record with the ip, we don't need to overwrite @@ -144,7 +136,7 @@ async function checkDnsRecords(location, domain) { const ipv6Enabled = await settings.getIPv6Config(); if (ipv6Enabled) { - const ipv6Records = await getDnsRecords(location, domain, 'AAAA'); + const ipv6Records = await getDnsRecords(subdomain, domain, 'AAAA'); const ipv6 = await sysinfo.getServerIPv6(); // if empty OR exactly one record with the ip, we don't need to overwrite @@ -155,33 +147,33 @@ async function checkDnsRecords(location, domain) { } // note: for TXT records the values must be quoted -async function upsertDnsRecords(location, domain, type, values) { - assert.strictEqual(typeof location, 'string'); +async function upsertDnsRecords(subdomain, domain, type, values) { + assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof type, 'string'); assert(Array.isArray(values)); - debug(`upsertDNSRecord: location ${location} on domain ${domain} of type ${type} with values ${JSON.stringify(values)}`); + debug(`upsertDNSRecord: location ${subdomain} on domain ${domain} of type ${type} with values ${JSON.stringify(values)}`); const domainObject = await domains.get(domain); - await maybePromisify(api(domainObject.provider).upsert)(domainObject, location, type, values); + await maybePromisify(api(domainObject.provider).upsert)(domainObject, subdomain, type, values); } -async function removeDnsRecords(location, domain, type, values) { - assert.strictEqual(typeof location, 'string'); +async function removeDnsRecords(subdomain, domain, type, values) { + assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof type, 'string'); assert(Array.isArray(values)); - debug('removeDNSRecord: %s on %s type %s values', location, domain, type, values); + debug('removeDNSRecord: %s on %s type %s values', subdomain, domain, type, values); const domainObject = await domains.get(domain); - const [error] = await safe(maybePromisify(api(domainObject.provider).del)(domainObject, location, type, values)); + const [error] = await safe(maybePromisify(api(domainObject.provider).del)(domainObject, subdomain, type, values)); if (error && error.reason !== BoxError.NOT_FOUND) throw error; } -async function waitForDnsRecord(location, domain, type, value, options) { - assert.strictEqual(typeof location, 'string'); +async function waitForDnsRecord(subdomain, domain, type, value, options) { + assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof domain, 'string'); assert(type === 'A' || type === 'AAAA' || type === 'TXT'); assert.strictEqual(typeof value, 'string'); @@ -192,7 +184,7 @@ async function waitForDnsRecord(location, domain, type, value, options) { // linode DNS takes ~15mins if (!options.interval) options.interval = domainObject.provider === 'linode' ? 20000 : 5000; - await maybePromisify(api(domainObject.provider).wait)(domainObject, location, type, value, options); + await api(domainObject.provider).wait(domainObject, subdomain, type, value, options); } function makeWildcard(vhost) { @@ -318,35 +310,3 @@ async function syncDnsRecords(options, progressCallback) { return { 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 b6b2d2e06..17f5982ea 100644 --- a/src/dns/cloudflare.js +++ b/src/dns/cloudflare.js @@ -245,36 +245,30 @@ function del(domainObject, location, type, values, callback) { }); } -function wait(domainObject, location, type, value, options, callback) { +async function wait(domainObject, subdomain, type, value, options) { assert.strictEqual(typeof domainObject, 'object'); - assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof value, 'string'); assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } - assert.strictEqual(typeof callback, 'function'); const domainConfig = domainObject.config, zoneName = domainObject.zoneName, - fqdn = dns.fqdn(location, domainObject); + fqdn = dns.fqdn(subdomain, domainObject); debug('wait: %s for zone %s of type %s', fqdn, zoneName, type); - getZoneByName(domainConfig, zoneName, function(error, result) { - if (error) return callback(error); + const result = await getZoneByName(domainConfig, zoneName); + const zoneId = result.id; - let zoneId = result.id; + const dnsRecords = await getDnsRecords(domainConfig, zoneId, fqdn, type); + if (dnsRecords.length === 0) throw new BoxError(BoxError.NOT_FOUND, 'Domain not found'); - getDnsRecords(domainConfig, zoneId, fqdn, type, function (error, dnsRecords) { - if (error) return callback(error); - if (dnsRecords.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Domain not found')); + if (!dnsRecords[0].proxied) return await waitForDns(fqdn, domainObject.zoneName, type, value, options); - if (!dnsRecords[0].proxied) return waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); + debug('wait: skipping wait of proxied domain'); - debug('wait: skipping wait of proxied domain'); - - callback(null); // maybe we can check for dns to be cloudflare IPs? https://api.cloudflare.com/#cloudflare-ips-cloudflare-ip-details - }); - }); + // maybe we can check for dns to be cloudflare IPs? https://api.cloudflare.com/#cloudflare-ips-cloudflare-ip-details } function verifyDomainConfig(domainObject, callback) { diff --git a/src/dns/digitalocean.js b/src/dns/digitalocean.js index 5838bf220..6c1870763 100644 --- a/src/dns/digitalocean.js +++ b/src/dns/digitalocean.js @@ -221,17 +221,16 @@ function del(domainObject, location, type, values, callback) { }); } -function wait(domainObject, location, type, value, options, callback) { +async function wait(domainObject, subdomain, type, value, options) { assert.strictEqual(typeof domainObject, 'object'); - assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof value, 'string'); assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } - assert.strictEqual(typeof callback, 'function'); - const fqdn = dns.fqdn(location, domainObject); + const fqdn = dns.fqdn(subdomain, domainObject); - waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); + await waitForDns(fqdn, domainObject.zoneName, type, value, options); } function verifyDomainConfig(domainObject, callback) { diff --git a/src/dns/gandi.js b/src/dns/gandi.js index 4f0d60e09..7c8cfabe7 100644 --- a/src/dns/gandi.js +++ b/src/dns/gandi.js @@ -121,17 +121,16 @@ function del(domainObject, location, type, values, callback) { }); } -function wait(domainObject, location, type, value, options, callback) { +async function wait(domainObject, subdomain, type, value, options) { assert.strictEqual(typeof domainObject, 'object'); - assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof value, 'string'); assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } - assert.strictEqual(typeof callback, 'function'); - const fqdn = dns.fqdn(location, domainObject); + const fqdn = dns.fqdn(subdomain, domainObject); - waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); + await waitForDns(fqdn, domainObject.zoneName, type, value, options); } function verifyDomainConfig(domainObject, callback) { diff --git a/src/dns/gcdns.js b/src/dns/gcdns.js index ce4a72846..8604885ad 100644 --- a/src/dns/gcdns.js +++ b/src/dns/gcdns.js @@ -173,17 +173,16 @@ function del(domainObject, location, type, values, callback) { }); } -function wait(domainObject, location, type, value, options, callback) { +async function wait(domainObject, subdomain, type, value, options) { assert.strictEqual(typeof domainObject, 'object'); - assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof value, 'string'); assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } - assert.strictEqual(typeof callback, 'function'); - const fqdn = dns.fqdn(location, domainObject); + const fqdn = dns.fqdn(subdomain, domainObject); - waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); + await waitForDns(fqdn, domainObject.zoneName, type, value, options); } function verifyDomainConfig(domainObject, callback) { diff --git a/src/dns/godaddy.js b/src/dns/godaddy.js index cc771dcb9..a654c060f 100644 --- a/src/dns/godaddy.js +++ b/src/dns/godaddy.js @@ -156,17 +156,16 @@ function del(domainObject, location, type, values, callback) { }); } -function wait(domainObject, location, type, value, options, callback) { +async function wait(domainObject, subdomain, type, value, options) { assert.strictEqual(typeof domainObject, 'object'); - assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof value, 'string'); assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } - assert.strictEqual(typeof callback, 'function'); - const fqdn = dns.fqdn(location, domainObject); + const fqdn = dns.fqdn(subdomain, domainObject); - waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); + await waitForDns(fqdn, domainObject.zoneName, type, value, options); } function verifyDomainConfig(domainObject, callback) { diff --git a/src/dns/interface.js b/src/dns/interface.js index 68bcc5605..327b12ea2 100644 --- a/src/dns/interface.js +++ b/src/dns/interface.js @@ -29,9 +29,9 @@ function injectPrivateFields(newConfig, currentConfig) { // in-place injection of tokens and api keys which came in with constants.SECRET_PLACEHOLDER } -function upsert(domainObject, location, type, values, callback) { +function upsert(domainObject, subdomain, type, values, callback) { assert.strictEqual(typeof domainObject, 'object'); - assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof type, 'string'); assert(Array.isArray(values)); assert.strictEqual(typeof callback, 'function'); @@ -41,9 +41,9 @@ function upsert(domainObject, location, type, values, callback) { callback(new BoxError(BoxError.NOT_IMPLEMENTED, 'upsert is not implemented')); } -function get(domainObject, location, type, callback) { +function get(domainObject, subdomain, type, callback) { assert.strictEqual(typeof domainObject, 'object'); - assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof callback, 'function'); @@ -52,9 +52,9 @@ function get(domainObject, location, type, callback) { callback(new BoxError(BoxError.NOT_IMPLEMENTED, 'get is not implemented')); } -function del(domainObject, location, type, values, callback) { +function del(domainObject, subdomain, type, values, callback) { assert.strictEqual(typeof domainObject, 'object'); - assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof type, 'string'); assert(Array.isArray(values)); assert.strictEqual(typeof callback, 'function'); @@ -64,22 +64,18 @@ function del(domainObject, location, type, values, callback) { callback(new BoxError(BoxError.NOT_IMPLEMENTED, 'del is not implemented')); } -function wait(domainObject, location, type, value, options, callback) { +async function wait(domainObject, subdomain, type, value, options) { assert.strictEqual(typeof domainObject, 'object'); - assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof value, 'string'); assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } - assert.strictEqual(typeof callback, 'function'); - - callback(); } -function verifyDomainConfig(domainObject, callback) { +async function verifyDomainConfig(domainObject) { assert.strictEqual(typeof domainObject, 'object'); - assert.strictEqual(typeof callback, 'function'); // Result: domainConfig object - callback(new BoxError(BoxError.NOT_IMPLEMENTED, 'verifyDomainConfig is not implemented')); + throw new BoxError(BoxError.NOT_IMPLEMENTED, 'verifyDomainConfig is not implemented'); } diff --git a/src/dns/linode.js b/src/dns/linode.js index 0790c7512..444a4b7ec 100644 --- a/src/dns/linode.js +++ b/src/dns/linode.js @@ -254,17 +254,16 @@ function del(domainObject, location, type, values, callback) { }); } -function wait(domainObject, location, type, value, options, callback) { +async function wait(domainObject, subdomain, type, value, options) { assert.strictEqual(typeof domainObject, 'object'); - assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof value, 'string'); assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } - assert.strictEqual(typeof callback, 'function'); - const fqdn = dns.fqdn(location, domainObject); + const fqdn = dns.fqdn(subdomain, domainObject); - waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); + await waitForDns(fqdn, domainObject.zoneName, type, value, options); } function verifyDomainConfig(domainObject, callback) { diff --git a/src/dns/manual.js b/src/dns/manual.js index ef1b0f2aa..656ed99fd 100644 --- a/src/dns/manual.js +++ b/src/dns/manual.js @@ -56,17 +56,16 @@ function del(domainObject, location, type, values, callback) { return callback(); } -function wait(domainObject, location, type, value, options, callback) { +async function wait(domainObject, subdomain, type, value, options) { assert.strictEqual(typeof domainObject, 'object'); - assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof value, 'string'); assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } - assert.strictEqual(typeof callback, 'function'); - const fqdn = dns.fqdn(location, domainObject); + const fqdn = dns.fqdn(subdomain, domainObject); - waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); + await waitForDns(fqdn, domainObject.zoneName, type, value, options); } function verifyDomainConfig(domainObject, callback) { diff --git a/src/dns/namecheap.js b/src/dns/namecheap.js index 52d3743b3..498b5c769 100644 --- a/src/dns/namecheap.js +++ b/src/dns/namecheap.js @@ -305,15 +305,14 @@ function verifyDomainConfig(domainObject, callback) { }); } -function wait(domainObject, subdomain, type, value, options, callback) { +async function wait(domainObject, subdomain, type, value, options) { assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof value, 'string'); assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } - assert.strictEqual(typeof callback, 'function'); const fqdn = dns.fqdn(subdomain, domainObject); - waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); + await waitForDns(fqdn, domainObject.zoneName, type, value, options); } diff --git a/src/dns/namecom.js b/src/dns/namecom.js index 24391ea1c..0af583f98 100644 --- a/src/dns/namecom.js +++ b/src/dns/namecom.js @@ -225,17 +225,16 @@ function del(domainObject, location, type, values, callback) { }); } -function wait(domainObject, location, type, value, options, callback) { +async function wait(domainObject, subdomain, type, value, options) { assert.strictEqual(typeof domainObject, 'object'); - assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof value, 'string'); assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } - assert.strictEqual(typeof callback, 'function'); - const fqdn = dns.fqdn(location, domainObject); + const fqdn = dns.fqdn(subdomain, domainObject); - waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); + await waitForDns(fqdn, domainObject.zoneName, type, value, options); } function verifyDomainConfig(domainObject, callback) { diff --git a/src/dns/netcup.js b/src/dns/netcup.js index 0dcc4376c..361989f4e 100644 --- a/src/dns/netcup.js +++ b/src/dns/netcup.js @@ -232,17 +232,16 @@ function del(domainObject, location, type, values, callback) { }); } -function wait(domainObject, location, type, value, options, callback) { +async function wait(domainObject, subdomain, type, value, options) { assert.strictEqual(typeof domainObject, 'object'); - assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof value, 'string'); assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } - assert.strictEqual(typeof callback, 'function'); - const fqdn = dns.fqdn(location, domainObject); + const fqdn = dns.fqdn(subdomain, domainObject); - waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); + await waitForDns(fqdn, domainObject.zoneName, type, value, options); } function verifyDomainConfig(domainObject, callback) { diff --git a/src/dns/noop.js b/src/dns/noop.js index 3dda59908..9222b6470 100644 --- a/src/dns/noop.js +++ b/src/dns/noop.js @@ -52,15 +52,14 @@ function del(domainObject, location, type, values, callback) { return callback(); } -function wait(domainObject, location, type, value, options, callback) { +async function wait(domainObject, location, type, value, options) { assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof location, 'string'); assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof value, 'string'); assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } - assert.strictEqual(typeof callback, 'function'); - callback(); + // do nothing } function verifyDomainConfig(domainObject, callback) { diff --git a/src/dns/route53.js b/src/dns/route53.js index 933a63025..22cb9e95f 100644 --- a/src/dns/route53.js +++ b/src/dns/route53.js @@ -231,17 +231,16 @@ function del(domainObject, location, type, values, callback) { }); } -function wait(domainObject, location, type, value, options, callback) { +async function wait(domainObject, subdomain, type, value, options) { assert.strictEqual(typeof domainObject, 'object'); - assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof value, 'string'); assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } - assert.strictEqual(typeof callback, 'function'); - const fqdn = dns.fqdn(location, domainObject); + const fqdn = dns.fqdn(subdomain, domainObject); - waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); + await waitForDns(fqdn, domainObject.zoneName, type, value, options); } function verifyDomainConfig(domainObject, callback) { diff --git a/src/dns/vultr.js b/src/dns/vultr.js index 831621fb8..4f7e5c093 100644 --- a/src/dns/vultr.js +++ b/src/dns/vultr.js @@ -221,17 +221,16 @@ function del(domainObject, location, type, values, callback) { }); } -function wait(domainObject, location, type, value, options, callback) { +async function wait(domainObject, subdomain, type, value, options) { assert.strictEqual(typeof domainObject, 'object'); - assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof value, 'string'); assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } - assert.strictEqual(typeof callback, 'function'); - const fqdn = dns.fqdn(location, domainObject); + const fqdn = dns.fqdn(subdomain, domainObject); - waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); + await waitForDns(fqdn, domainObject.zoneName, type, value, options); } function verifyDomainConfig(domainObject, callback) { diff --git a/src/dns/waitfordns.js b/src/dns/waitfordns.js index 6373c55e6..9e3153f1e 100644 --- a/src/dns/waitfordns.js +++ b/src/dns/waitfordns.js @@ -3,35 +3,33 @@ exports = module.exports = waitForDns; const assert = require('assert'), - async = require('async'), BoxError = require('../boxerror.js'), debug = require('debug')('box:dns/waitfordns'), - dns = require('../dns.js'); + dig = require('../dig.js'), + promiseRetry = require('../promise-retry.js'), + safe = require('safetydance'); -function resolveIp(hostname, type, options, callback) { +async function resolveIp(hostname, type, options) { assert.strictEqual(typeof hostname, 'string'); assert(type === 'A' || type === 'AAAA'); assert.strictEqual(typeof options, 'object'); - assert.strictEqual(typeof callback, 'function'); // try A record at authoritative server debug(`resolveIp: Checking if ${hostname} has ${type} record at ${options.server}`); - dns.resolve(hostname, type, options, function (error, results) { - if (!error && results.length !== 0) return callback(null, results); + const [error, results] = await safe(dig.resolve(hostname, type, options)); + if (!error && results.length !== 0) return results; - // try CNAME record at authoritative server - debug(`resolveIp: Checking if ${hostname} has CNAME record at ${options.server}`); - dns.resolve(hostname, 'CNAME', options, function (error, results) { - if (error || results.length === 0) return callback(error, results); + // try CNAME record at authoritative server + debug(`resolveIp: Checking if ${hostname} has CNAME record at ${options.server}`); + const cnameResults = await dig.resolve(hostname, 'CNAME', options); + if (cnameResults.length === 0) return cnameResults; - // recurse lookup the CNAME record - debug(`resolveIp: Resolving ${hostname}'s CNAME record ${results[0]}`); - dns.resolve(results[0], type, { server: '127.0.0.1', timeout: options.timeout }, callback); - }); - }); + // recurse lookup the CNAME record + debug(`resolveIp: Resolving ${hostname}'s CNAME record ${results[0]}`); + await dig.resolve(results[0], type, options); } -function isChangeSynced(hostname, type, value, nameserver, callback) { +async function isChangeSynced(hostname, type, value, nameserver) { assert.strictEqual(typeof hostname, 'string'); assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof value, 'string'); @@ -39,73 +37,65 @@ function isChangeSynced(hostname, type, value, nameserver, callback) { assert.strictEqual(typeof callback, 'function'); // ns records cannot have cname - dns.resolve(nameserver, 'A', { timeout: 5000 }, function (error, nsIps) { - if (error || !nsIps || nsIps.length === 0) { - debug(`isChangeSynced: cannot resolve NS ${nameserver}`); // it's fine if one or more ns are dead - return callback(null, true); + const [error, nsIps] = await safe(dig.resolve(nameserver, 'A', { timeout: 5000 })); + if (error || !nsIps || nsIps.length === 0) { + debug(`isChangeSynced: cannot resolve NS ${nameserver}`); // it's fine if one or more ns are dead + return true; + } + + const status = []; + for (let i = 0; i < nsIps.length; i++) { + const nsIp = nsIps[i]; + const resolveOptions = { server: nsIp, timeout: 5000 }; + const resolver = type === 'A' || type === 'AAAA' ? resolveIp(hostname, type, resolveOptions) : dig.resolve(hostname, 'TXT', resolveOptions); + + const [error, answer] = await safe(resolver); + if (error && error.code === 'TIMEOUT') { + debug(`isChangeSynced: NS ${nameserver} (${nsIp}) timed out when resolving ${hostname} (${type})`); + status[i] = true; // should be ok if dns server is down + continue; } - async.every(nsIps, function (nsIp, iteratorCallback) { - const resolveOptions = { server: nsIp, timeout: 5000 }; - const resolver = type === 'A' || type === 'AAAA' ? resolveIp.bind(null, hostname, type) : dns.resolve.bind(null, hostname, 'TXT'); + if (error) { + debug(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${hostname} (${type}): ${error}`); + status[i] = false; + continue; + } - resolver(resolveOptions, function (error, answer) { - if (error && error.code === 'TIMEOUT') { - debug(`isChangeSynced: NS ${nameserver} (${nsIp}) timed out when resolving ${hostname} (${type})`); - return iteratorCallback(null, true); // should be ok if dns server is down - } + let match; + if (type === 'A' || type === 'AAAA') { + match = answer.length === 1 && answer[0] === value; + } else if (type === 'TXT') { // answer is a 2d array of strings + match = answer.some(function (a) { return value === a.join(''); }); + } - if (error) { - debug(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${hostname} (${type}): ${error}`); - return iteratorCallback(null, false); - } + debug(`isChangeSynced: ${hostname} (${type}) was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}. Match ${match}`); + status[i] = match; + } - let match; - if (type === 'A' || type === 'AAAA') { - match = answer.length === 1 && answer[0] === value; - } else if (type === 'TXT') { // answer is a 2d array of strings - match = answer.some(function (a) { return value === a.join(''); }); - } - - debug(`isChangeSynced: ${hostname} (${type}) was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}. Match ${match}`); - - iteratorCallback(null, match); - }); - }, callback); - - }); + return status.every(s => s === true); } // check if IP change has propagated to every nameserver -function waitForDns(hostname, zoneName, type, value, options, callback) { +async function waitForDns(hostname, zoneName, type, value, options) { assert.strictEqual(typeof hostname, 'string'); assert.strictEqual(typeof zoneName, 'string'); assert(type === 'A' || type === 'AAAA' || type === 'TXT'); assert.strictEqual(typeof value, 'string'); assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } - assert.strictEqual(typeof callback, 'function'); - debug('waitForDns: hostname %s to be %s in zone %s.', hostname, value, zoneName); + debug(`waitForDns: ${hostname} to be ${value} in zone ${zoneName}`); - var attempt = 0; - async.retry(options, function (retryCallback) { - ++attempt; - debug(`waitForDns (try ${attempt}): ${hostname} to be ${value} in zone ${zoneName}`); + await promiseRetry(Object.assign({ debug }, options), async function () { + const nameservers = await dig.resolve(zoneName, 'NS', { timeout: 5000 }); + if (!nameservers) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Unable to get nameservers'); - dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) { - if (error || !nameservers) return retryCallback(error || new BoxError(BoxError.EXTERNAL_ERROR, 'Unable to get nameservers')); - - async.every(nameservers, isChangeSynced.bind(null, hostname, type, value), function (error, synced) { - debug('waitForDns: %s %s ns: %j', hostname, synced ? 'done' : 'not done', nameservers); - - retryCallback(synced ? null : new BoxError(BoxError.EXTERNAL_ERROR, 'ETRYAGAIN')); - }); - }); - }, function retryDone(error) { - if (error) return callback(error); - - debug(`waitForDns: ${hostname} has propagated`); - - callback(null); + for (const nameserver of nameservers) { + const synced = await isChangeSynced(hostname, type, value, nameserver); + debug('waitForDns: %s %s ns: %j', hostname, synced ? 'done' : 'not done', nameservers); + if (!synced) throw new BoxError(BoxError.EXTERNAL_ERROR, 'ETRYAGAIN'); + } }); + + debug(`waitForDns: ${hostname} has propagated`); } diff --git a/src/dns/wildcard.js b/src/dns/wildcard.js index b45151514..bdf6ebe4f 100644 --- a/src/dns/wildcard.js +++ b/src/dns/wildcard.js @@ -13,6 +13,7 @@ exports = module.exports = { const assert = require('assert'), BoxError = require('../boxerror.js'), debug = require('debug')('box:dns/manual'), + dig = require('../dig.js'), dns = require('../dns.js'), safe = require('safetydance'), settings = require('../settings.js'), @@ -58,17 +59,16 @@ function del(domainObject, location, type, values, callback) { return callback(); } -function wait(domainObject, location, type, value, options, callback) { +async function wait(domainObject, subdomain, type, value, options) { assert.strictEqual(typeof domainObject, 'object'); - assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof value, 'string'); assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } - assert.strictEqual(typeof callback, 'function'); - const fqdn = dns.fqdn(location, domainObject); + const fqdn = dns.fqdn(subdomain, domainObject); - waitForDns(fqdn, domainObject.zoneName, type, value, options, callback); + await waitForDns(fqdn, domainObject.zoneName, type, value, options); } async function verifyDomainConfig(domainObject) { @@ -76,14 +76,14 @@ async function verifyDomainConfig(domainObject) { const zoneName = domainObject.zoneName; - const [error, nameservers] = await safe(dns.promises.resolve(zoneName, 'NS', { timeout: 5000 })); + const [error, nameservers] = await safe(dig.resolve(zoneName, 'NS', { timeout: 5000 })); if (error && error.code === 'ENOTFOUND') throw new BoxError(BoxError.BAD_FIELD, 'Unable to resolve nameservers for this domain', { field: 'nameservers' }); if (error || !nameservers) throw new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' }); const location = 'cloudrontestdns'; const fqdn = dns.fqdn(location, domainObject); - const [ipv4Error, ipv4Result] = await safe(dns.promises.resolve(fqdn, 'A', { server: '127.0.0.1', timeout: 5000 })); + const [ipv4Error, ipv4Result] = await safe(dig.resolve(fqdn, 'A', { server: '127.0.0.1', timeout: 5000 })); if (ipv4Error && ipv4Error.code === 'ENOTFOUND') throw new BoxError(BoxError.BAD_FIELD, `Unable to resolve IPv4 of ${fqdn}`, { field: 'nameservers' }); if (ipv4Error || !ipv4Result) throw new BoxError(BoxError.BAD_FIELD, ipv4Error ? ipv4Error.message : `Unable to resolve IPv4 of ${fqdn}`, { field: 'nameservers' }); @@ -92,7 +92,7 @@ async function verifyDomainConfig(domainObject) { const ipv6Enabled = await settings.getIPv6Config(); if (ipv6Enabled) { - const [ipv6Error, ipv6Result] = await safe(dns.promises.resolve(fqdn, 'AAAA', { server: '127.0.0.1', timeout: 5000 })); + const [ipv6Error, ipv6Result] = await safe(dig.resolve(fqdn, 'AAAA', { server: '127.0.0.1', timeout: 5000 })); if (ipv6Error && ipv6Error.code === 'ENOTFOUND') throw new BoxError(BoxError.BAD_FIELD, `Unable to resolve IPv6 of ${fqdn}`, { field: 'nameservers' }); if (ipv6Error || !ipv6Result) throw new BoxError(BoxError.BAD_FIELD, ipv6Error ? ipv6Error.message : `Unable to resolve IPv6 of ${fqdn}`, { field: 'nameservers' }); diff --git a/src/mail.js b/src/mail.js index 88c665e6c..b5f25c274 100644 --- a/src/mail.js +++ b/src/mail.js @@ -77,6 +77,7 @@ const assert = require('assert'), crypto = require('crypto'), database = require('./database.js'), debug = require('debug')('box:mail'), + dig = require('./dig.js'), dns = require('./dns.js'), docker = require('./docker.js'), domains = require('./domains.js'), @@ -267,7 +268,7 @@ async function checkDkim(mailDomain) { dkim.expected = `v=DKIM1; t=s; p=${publicKey}`; - const [error, txtRecords] = await safe(dns.promises.resolve(dkim.domain, dkim.type, DNS_OPTIONS)); + const [error, txtRecords] = await safe(dig.resolve(dkim.domain, dkim.type, DNS_OPTIONS)); if (error) { dkim.errorMessage = error.message; return dkim; @@ -296,7 +297,7 @@ async function checkSpf(domain, mailFqdn) { errorMessage: '' }; - const [error, txtRecords] = await safe(dns.promises.resolve(spf.domain, spf.type, DNS_OPTIONS)); + const [error, txtRecords] = await safe(dig.resolve(spf.domain, spf.type, DNS_OPTIONS)); if (error) { spf.errorMessage = error.message; return spf; @@ -334,7 +335,7 @@ async function checkMx(domain, mailFqdn) { errorMessage: '' }; - const [error, mxRecords] = await safe(dns.promises.resolve(mx.domain, mx.type, DNS_OPTIONS)); + const [error, mxRecords] = await safe(dig.resolve(mx.domain, mx.type, DNS_OPTIONS)); if (error) { mx.errorMessage = error.message; return mx; @@ -347,7 +348,7 @@ async function checkMx(domain, mailFqdn) { if (mx.status) return mx; // MX record is "my." // cloudflare might create a conflict subdomain (https://support.cloudflare.com/hc/en-us/articles/360020296512-DNS-Troubleshooting-FAQ) - const [error2, mxIps] = await safe(dns.promises.resolve(mxRecords[0].exchange, 'A', DNS_OPTIONS)); + const [error2, mxIps] = await safe(dig.resolve(mxRecords[0].exchange, 'A', DNS_OPTIONS)); if (error2 || mxIps.length !== 1) return mx; const [error3, ip] = await safe(sysinfo.getServerIPv4()); @@ -379,7 +380,7 @@ async function checkDmarc(domain) { errorMessage: '' }; - const [error, txtRecords] = await safe(dns.promises.resolve(dmarc.domain, dmarc.type, DNS_OPTIONS)); + const [error, txtRecords] = await safe(dig.resolve(dmarc.domain, dmarc.type, DNS_OPTIONS)); if (error) { dmarc.errorMessage = error.message; @@ -417,7 +418,7 @@ async function checkPtr(mailFqdn) { ptr.domain = ip.split('.').reverse().join('.') + '.in-addr.arpa'; ptr.name = ip; - const [error2, ptrRecords] = await safe(dns.promises.resolve(ptr.domain, 'PTR', DNS_OPTIONS)); + const [error2, ptrRecords] = await safe(dig.resolve(ptr.domain, 'PTR', DNS_OPTIONS)); if (error2) { ptr.errorMessage = error2.message; return ptr; @@ -500,14 +501,14 @@ async function checkRblStatus(domain) { // https://tools.ietf.org/html/rfc5782 const blacklistedServers = []; for (const rblServer of RBL_LIST) { - const [error, records] = await safe(dns.promises.resolve(flippedIp + '.' + rblServer.dns, 'A', DNS_OPTIONS)); + const [error, records] = await safe(dig.resolve(flippedIp + '.' + rblServer.dns, 'A', DNS_OPTIONS)); if (error || !records) continue; // not listed debug(`checkRblStatus: ${domain} (ip: ${flippedIp}) is in the blacklist of ${JSON.stringify(rblServer)}`); const result = _.extend({ }, rblServer); - const [error2, txtRecords] = await safe(dns.promises.resolve(flippedIp + '.' + rblServer.dns, 'TXT', DNS_OPTIONS)); + const [error2, txtRecords] = await safe(dig.resolve(flippedIp + '.' + rblServer.dns, 'TXT', DNS_OPTIONS)); result.txtRecords = error2 || !txtRecords ? 'No txt record' : txtRecords.map(x => x.join('')); debug(`checkRblStatus: ${domain} (error: ${error2.message}) (txtRecords: ${JSON.stringify(txtRecords)})`); diff --git a/src/routes/test/mail-test.js b/src/routes/test/mail-test.js index 5d255150a..bb2aea95e 100644 --- a/src/routes/test/mail-test.js +++ b/src/routes/test/mail-test.js @@ -54,11 +54,11 @@ describe('Mail API', function () { let dkimDomain, spfDomain, mxDomain, dmarcDomain; before(function (done) { - const dns = require('../../dns.js'); + const dig = require('../../dig.js'); // replace dns resolveTxt() - resolve = dns.promises.resolve; - dns.promises.resolve = async function (hostname, type/*, options*/) { + resolve = dig.resolve; + dig.resolve = async function (hostname, type/*, options*/) { expect(hostname).to.be.a('string'); if (!dnsAnswerQueue[hostname] || !(type in dnsAnswerQueue[hostname])) throw new Error('no mock answer'); @@ -84,9 +84,9 @@ describe('Mail API', function () { }); after(function (done) { - const dns = require('../../dns.js'); + const dig = require('../../dig.js'); - dns.promises.resolve = resolve; + dig.resolve = resolve; done(); });