2015-10-28 16:02:06 -07:00
'use strict' ;
exports = module . exports = {
2021-08-13 17:22:28 -07:00
removePrivateFields ,
injectPrivateFields ,
upsert ,
get ,
del ,
wait ,
2025-02-11 11:29:30 +01:00
verifyDomainConfig ,
2015-10-28 16:02:06 -07:00
} ;
2025-08-14 11:17:38 +05:30
const assert = require ( 'node:assert' ) ,
2019-10-23 10:02:04 -07:00
BoxError = require ( '../boxerror.js' ) ,
2025-02-10 17:03:09 +01:00
{ ConfiguredRetryStrategy } = require ( '@smithy/util-retry' ) ,
2020-05-14 23:01:44 +02:00
constants = require ( '../constants.js' ) ,
2015-10-28 16:02:06 -07:00
debug = require ( 'debug' ) ( 'box:dns/route53' ) ,
2022-02-04 15:20:49 -08:00
dig = require ( '../dig.js' ) ,
2021-08-13 17:22:28 -07:00
dns = require ( '../dns.js' ) ,
2025-02-10 17:03:09 +01:00
{ Route53 } = require ( '@aws-sdk/client-route-53' ) ,
2022-02-04 15:20:49 -08:00
safe = require ( 'safetydance' ) ,
2019-01-04 18:44:54 -08:00
waitForDns = require ( './waitfordns.js' ) ,
2025-02-13 14:03:25 +01:00
_ = require ( '../underscore.js' ) ;
2015-10-28 16:02:06 -07:00
2019-02-08 11:11:49 +01:00
function removePrivateFields ( domainObject ) {
2020-05-14 23:01:44 +02:00
domainObject . config . secretAccessKey = constants . SECRET _PLACEHOLDER ;
2019-02-08 11:11:49 +01:00
return domainObject ;
}
2019-02-09 19:08:15 +01:00
function injectPrivateFields ( newConfig , currentConfig ) {
2020-05-14 23:01:44 +02:00
if ( newConfig . secretAccessKey === constants . SECRET _PLACEHOLDER ) newConfig . secretAccessKey = currentConfig . secretAccessKey ;
2019-02-09 19:08:15 +01:00
}
2025-02-11 11:29:30 +01:00
function createRoute53Client ( domainConfig ) {
2022-01-05 22:41:41 -08:00
assert . strictEqual ( typeof domainConfig , 'object' ) ;
2015-10-28 16:02:06 -07:00
2022-02-04 15:20:49 -08:00
const credentials = {
2022-01-05 22:41:41 -08:00
accessKeyId : domainConfig . accessKeyId ,
secretAccessKey : domainConfig . secretAccessKey ,
2015-11-08 23:14:39 -08:00
} ;
2015-10-28 16:02:06 -07:00
2025-02-11 11:29:30 +01:00
const clientConfig = {
2025-02-10 17:03:09 +01:00
region : domainConfig . region ,
credentials ,
2025-02-11 11:29:30 +01:00
// route53 has a limit of 5 req/sec/region - https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DNSLimitations.html#limits-api-requests
2025-02-10 17:03:09 +01:00
retryStrategy : new ConfiguredRetryStrategy ( 20 /* max attempts */ , ( /* attempt */ ) => 3000 /* constant backoff */ )
} ;
2025-02-11 11:29:30 +01:00
return constants . TEST ? new globalThis . Route53Mock ( clientConfig ) : new Route53 ( clientConfig ) ;
2015-10-28 16:02:06 -07:00
}
2022-02-04 15:20:49 -08:00
async function getZoneByName ( domainConfig , zoneName ) {
2022-01-05 22:41:41 -08:00
assert . strictEqual ( typeof domainConfig , 'object' ) ;
2015-10-28 16:02:06 -07:00
assert . strictEqual ( typeof zoneName , 'string' ) ;
2025-02-11 11:29:30 +01:00
const route53 = createRoute53Client ( domainConfig ) ;
2018-05-07 11:18:15 -07:00
// backward compat for 2.2, where we only required access to "listHostedZones"
let listHostedZones ;
2022-01-05 22:41:41 -08:00
if ( domainConfig . listHostedZonesByName ) {
2025-02-10 17:03:09 +01:00
listHostedZones = route53 . listHostedZonesByName ( { MaxItems : '1' , DNSName : zoneName + '.' } ) ;
2018-05-07 11:18:15 -07:00
} else {
2025-02-10 17:03:09 +01:00
listHostedZones = route53 . listHostedZones ( { } ) ; // currently, this route does not support > 100 zones
2018-05-07 11:18:15 -07:00
}
2022-02-04 15:20:49 -08:00
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 ) ;
2015-10-28 16:02:06 -07:00
2022-02-04 15:20:49 -08:00
const zone = result . HostedZones . filter ( function ( zone ) {
return zone . Name . slice ( 0 , - 1 ) === zoneName ; // aws zone name contains a '.' at the end
} ) [ 0 ] ;
2018-05-07 11:23:17 -07:00
2022-02-04 15:20:49 -08:00
if ( ! zone ) throw new BoxError ( BoxError . NOT _FOUND , 'no such zone' ) ;
2018-05-07 11:23:17 -07:00
2022-02-04 15:20:49 -08:00
return zone ;
2015-10-28 16:02:06 -07:00
}
2022-02-04 15:20:49 -08:00
async function getHostedZone ( domainConfig , zoneName ) {
2022-01-05 22:41:41 -08:00
assert . strictEqual ( typeof domainConfig , 'object' ) ;
2016-07-03 21:37:17 -05:00
assert . strictEqual ( typeof zoneName , 'string' ) ;
2016-09-15 11:57:25 +02:00
2022-02-04 15:20:49 -08:00
const zone = await getZoneByName ( domainConfig , zoneName ) ;
2016-07-04 23:31:26 -05:00
2025-02-11 11:29:30 +01:00
const route53 = createRoute53Client ( domainConfig ) ;
2025-02-10 17:03:09 +01:00
const [ error , result ] = await safe ( route53 . getHostedZone ( { Id : zone . Id } ) ) ;
2022-02-04 15:20:49 -08:00
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 ) ;
2016-07-03 21:37:17 -05:00
2022-02-04 15:20:49 -08:00
return result ;
2016-07-03 21:37:17 -05:00
}
2022-02-04 15:20:49 -08:00
async function upsert ( domainObject , location , type , values ) {
2019-01-04 18:44:54 -08:00
assert . strictEqual ( typeof domainObject , 'object' ) ;
assert . strictEqual ( typeof location , 'string' ) ;
2015-10-28 16:02:06 -07:00
assert . strictEqual ( typeof type , 'string' ) ;
2021-05-02 11:26:08 -07:00
assert ( Array . isArray ( values ) ) ;
2015-10-28 16:02:06 -07:00
2022-01-05 22:41:41 -08:00
const domainConfig = domainObject . config ,
2019-01-04 18:44:54 -08:00
zoneName = domainObject . zoneName ,
2022-11-28 21:23:06 +01:00
fqdn = dns . fqdn ( location , domainObject . domain ) ;
2019-01-04 18:44:54 -08:00
debug ( 'add: %s for zone %s of type %s with values %j' , fqdn , zoneName , type , values ) ;
2015-10-30 13:04:43 -07:00
2022-02-04 15:20:49 -08:00
const zone = await getZoneByName ( domainConfig , zoneName ) ;
const records = values . map ( function ( v ) { return { Value : v } ; } ) ; // for mx records, value is already of the '<priority> <server>' format
const params = {
ChangeBatch : {
Changes : [ {
Action : 'UPSERT' ,
ResourceRecordSet : {
Type : type ,
Name : fqdn ,
ResourceRecords : records ,
TTL : 1
}
} ]
} ,
HostedZoneId : zone . Id
} ;
2025-02-11 11:29:30 +01:00
const route53 = createRoute53Client ( domainConfig ) ;
2025-02-10 17:03:09 +01:00
const [ error ] = await safe ( route53 . changeResourceRecordSets ( params ) ) ;
2022-02-04 15:20:49 -08:00
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 ) ;
2015-10-28 16:02:06 -07:00
}
2022-02-04 15:20:49 -08:00
async function get ( domainObject , location , type ) {
2019-01-04 18:44:54 -08:00
assert . strictEqual ( typeof domainObject , 'object' ) ;
assert . strictEqual ( typeof location , 'string' ) ;
2015-10-29 15:37:42 -07:00
assert . strictEqual ( typeof type , 'string' ) ;
2022-01-05 22:41:41 -08:00
const domainConfig = domainObject . config ,
2019-01-04 18:44:54 -08:00
zoneName = domainObject . zoneName ,
2022-11-28 21:23:06 +01:00
fqdn = dns . fqdn ( location , domainObject . domain ) ;
2015-10-30 13:17:33 -07:00
2022-02-04 15:20:49 -08:00
const zone = await getZoneByName ( domainConfig , zoneName ) ;
const params = {
HostedZoneId : zone . Id ,
MaxItems : '1' ,
StartRecordName : fqdn + '.' ,
StartRecordType : type
} ;
2025-02-11 11:29:30 +01:00
const route53 = createRoute53Client ( domainConfig ) ;
2025-02-10 17:03:09 +01:00
const [ error , result ] = await safe ( route53 . listResourceRecordSets ( params ) ) ;
2022-02-04 15:20:49 -08:00
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 [ ] ;
const values = result . ResourceRecordSets [ 0 ] . ResourceRecords . map ( function ( record ) { return record . Value ; } ) ;
return values ;
2015-10-29 15:37:42 -07:00
}
2022-02-04 15:20:49 -08:00
async function del ( domainObject , location , type , values ) {
2019-01-04 18:44:54 -08:00
assert . strictEqual ( typeof domainObject , 'object' ) ;
assert . strictEqual ( typeof location , 'string' ) ;
2015-10-28 16:02:06 -07:00
assert . strictEqual ( typeof type , 'string' ) ;
2021-05-02 11:26:08 -07:00
assert ( Array . isArray ( values ) ) ;
2015-10-28 16:02:06 -07:00
2022-01-05 22:41:41 -08:00
const domainConfig = domainObject . config ,
2019-01-04 18:44:54 -08:00
zoneName = domainObject . zoneName ,
2022-11-28 21:23:06 +01:00
fqdn = dns . fqdn ( location , domainObject . domain ) ;
2019-01-04 18:44:54 -08:00
2022-02-04 15:20:49 -08:00
const zone = await getZoneByName ( domainConfig , zoneName ) ;
const records = values . map ( function ( v ) { return { Value : v } ; } ) ;
const resourceRecordSet = {
Name : fqdn ,
Type : type ,
ResourceRecords : records ,
TTL : 1
} ;
const params = {
ChangeBatch : {
Changes : [ {
Action : 'DELETE' ,
ResourceRecordSet : resourceRecordSet
} ]
} ,
HostedZoneId : zone . Id
} ;
2025-02-11 11:29:30 +01:00
const route53 = createRoute53Client ( domainConfig ) ;
2025-02-10 17:03:09 +01:00
const [ error ] = await safe ( route53 . changeResourceRecordSets ( params ) ) ;
2022-02-04 15:20:49 -08:00
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 ) ;
}
2015-10-28 16:02:06 -07:00
}
2022-02-03 16:15:14 -08:00
async function wait ( domainObject , subdomain , type , value , options ) {
2019-01-04 18:44:54 -08:00
assert . strictEqual ( typeof domainObject , 'object' ) ;
2022-02-03 16:15:14 -08:00
assert . strictEqual ( typeof subdomain , 'string' ) ;
2019-01-04 18:44:54 -08:00
assert . strictEqual ( typeof type , 'string' ) ;
assert . strictEqual ( typeof value , 'string' ) ;
assert ( options && typeof options === 'object' ) ; // { interval: 5000, times: 50000 }
2017-01-10 11:12:25 +01:00
2022-11-28 21:23:06 +01:00
const fqdn = dns . fqdn ( subdomain , domainObject . domain ) ;
2019-01-04 18:44:54 -08:00
2022-02-03 16:15:14 -08:00
await waitForDns ( fqdn , domainObject . zoneName , type , value , options ) ;
2019-01-04 18:44:54 -08:00
}
2022-02-04 15:20:49 -08:00
async function verifyDomainConfig ( domainObject ) {
2019-01-04 18:44:54 -08:00
assert . strictEqual ( typeof domainObject , 'object' ) ;
2022-01-05 22:41:41 -08:00
const domainConfig = domainObject . config ,
2019-01-04 18:44:54 -08:00
zoneName = domainObject . zoneName ;
2022-02-04 15:20:49 -08:00
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' ) ;
2025-03-02 07:27:09 +01:00
if ( 'customNameservers' in domainConfig && typeof domainConfig . customNameservers !== 'boolean' ) throw new BoxError ( BoxError . BAD _FIELD , 'customNameservers must be a boolean' ) ;
2018-06-17 21:44:08 -07:00
2022-02-04 15:20:49 -08:00
const credentials = {
2022-01-05 22:41:41 -08:00
accessKeyId : domainConfig . accessKeyId ,
secretAccessKey : domainConfig . secretAccessKey ,
region : domainConfig . region || 'us-east-1' ,
endpoint : domainConfig . endpoint || null ,
2018-08-22 12:16:19 +02:00
listHostedZonesByName : true , // new/updated creds require this perm
2025-03-02 07:27:09 +01:00
customNameservers : ! ! domainConfig . customNameservers
2017-01-10 11:32:44 +01:00
} ;
2019-01-04 18:44:54 -08:00
const ip = '127.0.0.1' ;
2023-10-01 13:52:19 +05:30
if ( constants . TEST ) return credentials ; // this shouldn't be here
2017-01-10 11:12:25 +01:00
2022-02-04 15:20:49 -08:00
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' ) ;
2017-01-10 11:12:25 +01:00
2022-02-04 15:20:49 -08:00
const zone = await getHostedZone ( credentials , zoneName ) ;
2017-06-11 22:32:05 -07:00
2022-02-04 15:20:49 -08:00
if ( ! _ . isEqual ( zone . DelegationSet . NameServers . sort ( ) , nameservers . sort ( ) ) ) {
debug ( 'verifyDomainConfig: %j and %j do not match' , nameservers , zone . DelegationSet . NameServers ) ;
2025-03-02 07:27:09 +01:00
if ( ! domainConfig . customNameservers ) throw new BoxError ( BoxError . BAD _FIELD , 'Domain nameservers are not set to Route53' ) ;
2022-02-04 15:20:49 -08:00
}
2017-01-10 11:12:25 +01:00
2022-02-04 15:20:49 -08:00
const location = 'cloudrontestdns' ;
const newDomainObject = Object . assign ( { } , domainObject , { config : credentials } ) ;
2017-01-10 11:12:25 +01:00
2022-02-04 15:20:49 -08:00
await upsert ( newDomainObject , location , 'A' , [ ip ] ) ;
debug ( 'verifyDomainConfig: Test A record added' ) ;
2017-11-07 23:13:58 +01:00
2022-03-02 10:44:52 -08:00
await get ( newDomainObject , location , 'A' ) ;
debug ( 'verifyDomainConfig: Can list record sets' ) ;
2022-02-04 15:20:49 -08:00
await del ( newDomainObject , location , 'A' , [ ip ] ) ;
debug ( 'verifyDomainConfig: Test A record removed again' ) ;
2017-11-07 23:13:58 +01:00
2022-02-04 15:20:49 -08:00
return credentials ;
2017-01-10 11:12:25 +01:00
}