2020-03-12 17:13:13 -07:00
'use strict' ;
exports = module . exports = {
removePrivateFields : removePrivateFields ,
injectPrivateFields : injectPrivateFields ,
upsert : upsert ,
get : get ,
del : del ,
wait : wait ,
verifyDnsConfig : verifyDnsConfig
} ;
let async = require ( 'async' ) ,
assert = require ( 'assert' ) ,
2020-05-14 23:01:44 +02:00
constants = require ( '../constants.js' ) ,
2020-03-12 17:13:13 -07:00
BoxError = require ( '../boxerror.js' ) ,
debug = require ( 'debug' ) ( 'box:dns/linode' ) ,
dns = require ( '../native-dns.js' ) ,
domains = require ( '../domains.js' ) ,
superagent = require ( 'superagent' ) ,
util = require ( 'util' ) ,
waitForDns = require ( './waitfordns.js' ) ;
const LINODE _ENDPOINT = 'https://api.linode.com/v4' ;
function formatError ( response ) {
return util . format ( 'Linode DNS error [%s] %j' , response . statusCode , response . body ) ;
}
function removePrivateFields ( domainObject ) {
2020-05-14 23:01:44 +02:00
domainObject . config . token = constants . SECRET _PLACEHOLDER ;
2020-03-12 17:13:13 -07:00
return domainObject ;
}
function injectPrivateFields ( newConfig , currentConfig ) {
2020-05-14 23:01:44 +02:00
if ( newConfig . token === constants . SECRET _PLACEHOLDER ) newConfig . token = currentConfig . token ;
2020-03-12 17:13:13 -07:00
}
function getZoneId ( dnsConfig , zoneName , callback ) {
assert . strictEqual ( typeof dnsConfig , 'object' ) ;
assert . strictEqual ( typeof zoneName , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
// returns 100 at a time
superagent . get ( ` ${ LINODE _ENDPOINT } /domains ` )
. set ( 'Authorization' , 'Bearer ' + dnsConfig . 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 ) ) ) ;
if ( ! Array . isArray ( result . body . data ) ) return callback ( new BoxError ( BoxError . EXTERNAL _ERROR , 'Invalid response' ) ) ;
const zone = result . body . data . find ( d => d . domain === zoneName ) ;
if ( ! zone || ! zone . id ) return callback ( new BoxError ( BoxError . NOT _FOUND , 'Zone not found' ) ) ;
debug ( ` getZoneId: zone id of ${ zoneName } is ${ zone . id } ` ) ;
callback ( null , zone . id ) ;
} ) ;
}
function getZoneRecords ( dnsConfig , zoneName , name , type , callback ) {
assert . strictEqual ( typeof dnsConfig , '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 ( dnsConfig , zoneName , function ( error , zoneId ) {
if ( error ) return callback ( error ) ;
let page = 0 , more = false ;
let records = [ ] ;
async . doWhilst ( function ( iteratorDone ) {
const url = ` ${ LINODE _ENDPOINT } /domains/ ${ zoneId } /records?page= ${ ++ page } ` ;
superagent . get ( url )
. set ( 'Authorization' , 'Bearer ' + dnsConfig . 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 ) ) ) ;
records = records . concat ( result . body . data . filter ( function ( record ) {
return ( record . type === type && record . name === name ) ;
} ) ) ;
more = result . body . page !== result . body . pages ;
iteratorDone ( ) ;
} ) ;
} , function ( ) { return more ; } , function ( error ) {
debug ( 'getZoneRecords:' , error , JSON . stringify ( records ) ) ;
if ( error ) return callback ( error ) ;
callback ( null , { zoneId , records } ) ;
} ) ;
} ) ;
}
function get ( domainObject , location , type , callback ) {
assert . strictEqual ( typeof domainObject , 'object' ) ;
assert . strictEqual ( typeof location , 'string' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
const dnsConfig = domainObject . config ,
zoneName = domainObject . zoneName ,
name = domains . getName ( domainObject , location , type ) || '' ;
getZoneRecords ( dnsConfig , zoneName , name , type , function ( error , { records } ) {
if ( error ) return callback ( error ) ;
var tmp = records . map ( function ( record ) { return record . target ; } ) ;
debug ( 'get: %j' , tmp ) ;
return callback ( null , tmp ) ;
} ) ;
}
function upsert ( domainObject , location , type , values , callback ) {
assert . strictEqual ( typeof domainObject , 'object' ) ;
assert . strictEqual ( typeof location , 'string' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
assert ( util . isArray ( values ) ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
const dnsConfig = domainObject . config ,
zoneName = domainObject . zoneName ,
name = domains . getName ( domainObject , location , type ) || '' ;
debug ( 'upsert: %s for zone %s of type %s with values %j' , name , zoneName , type , values ) ;
getZoneRecords ( dnsConfig , zoneName , name , type , function ( error , { zoneId , records } ) {
if ( error ) return callback ( error ) ;
let i = 0 , recordIds = [ ] ; // used to track available records to update instead of create
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 ( i >= records . length ) {
data . name = name ; // only set for new records
superagent . post ( ` ${ LINODE _ENDPOINT } /domains/ ${ zoneId } /records ` )
. set ( 'Authorization' , 'Bearer ' + dnsConfig . 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 ) ) ) ;
recordIds . push ( result . body . id ) ;
return iteratorCallback ( null ) ;
} ) ;
} else {
superagent . put ( ` ${ LINODE _ENDPOINT } /domains/ ${ zoneId } /records/ ${ records [ i ] . id } ` )
. set ( 'Authorization' , 'Bearer ' + dnsConfig . token )
. send ( data )
. timeout ( 30 * 1000 )
. retry ( 5 )
. end ( function ( error , result ) {
// increment, as we have consumed the record
++ i ;
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 ) ) ) ;
recordIds . push ( result . body . id ) ;
return iteratorCallback ( null ) ;
} ) ;
}
} , function ( error ) {
if ( error ) return callback ( error ) ;
debug ( 'upsert: completed with recordIds:%j' , recordIds ) ;
callback ( ) ;
} ) ;
} ) ;
}
function del ( domainObject , location , type , values , callback ) {
assert . strictEqual ( typeof domainObject , 'object' ) ;
assert . strictEqual ( typeof location , 'string' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
assert ( util . isArray ( values ) ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
const dnsConfig = domainObject . config ,
zoneName = domainObject . zoneName ,
name = domains . getName ( domainObject , location , type ) || '' ;
getZoneRecords ( dnsConfig , zoneName , name , type , function ( error , { zoneId , records } ) {
if ( error ) return callback ( error ) ;
if ( records . length === 0 ) return callback ( null ) ;
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 } ` )
. set ( 'Authorization' , 'Bearer ' + dnsConfig . 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 ) ;
} ) ;
} ) ;
}
function wait ( domainObject , location , type , value , options , callback ) {
assert . strictEqual ( typeof domainObject , 'object' ) ;
assert . strictEqual ( typeof location , 'string' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
assert . strictEqual ( typeof value , 'string' ) ;
assert ( options && typeof options === 'object' ) ; // { interval: 5000, times: 50000 }
assert . strictEqual ( typeof callback , 'function' ) ;
const fqdn = domains . fqdn ( location , domainObject ) ;
waitForDns ( fqdn , domainObject . zoneName , type , value , options , callback ) ;
}
function verifyDnsConfig ( domainObject , callback ) {
assert . strictEqual ( typeof domainObject , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
const dnsConfig = domainObject . config ,
zoneName = domainObject . zoneName ;
if ( ! dnsConfig . token || typeof dnsConfig . token !== 'string' ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'token must be a non-empty string' , { field : 'token' } ) ) ;
const ip = '127.0.0.1' ;
var credentials = {
token : dnsConfig . token
} ;
if ( process . env . BOX _ENV === 'test' ) return callback ( null , 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' } ) ) ;
if ( nameservers . map ( function ( n ) { return n . toLowerCase ( ) ; } ) . indexOf ( 'ns1.linode.com' ) === - 1 ) {
debug ( 'verifyDnsConfig: %j does not contains DO NS' , nameservers ) ;
return callback ( new BoxError ( BoxError . BAD _FIELD , 'Domain nameservers are not set to Linode' , { field : 'nameservers' } ) ) ;
}
const location = 'cloudrontestdns' ;
upsert ( domainObject , location , 'A' , [ ip ] , function ( error ) {
if ( error ) return callback ( error ) ;
debug ( 'verifyDnsConfig: Test A record added' ) ;
del ( domainObject , location , 'A' , [ ip ] , function ( error ) {
if ( error ) return callback ( error ) ;
debug ( 'verifyDnsConfig: Test A record removed again' ) ;
callback ( null , credentials ) ;
} ) ;
} ) ;
} ) ;
}