diff --git a/src/dns/digitalocean.js b/src/dns/digitalocean.js index 85b309317..34a29767e 100644 --- a/src/dns/digitalocean.js +++ b/src/dns/digitalocean.js @@ -208,7 +208,6 @@ async function wait(domainObject, subdomain, type, value, options) { async function verifyDomainConfig(domainObject) { assert.strictEqual(typeof domainObject, 'object'); - assert.strictEqual(typeof callback, 'function'); const domainConfig = domainObject.config, zoneName = domainObject.zoneName; diff --git a/src/dns/linode.js b/src/dns/linode.js index 444a4b7ec..e9793dc85 100644 --- a/src/dns/linode.js +++ b/src/dns/linode.js @@ -10,12 +10,13 @@ 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/linode'), + dig = require('../dig.js'), dns = require('../dns.js'), + safe = require('safetydance'), superagent = require('superagent'), util = require('util'), waitForDns = require('./waitfordns.js'); @@ -35,107 +36,88 @@ function injectPrivateFields(newConfig, currentConfig) { if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token; } -function getZoneId(domainConfig, zoneName, callback) { +async function getZoneId(domainConfig, zoneName) { assert.strictEqual(typeof domainConfig, 'object'); assert.strictEqual(typeof zoneName, 'string'); - assert.strictEqual(typeof callback, 'function'); // returns 100 at a time - superagent.get(`${LINODE_ENDPOINT}/domains`) + const [error, response] = await safe(superagent.get(`${LINODE_ENDPOINT}/domains`) .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 === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.ACCESS_DENIED, formatError(result))); - if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result))); + .ok(() => true)); - if (!Array.isArray(result.body.data)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response')); + 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 !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response)); - const zone = result.body.data.find(d => d.domain === zoneName); + if (!Array.isArray(response.body.data)) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response'); - if (!zone || !zone.id) return callback(new BoxError(BoxError.NOT_FOUND, 'Zone not found')); + const zone = response.body.data.find(d => d.domain === zoneName); - debug(`getZoneId: zone id of ${zoneName} is ${zone.id}`); + if (!zone || !zone.id) throw new BoxError(BoxError.NOT_FOUND, 'Zone not found'); - callback(null, zone.id); - }); + debug(`getZoneId: zone id of ${zoneName} is ${zone.id}`); + + return zone.id; } -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}`); - getZoneId(domainConfig, zoneName, function (error, zoneId) { - if (error) return callback(error); + const zoneId = await getZoneId(domainConfig, zoneName); - let page = 0, more = false; - let records = []; + let page = 0, more = false; + let records = []; - async.doWhilst(function (iteratorDone) { - const url = `${LINODE_ENDPOINT}/domains/${zoneId}/records?page=${++page}`; + do { + const url = `${LINODE_ENDPOINT}/domains/${zoneId}/records?page=${++page}`; - 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)); - records = records.concat(result.body.data.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)); - more = result.body.page !== result.body.pages; + records = records.concat(response.body.data.filter(function (record) { + return (record.type === type && record.name === name); + })); - iteratorDone(); - }); - }, function (testDone) { return testDone(null, more); }, function (error) { - debug('getZoneRecords:', error, JSON.stringify(records)); + more = response.body.page !== response.body.pages; + } while (more); - if (error) return callback(error); - - callback(null, { zoneId, records }); - }); - }); + return { zoneId, 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, result) { - if (error) return callback(error); - - const { records } = result; - var tmp = records.map(function (record) { return record.target; }); - - debug('get: %j', tmp); - - return callback(null, tmp); - }); + const { records } = await getZoneRecords(domainConfig, zoneName, name, type); + const tmp = records.map(function (record) { return record.target; }); + 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, @@ -143,115 +125,97 @@ 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, result) { - if (error) return callback(error); + const { zoneId, records } = await getZoneRecords(domainConfig, zoneName, name, type); + let i = 0, recordIds = []; // used to track available records to update instead of create - const { zoneId, records } = result; - let i = 0, recordIds = []; // used to track available records to update instead of create + for (const value of values) { + const data = { + type: type, + ttl_sec: 300 // lowest + }; - async.eachSeries(values, function (value, iteratorCallback) { - let data = { - type: type, - ttl_sec: 300 // lowest - }; + if (type === 'MX') { + data.priority = parseInt(value.split(' ')[0], 10); + data.target = value.split(' ')[1]; + } else if (type === 'TXT') { + data.target = value.replace(/^"(.*)"$/, '$1'); // strip any double quotes + } else { + data.target = value; + } - if (type === 'MX') { - data.priority = parseInt(value.split(' ')[0], 10); - data.target = value.split(' ')[1]; - } else if (type === 'TXT') { - data.target = value.replace(/^"(.*)"$/, '$1'); // strip any double quotes - } else { - data.target = 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 + const [error, response] = await safe(superagent.post(`${LINODE_ENDPOINT}/domains/${zoneId}/records`) + .set('Authorization', 'Bearer ' + domainConfig.token) + .send(data) + .timeout(30 * 1000) + .retry(5) + .ok(() => true)); - superagent.post(`${LINODE_ENDPOINT}/domains/${zoneId}/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 !== 200) return iteratorCallback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result))); + 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 !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response)); - recordIds.push(result.body.id); + recordIds.push(response.body.id); + } else { + const [error, response] = await safe(superagent.put(`${LINODE_ENDPOINT}/domains/${zoneId}/records/${records[i].id}`) + .set('Authorization', 'Bearer ' + domainConfig.token) + .send(data) + .timeout(30 * 1000) + .retry(5) + .ok(() => true)); - return iteratorCallback(null); - }); - } else { - superagent.put(`${LINODE_ENDPOINT}/domains/${zoneId}/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; + 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 !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response)); - 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 !== 200) return iteratorCallback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result))); + ++i; - recordIds.push(result.body.id); + recordIds.push(response.body.id); + } + } - return iteratorCallback(null); - }); - } - }, function (error) { - if (error) return callback(error); + for (let j = values.length + 1; j < records.length; j++) { + const [error] = await safe(superagent.del(`${LINODE_ENDPOINT}/domains/${zoneId}/records/${records[j].id}`) + .set('Authorization', 'Bearer ' + domainConfig.token) + .timeout(30 * 1000) + .retry(5) + .ok(() => true)); - debug('upsert: completed with recordIds:%j', recordIds); - - callback(); - }); - }); + if (error) debug(`upsert: error removing record ${records[j].id}: ${error.message}`); + } } -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, result) { - if (error) return callback(error); + const { zoneId, records } = await getZoneRecords(domainConfig, zoneName, name, type); + if (records.length === 0) return; - const { zoneId, records } = result; - if (records.length === 0) return callback(null); + const tmp = records.filter(function (record) { return values.some(function (value) { return value === record.target; }); }); + if (tmp.length === 0) return; - var tmp = records.filter(function (record) { return values.some(function (value) { return value === record.target; }); }); - - debug('del: %j', tmp); - - if (tmp.length === 0) return callback(null); - - // FIXME we only handle the first one currently - - superagent.del(`${LINODE_ENDPOINT}/domains/${zoneId}/records/${tmp[0].id}`) + for (const r of tmp) { + const [error, response] = await safe(superagent.del(`${LINODE_ENDPOINT}/domains/${zoneId}/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 !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result))); - - debug('del: done'); - - return callback(null); - }); - }); + .ok(() => true)); + if (error && !error.response) 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 !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response)); + } } async function wait(domainObject, subdomain, type, value, options) { @@ -266,46 +230,38 @@ 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.linode.com') === -1) { - debug('verifyDomainConfig: %j does not contains linode NS', nameservers); - return callback(new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to Linode', { field: 'nameservers' })); - } + if (nameservers.map(function (n) { return n.toLowerCase(); }).indexOf('ns1.linode.com') === -1) { + debug('verifyDomainConfig: %j does not contains linode NS', nameservers); + throw new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to Linode'); + } - 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; }