2018-05-09 12:24:33 +02:00
'use strict' ;
exports = module . exports = {
2021-08-13 17:22:28 -07:00
removePrivateFields ,
injectPrivateFields ,
upsert ,
get ,
del ,
wait ,
2022-01-05 22:41:41 -08:00
verifyDomainConfig
2018-05-09 12:24:33 +02: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' ) ,
2020-05-14 23:01:44 +02:00
constants = require ( '../constants.js' ) ,
2018-05-09 12:24:33 +02:00
debug = require ( 'debug' ) ( 'box:dns/namecom' ) ,
2022-02-04 13:58:29 -08:00
dig = require ( '../dig.js' ) ,
2021-08-13 17:22:28 -07:00
dns = require ( '../dns.js' ) ,
2019-01-04 18:44:54 -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' ) ;
2018-05-09 12:24:33 +02:00
const NAMECOM _API = 'https://api.name.com/v4' ;
function formatError ( response ) {
2025-02-14 17:26:54 +01:00
return ` name.com DNS error [ ${ response . status } ] ${ response . text } ` ;
2018-05-09 12:24:33 +02: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
}
2022-02-04 13:58:29 -08:00
async function addRecord ( domainConfig , zoneName , name , type , values ) {
2022-01-05 22:41:41 -08:00
assert . strictEqual ( typeof domainConfig , 'object' ) ;
2018-05-09 12:24:33 +02:00
assert . strictEqual ( typeof zoneName , 'string' ) ;
2019-01-04 18:44:54 -08:00
assert . strictEqual ( typeof name , 'string' ) ;
2018-05-09 12:24:33 +02:00
assert . strictEqual ( typeof type , 'string' ) ;
assert ( Array . isArray ( values ) ) ;
2019-01-04 18:44:54 -08:00
debug ( ` add: ${ name } in zone ${ zoneName } of type ${ type } with values ${ JSON . stringify ( values ) } ` ) ;
2018-05-09 12:24:33 +02:00
2022-02-04 13:58:29 -08:00
const data = {
2019-01-04 18:44:54 -08:00
host : name ,
2018-05-09 12:24:33 +02:00
type : type ,
ttl : 300 // 300 is the lowest
} ;
2018-05-24 09:38:01 -07:00
if ( type === 'MX' ) {
data . priority = parseInt ( values [ 0 ] . split ( ' ' ) [ 0 ] , 10 ) ;
data . answer = values [ 0 ] . split ( ' ' ) [ 1 ] ;
2020-05-14 01:03:08 +02:00
} else if ( type === 'TXT' ) {
// we have to strip the quoting for some odd reason for name.com! If you change that also change updateRecord
2024-11-19 17:08:19 +05:30
const tmp = values [ 0 ] ;
2020-05-14 01:03:08 +02:00
data . answer = tmp . indexOf ( '"' ) === 0 && tmp . lastIndexOf ( '"' ) === tmp . length - 1 ? tmp . slice ( 1 , tmp . length - 1 ) : tmp ;
2018-05-24 09:38:01 -07:00
} else {
data . answer = values [ 0 ] ;
}
2022-02-04 13:58:29 -08:00
const [ error , response ] = await safe ( superagent . post ( ` ${ NAMECOM _API } /domains/ ${ zoneName } /records ` )
2022-01-05 22:41:41 -08:00
. auth ( domainConfig . username , domainConfig . token )
2018-05-09 12:24:33 +02:00
. timeout ( 30 * 1000 )
. send ( data )
2022-02-04 13:58:29 -08:00
. ok ( ( ) => true ) ) ;
2018-05-09 12:24:33 +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 === 403 ) throw new BoxError ( BoxError . ACCESS _DENIED , formatError ( response ) ) ;
if ( response . status !== 200 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , formatError ( response ) ) ;
2018-05-24 09:38:01 -07:00
}
2018-05-09 12:24:33 +02:00
2022-02-04 13:58:29 -08:00
async function updateRecord ( domainConfig , zoneName , recordId , name , type , values ) {
2022-01-05 22:41:41 -08:00
assert . strictEqual ( typeof domainConfig , 'object' ) ;
2018-05-09 12:24:33 +02:00
assert . strictEqual ( typeof zoneName , 'string' ) ;
assert . strictEqual ( typeof recordId , 'number' ) ;
2019-01-04 18:44:54 -08:00
assert . strictEqual ( typeof name , 'string' ) ;
2018-05-09 12:24:33 +02:00
assert . strictEqual ( typeof type , 'string' ) ;
assert ( Array . isArray ( values ) ) ;
2019-01-04 18:44:54 -08:00
debug ( ` update: ${ recordId } on ${ name } in zone ${ zoneName } of type ${ type } with values ${ JSON . stringify ( values ) } ` ) ;
2018-05-09 12:24:33 +02:00
2022-02-04 13:58:29 -08:00
const data = {
2019-01-04 18:44:54 -08:00
host : name ,
2018-05-09 12:24:33 +02:00
type : type ,
ttl : 300 // 300 is the lowest
} ;
2018-05-24 09:38:01 -07:00
if ( type === 'MX' ) {
data . priority = parseInt ( values [ 0 ] . split ( ' ' ) [ 0 ] , 10 ) ;
data . answer = values [ 0 ] . split ( ' ' ) [ 1 ] ;
2020-05-14 01:03:08 +02:00
} else if ( type === 'TXT' ) {
// we have to strip the quoting for some odd reason for name.com! If you change that also change addRecord
2024-11-19 17:08:19 +05:30
const tmp = values [ 0 ] ;
2020-05-14 01:03:08 +02:00
data . answer = tmp . indexOf ( '"' ) === 0 && tmp . lastIndexOf ( '"' ) === tmp . length - 1 ? tmp . slice ( 1 , tmp . length - 1 ) : tmp ;
2018-05-24 09:38:01 -07:00
} else {
data . answer = values [ 0 ] ;
}
2022-02-04 13:58:29 -08:00
const [ error , response ] = await safe ( superagent . put ( ` ${ NAMECOM _API } /domains/ ${ zoneName } /records/ ${ recordId } ` )
2022-01-05 22:41:41 -08:00
. auth ( domainConfig . username , domainConfig . token )
2018-05-09 12:24:33 +02:00
. timeout ( 30 * 1000 )
. send ( data )
2022-02-04 13:58:29 -08:00
. ok ( ( ) => true ) ) ;
2018-05-09 12:24:33 +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 === 403 ) throw new BoxError ( BoxError . ACCESS _DENIED , formatError ( response ) ) ;
if ( response . status !== 200 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , formatError ( response ) ) ;
2018-05-09 12:24:33 +02:00
}
2022-02-04 13:58:29 -08:00
async function getInternal ( domainConfig , zoneName , name , type ) {
2022-01-05 22:41:41 -08:00
assert . strictEqual ( typeof domainConfig , 'object' ) ;
2018-05-09 12:24:33 +02:00
assert . strictEqual ( typeof zoneName , 'string' ) ;
2019-01-04 18:44:54 -08:00
assert . strictEqual ( typeof name , 'string' ) ;
2018-05-09 12:24:33 +02:00
assert . strictEqual ( typeof type , 'string' ) ;
2019-01-04 18:44:54 -08:00
debug ( ` getInternal: ${ name } in zone ${ zoneName } of type ${ type } ` ) ;
2018-05-09 12:24:33 +02:00
2022-02-04 13:58:29 -08:00
const [ error , response ] = await safe ( superagent . get ( ` ${ NAMECOM _API } /domains/ ${ zoneName } /records ` )
2022-01-05 22:41:41 -08:00
. auth ( domainConfig . username , domainConfig . token )
2018-05-09 12:24:33 +02:00
. timeout ( 30 * 1000 )
2022-02-04 13:58:29 -08:00
. ok ( ( ) => true ) ) ;
2018-05-09 12:24:33 +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 === 403 ) throw new BoxError ( BoxError . ACCESS _DENIED , formatError ( response ) ) ;
if ( response . status !== 200 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , formatError ( response ) ) ;
2018-05-09 12:24:33 +02:00
2022-02-04 13:58:29 -08:00
// name.com does not return the correct content-type
response . body = safe . JSON . parse ( response . text ) ;
if ( ! response . body . records ) response . body . records = [ ] ;
2018-05-09 12:24:33 +02:00
2022-02-04 13:58:29 -08:00
response . body . records . forEach ( function ( r ) {
// name.com api simply strips empty properties
r . host = r . host || '' ;
} ) ;
2018-05-09 12:24:33 +02:00
2022-02-04 13:58:29 -08:00
const results = response . body . records . filter ( function ( r ) {
return ( r . host === name && r . type === type ) ;
} ) ;
2018-05-09 12:24:33 +02:00
2022-02-04 13:58:29 -08:00
return results ;
2018-05-09 12:24:33 +02: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' ) ;
2018-05-09 12:24:33 +02:00
assert . strictEqual ( typeof type , 'string' ) ;
2021-05-02 11:26:08 -07:00
assert ( Array . isArray ( values ) ) ;
2018-05-09 12:24:33 +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 ,
2021-08-13 17:22:28 -07:00
name = dns . getName ( domainObject , location , type ) || '' ;
2018-05-09 12:24:33 +02:00
2019-01-04 18:44:54 -08:00
debug ( ` upsert: ${ name } in zone ${ zoneName } of type ${ type } with values ${ JSON . stringify ( values ) } ` ) ;
2018-05-09 12:24:33 +02:00
2022-02-04 13:58:29 -08:00
const result = await getInternal ( domainConfig , zoneName , name , type ) ;
if ( result . length === 0 ) return await addRecord ( domainConfig , zoneName , name , type , values ) ;
2018-05-09 12:24:33 +02:00
2022-02-04 13:58:29 -08:00
return await updateRecord ( domainConfig , zoneName , result [ 0 ] . id , name , type , values ) ;
2018-05-09 12:24:33 +02: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' ) ;
2018-05-09 12:24:33 +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 ,
2021-08-13 17:22:28 -07:00
name = dns . getName ( domainObject , location , type ) || '' ;
2019-01-04 18:44:54 -08:00
2022-02-04 13:58:29 -08:00
const result = await getInternal ( domainConfig , zoneName , name , type ) ;
const tmp = result . map ( function ( record ) { return record . answer ; } ) ;
return tmp ;
2018-05-09 12:24:33 +02: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' ) ;
2018-05-09 12:24:33 +02:00
assert . strictEqual ( typeof type , 'string' ) ;
2021-05-02 11:26:08 -07:00
assert ( Array . isArray ( values ) ) ;
2018-05-09 12:24:33 +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 ,
2021-08-13 17:22:28 -07:00
name = dns . getName ( domainObject , location , type ) || '' ;
2018-05-09 12:24:33 +02:00
2019-01-04 18:44:54 -08:00
debug ( ` del: ${ name } in zone ${ zoneName } of type ${ type } with values ${ JSON . stringify ( values ) } ` ) ;
2018-05-09 12:24:33 +02:00
2022-02-04 13:58:29 -08:00
const result = await getInternal ( domainConfig , zoneName , name , type ) ;
if ( result . length === 0 ) return ;
2018-05-09 12:24:33 +02:00
2022-02-04 13:58:29 -08:00
const [ error , response ] = await safe ( superagent . del ( ` ${ NAMECOM _API } /domains/ ${ zoneName } /records/ ${ result [ 0 ] . id } ` )
. auth ( domainConfig . username , domainConfig . token )
. timeout ( 30 * 1000 )
. ok ( ( ) => true ) ) ;
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 === 403 ) throw new BoxError ( BoxError . ACCESS _DENIED , formatError ( response ) ) ;
if ( response . status !== 200 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , formatError ( response ) ) ;
2018-05-09 12:24:33 +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 }
2018-05-09 12:24:33 +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 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 ;
2022-02-04 13:58:29 -08:00
if ( typeof domainConfig . username !== 'string' ) throw new BoxError ( BoxError . BAD _FIELD , 'username must be a string' ) ;
if ( typeof domainConfig . token !== 'string' ) throw new BoxError ( BoxError . BAD _FIELD , 'token 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-08-25 18:12:55 -07:00
2022-02-04 13:58:29 -08:00
const credentials = {
2022-01-05 22:41:41 -08:00
username : domainConfig . username ,
2025-03-02 07:27:09 +01:00
token : domainConfig . token ,
customNameservers : ! ! domainConfig . customNameservers
2018-05-09 12:24:33 +02: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
2018-05-09 12:24:33 +02: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' ) ;
2018-05-09 12:24:33 +02:00
2022-02-04 13:58:29 -08:00
if ( ! nameservers . every ( function ( n ) { return n . toLowerCase ( ) . indexOf ( '.name.com' ) !== - 1 ; } ) ) {
debug ( 'verifyDomainConfig: %j does not contain Name.com NS' , nameservers ) ;
2025-03-02 07:27:09 +01:00
if ( ! domainConfig . customNameservers ) throw new BoxError ( BoxError . BAD _FIELD , 'Domain nameservers are not set to name.com' ) ;
2022-02-04 13:58:29 -08:00
}
2018-05-09 12:24:33 +02:00
2022-02-04 13:58:29 -08:00
const location = 'cloudrontestdns' ;
2018-05-09 12:24:33 +02:00
2022-02-04 13:58:29 -08:00
await upsert ( domainObject , location , 'A' , [ ip ] ) ;
debug ( 'verifyDomainConfig: Test A record added' ) ;
2018-05-09 12:24:33 +02:00
2022-02-04 13:58:29 -08:00
await del ( domainObject , location , 'A' , [ ip ] ) ;
debug ( 'verifyDomainConfig: Test A record removed again' ) ;
2018-05-09 12:24:33 +02:00
2022-02-04 13:58:29 -08:00
return credentials ;
2018-05-09 12:24:33 +02:00
}