2015-10-28 16:02:06 -07:00
'use strict' ;
exports = module . exports = {
2016-09-15 11:57:25 +02:00
upsert : upsert ,
2015-10-30 13:17:33 -07:00
get : get ,
2015-10-30 13:30:19 -07:00
del : del ,
2016-12-14 12:27:11 -08:00
waitForDns : require ( './waitfordns.js' ) ,
2017-01-10 11:12:25 +01:00
verifyDnsConfig : verifyDnsConfig ,
2016-07-03 21:37:17 -05:00
// not part of "dns" interface
getHostedZone : getHostedZone
2015-10-28 16:02:06 -07:00
} ;
var assert = require ( 'assert' ) ,
AWS = require ( 'aws-sdk' ) ,
debug = require ( 'debug' ) ( 'box:dns/route53' ) ,
2018-02-08 10:21:31 -08:00
dns = require ( '../native-dns.js' ) ,
2018-04-29 11:20:12 -07:00
DomainsError = require ( '../domains.js' ) . DomainsError ,
2017-01-10 11:12:25 +01:00
util = require ( 'util' ) ,
_ = require ( 'underscore' ) ;
2015-10-28 16:02:06 -07:00
2015-11-08 23:14:39 -08:00
function getDnsCredentials ( dnsConfig ) {
assert . strictEqual ( typeof dnsConfig , 'object' ) ;
2015-10-28 16:02:06 -07:00
2015-11-08 23:14:39 -08:00
var credentials = {
accessKeyId : dnsConfig . accessKeyId ,
secretAccessKey : dnsConfig . secretAccessKey ,
region : dnsConfig . region
} ;
2015-10-28 16:02:06 -07:00
2015-11-08 23:14:39 -08:00
if ( dnsConfig . endpoint ) credentials . endpoint = new AWS . Endpoint ( dnsConfig . endpoint ) ;
2015-10-28 16:02:06 -07:00
2015-11-08 23:14:39 -08:00
return credentials ;
2015-10-28 16:02:06 -07:00
}
2015-11-08 23:14:39 -08:00
function getZoneByName ( dnsConfig , zoneName , callback ) {
assert . strictEqual ( typeof dnsConfig , 'object' ) ;
2015-10-28 16:02:06 -07:00
assert . strictEqual ( typeof zoneName , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2015-11-08 23:14:39 -08:00
var route53 = new AWS . Route53 ( getDnsCredentials ( dnsConfig ) ) ;
2018-05-07 11:18:15 -07:00
// backward compat for 2.2, where we only required access to "listHostedZones"
let listHostedZones ;
if ( dnsConfig . listHostedZonesByName ) {
listHostedZones = route53 . listHostedZonesByName . bind ( route53 , { MaxItems : '1' , DNSName : zoneName + '.' } ) ;
} else {
listHostedZones = route53 . listHostedZones . bind ( route53 , { } ) ; // currently, this route does not support > 100 zones
}
listHostedZones ( function ( error , result ) {
2018-04-29 11:20:12 -07:00
if ( error && error . code === 'AccessDenied' ) return callback ( new DomainsError ( DomainsError . ACCESS _DENIED , error . message ) ) ;
if ( error && error . code === 'InvalidClientTokenId' ) return callback ( new DomainsError ( DomainsError . ACCESS _DENIED , error . message ) ) ;
if ( error ) return callback ( new DomainsError ( DomainsError . EXTERNAL _ERROR , error . message ) ) ;
2015-10-28 16:02:06 -07:00
2018-05-07 11:23:17 -07:00
var 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 DomainsError ( DomainsError . NOT _FOUND , 'no such zone' ) ) ;
callback ( null , zone ) ;
2015-10-28 16:02:06 -07:00
} ) ;
}
2016-07-03 21:37:17 -05:00
function getHostedZone ( dnsConfig , zoneName , callback ) {
assert . strictEqual ( typeof dnsConfig , 'object' ) ;
assert . strictEqual ( typeof zoneName , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2016-09-15 11:57:25 +02:00
2016-07-03 21:37:17 -05:00
getZoneByName ( dnsConfig , zoneName , function ( error , zone ) {
2016-07-04 23:31:26 -05:00
if ( error ) return callback ( error ) ;
2016-07-03 21:37:17 -05:00
var route53 = new AWS . Route53 ( getDnsCredentials ( dnsConfig ) ) ;
route53 . getHostedZone ( { Id : zone . Id } , function ( error , result ) {
2018-04-29 11:20:12 -07:00
if ( error && error . code === 'AccessDenied' ) return callback ( new DomainsError ( DomainsError . ACCESS _DENIED , error . message ) ) ;
if ( error && error . code === 'InvalidClientTokenId' ) return callback ( new DomainsError ( DomainsError . ACCESS _DENIED , error . message ) ) ;
if ( error ) return callback ( new DomainsError ( DomainsError . EXTERNAL _ERROR , error . message ) ) ;
2016-07-03 21:37:17 -05:00
callback ( null , result ) ;
} ) ;
} ) ;
}
2015-11-08 23:14:39 -08:00
function add ( dnsConfig , zoneName , subdomain , type , values , callback ) {
assert . strictEqual ( typeof dnsConfig , 'object' ) ;
2015-10-28 16:02:06 -07:00
assert . strictEqual ( typeof zoneName , 'string' ) ;
assert . strictEqual ( typeof subdomain , 'string' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
2015-10-30 13:16:07 -07:00
assert ( util . isArray ( values ) ) ;
2015-10-28 16:02:06 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2015-10-30 13:16:07 -07:00
debug ( 'add: %s for zone %s of type %s with values %j' , subdomain , zoneName , type , values ) ;
2015-10-30 13:04:43 -07:00
2015-11-08 23:14:39 -08:00
getZoneByName ( dnsConfig , zoneName , function ( error , zone ) {
2015-10-28 16:02:06 -07:00
if ( error ) return callback ( error ) ;
2016-07-07 13:14:15 -07:00
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName ;
2018-05-06 22:14:39 -07:00
var records = values . map ( function ( v ) { return { Value : v } ; } ) ; // for mx records, value is already of the '<priority> <server>' format
2015-10-30 13:04:43 -07:00
2015-10-28 16:02:06 -07:00
var params = {
ChangeBatch : {
Changes : [ {
Action : 'UPSERT' ,
ResourceRecordSet : {
Type : type ,
Name : fqdn ,
2015-10-30 13:04:43 -07:00
ResourceRecords : records ,
2015-10-28 16:02:06 -07:00
TTL : 1
}
} ]
} ,
HostedZoneId : zone . Id
} ;
2015-11-08 23:14:39 -08:00
var route53 = new AWS . Route53 ( getDnsCredentials ( dnsConfig ) ) ;
route53 . changeResourceRecordSets ( params , function ( error , result ) {
2018-04-29 11:20:12 -07:00
if ( error && error . code === 'AccessDenied' ) return callback ( new DomainsError ( DomainsError . ACCESS _DENIED , error . message ) ) ;
if ( error && error . code === 'InvalidClientTokenId' ) return callback ( new DomainsError ( DomainsError . ACCESS _DENIED , error . message ) ) ;
if ( error && error . code === 'PriorRequestNotComplete' ) return callback ( new DomainsError ( DomainsError . STILL _BUSY , error . message ) ) ;
if ( error && error . code === 'InvalidChangeBatch' ) return callback ( new DomainsError ( DomainsError . BAD _FIELD , error . message ) ) ;
if ( error ) return callback ( new DomainsError ( DomainsError . EXTERNAL _ERROR , error . message ) ) ;
2015-10-28 16:02:06 -07:00
2015-11-08 23:14:39 -08:00
callback ( null , result . ChangeInfo . Id ) ;
2015-10-28 16:02:06 -07:00
} ) ;
} ) ;
}
2016-09-05 16:41:04 -07:00
function upsert ( dnsConfig , zoneName , subdomain , type , values , callback ) {
2015-11-08 23:14:39 -08:00
assert . strictEqual ( typeof dnsConfig , 'object' ) ;
2015-10-29 15:37:42 -07:00
assert . strictEqual ( typeof zoneName , 'string' ) ;
assert . strictEqual ( typeof subdomain , 'string' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
2015-10-30 13:45:07 -07:00
assert ( util . isArray ( values ) ) ;
2015-10-29 15:37:42 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2016-09-05 16:41:04 -07:00
add ( dnsConfig , zoneName , subdomain , type , values , callback ) ;
2015-10-30 13:17:33 -07:00
}
2015-11-08 23:14:39 -08:00
function get ( dnsConfig , zoneName , subdomain , type , callback ) {
assert . strictEqual ( typeof dnsConfig , 'object' ) ;
2015-10-30 13:17:33 -07:00
assert . strictEqual ( typeof zoneName , 'string' ) ;
assert . strictEqual ( typeof subdomain , 'string' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2015-11-08 23:14:39 -08:00
getZoneByName ( dnsConfig , zoneName , function ( error , zone ) {
2015-10-30 13:17:33 -07:00
if ( error ) return callback ( error ) ;
var params = {
2015-10-30 18:05:08 -07:00
HostedZoneId : zone . Id ,
2015-10-30 13:17:33 -07:00
MaxItems : '1' ,
2015-10-30 14:41:34 -07:00
StartRecordName : ( subdomain ? subdomain + '.' : '' ) + zoneName + '.' ,
2015-10-30 13:17:33 -07:00
StartRecordType : type
} ;
2015-11-08 23:14:39 -08:00
var route53 = new AWS . Route53 ( getDnsCredentials ( dnsConfig ) ) ;
route53 . listResourceRecordSets ( params , function ( error , result ) {
2018-04-29 11:20:12 -07:00
if ( error && error . code === 'AccessDenied' ) return callback ( new DomainsError ( DomainsError . ACCESS _DENIED , error . message ) ) ;
if ( error && error . code === 'InvalidClientTokenId' ) return callback ( new DomainsError ( DomainsError . ACCESS _DENIED , error . message ) ) ;
if ( error ) return callback ( new DomainsError ( DomainsError . EXTERNAL _ERROR , error . message ) ) ;
2015-11-08 23:14:39 -08:00
if ( result . ResourceRecordSets . length === 0 ) return callback ( null , [ ] ) ;
2016-09-05 15:17:42 -07:00
if ( result . ResourceRecordSets [ 0 ] . Name !== params . StartRecordName || result . ResourceRecordSets [ 0 ] . Type !== params . StartRecordType ) return callback ( null , [ ] ) ;
2015-10-30 13:17:33 -07:00
2015-11-08 23:14:39 -08:00
var values = result . ResourceRecordSets [ 0 ] . ResourceRecords . map ( function ( record ) { return record . Value ; } ) ;
2015-10-30 13:17:33 -07:00
2015-11-08 23:14:39 -08:00
callback ( null , values ) ;
2015-10-30 13:17:33 -07:00
} ) ;
} ) ;
2015-10-29 15:37:42 -07:00
}
2015-11-08 23:14:39 -08:00
function del ( dnsConfig , zoneName , subdomain , type , values , callback ) {
assert . strictEqual ( typeof dnsConfig , 'object' ) ;
2015-10-28 16:02:06 -07:00
assert . strictEqual ( typeof zoneName , 'string' ) ;
assert . strictEqual ( typeof subdomain , 'string' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
2015-10-30 13:30:19 -07:00
assert ( util . isArray ( values ) ) ;
2015-10-28 16:02:06 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2015-11-08 23:14:39 -08:00
getZoneByName ( dnsConfig , zoneName , function ( error , zone ) {
2015-10-28 16:02:06 -07:00
if ( error ) return callback ( error ) ;
2016-07-07 13:14:15 -07:00
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName ;
2015-10-30 13:04:43 -07:00
var records = values . map ( function ( v ) { return { Value : v } ; } ) ;
2015-10-28 16:02:06 -07:00
var resourceRecordSet = {
Name : fqdn ,
Type : type ,
2015-10-30 13:04:43 -07:00
ResourceRecords : records ,
2015-10-28 16:02:06 -07:00
TTL : 1
} ;
var params = {
ChangeBatch : {
Changes : [ {
Action : 'DELETE' ,
ResourceRecordSet : resourceRecordSet
} ]
} ,
HostedZoneId : zone . Id
} ;
2015-11-08 23:14:39 -08:00
var route53 = new AWS . Route53 ( getDnsCredentials ( dnsConfig ) ) ;
2018-02-08 10:21:31 -08:00
route53 . changeResourceRecordSets ( params , function ( error ) {
2018-04-29 11:20:12 -07:00
if ( error && error . code === 'AccessDenied' ) return callback ( new DomainsError ( DomainsError . ACCESS _DENIED , error . message ) ) ;
if ( error && error . code === 'InvalidClientTokenId' ) return callback ( new DomainsError ( DomainsError . ACCESS _DENIED , error . message ) ) ;
2015-11-08 23:14:39 -08:00
if ( error && error . message && error . message . indexOf ( 'it was not found' ) !== - 1 ) {
2015-12-17 20:30:30 -08:00
debug ( 'del: resource record set not found.' , error ) ;
2018-04-29 11:20:12 -07:00
return callback ( new DomainsError ( DomainsError . NOT _FOUND , error . message ) ) ;
2015-11-08 23:14:39 -08:00
} else if ( error && error . code === 'NoSuchHostedZone' ) {
2015-12-17 20:30:30 -08:00
debug ( 'del: hosted zone not found.' , error ) ;
2018-04-29 11:20:12 -07:00
return callback ( new DomainsError ( DomainsError . NOT _FOUND , error . message ) ) ;
2015-11-08 23:14:39 -08:00
} else if ( error && error . code === 'PriorRequestNotComplete' ) {
2015-12-17 20:30:30 -08:00
debug ( 'del: resource is still busy' , error ) ;
2018-04-29 11:20:12 -07:00
return callback ( new DomainsError ( DomainsError . STILL _BUSY , error . message ) ) ;
2015-11-08 23:14:39 -08:00
} else if ( error && error . code === 'InvalidChangeBatch' ) {
2015-12-17 20:30:30 -08:00
debug ( 'del: invalid change batch. No such record to be deleted.' ) ;
2018-04-29 11:20:12 -07:00
return callback ( new DomainsError ( DomainsError . NOT _FOUND , error . message ) ) ;
2015-11-08 23:14:39 -08:00
} else if ( error ) {
2015-12-17 20:30:30 -08:00
debug ( 'del: error' , error ) ;
2018-04-29 11:20:12 -07:00
return callback ( new DomainsError ( DomainsError . EXTERNAL _ERROR , error . message ) ) ;
2015-11-08 23:14:39 -08:00
}
callback ( null ) ;
2015-10-28 16:02:06 -07:00
} ) ;
} ) ;
}
2017-06-12 21:06:40 -07:00
function verifyDnsConfig ( dnsConfig , fqdn , zoneName , ip , callback ) {
2017-01-10 11:12:25 +01:00
assert . strictEqual ( typeof dnsConfig , 'object' ) ;
2017-06-12 21:06:40 -07:00
assert . strictEqual ( typeof fqdn , 'string' ) ;
2017-06-11 22:32:05 -07:00
assert . strictEqual ( typeof zoneName , 'string' ) ;
2017-01-10 11:12:25 +01:00
assert . strictEqual ( typeof ip , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2018-06-17 21:44:08 -07:00
if ( ! dnsConfig . accessKeyId || typeof dnsConfig . accessKeyId !== 'string' ) return callback ( new DomainsError ( DomainsError . BAD _FIELD , 'accessKeyId must be a non-empty string' ) ) ;
if ( ! dnsConfig . secretAccessKey || typeof dnsConfig . secretAccessKey !== 'string' ) return callback ( new DomainsError ( DomainsError . BAD _FIELD , 'secretAccessKey must be a non-empty string' ) ) ;
2017-01-10 11:32:44 +01:00
var credentials = {
accessKeyId : dnsConfig . accessKeyId ,
secretAccessKey : dnsConfig . secretAccessKey ,
region : dnsConfig . region || 'us-east-1' ,
2018-05-07 13:42:10 -07:00
endpoint : dnsConfig . endpoint || null ,
listHostedZonesByName : true // new/updated creds require this perm
2017-01-10 11:32:44 +01:00
} ;
2017-01-10 16:44:28 -08:00
if ( process . env . BOX _ENV === 'test' ) return callback ( null , credentials ) ; // this shouldn't be here
2018-02-08 14:39:35 -08:00
dns . resolve ( zoneName , 'NS' , { timeout : 5000 } , function ( error , nameservers ) {
2018-04-29 11:20:12 -07:00
if ( error && error . code === 'ENOTFOUND' ) return callback ( new DomainsError ( DomainsError . BAD _FIELD , 'Unable to resolve nameservers for this domain' ) ) ;
if ( error || ! nameservers ) return callback ( new DomainsError ( DomainsError . BAD _FIELD , error ? error . message : 'Unable to get nameservers' ) ) ;
2017-01-10 11:12:25 +01:00
2017-06-11 22:32:05 -07:00
getHostedZone ( credentials , zoneName , function ( error , zone ) {
2017-01-10 11:12:25 +01:00
if ( error ) return callback ( error ) ;
if ( ! _ . isEqual ( zone . DelegationSet . NameServers . sort ( ) , nameservers . sort ( ) ) ) {
debug ( 'verifyDnsConfig: %j and %j do not match' , nameservers , zone . DelegationSet . NameServers ) ;
2018-04-29 11:20:12 -07:00
return callback ( new DomainsError ( DomainsError . BAD _FIELD , 'Domain nameservers are not set to Route53' ) ) ;
2017-01-10 11:12:25 +01:00
}
2017-11-07 23:13:58 +01:00
const testSubdomain = 'cloudrontestdns' ;
2017-06-11 22:32:05 -07:00
2017-11-07 23:13:58 +01:00
upsert ( credentials , zoneName , testSubdomain , 'A' , [ ip ] , function ( error , changeId ) {
2017-02-14 22:29:33 -08:00
if ( error ) return callback ( error ) ;
2017-01-10 11:12:25 +01:00
2017-11-07 23:13:58 +01:00
debug ( 'verifyDnsConfig: Test A record added with change id %s' , changeId ) ;
2017-01-10 11:12:25 +01:00
2018-05-07 23:41:03 -07:00
del ( credentials , zoneName , testSubdomain , 'A' , [ ip ] , function ( error ) {
2017-11-07 23:13:58 +01:00
if ( error ) return callback ( error ) ;
debug ( 'verifyDnsConfig: Test A record removed again' ) ;
callback ( null , credentials ) ;
} ) ;
2017-01-10 11:12:25 +01:00
} ) ;
} ) ;
} ) ;
}