2017-04-26 08:45:20 +04:00
'use strict' ;
exports = module . exports = {
2019-02-08 11:11:49 +01:00
removePrivateFields : removePrivateFields ,
2019-02-09 19:08:15 +01:00
injectPrivateFields : injectPrivateFields ,
2017-04-26 08:45:20 +04:00
upsert : upsert ,
get : get ,
del : del ,
2019-01-04 18:44:54 -08:00
wait : wait ,
2017-04-26 08:45:20 +04:00
verifyDnsConfig : verifyDnsConfig
} ;
var assert = require ( 'assert' ) ,
2017-04-26 09:59:08 +02:00
async = require ( 'async' ) ,
2019-10-23 10:02:04 -07:00
BoxError = require ( '../boxerror.js' ) ,
2020-05-14 23:01:44 +02:00
constants = require ( '../constants.js' ) ,
2017-10-29 01:48:55 +02:00
debug = require ( 'debug' ) ( 'box:dns/cloudflare' ) ,
2018-02-08 10:21:31 -08:00
dns = require ( '../native-dns.js' ) ,
2019-01-04 18:44:54 -08:00
domains = require ( '../domains.js' ) ,
2017-04-26 08:45:20 +04:00
superagent = require ( 'superagent' ) ,
2017-10-29 01:48:55 +02:00
util = require ( 'util' ) ,
2019-01-04 18:44:54 -08:00
waitForDns = require ( './waitfordns.js' ) ,
2017-10-29 01:48:55 +02:00
_ = require ( 'underscore' ) ;
2017-04-26 08:45:20 +04:00
2017-04-26 11:48:31 +02:00
// we are using latest v4 stable API https://api.cloudflare.com/#getting-started-endpoints
2017-07-28 16:12:41 +02:00
var CLOUDFLARE _ENDPOINT = 'https://api.cloudflare.com/client/v4' ;
2017-04-26 08:45:20 +04:00
2019-02-08 11:11:49 +01:00
function removePrivateFields ( domainObject ) {
2020-05-14 23:01:44 +02:00
domainObject . config . token = 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 . token === constants . SECRET _PLACEHOLDER ) newConfig . token = currentConfig . token ;
2019-02-09 19:08:15 +01:00
}
2017-07-31 11:25:17 +02:00
function translateRequestError ( result , callback ) {
assert . strictEqual ( typeof result , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2019-10-23 10:02:04 -07:00
if ( result . statusCode === 404 ) return callback ( new BoxError ( BoxError . NOT _FOUND , util . format ( '%s %j' , result . statusCode , 'API does not exist' ) ) ) ;
if ( result . statusCode === 422 ) return callback ( new BoxError ( BoxError . BAD _FIELD , result . body . message ) ) ;
2020-04-23 12:07:45 -07:00
if ( result . statusCode === 400 || result . statusCode === 401 || result . statusCode === 403 ) {
let message = 'Unknown error' ;
if ( typeof result . body . error === 'string' ) {
message = ` message: ${ result . body . error } statusCode: ${ result . statusCode } ` ;
} else if ( Array . isArray ( result . body . errors ) && result . body . errors . length > 0 ) {
let error = result . body . errors [ 0 ] ;
message = ` message: ${ error . message } statusCode: ${ result . statusCode } code: ${ error . code } ` ;
}
2019-10-23 10:02:04 -07:00
return callback ( new BoxError ( BoxError . ACCESS _DENIED , message ) ) ;
2017-07-31 11:25:17 +02:00
}
2019-10-23 10:02:04 -07:00
callback ( new BoxError ( BoxError . EXTERNAL _ERROR , util . format ( '%s %j' , result . statusCode , result . body ) ) ) ;
2017-07-31 11:25:17 +02:00
}
2019-12-31 16:25:49 -08:00
function createRequest ( method , url , dnsConfig ) {
assert . strictEqual ( typeof method , 'string' ) ;
assert . strictEqual ( typeof url , 'string' ) ;
assert . strictEqual ( typeof dnsConfig , 'object' ) ;
2019-12-31 16:44:14 -08:00
let request = superagent ( method , url )
2019-12-31 16:25:49 -08:00
. timeout ( 30 * 1000 ) ;
2019-12-31 16:44:14 -08:00
if ( dnsConfig . tokenType === 'GlobalApiKey' ) {
request . set ( 'X-Auth-Key' , dnsConfig . token ) . set ( 'X-Auth-Email' , dnsConfig . email ) ;
} else {
request . set ( 'Authorization' , 'Bearer ' + dnsConfig . token ) ;
}
return request ;
2019-12-31 16:25:49 -08:00
}
2017-04-26 08:45:20 +04:00
function getZoneByName ( dnsConfig , zoneName , callback ) {
assert . strictEqual ( typeof dnsConfig , 'object' ) ;
assert . strictEqual ( typeof zoneName , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2017-04-26 11:48:31 +02:00
2019-12-31 16:25:49 -08:00
createRequest ( 'GET' , CLOUDFLARE _ENDPOINT + '/zones?name=' + zoneName + '&status=active' , dnsConfig )
2018-01-19 09:55:27 -08:00
. end ( function ( error , result ) {
if ( error && ! error . response ) return callback ( error ) ;
if ( result . statusCode !== 200 || result . body . success !== true ) return translateRequestError ( result , callback ) ;
2019-10-23 10:02:04 -07:00
if ( ! result . body . result . length ) return callback ( new BoxError ( BoxError . NOT _FOUND , util . format ( '%s %j' , result . statusCode , result . body ) ) ) ;
2018-01-19 09:55:27 -08:00
callback ( null , result . body . result [ 0 ] ) ;
} ) ;
2017-04-26 08:45:20 +04:00
}
2019-01-03 10:35:17 -08:00
// gets records filtered by zone, type and fqdn
2019-01-04 18:44:54 -08:00
function getDnsRecords ( dnsConfig , zoneId , fqdn , type , callback ) {
2017-04-26 08:45:20 +04:00
assert . strictEqual ( typeof dnsConfig , 'object' ) ;
2017-07-28 16:41:00 +02:00
assert . strictEqual ( typeof zoneId , 'string' ) ;
2019-01-04 18:44:54 -08:00
assert . strictEqual ( typeof fqdn , 'string' ) ;
2017-04-26 08:45:20 +04:00
assert . strictEqual ( typeof type , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2017-04-26 11:48:31 +02:00
2019-12-31 16:25:49 -08:00
createRequest ( 'GET' , CLOUDFLARE _ENDPOINT + '/zones/' + zoneId + '/dns_records' , dnsConfig )
2018-01-19 09:55:27 -08:00
. query ( { type : type , name : fqdn } )
. end ( function ( error , result ) {
if ( error && ! error . response ) return callback ( error ) ;
if ( result . statusCode !== 200 || result . body . success !== true ) return translateRequestError ( result , callback ) ;
2017-07-28 16:41:00 +02:00
2018-01-19 09:55:27 -08:00
var tmp = result . body . result ;
2017-04-26 08:45:20 +04:00
2018-01-19 09:55:27 -08:00
return callback ( null , tmp ) ;
} ) ;
2017-04-26 08:45:20 +04:00
}
2019-01-04 18:44:54 -08:00
function upsert ( domainObject , location , type , values , callback ) {
assert . strictEqual ( typeof domainObject , 'object' ) ;
assert . strictEqual ( typeof location , 'string' ) ;
2017-04-26 08:45:20 +04:00
assert . strictEqual ( typeof type , 'string' ) ;
2021-05-02 11:26:08 -07:00
assert ( Array . isArray ( values ) ) ;
2017-04-26 08:45:20 +04:00
assert . strictEqual ( typeof callback , 'function' ) ;
2019-01-04 18:44:54 -08:00
const dnsConfig = domainObject . config ,
zoneName = domainObject . zoneName ,
fqdn = domains . fqdn ( location , domainObject ) ;
2017-04-26 08:45:20 +04:00
2019-01-04 18:44:54 -08:00
debug ( 'upsert: %s for zone %s of type %s with values %j' , fqdn , zoneName , type , values ) ;
2017-04-26 08:45:20 +04:00
2019-01-03 10:35:17 -08:00
getZoneByName ( dnsConfig , zoneName , function ( error , result ) {
2017-04-26 08:45:20 +04:00
if ( error ) return callback ( error ) ;
2019-01-03 10:35:17 -08:00
let zoneId = result . id ;
2017-04-26 08:45:20 +04:00
2019-01-04 18:44:54 -08:00
getDnsRecords ( dnsConfig , zoneId , fqdn , type , function ( error , dnsRecords ) {
2017-04-26 08:45:20 +04:00
if ( error ) return callback ( error ) ;
2019-01-03 10:35:17 -08:00
let i = 0 ; // // used to track available records to update instead of create
2017-07-28 16:41:00 +02:00
2019-01-03 10:37:44 -08:00
async . eachSeries ( values , function ( value , iteratorCallback ) {
2017-08-26 15:54:34 -07:00
var priority = null ;
if ( type === 'MX' ) {
2018-09-07 11:44:31 -07:00
priority = parseInt ( value . split ( ' ' ) [ 0 ] , 10 ) ;
2017-08-26 15:54:34 -07:00
value = value . split ( ' ' ) [ 1 ] ;
}
2017-07-28 16:41:00 +02:00
var data = {
type : type ,
name : fqdn ,
content : value ,
2017-08-26 15:54:34 -07:00
priority : priority ,
2019-01-03 10:41:35 -08:00
proxied : false ,
2017-07-28 16:41:00 +02:00
ttl : 120 // 1 means "automatic" (meaning 300ms) and 120 is the lowest supported
} ;
2019-01-03 10:35:17 -08:00
if ( i >= dnsRecords . length ) { // create a new record
2019-01-04 18:44:54 -08:00
debug ( ` upsert: Adding new record fqdn: ${ fqdn } , zoneName: ${ zoneName } proxied: false ` ) ;
2019-01-03 10:41:35 -08:00
2019-12-31 16:25:49 -08:00
createRequest ( 'POST' , CLOUDFLARE _ENDPOINT + '/zones/' + zoneId + '/dns_records' , dnsConfig )
2018-01-19 09:55:27 -08:00
. send ( data )
. end ( function ( error , result ) {
2019-01-03 10:37:44 -08:00
if ( error && ! error . response ) return iteratorCallback ( error ) ;
if ( result . statusCode !== 200 || result . body . success !== true ) return translateRequestError ( result , iteratorCallback ) ;
2018-01-19 09:55:27 -08:00
2019-01-03 10:37:44 -08:00
iteratorCallback ( null ) ;
2018-01-19 09:55:27 -08:00
} ) ;
2019-01-03 10:35:17 -08:00
} else { // replace existing record
2019-01-03 10:41:35 -08:00
data . proxied = dnsRecords [ i ] . proxied ; // preserve proxied parameter
2019-01-04 18:44:54 -08:00
debug ( ` upsert: Updating existing record fqdn: ${ fqdn } , zoneName: ${ zoneName } proxied: ${ data . proxied } ` ) ;
2019-01-03 10:41:35 -08:00
2019-12-31 16:25:49 -08:00
createRequest ( 'PUT' , CLOUDFLARE _ENDPOINT + '/zones/' + zoneId + '/dns_records/' + dnsRecords [ i ] . id , dnsConfig )
2018-01-19 09:55:27 -08:00
. send ( data )
. end ( function ( error , result ) {
2019-01-03 10:35:17 -08:00
++ i ; // increment, as we have consumed the record
2017-07-28 16:41:00 +02:00
2019-01-03 10:37:44 -08:00
if ( error && ! error . response ) return iteratorCallback ( error ) ;
if ( result . statusCode !== 200 || result . body . success !== true ) return translateRequestError ( result , iteratorCallback ) ;
2017-07-28 16:41:00 +02:00
2019-01-03 10:37:44 -08:00
iteratorCallback ( null ) ;
2018-01-19 09:55:27 -08:00
} ) ;
2017-07-28 16:41:00 +02:00
}
2018-06-29 22:25:34 +02:00
} , callback ) ;
2017-04-26 08:45:20 +04:00
} ) ;
2017-04-26 09:59:08 +02:00
} ) ;
2017-04-26 08:45:20 +04:00
}
2019-01-04 18:44:54 -08:00
function get ( domainObject , location , type , callback ) {
assert . strictEqual ( typeof domainObject , 'object' ) ;
assert . strictEqual ( typeof location , 'string' ) ;
2017-04-26 08:45:20 +04:00
assert . strictEqual ( typeof type , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2019-01-04 18:44:54 -08:00
const dnsConfig = domainObject . config ,
zoneName = domainObject . zoneName ,
fqdn = domains . fqdn ( location , domainObject ) ;
getZoneByName ( dnsConfig , zoneName , function ( error , zone ) {
2017-04-26 08:45:20 +04:00
if ( error ) return callback ( error ) ;
2019-01-04 18:44:54 -08:00
getDnsRecords ( dnsConfig , zone . id , fqdn , type , function ( error , result ) {
2017-07-28 16:41:00 +02:00
if ( error ) return callback ( error ) ;
var tmp = result . map ( function ( record ) { return record . content ; } ) ;
debug ( 'get: %j' , tmp ) ;
2017-04-26 08:45:20 +04:00
2017-07-28 16:41:00 +02:00
callback ( null , tmp ) ;
} ) ;
2017-04-26 08:45:20 +04:00
} ) ;
}
2019-01-04 18:44:54 -08:00
function del ( domainObject , location , type , values , callback ) {
assert . strictEqual ( typeof domainObject , 'object' ) ;
assert . strictEqual ( typeof location , 'string' ) ;
2017-04-26 08:45:20 +04:00
assert . strictEqual ( typeof type , 'string' ) ;
2021-05-02 11:26:08 -07:00
assert ( Array . isArray ( values ) ) ;
2017-04-26 08:45:20 +04:00
assert . strictEqual ( typeof callback , 'function' ) ;
2017-04-26 09:59:08 +02:00
2019-01-04 18:44:54 -08:00
const dnsConfig = domainObject . config ,
zoneName = domainObject . zoneName ,
fqdn = domains . fqdn ( location , domainObject ) ;
getZoneByName ( dnsConfig , zoneName , function ( error , zone ) {
2017-04-26 08:45:20 +04:00
if ( error ) return callback ( error ) ;
2019-01-04 18:44:54 -08:00
getDnsRecords ( dnsConfig , zone . id , fqdn , type , function ( error , result ) {
2017-07-28 16:41:00 +02:00
if ( error ) return callback ( error ) ;
if ( result . length === 0 ) return callback ( null ) ;
var zoneId = result [ 0 ] . zone _id ;
var tmp = result . filter ( function ( record ) { return values . some ( function ( value ) { return value === record . content ; } ) ; } ) ;
debug ( 'del: %j' , tmp ) ;
2017-04-26 08:45:20 +04:00
2017-07-28 16:41:00 +02:00
if ( tmp . length === 0 ) return callback ( null ) ;
2017-04-26 08:45:20 +04:00
2017-07-28 16:41:00 +02:00
async . eachSeries ( tmp , function ( record , callback ) {
2019-12-31 16:25:49 -08:00
createRequest ( 'DELETE' , CLOUDFLARE _ENDPOINT + '/zones/' + zoneId + '/dns_records/' + record . id , dnsConfig )
2018-01-19 09:55:27 -08:00
. end ( function ( error , result ) {
if ( error && ! error . response ) return callback ( error ) ;
2018-01-23 14:52:39 -08:00
if ( result . statusCode !== 200 || result . body . success !== true ) return translateRequestError ( result , callback ) ;
2017-04-26 08:45:20 +04:00
2018-01-19 09:55:27 -08:00
debug ( 'del: done' ) ;
2017-04-26 08:45:20 +04:00
2018-01-19 09:55:27 -08:00
callback ( null ) ;
} ) ;
2017-07-28 16:41:00 +02:00
} , function ( error ) {
if ( error ) return callback ( error ) ;
2017-04-26 08:45:20 +04:00
2017-07-28 16:41:00 +02:00
callback ( null , 'unused' ) ;
2017-04-26 08:45:20 +04:00
} ) ;
} ) ;
} ) ;
}
2019-01-04 18:44:54 -08:00
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 }
2017-04-26 08:45:20 +04:00
assert . strictEqual ( typeof callback , 'function' ) ;
2019-01-05 18:24:21 -08:00
const dnsConfig = domainObject . config ,
zoneName = domainObject . zoneName ,
fqdn = domains . fqdn ( location , domainObject ) ;
debug ( 'wait: %s for zone %s of type %s' , fqdn , zoneName , type ) ;
getZoneByName ( dnsConfig , zoneName , function ( error , result ) {
if ( error ) return callback ( error ) ;
let zoneId = result . id ;
getDnsRecords ( dnsConfig , zoneId , fqdn , type , function ( error , dnsRecords ) {
if ( error ) return callback ( error ) ;
2019-10-23 10:02:04 -07:00
if ( dnsRecords . length === 0 ) return callback ( new BoxError ( BoxError . NOT _FOUND , 'Domain not found' ) ) ;
2019-01-04 18:44:54 -08:00
2019-01-05 18:24:21 -08:00
if ( ! dnsRecords [ 0 ] . proxied ) return waitForDns ( fqdn , domainObject . zoneName , type , value , options , callback ) ;
debug ( 'wait: skipping wait of proxied domain' ) ;
callback ( null ) ; // maybe we can check for dns to be cloudflare IPs? https://api.cloudflare.com/#cloudflare-ips-cloudflare-ip-details
} ) ;
} ) ;
2019-01-04 18:44:54 -08:00
}
function verifyDnsConfig ( domainObject , callback ) {
assert . strictEqual ( typeof domainObject , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
const dnsConfig = domainObject . config ,
zoneName = domainObject . zoneName ;
2019-12-31 16:44:14 -08:00
// token can be api token or global api key
2019-10-23 10:02:04 -07:00
if ( ! dnsConfig . token || typeof dnsConfig . token !== 'string' ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'token must be a non-empty string' , { field : 'token' } ) ) ;
2019-12-31 16:44:14 -08:00
if ( dnsConfig . tokenType !== 'GlobalApiKey' && dnsConfig . tokenType !== 'ApiToken' ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'tokenType is required' , { field : 'tokenType' } ) ) ;
if ( dnsConfig . tokenType === 'GlobalApiKey' ) {
2020-05-02 18:06:21 -07:00
if ( typeof dnsConfig . email !== 'string' ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'email must be a non-empty string' , { field : 'email' } ) ) ;
2019-12-31 16:44:14 -08:00
}
2017-04-26 11:06:33 +02:00
2019-01-04 18:44:54 -08:00
const ip = '127.0.0.1' ;
2017-04-26 08:45:20 +04:00
var credentials = {
2017-04-26 11:06:33 +02:00
token : dnsConfig . token ,
2019-12-31 16:44:14 -08:00
tokenType : dnsConfig . tokenType ,
email : dnsConfig . email || null
2017-04-26 08:45:20 +04: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 ) {
2019-10-23 10:02:04 -07:00
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' } ) ) ;
2017-04-26 08:45:20 +04:00
2019-01-04 18:44:54 -08:00
getZoneByName ( dnsConfig , zoneName , function ( error , zone ) {
2017-04-26 08:45:20 +04:00
if ( error ) return callback ( error ) ;
2017-07-28 16:41:00 +02:00
2019-01-04 18:44:54 -08:00
if ( ! _ . isEqual ( zone . name _servers . sort ( ) , nameservers . sort ( ) ) ) {
debug ( 'verifyDnsConfig: %j and %j do not match' , nameservers , zone . name _servers ) ;
2019-10-23 10:02:04 -07:00
return callback ( new BoxError ( BoxError . BAD _FIELD , 'Domain nameservers are not set to Cloudflare' , { field : 'nameservers' } ) ) ;
2017-04-26 08:45:20 +04:00
}
2017-04-26 11:48:31 +02:00
2019-01-04 18:44:54 -08:00
const location = 'cloudrontestdns' ;
2017-11-07 23:13:58 +01:00
2019-01-04 18:44:54 -08:00
upsert ( domainObject , location , 'A' , [ ip ] , function ( error ) {
2017-04-26 08:45:20 +04:00
if ( error ) return callback ( error ) ;
2019-01-04 18:44:54 -08:00
debug ( 'verifyDnsConfig: Test A record added' ) ;
2017-11-07 23:13:58 +01:00
2019-01-04 18:44:54 -08:00
del ( domainObject , location , 'A' , [ ip ] , function ( error ) {
2017-11-07 23:13:58 +01:00
if ( error ) return callback ( error ) ;
2017-04-26 08:45:20 +04:00
2017-11-07 23:13:58 +01:00
debug ( 'verifyDnsConfig: Test A record removed again' ) ;
callback ( null , credentials ) ;
} ) ;
2017-04-26 08:45:20 +04:00
} ) ;
} ) ;
} ) ;
}