2017-04-26 08:45:20 +04:00
'use strict' ;
2025-10-08 20:11:55 +02:00
exports = module . exports = {
removePrivateFields ,
injectPrivateFields ,
upsert ,
get ,
del ,
wait ,
verifyDomainConfig
} ;
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' ) ,
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' ) ,
2022-02-04 13:58:29 -08:00
dig = require ( '../dig.js' ) ,
2021-08-13 17:22:28 -07:00
dns = require ( '../dns.js' ) ,
2022-02-04 13:58:29 -08:00
safe = require ( 'safetydance' ) ,
2025-07-10 10:55:52 +02:00
superagent = require ( '@cloudron/superagent' ) ,
2019-01-04 18:44:54 -08:00
waitForDns = require ( './waitfordns.js' ) ,
2025-02-13 14:03:25 +01:00
_ = require ( '../underscore.js' ) ;
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
2022-02-04 13:58:29 -08:00
const 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 ) {
2025-10-08 12:04:31 +02:00
delete domainObject . config . token ;
2019-02-08 11:11:49 +01:00
return domainObject ;
}
2019-02-09 19:08:15 +01:00
function injectPrivateFields ( newConfig , currentConfig ) {
2025-10-08 12:04:31 +02:00
if ( ! Object . hasOwn ( newConfig , 'token' ) ) newConfig . token = currentConfig . token ;
2019-02-09 19:08:15 +01:00
}
2025-02-14 17:26:54 +01:00
function translateResponseError ( response ) {
assert . strictEqual ( typeof response , 'object' ) ;
2017-07-31 11:25:17 +02:00
2025-02-14 17:26:54 +01:00
if ( response . status === 404 ) return new BoxError ( BoxError . NOT _FOUND , ` [ ${ response . status } ] ${ response . text } ` ) ;
if ( response . status === 422 ) return new BoxError ( BoxError . BAD _FIELD , response . body . message ) ;
if ( response . status === 400 || response . status === 401 || response . status === 403 ) {
2020-04-23 12:07:45 -07:00
let message = 'Unknown error' ;
2025-02-14 17:26:54 +01:00
if ( typeof response . body . error === 'string' ) {
message = ` [ ${ response . status } ] ${ response . body . error } ` ;
} else if ( Array . isArray ( response . body . errors ) && response . body . errors . length > 0 ) {
const error = response . body . errors [ 0 ] ;
message = ` [ ${ response . status } ] ${ error . message } code: ${ error . code } ` ;
2020-04-23 12:07:45 -07:00
}
2022-02-04 13:58:29 -08:00
return new BoxError ( BoxError . ACCESS _DENIED , message ) ;
2017-07-31 11:25:17 +02:00
}
2025-02-14 17:26:54 +01:00
return new BoxError ( BoxError . EXTERNAL _ERROR , ` ${ response . status } ${ response . text } ` ) ;
2017-07-31 11:25:17 +02:00
}
2022-01-05 22:41:41 -08:00
function createRequest ( method , url , domainConfig ) {
2019-12-31 16:25:49 -08:00
assert . strictEqual ( typeof method , 'string' ) ;
assert . strictEqual ( typeof url , 'string' ) ;
2022-01-05 22:41:41 -08:00
assert . strictEqual ( typeof domainConfig , 'object' ) ;
2019-12-31 16:25:49 -08:00
2025-02-14 17:26:54 +01:00
const request = superagent . request ( method , url ) . timeout ( 30 * 1000 ) . ok ( ( ) => true ) ;
2019-12-31 16:44:14 -08:00
2022-01-05 22:41:41 -08:00
if ( domainConfig . tokenType === 'GlobalApiKey' ) {
request . set ( 'X-Auth-Key' , domainConfig . token ) . set ( 'X-Auth-Email' , domainConfig . email ) ;
2019-12-31 16:44:14 -08:00
} else {
2022-01-05 22:41:41 -08:00
request . set ( 'Authorization' , 'Bearer ' + domainConfig . token ) ;
2019-12-31 16:44:14 -08:00
}
return request ;
2019-12-31 16:25:49 -08:00
}
2022-02-04 13:58:29 -08:00
async function getZoneByName ( domainConfig , zoneName ) {
2022-01-05 22:41:41 -08:00
assert . strictEqual ( typeof domainConfig , 'object' ) ;
2017-04-26 08:45:20 +04:00
assert . strictEqual ( typeof zoneName , 'string' ) ;
2017-04-26 11:48:31 +02:00
2022-02-04 13:58:29 -08:00
const [ error , response ] = await safe ( createRequest ( 'GET' , ` ${ CLOUDFLARE _ENDPOINT } /zones?name= ${ zoneName } &status=active ` , domainConfig ) ) ;
2024-11-19 17:08:19 +05:30
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2025-02-14 17:26:54 +01:00
if ( response . status !== 200 || response . body . success !== true ) throw translateResponseError ( response ) ;
if ( ! response . body . result || ! response . body . result . length ) throw new BoxError ( BoxError . NOT _FOUND , ` ${ response . status } ${ response . text } ` ) ;
2018-01-19 09:55:27 -08:00
2024-01-12 14:52:24 +01:00
// check 'id' and 'name_servers' exist in the response
const zone = response . body . result [ 0 ] ;
const zoneId = safe . query ( zone , 'id' ) ;
2025-02-14 17:26:54 +01:00
if ( typeof zoneId !== 'string' ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` No zone id in response: ${ response . status } ${ response . text } ` ) ;
2024-01-12 14:52:24 +01:00
const name _servers = safe . query ( zone , 'name_servers' ) ;
2025-02-14 17:26:54 +01:00
if ( ! Array . isArray ( name _servers ) ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` name_servers is not an array: ${ response . status } ${ response . text } ` ) ;
2024-01-12 14:52:24 +01:00
return zone ;
2017-04-26 08:45:20 +04:00
}
2019-01-03 10:35:17 -08:00
// gets records filtered by zone, type and fqdn
2022-02-04 13:58:29 -08:00
async function getDnsRecords ( domainConfig , zoneId , fqdn , type ) {
2022-01-05 22:41:41 -08:00
assert . strictEqual ( typeof domainConfig , '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' ) ;
2017-04-26 11:48:31 +02:00
2022-02-04 13:58:29 -08:00
const [ error , response ] = await safe ( createRequest ( 'GET' , ` ${ CLOUDFLARE _ENDPOINT } /zones/ ${ zoneId } /dns_records ` , domainConfig )
. query ( { type : type , name : fqdn } ) ) ;
2017-07-28 16:41:00 +02:00
2024-11-19 17:08:19 +05:30
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2025-02-14 17:26:54 +01:00
if ( response . status !== 200 || response . body . success !== true ) throw translateResponseError ( response ) ;
2017-04-26 08:45:20 +04:00
2024-04-01 17:31:05 +02:00
const result = response . body . result ;
2024-04-01 17:58:40 +02:00
if ( result === null ) return [ ] ; // sometime about now, cloudflare API has started returning null instead of empty array
2025-02-14 17:26:54 +01:00
if ( ! Array . isArray ( result ) ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` result is not an array when getting records: ${ response . status } ${ response . text } ` ) ;
2024-04-01 17:31:05 +02:00
return result ;
2017-04-26 08:45:20 +04:00
}
2022-02-04 13:58:29 -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' ) ;
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
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 ) ;
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
2024-01-12 14:52:24 +01:00
const zone = await getZoneByName ( domainConfig , zoneName ) ;
const zoneId = zone . id ;
2019-01-03 10:41:35 -08:00
2022-02-04 13:58:29 -08:00
const records = await getDnsRecords ( domainConfig , zoneId , fqdn , type ) ;
2018-01-19 09:55:27 -08:00
2022-02-04 13:58:29 -08:00
let i = 0 ; // // used to track available records to update instead of create
2019-01-03 10:41:35 -08:00
2022-02-04 13:58:29 -08:00
for ( let value of values ) {
let priority = null ;
2019-01-03 10:41:35 -08:00
2022-02-04 13:58:29 -08:00
if ( type === 'MX' ) {
priority = parseInt ( value . split ( ' ' ) [ 0 ] , 10 ) ;
value = value . split ( ' ' ) [ 1 ] ;
}
2017-07-28 16:41:00 +02:00
2022-02-04 13:58:29 -08:00
const data = {
type : type ,
name : fqdn ,
content : value ,
priority : priority ,
proxied : false ,
ttl : 120 // 1 means "automatic" (meaning 300ms) and 120 is the lowest supported
} ;
if ( i >= records . length ) { // create a new record
2023-02-11 08:40:52 +01:00
if ( type === 'A' || type === 'AAAA' || type === 'CNAME' ) {
2025-05-06 16:16:33 +02:00
data . proxied = ! ! domainConfig . defaultProxyStatus ; // note that cloudflare will error if proxied is set for wrong record type or IP. only set at install time
2023-02-11 08:40:52 +01:00
}
debug ( ` upsert: Adding new record fqdn: ${ fqdn } , zoneName: ${ zoneName } proxied: ${ data . proxied } ` ) ;
2022-02-04 13:58:29 -08:00
const [ error , response ] = await safe ( createRequest ( 'POST' , ` ${ CLOUDFLARE _ENDPOINT } /zones/ ${ zoneId } /dns_records ` , domainConfig )
. send ( data ) ) ;
2024-11-19 17:08:19 +05:30
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2025-02-14 17:26:54 +01:00
if ( response . status !== 200 || response . body . success !== true ) throw translateResponseError ( response ) ;
2022-02-04 13:58:29 -08:00
} else { // replace existing record
data . proxied = records [ i ] . proxied ; // preserve proxied parameter
debug ( ` upsert: Updating existing record fqdn: ${ fqdn } , zoneName: ${ zoneName } proxied: ${ data . proxied } ` ) ;
const [ error , response ] = await safe ( createRequest ( 'PUT' , ` ${ CLOUDFLARE _ENDPOINT } /zones/ ${ zoneId } /dns_records/ ${ records [ i ] . id } ` , domainConfig )
. send ( data ) ) ;
2024-11-19 17:08:19 +05:30
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2025-02-14 17:26:54 +01:00
if ( response . status !== 200 || response . body . success !== true ) throw translateResponseError ( response ) ;
2022-02-04 13:58:29 -08:00
++ i ; // increment, as we have consumed the record
}
}
2017-07-28 16:41:00 +02:00
2022-02-04 13:58:29 -08:00
for ( let j = values . length + 1 ; j < records . length ; j ++ ) {
2024-11-26 13:11:26 +05:30
2022-02-04 13:58:29 -08:00
const [ error ] = await safe ( createRequest ( 'DELETE' , ` ${ CLOUDFLARE _ENDPOINT } /zones/ ${ zoneId } /dns_records/ ${ records [ j ] . id } ` , domainConfig ) ) ;
if ( error ) debug ( ` upsert: error removing record ${ records [ j ] . id } : ${ error . message } ` ) ;
}
2017-04-26 08:45:20 +04:00
}
2022-02-04 13:58:29 -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' ) ;
2017-04-26 08:45:20 +04: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 ) ;
2019-01-04 18:44:54 -08:00
2022-02-04 13:58:29 -08:00
const zone = await getZoneByName ( domainConfig , zoneName ) ;
const result = await getDnsRecords ( domainConfig , zone . id , fqdn , type ) ;
const tmp = result . map ( function ( record ) { return record . content ; } ) ;
return tmp ;
2017-04-26 08:45:20 +04:00
}
2022-02-04 13:58:29 -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' ) ;
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 09:59:08 +02: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 13:58:29 -08:00
const zone = await getZoneByName ( domainConfig , zoneName ) ;
2017-07-28 16:41:00 +02:00
2022-02-04 13:58:29 -08:00
const result = await getDnsRecords ( domainConfig , zone . id , fqdn , type ) ;
if ( result . length === 0 ) return ;
2017-04-26 08:45:20 +04:00
2022-02-04 13:58:29 -08:00
const 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
2022-02-04 13:58:29 -08:00
if ( tmp . length === 0 ) return ;
2017-04-26 08:45:20 +04:00
2022-02-04 13:58:29 -08:00
for ( const r of tmp ) {
2025-01-31 23:05:16 +01:00
const [ error , response ] = await safe ( createRequest ( 'DELETE' , ` ${ CLOUDFLARE _ENDPOINT } /zones/ ${ zone . id } /dns_records/ ${ r . id } ` , domainConfig ) ) ;
2024-11-19 17:08:19 +05:30
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2025-02-14 17:26:54 +01:00
if ( response . status !== 200 || response . body . success !== true ) throw translateResponseError ( response ) ;
2022-02-04 13:58:29 -08:00
}
2017-04-26 08:45:20 +04: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-04-26 08:45:20 +04:00
2022-01-05 22:41:41 -08:00
const domainConfig = domainObject . config ,
2019-01-05 18:24:21 -08:00
zoneName = domainObject . zoneName ,
2022-11-28 21:23:06 +01:00
fqdn = dns . fqdn ( subdomain , domainObject . domain ) ;
2019-01-05 18:24:21 -08:00
debug ( 'wait: %s for zone %s of type %s' , fqdn , zoneName , type ) ;
2024-01-12 14:52:24 +01:00
const zone = await getZoneByName ( domainConfig , zoneName ) ;
const zoneId = zone . id ;
2019-01-05 18:24:21 -08:00
2022-02-03 16:15:14 -08:00
const dnsRecords = await getDnsRecords ( domainConfig , zoneId , fqdn , type ) ;
if ( dnsRecords . length === 0 ) throw new BoxError ( BoxError . NOT _FOUND , 'Domain not found' ) ;
2019-01-05 18:24:21 -08:00
2022-02-03 16:15:14 -08:00
if ( ! dnsRecords [ 0 ] . proxied ) return await waitForDns ( fqdn , domainObject . zoneName , type , value , options ) ;
2019-01-05 18:24:21 -08:00
2022-02-03 16:15:14 -08:00
debug ( 'wait: skipping wait of proxied domain' ) ;
2019-01-05 18:24:21 -08:00
2022-02-03 16:15:14 -08:00
// 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
}
2022-02-04 13:58:29 -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 ;
2019-12-31 16:44:14 -08:00
// token can be api token or global api key
2022-02-04 13:58:29 -08:00
if ( ! domainConfig . token || typeof domainConfig . token !== 'string' ) throw new BoxError ( BoxError . BAD _FIELD , 'token must be a non-empty string' ) ;
if ( domainConfig . tokenType !== 'GlobalApiKey' && domainConfig . tokenType !== 'ApiToken' ) throw new BoxError ( BoxError . BAD _FIELD , 'tokenType is required' ) ;
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' ) ;
2019-12-31 16:44:14 -08:00
2022-01-05 22:41:41 -08:00
if ( domainConfig . tokenType === 'GlobalApiKey' ) {
2022-02-04 13:58:29 -08:00
if ( typeof domainConfig . email !== 'string' ) throw new BoxError ( BoxError . BAD _FIELD , 'email must be a non-empty string' ) ;
2019-12-31 16:44:14 -08:00
}
2017-04-26 11:06:33 +02:00
2023-03-14 11:35:10 +01:00
if ( typeof domainConfig . defaultProxyStatus !== 'boolean' ) throw new BoxError ( BoxError . BAD _FIELD , 'defaultProxyStatus must be a boolean' ) ;
2023-02-11 08:40:52 +01:00
2025-10-08 14:50:00 +02:00
const ip = ! domainConfig . defaultProxyStatus ? '127.0.0.1' : '103.31.4.0' ; // when proxying 127.0.0.1 is not valid. use an address owned by cloudflare itself
2019-01-04 18:44:54 -08:00
2023-02-11 08:40:52 +01:00
const sanitizedConfig = {
2022-01-05 22:41:41 -08:00
token : domainConfig . token ,
tokenType : domainConfig . tokenType ,
2023-02-11 08:40:52 +01:00
email : domainConfig . email || null ,
2025-03-02 07:27:09 +01:00
defaultProxyStatus : domainConfig . defaultProxyStatus ,
customNameservers : ! ! domainConfig . customNameservers
2017-04-26 08:45:20 +04:00
} ;
2023-10-01 13:52:19 +05:30
if ( constants . TEST ) return sanitizedConfig ; // this shouldn't be here
2017-04-26 08:45:20 +04:00
2022-02-04 13:58:29 -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-07-28 16:41:00 +02:00
2022-02-04 13:58:29 -08:00
const zone = await getZoneByName ( domainConfig , zoneName ) ;
2017-04-26 11:48:31 +02:00
2022-02-04 13:58:29 -08:00
if ( ! _ . isEqual ( zone . name _servers . sort ( ) , nameservers . sort ( ) ) ) {
debug ( 'verifyDomainConfig: %j and %j do not match' , nameservers , zone . name _servers ) ;
2025-03-02 07:27:09 +01:00
if ( ! domainConfig . customNameservers ) throw new BoxError ( BoxError . BAD _FIELD , 'Domain nameservers are not set to Cloudflare' ) ;
2022-02-04 13:58:29 -08:00
}
2017-04-26 08:45:20 +04:00
2022-02-04 13:58:29 -08:00
const location = 'cloudrontestdns' ;
2017-11-07 23:13:58 +01:00
2022-02-04 13:58:29 -08:00
await upsert ( domainObject , location , 'A' , [ ip ] ) ;
debug ( 'verifyDomainConfig: Test A record added' ) ;
2017-04-26 08:45:20 +04:00
2022-02-04 13:58:29 -08:00
await del ( domainObject , location , 'A' , [ ip ] ) ;
debug ( 'verifyDomainConfig: Test A record removed again' ) ;
2017-11-07 23:13:58 +01:00
2023-02-11 08:40:52 +01:00
return sanitizedConfig ;
2017-04-26 08:45:20 +04:00
}
2025-10-08 12:04:31 +02:00