2026-02-14 09:53:14 +01:00
import assert from 'node:assert' ;
import constants from '../constants.js' ;
import BoxError from '../boxerror.js' ;
import debugModule from 'debug' ;
2026-02-14 15:43:24 +01:00
import dig from '../dig.js' ;
import dns from '../dns.js' ;
2026-02-14 09:53:14 +01:00
import safe from 'safetydance' ;
import superagent from '@cloudron/superagent' ;
import waitForDns from './waitfordns.js' ;
2020-03-12 17:13:13 -07:00
2026-02-14 09:53:14 +01:00
const debug = debugModule ( 'box:dns/linode' ) ;
2025-10-08 20:11:55 +02:00
2020-03-12 17:13:13 -07:00
const LINODE _ENDPOINT = 'https://api.linode.com/v4' ;
function formatError ( response ) {
2025-02-14 17:26:54 +01:00
return ` Linode DNS error [ ${ response . status } ] ${ response . text } ` ;
2020-03-12 17:13:13 -07:00
}
function removePrivateFields ( domainObject ) {
2025-10-08 12:04:31 +02:00
delete domainObject . config . token ;
2020-03-12 17:13:13 -07:00
return domainObject ;
}
function injectPrivateFields ( newConfig , currentConfig ) {
2025-10-08 12:04:31 +02:00
if ( ! Object . hasOwn ( newConfig , 'token' ) ) newConfig . token = currentConfig . token ;
2020-03-12 17:13:13 -07:00
}
2022-02-04 16:01:18 -08:00
async function getZoneId ( domainConfig , zoneName ) {
2022-01-05 22:41:41 -08:00
assert . strictEqual ( typeof domainConfig , 'object' ) ;
2020-03-12 17:13:13 -07:00
assert . strictEqual ( typeof zoneName , 'string' ) ;
// returns 100 at a time
2022-02-04 16:01:18 -08:00
const [ error , response ] = await safe ( superagent . get ( ` ${ LINODE _ENDPOINT } /domains ` )
2022-01-05 22:41:41 -08:00
. set ( 'Authorization' , 'Bearer ' + domainConfig . token )
2020-03-12 17:13:13 -07:00
. timeout ( 30 * 1000 )
. retry ( 5 )
2022-02-04 16:01:18 -08:00
. ok ( ( ) => true ) ) ;
2020-03-12 17:13:13 -07: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 || response . status === 401 ) throw new BoxError ( BoxError . ACCESS _DENIED , formatError ( response ) ) ;
if ( response . status !== 200 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , formatError ( response ) ) ;
2020-03-12 17:13:13 -07:00
2022-02-04 16:01:18 -08:00
if ( ! Array . isArray ( response . body . data ) ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'Invalid response' ) ;
2020-03-12 17:13:13 -07:00
2022-02-04 16:01:18 -08:00
const zone = response . body . data . find ( d => d . domain === zoneName ) ;
2020-03-12 17:13:13 -07:00
2022-02-04 16:01:18 -08:00
if ( ! zone || ! zone . id ) throw new BoxError ( BoxError . NOT _FOUND , 'Zone not found' ) ;
2020-03-12 17:13:13 -07:00
2022-02-04 16:01:18 -08:00
debug ( ` getZoneId: zone id of ${ zoneName } is ${ zone . id } ` ) ;
return zone . id ;
2020-03-12 17:13:13 -07:00
}
2022-02-04 16:01:18 -08:00
async function getZoneRecords ( domainConfig , zoneName , name , type ) {
2022-01-05 22:41:41 -08:00
assert . strictEqual ( typeof domainConfig , 'object' ) ;
2020-03-12 17:13:13 -07:00
assert . strictEqual ( typeof zoneName , 'string' ) ;
assert . strictEqual ( typeof name , 'string' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
debug ( ` getInternal: getting dns records of ${ zoneName } with ${ name } and type ${ type } ` ) ;
2022-02-04 16:01:18 -08:00
const zoneId = await getZoneId ( domainConfig , zoneName ) ;
2020-03-12 17:13:13 -07:00
2022-02-04 16:01:18 -08:00
let page = 0 , more = false ;
let records = [ ] ;
2020-03-12 17:13:13 -07:00
2022-02-04 16:01:18 -08:00
do {
const url = ` ${ LINODE _ENDPOINT } /domains/ ${ zoneId } /records?page= ${ ++ page } ` ;
2020-03-12 17:13:13 -07:00
2022-02-04 16:01:18 -08:00
const [ error , response ] = await safe ( superagent . get ( url )
. set ( 'Authorization' , 'Bearer ' + domainConfig . token )
. timeout ( 30 * 1000 )
. retry ( 5 )
. ok ( ( ) => true ) ) ;
2020-03-12 17:13:13 -07: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 === 404 ) throw new BoxError ( BoxError . NOT _FOUND , formatError ( response ) ) ;
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 ) ) ;
2020-03-12 17:13:13 -07:00
2022-02-04 16:01:18 -08:00
records = records . concat ( response . body . data . filter ( function ( record ) {
return ( record . type === type && record . name === name ) ;
} ) ) ;
2020-03-12 17:13:13 -07:00
2022-02-04 16:01:18 -08:00
more = response . body . page !== response . body . pages ;
} while ( more ) ;
2020-03-12 17:13:13 -07:00
2022-02-04 16:01:18 -08:00
return { zoneId , records } ;
2020-03-12 17:13:13 -07:00
}
2022-02-04 16:01:18 -08:00
async function get ( domainObject , location , type ) {
2020-03-12 17:13:13 -07:00
assert . strictEqual ( typeof domainObject , 'object' ) ;
assert . strictEqual ( typeof location , 'string' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
2022-01-05 22:41:41 -08:00
const domainConfig = domainObject . config ,
2020-03-12 17:13:13 -07:00
zoneName = domainObject . zoneName ,
2021-08-13 17:22:28 -07:00
name = dns . getName ( domainObject , location , type ) || '' ;
2020-03-12 17:13:13 -07:00
2022-02-04 16:01:18 -08:00
const { records } = await getZoneRecords ( domainConfig , zoneName , name , type ) ;
const tmp = records . map ( function ( record ) { return record . target ; } ) ;
return tmp ;
2020-03-12 17:13:13 -07:00
}
2022-02-04 16:01:18 -08:00
async function upsert ( domainObject , location , type , values ) {
2020-03-12 17:13:13 -07:00
assert . strictEqual ( typeof domainObject , 'object' ) ;
assert . strictEqual ( typeof location , 'string' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
2021-05-02 11:26:08 -07:00
assert ( Array . isArray ( values ) ) ;
2020-03-12 17:13:13 -07:00
2022-01-05 22:41:41 -08:00
const domainConfig = domainObject . config ,
2020-03-12 17:13:13 -07:00
zoneName = domainObject . zoneName ,
2021-08-13 17:22:28 -07:00
name = dns . getName ( domainObject , location , type ) || '' ;
2020-03-12 17:13:13 -07:00
debug ( 'upsert: %s for zone %s of type %s with values %j' , name , zoneName , type , values ) ;
2022-02-04 16:01:18 -08:00
const { zoneId , records } = await getZoneRecords ( domainConfig , zoneName , name , type ) ;
2025-01-31 09:41:10 +01:00
let i = 0 ; // used to track available records to update instead of create
const recordIds = [ ] ;
2022-02-04 16:01:18 -08:00
for ( const value of values ) {
const data = {
type : type ,
ttl _sec : 300 // lowest
} ;
if ( type === 'MX' ) {
data . priority = parseInt ( value . split ( ' ' ) [ 0 ] , 10 ) ;
data . target = value . split ( ' ' ) [ 1 ] ;
} else if ( type === 'TXT' ) {
data . target = value . replace ( /^"(.*)"$/ , '$1' ) ; // strip any double quotes
} else {
data . target = value ;
}
if ( i >= records . length ) {
data . name = name ; // only set for new records
const [ error , response ] = await safe ( superagent . post ( ` ${ LINODE _ENDPOINT } /domains/ ${ zoneId } /records ` )
. set ( 'Authorization' , 'Bearer ' + domainConfig . token )
. send ( data )
. 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 === 400 ) throw new BoxError ( BoxError . BAD _FIELD , formatError ( response ) ) ;
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 ) ) ;
2022-02-04 16:01:18 -08:00
recordIds . push ( response . body . id ) ;
} else {
const [ error , response ] = await safe ( superagent . put ( ` ${ LINODE _ENDPOINT } /domains/ ${ zoneId } /records/ ${ records [ i ] . id } ` )
. set ( 'Authorization' , 'Bearer ' + domainConfig . token )
. send ( data )
. 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 === 400 ) throw new BoxError ( BoxError . BAD _FIELD , formatError ( response ) ) ;
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 ) ) ;
2022-02-04 16:01:18 -08:00
++ i ;
recordIds . push ( response . body . id ) ;
}
}
for ( let j = values . length + 1 ; j < records . length ; j ++ ) {
const [ error ] = await safe ( superagent . del ( ` ${ LINODE _ENDPOINT } /domains/ ${ zoneId } /records/ ${ records [ j ] . id } ` )
. set ( 'Authorization' , 'Bearer ' + domainConfig . token )
. timeout ( 30 * 1000 )
. retry ( 5 )
. ok ( ( ) => true ) ) ;
if ( error ) debug ( ` upsert: error removing record ${ records [ j ] . id } : ${ error . message } ` ) ;
}
2020-03-12 17:13:13 -07:00
}
2022-02-04 16:01:18 -08:00
async function del ( domainObject , location , type , values ) {
2020-03-12 17:13:13 -07:00
assert . strictEqual ( typeof domainObject , 'object' ) ;
assert . strictEqual ( typeof location , 'string' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
2021-05-02 11:26:08 -07:00
assert ( Array . isArray ( values ) ) ;
2020-03-12 17:13:13 -07:00
2022-01-05 22:41:41 -08:00
const domainConfig = domainObject . config ,
2020-03-12 17:13:13 -07:00
zoneName = domainObject . zoneName ,
2021-08-13 17:22:28 -07:00
name = dns . getName ( domainObject , location , type ) || '' ;
2020-03-12 17:13:13 -07:00
2022-02-04 16:01:18 -08:00
const { zoneId , records } = await getZoneRecords ( domainConfig , zoneName , name , type ) ;
if ( records . length === 0 ) return ;
2020-03-12 17:13:13 -07:00
2022-02-04 16:01:18 -08:00
const tmp = records . filter ( function ( record ) { return values . some ( function ( value ) { return value === record . target ; } ) ; } ) ;
if ( tmp . length === 0 ) return ;
2020-03-12 17:13:13 -07:00
2022-02-04 16:01:18 -08:00
for ( const r of tmp ) {
const [ error , response ] = await safe ( superagent . del ( ` ${ LINODE _ENDPOINT } /domains/ ${ zoneId } /records/ ${ r . id } ` )
2022-01-05 22:41:41 -08:00
. set ( 'Authorization' , 'Bearer ' + domainConfig . token )
2020-03-12 17:13:13 -07:00
. timeout ( 30 * 1000 )
. retry ( 5 )
2022-02-04 16:01:18 -08:00
. ok ( ( ) => true ) ) ;
2024-11-19 17:08:19 +05:30
if ( error && ! error . response ) 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 ) ) ;
2022-02-04 16:01:18 -08:00
}
2020-03-12 17:13:13 -07:00
}
2022-02-03 16:15:14 -08:00
async function wait ( domainObject , subdomain , type , value , options ) {
2020-03-12 17:13:13 -07:00
assert . strictEqual ( typeof domainObject , 'object' ) ;
2022-02-03 16:15:14 -08:00
assert . strictEqual ( typeof subdomain , 'string' ) ;
2020-03-12 17:13:13 -07:00
assert . strictEqual ( typeof type , 'string' ) ;
assert . strictEqual ( typeof value , 'string' ) ;
assert ( options && typeof options === 'object' ) ; // { interval: 5000, times: 50000 }
2022-11-28 21:23:06 +01:00
const fqdn = dns . fqdn ( subdomain , domainObject . domain ) ;
2020-03-12 17:13:13 -07:00
2022-02-03 16:15:14 -08:00
await waitForDns ( fqdn , domainObject . zoneName , type , value , options ) ;
2020-03-12 17:13:13 -07:00
}
2022-02-04 16:01:18 -08:00
async function verifyDomainConfig ( domainObject ) {
2020-03-12 17:13:13 -07:00
assert . strictEqual ( typeof domainObject , 'object' ) ;
2022-01-05 22:41:41 -08:00
const domainConfig = domainObject . config ,
2020-03-12 17:13:13 -07:00
zoneName = domainObject . zoneName ;
2022-02-04 16:01:18 -08:00
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' ) ;
2020-03-12 17:13:13 -07:00
const ip = '127.0.0.1' ;
2022-02-04 16:01:18 -08:00
const credentials = {
2025-03-02 07:27:09 +01:00
token : domainConfig . token ,
customNameservers : ! ! domainConfig . customNameservers
2020-03-12 17:13:13 -07:00
} ;
2023-10-01 13:52:19 +05:30
if ( constants . TEST ) return credentials ; // this shouldn't be here
2020-03-12 17:13:13 -07:00
2022-02-04 16:01:18 -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' ) ;
2020-03-12 17:13:13 -07:00
2022-02-04 16:01:18 -08:00
if ( nameservers . map ( function ( n ) { return n . toLowerCase ( ) ; } ) . indexOf ( 'ns1.linode.com' ) === - 1 ) {
debug ( 'verifyDomainConfig: %j does not contains linode 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 Linode' ) ;
2022-02-04 16:01:18 -08:00
}
2020-03-12 17:13:13 -07:00
2022-02-04 16:01:18 -08:00
const location = 'cloudrontestdns' ;
2020-03-12 17:13:13 -07:00
2022-02-04 16:01:18 -08:00
await upsert ( domainObject , location , 'A' , [ ip ] ) ;
debug ( 'verifyDomainConfig: Test A record added' ) ;
2020-03-12 17:13:13 -07:00
2022-02-04 16:01:18 -08:00
await del ( domainObject , location , 'A' , [ ip ] ) ;
debug ( 'verifyDomainConfig: Test A record removed again' ) ;
2020-03-12 17:13:13 -07:00
2022-02-04 16:01:18 -08:00
return credentials ;
2020-03-12 17:13:13 -07:00
}
2025-10-08 12:04:31 +02:00
2026-02-14 15:43:24 +01:00
export default {
removePrivateFields ,
injectPrivateFields ,
upsert ,
get ,
del ,
wait ,
verifyDomainConfig
} ;