2024-04-24 20:32:54 +02:00
'use strict' ;
2025-08-14 11:17:38 +05:30
const assert = require ( 'node:assert' ) ,
2024-04-24 20:32:54 +02:00
constants = require ( '../constants.js' ) ,
BoxError = require ( '../boxerror.js' ) ,
debug = require ( 'debug' ) ( 'box:dns/vultr' ) ,
dig = require ( '../dig.js' ) ,
dns = require ( '../dns.js' ) ,
safe = require ( 'safetydance' ) ,
timers = require ( 'timers/promises' ) ,
2025-07-10 10:55:52 +02:00
superagent = require ( '@cloudron/superagent' ) ,
2024-04-24 20:32:54 +02:00
waitForDns = require ( './waitfordns.js' ) ;
const DESEC _ENDPOINT = 'https://desec.io/api/v1' ;
function formatError ( response ) {
2025-02-14 17:26:54 +01:00
return ` deSEC DNS error [ ${ response . status } ] ${ response . text } ` ;
2024-04-24 20:32:54 +02:00
}
function removePrivateFields ( domainObject ) {
2025-10-08 12:04:31 +02:00
delete domainObject . config . token ;
2024-04-24 20:32:54 +02:00
return domainObject ;
}
function injectPrivateFields ( newConfig , currentConfig ) {
2025-10-08 12:04:31 +02:00
if ( ! Object . hasOwn ( newConfig , 'token' ) ) newConfig . token = currentConfig . token ;
2024-04-24 20:32:54 +02:00
}
async function get ( domainObject , location , type ) {
assert . strictEqual ( typeof domainObject , 'object' ) ;
assert . strictEqual ( typeof location , 'string' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
const domainConfig = domainObject . config ,
zoneName = domainObject . zoneName ,
name = dns . getName ( domainObject , location , type ) || '@' ;
await timers . setTimeout ( 1000 ) ; // https://desec.readthedocs.io/en/latest/rate-limits.html
const [ error , response ] = await safe ( superagent . get ( ` ${ DESEC _ENDPOINT } /domains/ ${ zoneName } /rrsets/ ${ name } / ${ type } ` )
. set ( 'Authorization' , ` Token ${ domainConfig . token } ` )
. timeout ( 30 * 1000 )
. retry ( 5 )
. 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 === 404 ) return [ ] ;
if ( response . status === 403 || response . status === 401 ) throw new BoxError ( BoxError . ACCESS _DENIED , formatError ( response ) ) ;
if ( response . status !== 200 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , formatError ( response ) ) ;
2024-04-24 20:32:54 +02:00
if ( ! Array . isArray ( response . body . records ) ) throw new BoxError ( BoxError . EXTERNAL _ERROR , formatError ( response ) ) ;
return response . body . records ;
}
2025-10-08 12:04:31 +02:00
async function del ( domainObject , location , type , values ) {
2024-04-24 20:32:54 +02:00
assert . strictEqual ( typeof domainObject , 'object' ) ;
assert . strictEqual ( typeof location , 'string' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
assert ( Array . isArray ( values ) ) ;
const domainConfig = domainObject . config ,
zoneName = domainObject . zoneName ,
2025-10-08 12:04:31 +02:00
name = dns . getName ( domainObject , location , type ) || '@' ;
2024-04-24 20:32:54 +02:00
await timers . setTimeout ( 1000 ) ; // https://desec.readthedocs.io/en/latest/rate-limits.html
2025-10-08 12:04:31 +02:00
const [ error , response ] = await safe ( superagent . del ( ` ${ DESEC _ENDPOINT } /domains/ ${ zoneName } /rrsets/ ${ name } / ${ type } / ` )
2024-04-24 20:32:54 +02:00
. set ( 'Authorization' , ` Token ${ domainConfig . token } ` )
. timeout ( 30 * 1000 )
. retry ( 5 )
. ok ( ( ) => true ) ) ;
2024-11-19 17:08:19 +05:30
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2025-10-08 12:04:31 +02:00
if ( response . status === 404 ) return ;
2025-02-14 17:26:54 +01:00
if ( response . status === 403 || response . status === 401 ) throw new BoxError ( BoxError . ACCESS _DENIED , formatError ( response ) ) ;
2025-10-08 12:04:31 +02:00
if ( response . status !== 204 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , formatError ( response ) ) ;
2024-04-24 20:32:54 +02:00
}
2025-10-08 12:04:31 +02:00
async function upsert ( domainObject , location , type , values ) {
2024-04-24 20:32:54 +02:00
assert . strictEqual ( typeof domainObject , 'object' ) ;
assert . strictEqual ( typeof location , 'string' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
assert ( Array . isArray ( values ) ) ;
const domainConfig = domainObject . config ,
zoneName = domainObject . zoneName ,
2025-10-08 12:04:31 +02:00
name = dns . getName ( domainObject , location , type ) || '' ; // when upsert, we use '' and not '@'
await del ( domainObject , location , type , values ) ;
const data = {
subname : name ,
type ,
ttl : 3600 , // min
records : values
} ;
2024-04-24 20:32:54 +02:00
await timers . setTimeout ( 1000 ) ; // https://desec.readthedocs.io/en/latest/rate-limits.html
2025-10-08 12:04:31 +02:00
const [ error , response ] = await safe ( superagent . post ( ` ${ DESEC _ENDPOINT } /domains/ ${ zoneName } /rrsets/ ` )
2024-04-24 20:32:54 +02:00
. set ( 'Authorization' , ` Token ${ domainConfig . token } ` )
2025-10-08 12:04:31 +02:00
. send ( data )
2024-04-24 20:32:54 +02:00
. timeout ( 30 * 1000 )
. retry ( 5 )
. 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 || response . status === 401 ) throw new BoxError ( BoxError . ACCESS _DENIED , formatError ( response ) ) ;
2025-10-08 12:04:31 +02:00
if ( response . status !== 201 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , formatError ( response ) ) ;
2024-04-24 20:32:54 +02:00
}
async function wait ( domainObject , subdomain , type , value , options ) {
assert . strictEqual ( typeof domainObject , 'object' ) ;
assert . strictEqual ( typeof subdomain , 'string' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
assert . strictEqual ( typeof value , 'string' ) ;
assert ( options && typeof options === 'object' ) ; // { interval: 5000, times: 50000 }
const fqdn = dns . fqdn ( subdomain , domainObject . domain ) ;
await waitForDns ( fqdn , domainObject . zoneName , type , value , options ) ;
}
async function verifyDomainConfig ( domainObject ) {
assert . strictEqual ( typeof domainObject , 'object' ) ;
const domainConfig = domainObject . config ,
zoneName = domainObject . zoneName ;
if ( ! domainConfig . token || typeof domainConfig . token !== 'string' ) throw new BoxError ( BoxError . BAD _FIELD , 'token must be a non-empty 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' ) ;
2024-04-24 20:32:54 +02:00
const ip = '127.0.0.1' ;
const credentials = {
2025-03-02 07:27:09 +01:00
token : domainConfig . token ,
customNameservers : ! ! domainConfig . customNameservers
2024-04-24 20:32:54 +02:00
} ;
if ( constants . TEST ) credentials ; // this shouldn't be here
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' ) ;
if ( ! nameservers . every ( function ( n ) { return n . toLowerCase ( ) . indexOf ( '.desec.' ) !== - 1 ; } ) ) {
debug ( 'verifyDomainConfig: %j does not contains deSEC 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 deSEC' ) ;
2024-04-24 20:32:54 +02:00
}
const location = 'cloudrontestdns' ;
await upsert ( domainObject , location , 'A' , [ ip ] ) ;
debug ( 'verifyDomainConfig: Test A record added' ) ;
await del ( domainObject , location , 'A' , [ ip ] ) ;
debug ( 'verifyDomainConfig: Test A record removed again' ) ;
return credentials ;
}
2025-10-08 12:04:31 +02:00
exports = module . exports = {
removePrivateFields ,
injectPrivateFields ,
upsert ,
get ,
del ,
wait ,
verifyDomainConfig
} ;