diff --git a/src/dig.js b/src/dig.js index 3e71b9aff..efa459612 100644 --- a/src/dig.js +++ b/src/dig.js @@ -19,7 +19,7 @@ async function resolve(hostname, rrtype, options) { 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(); + const resolver = new dns.promises.Resolver(); options = _.extend({ }, defaultOptions, options); // Only use unbound on a Cloudron @@ -28,7 +28,7 @@ async function resolve(hostname, rrtype, options) { // 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)); + const [error, result] = await safe(resolver.resolve(hostname, rrtype)); clearTimeout(timerId); if (error && error.code === 'ECANCELLED') error.code = 'TIMEOUT'; diff --git a/src/dns.js b/src/dns.js index a941123e0..af375017c 100644 --- a/src/dns.js +++ b/src/dns.js @@ -33,8 +33,7 @@ const apps = require('./apps.js'), safe = require('safetydance'), settings = require('./settings.js'), sysinfo = require('./sysinfo.js'), - tld = require('tldjs'), - util = require('util'); + tld = require('tldjs'); // choose which subdomain backend we use for test purpose we use route53 function api(provider) { @@ -110,18 +109,13 @@ function getName(domain, subdomain, type) { return part ? `${subdomain}.${part}` : subdomain; } -function maybePromisify(func) { - if (util.types.isAsyncFunction(func)) return func; - return util.promisify(func); -} - 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, subdomain, type); + return await api(domainObject.provider).get(domainObject, subdomain, type); } async function checkDnsRecords(subdomain, domain) { @@ -156,7 +150,7 @@ async function upsertDnsRecords(subdomain, domain, type, 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, subdomain, type, values); + await api(domainObject.provider).upsert(domainObject, subdomain, type, values); } async function removeDnsRecords(subdomain, domain, type, values) { @@ -168,8 +162,8 @@ async function removeDnsRecords(subdomain, 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, subdomain, type, values)); - if (error && error.reason !== BoxError.NOT_FOUND) throw error; + const [error] = await safe(api(domainObject.provider).del(domainObject, subdomain, type, values)); + if (error && error.reason !== BoxError.NOT_FOUND) throw error; // this is never returned afaict } async function waitForDnsRecord(subdomain, domain, type, value, options) { diff --git a/src/dns/digitalocean.js b/src/dns/digitalocean.js index 6c1870763..28393cb77 100644 --- a/src/dns/digitalocean.js +++ b/src/dns/digitalocean.js @@ -11,10 +11,10 @@ exports = module.exports = { }; const assert = require('assert'), - async = require('async'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), debug = require('debug')('box:dns/digitalocean'), + dig = require('../dig.js'), dns = require('../dns.js'), safe = require('safetydance'), superagent = require('superagent'), @@ -36,53 +36,45 @@ function injectPrivateFields(newConfig, currentConfig) { if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token; } -function getInternal(domainConfig, zoneName, name, type, callback) { +async function getZoneRecords(domainConfig, zoneName, name, type) { assert.strictEqual(typeof domainConfig, 'object'); assert.strictEqual(typeof zoneName, 'string'); assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof type, 'string'); - assert.strictEqual(typeof callback, 'function'); let nextPage = null, matchingRecords = []; debug(`getInternal: getting dns records of ${zoneName} with ${name} and type ${type}`); - async.doWhilst(function (iteratorDone) { + do { const url = nextPage ? nextPage : DIGITALOCEAN_ENDPOINT + '/v2/domains/' + zoneName + '/records'; - superagent.get(url) + const [error, response] = await safe(superagent.get(url) .set('Authorization', 'Bearer ' + domainConfig.token) .timeout(30 * 1000) .retry(5) - .end(function (error, result) { - if (error && !error.response) return iteratorDone(new BoxError(BoxError.NETWORK_ERROR, error.message)); - if (result.statusCode === 404) return iteratorDone(new BoxError(BoxError.NOT_FOUND, formatError(result))); - if (result.statusCode === 403 || result.statusCode === 401) return iteratorDone(new BoxError(BoxError.ACCESS_DENIED, formatError(result))); - if (result.statusCode !== 200) return iteratorDone(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result))); + .ok(() => true)); - matchingRecords = matchingRecords.concat(result.body.domain_records.filter(function (record) { - return (record.type === type && record.name === name); - })); + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); + if (response.statusCode === 404) throw new BoxError(BoxError.NOT_FOUND, formatError(response)); + if (response.statusCode === 403 || response.statusCode === 401) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response)); + if (response.statusCode !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response)); - nextPage = (result.body.links && result.body.links.pages) ? result.body.links.pages.next : null; + matchingRecords = matchingRecords.concat(response.body.domain_records.filter(function (record) { + return (record.type === type && record.name === name); + })); - iteratorDone(); - }); - }, function (testDone) { return testDone(null, !!nextPage); }, function (error) { - debug('getInternal:', error, JSON.stringify(matchingRecords)); + nextPage = (response.body.links && response.body.links.pages) ? response.body.links.pages.next : null; + } while (nextPage); - if (error) return callback(error); - - return callback(null, matchingRecords); - }); + return matchingRecords; } -function upsert(domainObject, location, type, values, callback) { +async function upsert(domainObject, location, type, values) { assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof location, 'string'); assert.strictEqual(typeof type, 'string'); assert(Array.isArray(values)); - assert.strictEqual(typeof callback, 'function'); const domainConfig = domainObject.config, zoneName = domainObject.zoneName, @@ -90,135 +82,106 @@ function upsert(domainObject, location, type, values, callback) { debug('upsert: %s for zone %s of type %s with values %j', name, zoneName, type, values); - getInternal(domainConfig, zoneName, name, type, function (error, result) { - if (error) return callback(error); + const result = await getZoneRecords(domainConfig, zoneName, name, type); - // used to track available records to update instead of create - var i = 0, recordIds = []; + // used to track available records to update instead of create + let i = 0, recordIds = []; - async.eachSeries(values, function (value, iteratorCallback) { - var priority = null; + for (let value of values) { + let priority = null; - if (type === 'MX') { - priority = value.split(' ')[0]; - value = value.split(' ')[1]; - } + if (type === 'MX') { + priority = value.split(' ')[0]; + value = value.split(' ')[1]; + } - var data = { - type: type, - name: name, - data: value, - priority: priority, - ttl: 30 // Recent DO DNS API break means this value must atleast be 30 - }; + const data = { + type: type, + name: name, + data: value, + priority: priority, + ttl: 30 // Recent DO DNS API break means this value must atleast be 30 + }; - if (i >= result.length) { - superagent.post(DIGITALOCEAN_ENDPOINT + '/v2/domains/' + zoneName + '/records') - .set('Authorization', 'Bearer ' + domainConfig.token) - .send(data) - .timeout(30 * 1000) - .retry(5) - .end(function (error, result) { - if (error && !error.response) return iteratorCallback(new BoxError(BoxError.NETWORK_ERROR, error.message)); - if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new BoxError(BoxError.ACCESS_DENIED, formatError(result))); - if (result.statusCode === 422) return iteratorCallback(new BoxError(BoxError.BAD_FIELD, result.body.message)); - if (result.statusCode !== 201) return iteratorCallback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result))); + if (i >= result.length) { + const [error, response] = await safe(superagent.post(DIGITALOCEAN_ENDPOINT + '/v2/domains/' + zoneName + '/records') + .set('Authorization', 'Bearer ' + domainConfig.token) + .send(data) + .timeout(30 * 1000) + .retry(5) + .ok(() => true)); - recordIds.push(safe.query(result.body, 'domain_record.id')); + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); + if (response.statusCode === 403 || response.statusCode === 401) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response)); + if (response.statusCode === 422) throw new BoxError(BoxError.BAD_FIELD, response.body.message); + if (response.statusCode !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response)); - return iteratorCallback(null); - }); - } else { - superagent.put(DIGITALOCEAN_ENDPOINT + '/v2/domains/' + zoneName + '/records/' + result[i].id) - .set('Authorization', 'Bearer ' + domainConfig.token) - .send(data) - .timeout(30 * 1000) - .retry(5) - .end(function (error, result) { - // increment, as we have consumed the record - ++i; + recordIds.push(safe.query(result.body, 'domain_record.id')); + } else { + const [error, response] = await safe(superagent.put(DIGITALOCEAN_ENDPOINT + '/v2/domains/' + zoneName + '/records/' + result[i].id) + .set('Authorization', 'Bearer ' + domainConfig.token) + .send(data) + .timeout(30 * 1000) + .retry(5) + .ok(() => true)); - if (error && !error.response) return iteratorCallback(new BoxError(BoxError.NETWORK_ERROR, error.message)); - if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new BoxError(BoxError.ACCESS_DENIED, formatError(result))); - if (result.statusCode === 422) return iteratorCallback(new BoxError(BoxError.BAD_FIELD, result.body.message)); - if (result.statusCode !== 200) return iteratorCallback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result))); + ++i; - recordIds.push(safe.query(result.body, 'domain_record.id')); + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); + if (response.statusCode === 403 || response.statusCode === 401) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response)); + if (response.statusCode === 422) throw new BoxError(BoxError.BAD_FIELD, response.body.message); + if (response.statusCode !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response)); - return iteratorCallback(null); - }); - } - }, function (error) { - if (error) return callback(error); + recordIds.push(safe.query(result.body, 'domain_record.id')); + } + } - debug('upsert: completed with recordIds:%j', recordIds); - - callback(); - }); - }); + debug('upsert: completed with recordIds:%j', recordIds); } -function get(domainObject, location, type, callback) { +async function get(domainObject, location, type) { assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof location, 'string'); assert.strictEqual(typeof type, 'string'); - assert.strictEqual(typeof callback, 'function'); const domainConfig = domainObject.config, zoneName = domainObject.zoneName, name = dns.getName(domainObject, location, type) || '@'; - getInternal(domainConfig, zoneName, name, type, function (error, result) { - if (error) return callback(error); + const result = await getZoneRecords(domainConfig, zoneName, name, type); - // We only return the value string - var tmp = result.map(function (record) { return record.data; }); - - debug('get: %j', tmp); - - return callback(null, tmp); - }); + const tmp = result.map(function (record) { return record.data; }); + return tmp; } -function del(domainObject, location, type, values, callback) { +async function del(domainObject, location, type, values) { assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof location, 'string'); assert.strictEqual(typeof type, 'string'); assert(Array.isArray(values)); - assert.strictEqual(typeof callback, 'function'); const domainConfig = domainObject.config, zoneName = domainObject.zoneName, name = dns.getName(domainObject, location, type) || '@'; - getInternal(domainConfig, zoneName, name, type, function (error, result) { - if (error) return callback(error); + const result = await getZoneRecords(domainConfig, zoneName, name, type); + if (result.length === 0) return; - if (result.length === 0) return callback(null); + const tmp = result.filter(function (record) { return values.some(function (value) { return value === record.data; }); }); + if (tmp.length === 0) return; - var tmp = result.filter(function (record) { return values.some(function (value) { return value === record.data; }); }); - - debug('del: %j', tmp); - - if (tmp.length === 0) return callback(null); - - // FIXME we only handle the first one currently - - superagent.del(DIGITALOCEAN_ENDPOINT + '/v2/domains/' + zoneName + '/records/' + tmp[0].id) + for (const r of tmp) { + const [error, response] = await safe(superagent.del(`${DIGITALOCEAN_ENDPOINT}/v2/domains/${zoneName}/records/${r.id}`) .set('Authorization', 'Bearer ' + domainConfig.token) .timeout(30 * 1000) .retry(5) - .end(function (error, result) { - if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message)); - if (result.statusCode === 404) return callback(null); - if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.ACCESS_DENIED, formatError(result))); - if (result.statusCode !== 204) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result))); + .ok(() => true)); - debug('del: done'); - - return callback(null); - }); - }); + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); + if (response.statusCode === 404) return; + if (response.statusCode === 403 || response.statusCode === 401) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response)); + if (response.statusCode !== 204) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response)); + } } async function wait(domainObject, subdomain, type, value, options) { @@ -233,46 +196,39 @@ async function wait(domainObject, subdomain, type, value, options) { await waitForDns(fqdn, domainObject.zoneName, type, value, options); } -function verifyDomainConfig(domainObject, callback) { +async function verifyDomainConfig(domainObject) { assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof callback, 'function'); const domainConfig = domainObject.config, zoneName = domainObject.zoneName; - if (!domainConfig.token || typeof domainConfig.token !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'token must be a non-empty string', { field: 'token' })); + if (!domainConfig.token || typeof domainConfig.token !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'token must be a non-empty string'); const ip = '127.0.0.1'; - var credentials = { + const credentials = { token: domainConfig.token }; - if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here + if (process.env.BOX_ENV === 'test') return credentials; // this shouldn't be here - dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) { - if (error && error.code === 'ENOTFOUND') return callback(new BoxError(BoxError.BAD_FIELD, 'Unable to resolve nameservers for this domain', { field: 'nameservers' })); - if (error || !nameservers) return callback(new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' })); + 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'); + if (error || !nameservers) throw new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'); - if (nameservers.map(function (n) { return n.toLowerCase(); }).indexOf('ns1.digitalocean.com') === -1) { - debug('verifyDomainConfig: %j does not contains DO NS', nameservers); - return callback(new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to DigitalOcean', { field: 'nameservers' })); - } + if (nameservers.map(function (n) { return n.toLowerCase(); }).indexOf('ns1.digitalocean.com') === -1) { + debug('verifyDomainConfig: %j does not contains DO NS', nameservers); + throw new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to DigitalOcean'); + } - const location = 'cloudrontestdns'; + const location = 'cloudrontestdns'; - upsert(domainObject, location, 'A', [ ip ], function (error) { - if (error) return callback(error); + await upsert(domainObject, location, 'A', [ ip ]); + debug('verifyDomainConfig: Test A record added'); - debug('verifyDomainConfig: Test A record added'); + await del(domainObject, location, 'A', [ ip ]); + debug('verifyDomainConfig: Test A record removed again'); - del(domainObject, location, 'A', [ ip ], function (error) { - if (error) return callback(error); - - debug('verifyDomainConfig: Test A record removed again'); - - callback(null, credentials); - }); - }); - }); + return credentials; } diff --git a/src/dns/interface.js b/src/dns/interface.js index 327b12ea2..3c6f473ee 100644 --- a/src/dns/interface.js +++ b/src/dns/interface.js @@ -29,39 +29,36 @@ function injectPrivateFields(newConfig, currentConfig) { // in-place injection of tokens and api keys which came in with constants.SECRET_PLACEHOLDER } -function upsert(domainObject, subdomain, type, values, callback) { +async function upsert(domainObject, subdomain, type, values) { assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof type, 'string'); assert(Array.isArray(values)); - assert.strictEqual(typeof callback, 'function'); // Result: none - callback(new BoxError(BoxError.NOT_IMPLEMENTED, 'upsert is not implemented')); + throw new BoxError(BoxError.NOT_IMPLEMENTED, 'upsert is not implemented'); } -function get(domainObject, subdomain, type, callback) { +async function get(domainObject, subdomain, type) { assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof type, 'string'); - assert.strictEqual(typeof callback, 'function'); // Result: Array of matching DNS records in string format - callback(new BoxError(BoxError.NOT_IMPLEMENTED, 'get is not implemented')); + throw new BoxError(BoxError.NOT_IMPLEMENTED, 'get is not implemented'); } -function del(domainObject, subdomain, type, values, callback) { +async function del(domainObject, subdomain, type, values) { assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof type, 'string'); assert(Array.isArray(values)); - assert.strictEqual(typeof callback, 'function'); // Result: none - callback(new BoxError(BoxError.NOT_IMPLEMENTED, 'del is not implemented')); + throw new BoxError(BoxError.NOT_IMPLEMENTED, 'del is not implemented'); } async function wait(domainObject, subdomain, type, value, options) { diff --git a/src/dns/vultr.js b/src/dns/vultr.js index 4f7e5c093..bbfe8a930 100644 --- a/src/dns/vultr.js +++ b/src/dns/vultr.js @@ -10,11 +10,11 @@ exports = module.exports = { verifyDomainConfig }; -const async = require('async'), - assert = require('assert'), +const assert = require('assert'), constants = require('../constants.js'), BoxError = require('../boxerror.js'), debug = require('debug')('box:dns/vultr'), + dig = require('../dig.js'), dns = require('../dns.js'), safe = require('safetydance'), superagent = require('superagent'), @@ -36,75 +36,55 @@ function injectPrivateFields(newConfig, currentConfig) { if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token; } -function getZoneRecords(domainConfig, zoneName, name, type, callback) { +async function getZoneRecords(domainConfig, zoneName, name, type) { assert.strictEqual(typeof domainConfig, 'object'); assert.strictEqual(typeof zoneName, 'string'); assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof type, 'string'); - assert.strictEqual(typeof callback, 'function'); debug(`getInternal: getting dns records of ${zoneName} with ${name} and type ${type}`); - let per_page = 100, cursor= null; + let per_page = 100, cursor = null; let records = []; - async.doWhilst(function (iteratorDone) { + do { const url = `${VULTR_ENDPOINT}/domains/${zoneName}/records?per_page=${per_page}` + (cursor ? `&cursor=${cursor}` : ''); - superagent.get(url) - .set('Authorization', 'Bearer ' + domainConfig.token) - .timeout(30 * 1000) - .retry(5) - .end(function (error, result) { - if (error && !error.response) return iteratorDone(new BoxError(BoxError.NETWORK_ERROR, error.message)); - if (result.statusCode === 404) return iteratorDone(new BoxError(BoxError.NOT_FOUND, formatError(result))); - if (result.statusCode === 403 || result.statusCode === 401) return iteratorDone(new BoxError(BoxError.ACCESS_DENIED, formatError(result))); - if (result.statusCode !== 200) return iteratorDone(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result))); + const [error, response] = await safe(superagent.get(url).set('Authorization', 'Bearer ' + domainConfig.token).timeout(30 * 1000).retry(5).ok(() => true)); + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); + if (response.statusCode === 404) throw new BoxError(BoxError.NOT_FOUND, formatError(response)); + if (response.statusCode === 403 || response.statusCode === 401) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response)); + if (response.statusCode !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response)); - records = records.concat(result.body.records.filter(function (record) { - return (record.type === type && record.name === name); - })); + records = records.concat(response.body.records.filter(function (record) { + return (record.type === type && record.name === name); + })); - cursor = safe.query(result.body, 'meta.links.next'); + cursor = safe.query(response.body, 'meta.links.next'); + } while (cursor); - iteratorDone(); - }); - }, function (testDone) { return testDone(null, !!cursor); }, function (error) { - debug('getZoneRecords: error:', error, JSON.stringify(records)); - - if (error) return callback(error); - - callback(null, records); - }); + return records; } -function get(domainObject, location, type, callback) { +async function get(domainObject, location, type) { assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof location, 'string'); assert.strictEqual(typeof type, 'string'); - assert.strictEqual(typeof callback, 'function'); const domainConfig = domainObject.config, zoneName = domainObject.zoneName, name = dns.getName(domainObject, location, type) || ''; - getZoneRecords(domainConfig, zoneName, name, type, function (error, records) { - if (error) return callback(error); - - var tmp = records.map(function (record) { return record.data; }); - - debug('get: %j', tmp); - - return callback(null, tmp); - }); + const records = await getZoneRecords(domainConfig, zoneName, name, type); + const tmp = records.map(function (record) { return record.data; }); + return tmp; } -function upsert(domainObject, location, type, values, callback) { +async function upsert(domainObject, location, type, values) { assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof location, 'string'); assert.strictEqual(typeof type, 'string'); assert(Array.isArray(values)); - assert.strictEqual(typeof callback, 'function'); const domainConfig = domainObject.config, zoneName = domainObject.zoneName, @@ -112,113 +92,91 @@ function upsert(domainObject, location, type, values, callback) { debug('upsert: %s for zone %s of type %s with values %j', name, zoneName, type, values); - getZoneRecords(domainConfig, zoneName, name, type, function (error, records) { - if (error) return callback(error); + const records = await getZoneRecords(domainConfig, zoneName, name, type); - let i = 0, recordIds = []; // used to track available records to update instead of create + let i = 0, recordIds = []; // used to track available records to update instead of create - async.eachSeries(values, function (value, iteratorCallback) { - let data = { - type, - ttl: 300 // lowest - }; + for (const value of values) { + const data = { + type, + ttl: 300 // lowest + }; - if (type === 'MX') { - data.priority = parseInt(value.split(' ')[0], 10); - data.data = value.split(' ')[1]; - } else if (type === 'TXT') { - data.data = value.replace(/^"(.*)"$/, '$1'); // strip any double quotes - } else { - data.data = value; - } + if (type === 'MX') { + data.priority = parseInt(value.split(' ')[0], 10); + data.data = value.split(' ')[1]; + } else if (type === 'TXT') { + data.data = value.replace(/^"(.*)"$/, '$1'); // strip any double quotes + } else { + data.data = value; + } - if (i >= records.length) { - data.name = name; // only set for new records + if (i >= records.length) { + data.name = name; // only set for new records - superagent.post(`${VULTR_ENDPOINT}/domains/${zoneName}/records`) - .set('Authorization', 'Bearer ' + domainConfig.token) - .send(data) - .timeout(30 * 1000) - .retry(5) - .end(function (error, result) { - if (error && !error.response) return iteratorCallback(new BoxError(BoxError.NETWORK_ERROR, error.message)); - if (result.statusCode === 400) return iteratorCallback(new BoxError(BoxError.BAD_FIELD, formatError(result))); - if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new BoxError(BoxError.ACCESS_DENIED, formatError(result))); - if (result.statusCode !== 201) return iteratorCallback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result))); + const [error, response] = await safe(superagent.post(`${VULTR_ENDPOINT}/domains/${zoneName}/records`) + .set('Authorization', 'Bearer ' + domainConfig.token) + .send(data) + .timeout(30 * 1000) + .retry(5) + .ok(() => true)); - recordIds.push(result.body.record.id); + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); + if (response.statusCode === 400) throw new BoxError(BoxError.BAD_FIELD, formatError(response)); + if (response.statusCode === 403 || response.statusCode === 401) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response)); + if (response.statusCode !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response)); - return iteratorCallback(null); - }); - } else { - superagent.patch(`${VULTR_ENDPOINT}/domains/${zoneName}/records/${records[i].id}`) - .set('Authorization', 'Bearer ' + domainConfig.token) - .send(data) - .timeout(30 * 1000) - .retry(5) - .end(function (error, result) { - // increment, as we have consumed the record - ++i; + recordIds.push(response.body.record.id); + } else { + const [error, response] = await safe(superagent.patch(`${VULTR_ENDPOINT}/domains/${zoneName}/records/${records[i].id}`) + .set('Authorization', 'Bearer ' + domainConfig.token) + .send(data) + .timeout(30 * 1000) + .retry(5) + .ok(() => true)); - if (error && !error.response) return iteratorCallback(new BoxError(BoxError.NETWORK_ERROR, error.message)); - if (result.statusCode === 400) return iteratorCallback(new BoxError(BoxError.BAD_FIELD, formatError(result))); - if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new BoxError(BoxError.ACCESS_DENIED, formatError(result))); - if (result.statusCode !== 204) return iteratorCallback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result))); + ++i; - recordIds.push(records[i-1].id); + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); + if (response.statusCode === 400) throw new BoxError(BoxError.BAD_FIELD, formatError(response)); + if (response.statusCode === 403 || response.statusCode === 401) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response)); + if (response.statusCode !== 204) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response)); - return iteratorCallback(null); - }); - } - }, function (error) { - if (error) return callback(error); + recordIds.push(records[i-1].id); + } + } - debug('upsert: completed with recordIds:%j', recordIds); - - callback(); - }); - }); + debug('upsert: completed with recordIds:%j', recordIds); } -function del(domainObject, location, type, values, callback) { +async function del(domainObject, location, type, values) { assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof location, 'string'); assert.strictEqual(typeof type, 'string'); assert(Array.isArray(values)); - assert.strictEqual(typeof callback, 'function'); const domainConfig = domainObject.config, zoneName = domainObject.zoneName, name = dns.getName(domainObject, location, type) || ''; - getZoneRecords(domainConfig, zoneName, name, type, function (error, records) { - if (error) return callback(error); + const records = await getZoneRecords(domainConfig, zoneName, name, type); + if (records.length === 0) return; - if (records.length === 0) return callback(null); + const tmp = records.filter(function (record) { return values.some(function (value) { return value === record.data; }); }); + if (tmp.length === 0) return; - const tmp = records.filter(function (record) { return values.some(function (value) { return value === record.data; }); }); - - debug('del: %j', tmp); - - if (tmp.length === 0) return callback(null); - - // FIXME we only handle the first one currently - - superagent.del(`${VULTR_ENDPOINT}/domains/${zoneName}/records/${tmp[0].id}`) + for (const r of tmp) { + const [error, response] = await safe(superagent.del(`${VULTR_ENDPOINT}/domains/${zoneName}/records/${r.id}`) .set('Authorization', 'Bearer ' + domainConfig.token) .timeout(30 * 1000) .retry(5) - .end(function (error, result) { - if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message)); - if (result.statusCode === 404) return callback(null); - if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.ACCESS_DENIED, formatError(result))); - if (result.statusCode !== 204) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result))); + .ok(() => true)); - debug('del: done'); - - return callback(null); - }); - }); + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); + if (response.statusCode === 404) continue; + if (response.statusCode === 403 || response.statusCode === 401) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response)); + if (response.statusCode !== 204) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response)); + } } async function wait(domainObject, subdomain, type, value, options) { @@ -233,46 +191,39 @@ async function wait(domainObject, subdomain, type, value, options) { await waitForDns(fqdn, domainObject.zoneName, type, value, options); } -function verifyDomainConfig(domainObject, callback) { +async function verifyDomainConfig(domainObject) { assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof callback, 'function'); const domainConfig = domainObject.config, zoneName = domainObject.zoneName; - if (!domainConfig.token || typeof domainConfig.token !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'token must be a non-empty string', { field: 'token' })); + if (!domainConfig.token || typeof domainConfig.token !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'token must be a non-empty string'); const ip = '127.0.0.1'; - var credentials = { + const credentials = { token: domainConfig.token }; - if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here + if (process.env.BOX_ENV === 'test') credentials; // this shouldn't be here - dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) { - if (error && error.code === 'ENOTFOUND') return callback(new BoxError(BoxError.BAD_FIELD, 'Unable to resolve nameservers for this domain', { field: 'nameservers' })); - if (error || !nameservers) return callback(new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' })); + 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'); + if (error || !nameservers) throw new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'); - if (nameservers.map(function (n) { return n.toLowerCase(); }).indexOf('ns1.vultr.com') === -1) { - debug('verifyDomainConfig: %j does not contains vultr NS', nameservers); - return callback(new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to Vultr', { field: 'nameservers' })); - } + if (nameservers.map(function (n) { return n.toLowerCase(); }).indexOf('ns1.vultr.com') === -1) { + debug('verifyDomainConfig: %j does not contains vultr NS', nameservers); + throw new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to Vultr'); + } - const location = 'cloudrontestdns'; + const location = 'cloudrontestdns'; - upsert(domainObject, location, 'A', [ ip ], function (error) { - if (error) return callback(error); + await upsert(domainObject, location, 'A', [ ip ]); + debug('verifyDomainConfig: Test A record added'); - debug('verifyDomainConfig: Test A record added'); + await del(domainObject, location, 'A', [ ip ]); + debug('verifyDomainConfig: Test A record removed again'); - del(domainObject, location, 'A', [ ip ], function (error) { - if (error) return callback(error); - - debug('verifyDomainConfig: Test A record removed again'); - - callback(null, credentials); - }); - }); - }); + return credentials; } diff --git a/src/domains.js b/src/domains.js index 8362b0231..61d5d2896 100644 --- a/src/domains.js +++ b/src/domains.js @@ -23,7 +23,6 @@ const assert = require('assert'), safe = require('safetydance'), settings = require('./settings.js'), tld = require('tldjs'), - util = require('util'), _ = require('underscore'); const DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson', 'wellKnownJson', 'fallbackCertificateJson' ].join(','); @@ -67,11 +66,6 @@ function api(provider) { } } -function maybePromisify(func) { - if (util.types.isAsyncFunction(func)) return func; - return util.promisify(func); -} - async function verifyDomainConfig(domainConfig, domain, zoneName, provider) { assert(domainConfig && typeof domainConfig === 'object'); // the dns config to test with assert.strictEqual(typeof domain, 'string'); @@ -82,7 +76,7 @@ async function verifyDomainConfig(domainConfig, domain, zoneName, provider) { if (!backend) throw new BoxError(BoxError.BAD_FIELD, 'Invalid provider', { field: 'provider' }); const domainObject = { config: domainConfig, domain: domain, zoneName: zoneName }; - const [error, result] = await safe(maybePromisify(api(provider).verifyDomainConfig)(domainObject)); + const [error, result] = await safe(api(provider).verifyDomainConfig)(domainObject); if (error && error.reason === BoxError.ACCESS_DENIED) return { error: new BoxError(BoxError.BAD_FIELD, `Access denied: ${error.message}`) }; if (error && error.reason === BoxError.NOT_FOUND) return { error: new BoxError(BoxError.BAD_FIELD, `Zone not found: ${error.message}`) }; if (error && error.reason === BoxError.EXTERNAL_ERROR) return { error: new BoxError(BoxError.BAD_FIELD, `Configuration error: ${error.message}`) };