2017-09-05 22:23:24 +02: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-09-12 16:29:07 +02:00
debug = require ( 'debug' ) ( 'box:dns/gcdns' ) ,
2022-02-04 15:46:17 -08:00
dig = require ( '../dig.js' ) ,
2021-08-13 17:22:28 -07:00
dns = require ( '../dns.js' ) ,
2019-05-11 19:17:58 -07:00
GCDNS = require ( '@google-cloud/dns' ) . DNS ,
2025-10-08 12:04:31 +02: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' ) ;
2017-09-05 22:23:24 +02:00
2019-02-08 11:11:49 +01:00
function removePrivateFields ( domainObject ) {
2025-10-08 12:04:31 +02:00
delete domainObject . config . credentials . private _key ;
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 . credentials , 'private_key' ) && currentConfig . credentials ) newConfig . credentials . private _key = currentConfig . credentials . private _key ;
2019-02-09 19:08:15 +01:00
}
2022-01-05 22:41:41 -08:00
function getDnsCredentials ( domainConfig ) {
assert . strictEqual ( typeof domainConfig , 'object' ) ;
2017-09-05 22:23:24 +02:00
2018-06-17 21:44:08 -07:00
return {
2022-01-05 22:41:41 -08:00
projectId : domainConfig . projectId ,
2018-06-17 21:44:08 -07:00
credentials : {
2022-01-05 22:41:41 -08:00
client _email : domainConfig . credentials . client _email ,
private _key : domainConfig . credentials . private _key
2025-03-02 07:27:09 +01:00
} ,
customNameservers : ! ! domainConfig . customNameservers
2018-06-17 21:44:08 -07:00
} ;
2017-09-05 22:23:24 +02:00
}
2022-02-04 15:46:17 -08:00
async function getZoneByName ( domainConfig , zoneName ) {
2022-01-05 22:41:41 -08:00
assert . strictEqual ( typeof domainConfig , 'object' ) ;
2017-09-05 22:23:24 +02:00
assert . strictEqual ( typeof zoneName , 'string' ) ;
2022-02-04 15:46:17 -08:00
const gcdns = new GCDNS ( getDnsCredentials ( domainConfig ) ) ;
2017-09-05 22:23:24 +02:00
2022-02-18 19:42:24 +01:00
const [ error , result ] = await safe ( gcdns . getZones ( ) ) ;
2022-02-04 15:46:17 -08:00
if ( error && error . message === 'invalid_grant' ) throw new BoxError ( BoxError . ACCESS _DENIED , 'The key was probably revoked' ) ;
if ( error && error . reason === 'No such domain' ) throw new BoxError ( BoxError . NOT _FOUND , error . message ) ;
if ( error && error . code === 403 ) throw new BoxError ( BoxError . ACCESS _DENIED , error . message ) ;
if ( error && error . code === 404 ) throw new BoxError ( BoxError . NOT _FOUND , error . message ) ;
2023-04-16 10:49:59 +02:00
if ( error ) throw new BoxError ( BoxError . EXTERNAL _ERROR , error ) ;
2017-09-05 22:23:24 +02:00
2022-02-18 19:42:24 +01:00
const zone = result [ 0 ] . filter ( function ( zone ) {
2022-02-04 15:46:17 -08:00
return zone . metadata . dnsName . slice ( 0 , - 1 ) === zoneName ; // the zone name contains a '.' at the end
} ) [ 0 ] ;
2017-09-05 22:23:24 +02:00
2022-02-04 15:46:17 -08:00
if ( ! zone ) throw new BoxError ( BoxError . NOT _FOUND , 'no such zone' ) ;
2017-09-05 22:23:24 +02:00
2022-02-04 15:46:17 -08:00
return zone ; //zone.metadata ~= {name="", dnsName="", nameServers:[]}
2017-09-05 22:23:24 +02:00
}
2022-02-04 15:46:17 -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-09-05 22:23:24 +02:00
assert . strictEqual ( typeof type , 'string' ) ;
2021-05-02 11:26:08 -07:00
assert ( Array . isArray ( values ) ) ;
2017-09-05 22:23:24 +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
debug ( 'add: %s for zone %s of type %s with values %j' , fqdn , zoneName , type , values ) ;
2017-09-05 22:23:24 +02:00
2022-02-04 15:46:17 -08:00
const zone = await getZoneByName ( getDnsCredentials ( domainConfig ) , zoneName ) ;
2022-02-18 19:42:24 +01:00
const [ error , result ] = await safe ( zone . getRecords ( { type : type , name : fqdn + '.' } ) ) ;
2022-02-04 15:46:17 -08:00
if ( error && error . code === 403 ) throw new BoxError ( BoxError . ACCESS _DENIED , error . message ) ;
if ( error ) throw new BoxError ( BoxError . EXTERNAL _ERROR , error . message ) ;
const newRecord = zone . record ( type , {
name : fqdn + '.' ,
data : values ,
ttl : 1
2017-09-05 22:23:24 +02:00
} ) ;
2022-02-04 15:46:17 -08:00
2022-02-18 19:42:24 +01:00
const [ changeError ] = await safe ( zone . createChange ( { delete : result [ 0 ] , add : newRecord } ) ) ;
2022-02-04 15:46:17 -08:00
if ( changeError && changeError . code === 403 ) throw new BoxError ( BoxError . ACCESS _DENIED , changeError . message ) ;
if ( changeError && changeError . code === 412 ) throw new BoxError ( BoxError . BUSY , changeError . message ) ;
if ( changeError ) throw new BoxError ( BoxError . EXTERNAL _ERROR , changeError . message ) ;
2017-09-05 22:23:24 +02:00
}
2022-02-04 15:46:17 -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-09-05 22:23:24 +02: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 15:46:17 -08:00
const zone = await getZoneByName ( getDnsCredentials ( domainConfig ) , zoneName ) ;
2017-09-05 22:23:24 +02:00
2022-02-04 15:46:17 -08:00
const params = {
name : fqdn + '.' ,
type : type
} ;
2017-09-05 22:23:24 +02:00
2022-02-18 19:42:24 +01:00
const [ error , result ] = await safe ( zone . getRecords ( params ) ) ;
2022-02-04 15:46:17 -08:00
if ( error && error . code === 403 ) throw new BoxError ( BoxError . ACCESS _DENIED , error . message ) ;
if ( error ) throw new BoxError ( BoxError . EXTERNAL _ERROR , error . message ) ;
2022-02-18 19:42:24 +01:00
if ( result [ 0 ] . length === 0 ) return [ ] ;
2017-09-18 23:45:21 +02:00
2022-02-18 19:42:24 +01:00
return result [ 0 ] [ 0 ] . data ;
2017-09-05 22:23:24 +02:00
}
2022-02-04 15:46:17 -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-09-05 22:23:24 +02:00
assert . strictEqual ( typeof type , 'string' ) ;
2021-05-02 11:26:08 -07:00
assert ( Array . isArray ( values ) ) ;
2017-09-05 22:23:24 +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 15:46:17 -08:00
const zone = await getZoneByName ( getDnsCredentials ( domainConfig ) , zoneName ) ;
2022-02-18 19:42:24 +01:00
const [ error , result ] = await safe ( zone . getRecords ( { type : type , name : fqdn + '.' } ) ) ;
2022-02-04 15:46:17 -08:00
if ( error && error . code === 403 ) throw new BoxError ( BoxError . ACCESS _DENIED , error . message ) ;
if ( error ) throw new BoxError ( BoxError . EXTERNAL _ERROR , error . message ) ;
2022-02-18 19:42:24 +01:00
const [ delError ] = await safe ( zone . deleteRecords ( result [ 0 ] ) ) ;
2022-02-04 15:46:17 -08:00
if ( delError && delError . code === 403 ) throw new BoxError ( BoxError . ACCESS _DENIED , delError . message ) ;
if ( delError && delError . code === 412 ) throw new BoxError ( BoxError . BUSY , delError . message ) ;
if ( delError ) throw new BoxError ( BoxError . EXTERNAL _ERROR , delError . message ) ;
2017-09-05 22:23:24 +02: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-09-05 22:23:24 +02: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:46:17 -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:46:17 -08:00
if ( typeof domainConfig . projectId !== 'string' ) throw new BoxError ( BoxError . BAD _FIELD , 'projectId must be a string' ) ;
if ( ! domainConfig . credentials || typeof domainConfig . credentials !== 'object' ) throw new BoxError ( BoxError . BAD _FIELD , 'credentials must be an object' ) ;
if ( typeof domainConfig . credentials . client _email !== 'string' ) throw new BoxError ( BoxError . BAD _FIELD , 'credentials.client_email must be a string' ) ;
if ( typeof domainConfig . credentials . private _key !== 'string' ) throw new BoxError ( BoxError . BAD _FIELD , 'credentials.private_key must be a 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:46:17 -08:00
const credentials = getDnsCredentials ( domainConfig ) ;
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-09-05 22:23:24 +02:00
2022-02-04 15:46:17 -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-09-05 22:23:24 +02:00
2022-02-04 15:46:17 -08:00
const zone = await getZoneByName ( credentials , zoneName ) ;
2017-09-05 22:23:24 +02:00
2025-02-13 14:03:25 +01:00
const definedNS = zone . metadata . nameServers . map ( function ( r ) { return r . replace ( /\.$/ , '' ) ; } ) ;
if ( ! _ . isEqual ( definedNS . sort ( ) , nameservers . sort ( ) ) ) {
2022-02-04 15:46:17 -08:00
debug ( 'verifyDomainConfig: %j and %j do not match' , nameservers , definedNS ) ;
2025-03-02 07:27:09 +01:00
if ( ! domainConfig . customNameservers ) throw new BoxError ( BoxError . BAD _FIELD , 'Domain nameservers are not set to Google Cloud DNS' ) ;
2022-02-04 15:46:17 -08:00
}
2017-09-05 22:23:24 +02:00
2022-02-04 15:46:17 -08:00
const location = 'cloudrontestdns' ;
await upsert ( domainObject , location , 'A' , [ ip ] ) ;
debug ( 'verifyDomainConfig: Test A record added' ) ;
2017-09-05 22:23:24 +02:00
2022-02-04 15:46:17 -08:00
await del ( domainObject , location , 'A' , [ ip ] ) ;
debug ( 'verifyDomainConfig: Test A record removed again' ) ;
2017-09-05 22:23:24 +02:00
2022-02-04 15:46:17 -08:00
return credentials ;
2017-09-05 22:23:24 +02:00
}
2025-10-08 12:04:31 +02:00