diff --git a/src/dns/route53.js b/src/dns/route53.js index 22cb9e95f..7d1c9e80e 100644 --- a/src/dns/route53.js +++ b/src/dns/route53.js @@ -15,7 +15,9 @@ const assert = require('assert'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), debug = require('debug')('box:dns/route53'), + dig = require('../dig.js'), dns = require('../dns.js'), + safe = require('safetydance'), waitForDns = require('./waitfordns.js'), _ = require('underscore'); @@ -31,7 +33,7 @@ function injectPrivateFields(newConfig, currentConfig) { function getDnsCredentials(domainConfig) { assert.strictEqual(typeof domainConfig, 'object'); - var credentials = { + const credentials = { accessKeyId: domainConfig.accessKeyId, secretAccessKey: domainConfig.secretAccessKey, region: domainConfig.region @@ -42,61 +44,55 @@ function getDnsCredentials(domainConfig) { return credentials; } -function getZoneByName(domainConfig, zoneName, callback) { +async function getZoneByName(domainConfig, zoneName) { assert.strictEqual(typeof domainConfig, 'object'); assert.strictEqual(typeof zoneName, 'string'); - assert.strictEqual(typeof callback, 'function'); - var route53 = new AWS.Route53(getDnsCredentials(domainConfig)); + const route53 = new AWS.Route53(getDnsCredentials(domainConfig)); // backward compat for 2.2, where we only required access to "listHostedZones" let listHostedZones; if (domainConfig.listHostedZonesByName) { - listHostedZones = route53.listHostedZonesByName.bind(route53, { MaxItems: '1', DNSName: zoneName + '.' }); + listHostedZones = route53.listHostedZonesByName({ MaxItems: '1', DNSName: zoneName + '.' }).promise(); } else { - listHostedZones = route53.listHostedZones.bind(route53, {}); // currently, this route does not support > 100 zones + listHostedZones = route53.listHostedZones({}).promise(); // currently, this route does not support > 100 zones } - listHostedZones(function (error, result) { - if (error && error.code === 'AccessDenied') return callback(new BoxError(BoxError.ACCESS_DENIED, error.message)); - if (error && error.code === 'InvalidClientTokenId') return callback(new BoxError(BoxError.ACCESS_DENIED, error.message)); - if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message)); + const [error, result] = await safe(listHostedZones); + if (error && error.code === 'AccessDenied') throw new BoxError(BoxError.ACCESS_DENIED, error.message); + if (error && error.code === 'InvalidClientTokenId') throw new BoxError(BoxError.ACCESS_DENIED, error.message); + if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, error.message); - var zone = result.HostedZones.filter(function (zone) { - return zone.Name.slice(0, -1) === zoneName; // aws zone name contains a '.' at the end - })[0]; + const zone = result.HostedZones.filter(function (zone) { + return zone.Name.slice(0, -1) === zoneName; // aws zone name contains a '.' at the end + })[0]; - if (!zone) return callback(new BoxError(BoxError.NOT_FOUND, 'no such zone')); + if (!zone) throw new BoxError(BoxError.NOT_FOUND, 'no such zone'); - callback(null, zone); - }); + return zone; } -function getHostedZone(domainConfig, zoneName, callback) { +async function getHostedZone(domainConfig, zoneName) { assert.strictEqual(typeof domainConfig, 'object'); assert.strictEqual(typeof zoneName, 'string'); assert.strictEqual(typeof callback, 'function'); - getZoneByName(domainConfig, zoneName, function (error, zone) { - if (error) return callback(error); + const zone = await getZoneByName(domainConfig, zoneName); - var route53 = new AWS.Route53(getDnsCredentials(domainConfig)); - route53.getHostedZone({ Id: zone.Id }, function (error, result) { - if (error && error.code === 'AccessDenied') return callback(new BoxError(BoxError.ACCESS_DENIED, error.message)); - if (error && error.code === 'InvalidClientTokenId') return callback(new BoxError(BoxError.ACCESS_DENIED, error.message)); - if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message)); + const route53 = new AWS.Route53(getDnsCredentials(domainConfig)); + const [error, result] = await safe(route53.getHostedZone({ Id: zone.Id }).promise()); + if (error && error.code === 'AccessDenied') throw new BoxError(BoxError.ACCESS_DENIED, error.message); + if (error && error.code === 'InvalidClientTokenId') throw new BoxError(BoxError.ACCESS_DENIED, error.message); + if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, error.message); - callback(null, result); - }); - }); + return result; } -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, @@ -104,131 +100,110 @@ function upsert(domainObject, location, type, values, callback) { debug('add: %s for zone %s of type %s with values %j', fqdn, zoneName, type, values); - getZoneByName(domainConfig, zoneName, function (error, zone) { - if (error) return callback(error); + const zone = await getZoneByName(domainConfig, zoneName); - var records = values.map(function (v) { return { Value: v }; }); // for mx records, value is already of the ' ' format + const records = values.map(function (v) { return { Value: v }; }); // for mx records, value is already of the ' ' format - var params = { - ChangeBatch: { - Changes: [{ - Action: 'UPSERT', - ResourceRecordSet: { - Type: type, - Name: fqdn, - ResourceRecords: records, - TTL: 1 - } - }] - }, - HostedZoneId: zone.Id - }; + const params = { + ChangeBatch: { + Changes: [{ + Action: 'UPSERT', + ResourceRecordSet: { + Type: type, + Name: fqdn, + ResourceRecords: records, + TTL: 1 + } + }] + }, + HostedZoneId: zone.Id + }; - var route53 = new AWS.Route53(getDnsCredentials(domainConfig)); - route53.changeResourceRecordSets(params, function(error) { - if (error && error.code === 'AccessDenied') return callback(new BoxError(BoxError.ACCESS_DENIED, error.message)); - if (error && error.code === 'InvalidClientTokenId') return callback(new BoxError(BoxError.ACCESS_DENIED, error.message)); - if (error && error.code === 'PriorRequestNotComplete') return callback(new BoxError(BoxError.BUSY, error.message)); - if (error && error.code === 'InvalidChangeBatch') return callback(new BoxError(BoxError.BAD_FIELD, error.message)); - if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message)); - - callback(null); - }); - }); + const route53 = new AWS.Route53(getDnsCredentials(domainConfig)); + const [error] = await safe(route53.changeResourceRecordSets(params).promise()); + if (error && error.code === 'AccessDenied') throw new BoxError(BoxError.ACCESS_DENIED, error.message); + if (error && error.code === 'InvalidClientTokenId') throw new BoxError(BoxError.ACCESS_DENIED, error.message); + if (error && error.code === 'PriorRequestNotComplete') throw new BoxError(BoxError.BUSY, error.message); + if (error && error.code === 'InvalidChangeBatch') throw new BoxError(BoxError.BAD_FIELD, error.message); + if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, error.message); } -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, fqdn = dns.fqdn(location, domainObject); - getZoneByName(domainConfig, zoneName, function (error, zone) { - if (error) return callback(error); + const zone = await getZoneByName(domainConfig, zoneName); - var params = { - HostedZoneId: zone.Id, - MaxItems: '1', - StartRecordName: fqdn + '.', - StartRecordType: type - }; + const params = { + HostedZoneId: zone.Id, + MaxItems: '1', + StartRecordName: fqdn + '.', + StartRecordType: type + }; - var route53 = new AWS.Route53(getDnsCredentials(domainConfig)); - route53.listResourceRecordSets(params, function (error, result) { - if (error && error.code === 'AccessDenied') return callback(new BoxError(BoxError.ACCESS_DENIED, error.message)); - if (error && error.code === 'InvalidClientTokenId') return callback(new BoxError(BoxError.ACCESS_DENIED, error.message)); - if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message)); - if (result.ResourceRecordSets.length === 0) return callback(null, [ ]); - if (result.ResourceRecordSets[0].Name !== params.StartRecordName || result.ResourceRecordSets[0].Type !== params.StartRecordType) return callback(null, [ ]); + const route53 = new AWS.Route53(getDnsCredentials(domainConfig)); + const [error, result] = await safe(route53.listResourceRecordSets(params).promise()); + if (error && error.code === 'AccessDenied') throw new BoxError(BoxError.ACCESS_DENIED, error.message); + if (error && error.code === 'InvalidClientTokenId') throw new BoxError(BoxError.ACCESS_DENIED, error.message); + if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, error.message); + if (result.ResourceRecordSets.length === 0) return []; + if (result.ResourceRecordSets[0].Name !== params.StartRecordName || result.ResourceRecordSets[0].Type !== params.StartRecordType) return []; - var values = result.ResourceRecordSets[0].ResourceRecords.map(function (record) { return record.Value; }); - - callback(null, values); - }); - }); + const values = result.ResourceRecordSets[0].ResourceRecords.map(function (record) { return record.Value; }); + return values; } -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, fqdn = dns.fqdn(location, domainObject); - getZoneByName(domainConfig, zoneName, function (error, zone) { - if (error) return callback(error); + const zone = await getZoneByName(domainConfig, zoneName); - var records = values.map(function (v) { return { Value: v }; }); + const records = values.map(function (v) { return { Value: v }; }); - var resourceRecordSet = { - Name: fqdn, - Type: type, - ResourceRecords: records, - TTL: 1 - }; + const resourceRecordSet = { + Name: fqdn, + Type: type, + ResourceRecords: records, + TTL: 1 + }; - var params = { - ChangeBatch: { - Changes: [{ - Action: 'DELETE', - ResourceRecordSet: resourceRecordSet - }] - }, - HostedZoneId: zone.Id - }; + const params = { + ChangeBatch: { + Changes: [{ + Action: 'DELETE', + ResourceRecordSet: resourceRecordSet + }] + }, + HostedZoneId: zone.Id + }; - var route53 = new AWS.Route53(getDnsCredentials(domainConfig)); - route53.changeResourceRecordSets(params, function(error) { - if (error && error.code === 'AccessDenied') return callback(new BoxError(BoxError.ACCESS_DENIED, error.message)); - if (error && error.code === 'InvalidClientTokenId') return callback(new BoxError(BoxError.ACCESS_DENIED, error.message)); - if (error && error.message && error.message.indexOf('it was not found') !== -1) { - debug('del: resource record set not found.', error); - return callback(new BoxError(BoxError.NOT_FOUND, error.message)); - } else if (error && error.code === 'NoSuchHostedZone') { - debug('del: hosted zone not found.', error); - return callback(new BoxError(BoxError.NOT_FOUND, error.message)); - } else if (error && error.code === 'PriorRequestNotComplete') { - debug('del: resource is still busy', error); - return callback(new BoxError(BoxError.BUSY, error.message)); - } else if (error && error.code === 'InvalidChangeBatch') { - debug('del: invalid change batch. No such record to be deleted.'); - return callback(new BoxError(BoxError.NOT_FOUND, error.message)); - } else if (error) { - debug('del: error', error); - return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message)); - } - - callback(null); - }); - }); + const route53 = new AWS.Route53(getDnsCredentials(domainConfig)); + const [error] = await safe(route53.changeResourceRecordSets(params).promise()); + if (error && error.code === 'AccessDenied') throw new BoxError(BoxError.ACCESS_DENIED, error.message); + if (error && error.code === 'InvalidClientTokenId') throw new BoxError(BoxError.ACCESS_DENIED, error.message); + if (error && error.message && error.message.indexOf('it was not found') !== -1) { + throw new BoxError(BoxError.NOT_FOUND, error.message); + } else if (error && error.code === 'NoSuchHostedZone') { + throw new BoxError(BoxError.NOT_FOUND, error.message); + } else if (error && error.code === 'PriorRequestNotComplete') { + throw new BoxError(BoxError.BUSY, error.message); + } else if (error && error.code === 'InvalidChangeBatch') { + throw new BoxError(BoxError.NOT_FOUND, error.message); + } else if (error) { + throw new BoxError(BoxError.EXTERNAL_ERROR, error.message); + } } async function wait(domainObject, subdomain, type, value, options) { @@ -243,17 +218,16 @@ 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.accessKeyId || typeof domainConfig.accessKeyId !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'accessKeyId must be a non-empty string', { field: 'accessKeyId' })); - if (!domainConfig.secretAccessKey || typeof domainConfig.secretAccessKey !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'secretAccessKey must be a non-empty string', { field: 'secretAccessKey' })); + if (!domainConfig.accessKeyId || typeof domainConfig.accessKeyId !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'accessKeyId must be a non-empty string'); + if (!domainConfig.secretAccessKey || typeof domainConfig.secretAccessKey !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'secretAccessKey must be a non-empty string'); - var credentials = { + const credentials = { accessKeyId: domainConfig.accessKeyId, secretAccessKey: domainConfig.secretAccessKey, region: domainConfig.region || 'us-east-1', @@ -263,36 +237,27 @@ function verifyDomainConfig(domainObject, callback) { const ip = '127.0.0.1'; - 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'); - getHostedZone(credentials, zoneName, function (error, zone) { - if (error) return callback(error); + const zone = await getHostedZone(credentials, zoneName); - if (!_.isEqual(zone.DelegationSet.NameServers.sort(), nameservers.sort())) { - debug('verifyDomainConfig: %j and %j do not match', nameservers, zone.DelegationSet.NameServers); - return callback(new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to Route53', { field: 'nameservers' })); - } + if (!_.isEqual(zone.DelegationSet.NameServers.sort(), nameservers.sort())) { + debug('verifyDomainConfig: %j and %j do not match', nameservers, zone.DelegationSet.NameServers); + throw new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to Route53'); + } - const location = 'cloudrontestdns'; - const newDomainObject = Object.assign({ }, domainObject, { config: credentials }); + const location = 'cloudrontestdns'; + const newDomainObject = Object.assign({ }, domainObject, { config: credentials }); - upsert(newDomainObject, location, 'A', [ ip ], function (error) { - if (error) return callback(error); + await upsert(newDomainObject, location, 'A', [ ip ]); + debug('verifyDomainConfig: Test A record added'); - debug('verifyDomainConfig: Test A record added'); + await del(newDomainObject, location, 'A', [ ip ]); + debug('verifyDomainConfig: Test A record removed again'); - del(newDomainObject, location, 'A', [ ip ], function (error) { - if (error) return callback(error); - - debug('verifyDomainConfig: Test A record removed again'); - - callback(null, credentials); - }); - }); - }); - }); + return credentials; }