2021-08-13 17:22:28 -07:00
'use strict' ;
module . exports = exports = {
fqdn ,
getName ,
getDnsRecords ,
upsertDnsRecords ,
removeDnsRecords ,
waitForDnsRecord ,
2023-08-14 09:40:31 +05:30
waitForLocations ,
2021-08-13 17:22:28 -07:00
validateHostname ,
makeWildcard ,
registerLocations ,
unregisterLocations ,
checkDnsRecords ,
syncDnsRecords ,
2023-08-04 12:53:11 +05:30
startSyncDnsRecords
2021-08-13 17:22:28 -07:00
} ;
const apps = require ( './apps.js' ) ,
assert = require ( 'assert' ) ,
BoxError = require ( './boxerror.js' ) ,
constants = require ( './constants.js' ) ,
2023-08-11 19:41:05 +05:30
dashboard = require ( './dashboard.js' ) ,
2021-09-03 09:08:20 -07:00
debug = require ( 'debug' ) ( 'box:dns' ) ,
2021-08-13 17:22:28 -07:00
domains = require ( './domains.js' ) ,
2022-01-06 17:02:16 -08:00
ipaddr = require ( 'ipaddr.js' ) ,
2021-08-13 17:22:28 -07:00
mail = require ( './mail.js' ) ,
2023-08-04 21:37:38 +05:30
mailServer = require ( './mailserver.js' ) ,
2023-08-03 13:38:42 +05:30
network = require ( './network.js' ) ,
2021-08-27 09:52:24 -07:00
promiseRetry = require ( './promise-retry.js' ) ,
safe = require ( 'safetydance' ) ,
2023-08-04 12:53:11 +05:30
tasks = require ( './tasks.js' ) ,
2022-02-04 09:37:02 -08:00
tld = require ( 'tldjs' ) ;
2021-08-13 17:22:28 -07:00
// choose which subdomain backend we use for test purpose we use route53
function api ( provider ) {
assert . strictEqual ( typeof provider , 'string' ) ;
switch ( provider ) {
2023-04-21 10:39:11 +02:00
case 'bunny' : return require ( './dns/bunny.js' ) ;
2021-08-13 17:22:28 -07:00
case 'cloudflare' : return require ( './dns/cloudflare.js' ) ;
case 'route53' : return require ( './dns/route53.js' ) ;
case 'gcdns' : return require ( './dns/gcdns.js' ) ;
case 'digitalocean' : return require ( './dns/digitalocean.js' ) ;
case 'gandi' : return require ( './dns/gandi.js' ) ;
case 'godaddy' : return require ( './dns/godaddy.js' ) ;
case 'linode' : return require ( './dns/linode.js' ) ;
case 'vultr' : return require ( './dns/vultr.js' ) ;
case 'namecom' : return require ( './dns/namecom.js' ) ;
case 'namecheap' : return require ( './dns/namecheap.js' ) ;
case 'netcup' : return require ( './dns/netcup.js' ) ;
2022-05-02 22:00:42 -07:00
case 'hetzner' : return require ( './dns/hetzner.js' ) ;
2021-08-13 17:22:28 -07:00
case 'noop' : return require ( './dns/noop.js' ) ;
case 'manual' : return require ( './dns/manual.js' ) ;
2023-03-16 10:24:31 +01:00
case 'porkbun' : return require ( './dns/porkbun.js' ) ;
2021-08-13 17:22:28 -07:00
case 'wildcard' : return require ( './dns/wildcard.js' ) ;
default : return null ;
}
}
2022-11-28 21:23:06 +01:00
function fqdn ( subdomain , domain ) {
2022-02-03 16:15:14 -08:00
assert . strictEqual ( typeof subdomain , 'string' ) ;
2022-11-28 21:23:06 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
2021-08-13 17:22:28 -07:00
2022-11-28 21:23:06 +01:00
return subdomain + ( subdomain ? '.' : '' ) + domain ;
2021-08-13 17:22:28 -07:00
}
// Hostname validation comes from RFC 1123 (section 2.1)
// Domain name validation comes from RFC 2181 (Name syntax)
// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
// We are validating the validity of the location-fqdn as host name (and not dns name)
2022-11-28 21:23:06 +01:00
function validateHostname ( subdomain , domain ) {
2022-02-03 16:15:14 -08:00
assert . strictEqual ( typeof subdomain , 'string' ) ;
2022-11-28 21:23:06 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
2021-08-13 17:22:28 -07:00
2022-11-28 21:23:06 +01:00
const hostname = fqdn ( subdomain , domain ) ;
2021-08-13 17:22:28 -07:00
// workaround https://github.com/oncletom/tld.js/issues/73
2022-04-14 17:41:41 -05:00
const tmp = hostname . replace ( '_' , '-' ) ;
2022-02-07 13:19:59 -08:00
if ( ! tld . isValid ( tmp ) ) return new BoxError ( BoxError . BAD _FIELD , 'Hostname is not a valid domain name' ) ;
2021-08-13 17:22:28 -07:00
2022-02-07 13:19:59 -08:00
if ( hostname . length > 253 ) return new BoxError ( BoxError . BAD _FIELD , 'Hostname length exceeds 253 characters' ) ;
2021-08-13 17:22:28 -07:00
2022-02-03 16:15:14 -08:00
if ( subdomain ) {
2021-08-13 17:22:28 -07:00
// label validation
2022-02-07 13:19:59 -08:00
if ( subdomain . split ( '.' ) . some ( function ( p ) { return p . length > 63 || p . length < 1 ; } ) ) return new BoxError ( BoxError . BAD _FIELD , 'Invalid subdomain length' ) ;
if ( subdomain . match ( /^[A-Za-z0-9-.]+$/ ) === null ) return new BoxError ( BoxError . BAD _FIELD , 'Subdomain can only contain alphanumeric, hyphen and dot' ) ;
if ( /^[-.]/ . test ( subdomain ) ) return new BoxError ( BoxError . BAD _FIELD , 'Subdomain cannot start or end with hyphen or dot' ) ;
2021-08-13 17:22:28 -07:00
}
return null ;
}
// returns the 'name' that needs to be inserted into zone
// eslint-disable-next-line no-unused-vars
2022-02-03 16:15:14 -08:00
function getName ( domain , subdomain , type ) {
2021-08-13 17:22:28 -07:00
const part = domain . domain . slice ( 0 , - domain . zoneName . length - 1 ) ;
2022-02-03 16:15:14 -08:00
if ( subdomain === '' ) return part ;
2021-08-13 17:22:28 -07:00
2022-02-03 16:15:14 -08:00
return part ? ` ${ subdomain } . ${ part } ` : subdomain ;
2021-08-13 17:22:28 -07:00
}
2022-02-03 16:15:14 -08:00
async function getDnsRecords ( subdomain , domain , type ) {
assert . strictEqual ( typeof subdomain , 'string' ) ;
2021-08-13 17:22:28 -07:00
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
2021-08-27 09:52:24 -07:00
const domainObject = await domains . get ( domain ) ;
2022-02-04 09:37:02 -08:00
return await api ( domainObject . provider ) . get ( domainObject , subdomain , type ) ;
2021-08-13 17:22:28 -07:00
}
2022-02-03 16:15:14 -08:00
async function checkDnsRecords ( subdomain , domain ) {
assert . strictEqual ( typeof subdomain , 'string' ) ;
2021-08-13 17:22:28 -07:00
assert . strictEqual ( typeof domain , 'string' ) ;
2022-03-29 13:45:29 -07:00
const cnameRecords = await getDnsRecords ( subdomain , domain , 'CNAME' ) ;
if ( cnameRecords . length !== 0 ) return { needsOverwrite : true } ;
2022-02-03 16:15:14 -08:00
const ipv4Records = await getDnsRecords ( subdomain , domain , 'A' ) ;
2023-08-03 13:38:42 +05:30
const ipv4 = await network . getIPv4 ( ) ;
2022-01-06 17:02:16 -08:00
// if empty OR exactly one record with the ip, we don't need to overwrite
if ( ipv4Records . length !== 0 && ( ipv4Records . length !== 1 || ipv4Records [ 0 ] !== ipv4 ) ) return { needsOverwrite : true } ;
2021-08-13 17:22:28 -07:00
2023-08-03 13:38:42 +05:30
const ipv6 = await network . getIPv6 ( ) ;
2022-02-15 12:31:55 -08:00
if ( ipv6 ) {
2022-02-03 16:15:14 -08:00
const ipv6Records = await getDnsRecords ( subdomain , domain , 'AAAA' ) ;
2021-08-13 17:22:28 -07:00
2022-01-06 17:02:16 -08:00
// if empty OR exactly one record with the ip, we don't need to overwrite
if ( ipv6Records . length !== 0 && ( ipv6Records . length !== 1 || ipaddr . parse ( ipv6Records [ 0 ] ) . toRFC5952String ( ) !== ipv6 ) ) return { needsOverwrite : true } ;
}
2021-08-13 17:22:28 -07:00
2022-01-06 17:02:16 -08:00
return { needsOverwrite : false } ; // one record exists and in sync
2021-08-13 17:22:28 -07:00
}
// note: for TXT records the values must be quoted
2022-02-03 16:15:14 -08:00
async function upsertDnsRecords ( subdomain , domain , type , values ) {
assert . strictEqual ( typeof subdomain , 'string' ) ;
2021-08-13 17:22:28 -07:00
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
assert ( Array . isArray ( values ) ) ;
2022-02-03 16:15:14 -08:00
debug ( ` upsertDNSRecord: location ${ subdomain } on domain ${ domain } of type ${ type } with values ${ JSON . stringify ( values ) } ` ) ;
2021-08-13 17:22:28 -07:00
2021-08-27 09:52:24 -07:00
const domainObject = await domains . get ( domain ) ;
2022-02-04 09:37:02 -08:00
await api ( domainObject . provider ) . upsert ( domainObject , subdomain , type , values ) ;
2021-08-13 17:22:28 -07:00
}
2022-02-03 16:15:14 -08:00
async function removeDnsRecords ( subdomain , domain , type , values ) {
assert . strictEqual ( typeof subdomain , 'string' ) ;
2021-08-13 17:22:28 -07:00
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
assert ( Array . isArray ( values ) ) ;
2022-03-29 13:45:29 -07:00
debug ( 'removeDNSRecords: %s on %s type %s values' , subdomain , domain , type , values ) ;
2021-08-13 17:22:28 -07:00
2021-08-27 09:52:24 -07:00
const domainObject = await domains . get ( domain ) ;
2022-02-04 09:37:02 -08:00
const [ error ] = await safe ( api ( domainObject . provider ) . del ( domainObject , subdomain , type , values ) ) ;
if ( error && error . reason !== BoxError . NOT _FOUND ) throw error ; // this is never returned afaict
2021-08-13 17:22:28 -07:00
}
2022-02-03 16:15:14 -08:00
async function waitForDnsRecord ( subdomain , domain , type , value , options ) {
assert . strictEqual ( typeof subdomain , 'string' ) ;
2021-08-13 17:22:28 -07:00
assert . strictEqual ( typeof domain , 'string' ) ;
2022-01-06 22:07:26 -08:00
assert ( type === 'A' || type === 'AAAA' || type === 'TXT' ) ;
2021-08-13 17:22:28 -07:00
assert . strictEqual ( typeof value , 'string' ) ;
assert ( options && typeof options === 'object' ) ; // { interval: 5000, times: 50000 }
2021-08-27 09:52:24 -07:00
const domainObject = await domains . get ( domain ) ;
2021-08-13 17:22:28 -07:00
2021-08-27 09:52:24 -07:00
// linode DNS takes ~15mins
if ( ! options . interval ) options . interval = domainObject . provider === 'linode' ? 20000 : 5000 ;
2021-08-13 17:22:28 -07:00
2022-02-03 16:15:14 -08:00
await api ( domainObject . provider ) . wait ( domainObject , subdomain , type , value , options ) ;
2021-08-13 17:22:28 -07:00
}
2023-08-14 09:40:31 +05:30
async function waitForLocations ( locations , progressCallback ) {
assert ( Array . isArray ( locations ) ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
if ( constants . TEST ) return ;
const ipv4 = await network . getIPv4 ( ) ;
const ipv6 = await network . getIPv6 ( ) ;
for ( const location of locations ) {
const { subdomain , domain } = location ;
2023-08-21 14:40:57 +05:30
progressCallback ( { message : ` Waiting for propagation of ${ fqdn ( subdomain , domain ) } ` } ) ;
2023-08-14 09:40:31 +05:30
const [ error ] = await safe ( waitForDnsRecord ( subdomain , domain , 'A' , ipv4 , { times : 240 } ) ) ;
if ( error ) throw new BoxError ( BoxError . DNS _ERROR , ` DNS A Record is not synced yet: ${ error . message } ` , { ipv4 , subdomain , domain } ) ;
if ( ipv6 ) {
const [ error ] = await safe ( waitForDnsRecord ( subdomain , domain , 'AAAA' , ipv6 , { times : 240 } ) ) ;
if ( error ) throw new BoxError ( BoxError . DNS _ERROR , ` DNS AAAA Record is not synced yet: ${ error . message } ` , { ipv6 , subdomain , domain } ) ;
}
}
}
2022-07-13 09:26:27 +05:30
function makeWildcard ( fqdn ) {
assert . strictEqual ( typeof fqdn , 'string' ) ;
2021-08-13 17:22:28 -07:00
2022-07-13 09:26:27 +05:30
// if the fqdn is like *.example.com, this function will do nothing
const parts = fqdn . split ( '.' ) ;
2021-08-13 17:22:28 -07:00
parts [ 0 ] = '*' ;
return parts . join ( '.' ) ;
}
2022-01-06 16:34:33 -08:00
async function registerLocation ( location , options , recordType , recordValue ) {
const overwriteDns = options . overwriteDns || false ;
// get the current record before updating it
const [ getError , values ] = await safe ( getDnsRecords ( location . subdomain , location . domain , recordType ) ) ;
if ( getError ) {
2022-03-29 13:45:29 -07:00
const retryable = getError . reason !== BoxError . ACCESS _DENIED && getError . reason !== BoxError . NOT _FOUND ; // NOT_FOUND is when zone is not found
2022-02-25 11:03:16 -08:00
debug ( ` registerLocation: Get error. retryable: ${ retryable } . ${ getError . message } ` ) ;
2022-01-06 16:34:33 -08:00
throw new BoxError ( getError . reason , getError . message , { domain : location , retryable } ) ;
}
if ( values . length === 1 && values [ 0 ] === recordValue ) return ; // up-to-date
// refuse to update any existing DNS record for custom domains that we did not create
if ( values . length !== 0 && ! overwriteDns ) throw new BoxError ( BoxError . ALREADY _EXISTS , ` DNS ${ recordType } record already exists ` , { domain : location , retryable : false } ) ;
const [ upsertError ] = await safe ( upsertDnsRecords ( location . subdomain , location . domain , recordType , [ recordValue ] ) ) ;
if ( upsertError ) {
const retryable = upsertError . reason === BoxError . BUSY || upsertError . reason === BoxError . EXTERNAL _ERROR ;
debug ( ` registerLocation: Upsert error. retryable: ${ retryable } . ${ upsertError . message } ` ) ;
throw new BoxError ( BoxError . EXTERNAL _ERROR , upsertError . message , { domain : location , retryable } ) ;
}
}
2021-08-27 09:52:24 -07:00
async function registerLocations ( locations , options , progressCallback ) {
2021-08-13 17:22:28 -07:00
assert ( Array . isArray ( locations ) ) ;
assert . strictEqual ( typeof options , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
debug ( ` registerLocations: Will register ${ JSON . stringify ( locations ) } with options ${ JSON . stringify ( options ) } ` ) ;
2023-08-03 13:38:42 +05:30
const ipv4 = await network . getIPv4 ( ) ;
const ipv6 = await network . getIPv6 ( ) ;
2021-08-27 09:52:24 -07:00
for ( const location of locations ) {
2023-08-22 16:28:48 +05:30
progressCallback ( { message : ` Registering location ${ fqdn ( location . subdomain , location . domain ) } ` } ) ;
2021-08-27 09:52:24 -07:00
2022-01-06 16:34:33 -08:00
await promiseRetry ( { times : 200 , interval : 5000 , debug , retry : ( error ) => error . retryable } , async function ( ) {
2022-03-29 13:45:29 -07:00
// cname records cannot co-exist with other records
const [ getError , values ] = await safe ( getDnsRecords ( location . subdomain , location . domain , 'CNAME' ) ) ;
if ( ! getError && values . length === 1 ) {
if ( ! options . overwriteDns ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'DNS CNAME record already exists' , { domain : location , retryable : false } ) ;
2023-08-14 09:35:08 +05:30
debug ( ` registerLocations: removing CNAME record of ${ fqdn ( location . subdomain , location . domain ) } ` ) ;
2022-03-29 13:45:29 -07:00
await removeDnsRecords ( location . subdomain , location . domain , 'CNAME' , values ) ;
}
2022-01-06 16:34:33 -08:00
await registerLocation ( location , options , 'A' , ipv4 ) ;
2022-02-15 12:31:55 -08:00
if ( ipv6 ) await registerLocation ( location , options , 'AAAA' , ipv6 ) ;
2022-01-06 16:34:33 -08:00
} ) ;
}
}
2021-08-27 09:52:24 -07:00
2022-01-06 16:34:33 -08:00
async function unregisterLocation ( location , recordType , recordValue ) {
const [ error ] = await safe ( removeDnsRecords ( location . subdomain , location . domain , recordType , [ recordValue ] ) ) ;
if ( ! error || error . reason === BoxError . NOT _FOUND ) return ;
2021-08-27 09:52:24 -07:00
2022-01-06 16:34:33 -08:00
const retryable = error . reason === BoxError . BUSY || error . reason === BoxError . EXTERNAL _ERROR ;
debug ( ` unregisterLocation: Error unregistering location ${ recordType } . retryable: ${ retryable } . ${ error . message } ` ) ;
2021-08-27 09:52:24 -07:00
2022-01-06 16:34:33 -08:00
throw new BoxError ( BoxError . EXTERNAL _ERROR , error . message , { domain : location , retryable } ) ;
2021-08-13 17:22:28 -07:00
}
2021-08-27 09:52:24 -07:00
async function unregisterLocations ( locations , progressCallback ) {
2021-08-13 17:22:28 -07:00
assert ( Array . isArray ( locations ) ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2023-08-03 13:38:42 +05:30
const ipv4 = await network . getIPv4 ( ) ;
const ipv6 = await network . getIPv6 ( ) ;
2021-08-27 09:52:24 -07:00
for ( const location of locations ) {
2022-01-06 16:34:33 -08:00
progressCallback ( { message : ` Unregistering location: ${ location . subdomain ? ( location . subdomain + '.' ) : '' } ${ location . domain } ` } ) ;
2021-08-27 09:52:24 -07:00
2022-01-06 16:34:33 -08:00
await promiseRetry ( { times : 30 , interval : 5000 , debug , retry : ( error ) => error . retryable } , async function ( ) {
await unregisterLocation ( location , 'A' , ipv4 ) ;
2022-02-15 12:31:55 -08:00
if ( ipv6 ) await unregisterLocation ( location , 'AAAA' , ipv6 ) ;
2021-08-27 09:52:24 -07:00
} ) ;
}
2021-08-13 17:22:28 -07:00
}
2021-08-27 09:52:24 -07:00
async function syncDnsRecords ( options , progressCallback ) {
2021-08-13 17:22:28 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2021-08-27 09:52:24 -07:00
if ( options . domain && options . type === 'mail' ) return await mail . setDnsRecords ( options . domain ) ;
2021-08-13 17:22:28 -07:00
2021-09-05 12:10:37 +02:00
let allDomains = await domains . list ( ) ;
2021-08-13 17:22:28 -07:00
2021-09-05 12:10:37 +02:00
if ( options . domain ) allDomains = allDomains . filter ( d => d . domain === options . domain ) ;
2021-08-13 17:22:28 -07:00
2023-08-04 21:37:38 +05:30
const { domain : mailDomain , fqdn : mailFqdn , subdomain : mailSubdomain } = await mailServer . getLocation ( ) ;
2023-08-11 19:41:05 +05:30
const { domain : dashboardDomain , fqdn : dashboardFqdn } = await dashboard . getLocation ( ) ;
2021-08-13 17:22:28 -07:00
2021-08-27 09:52:24 -07:00
const allApps = await apps . list ( ) ;
2021-08-13 17:22:28 -07:00
2021-08-27 09:52:24 -07:00
let progress = 1 , errors = [ ] ;
2021-08-13 17:22:28 -07:00
2021-08-27 09:52:24 -07:00
// we sync by domain only to get some nice progress
2021-09-05 12:10:37 +02:00
for ( const domain of allDomains ) {
2021-08-27 09:52:24 -07:00
progressCallback ( { percent : progress , message : ` Updating DNS of ${ domain . domain } ` } ) ;
2021-09-06 09:46:27 +02:00
progress += Math . round ( 100 / ( 1 + allDomains . length ) ) ;
2021-08-13 17:22:28 -07:00
2021-08-27 09:52:24 -07:00
let locations = [ ] ;
2023-08-11 19:41:05 +05:30
if ( domain . domain === dashboardDomain ) locations . push ( { subdomain : constants . DASHBOARD _SUBDOMAIN , domain : dashboardDomain } ) ;
if ( domain . domain === mailDomain && mailFqdn !== dashboardFqdn ) locations . push ( { subdomain : mailSubdomain , domain : mailDomain } ) ;
2021-08-13 17:22:28 -07:00
2022-01-06 17:19:08 -08:00
for ( const app of allApps ) {
2023-02-08 23:16:48 +01:00
const appLocations = [ { subdomain : app . subdomain , domain : app . domain } ]
. concat ( app . secondaryDomains )
. concat ( app . redirectDomains )
. concat ( app . aliasDomains ) ;
2021-08-27 09:52:24 -07:00
locations = locations . concat ( appLocations . filter ( al => al . domain === domain . domain ) ) ;
2022-01-06 17:19:08 -08:00
}
2021-08-27 09:52:24 -07:00
try {
await registerLocations ( locations , { overwriteDns : true } , progressCallback ) ;
progressCallback ( { message : ` Updating mail DNS of ${ domain . domain } ` } ) ;
await mail . setDnsRecords ( domain . domain ) ;
} catch ( error ) {
errors . push ( { domain : domain . domain , message : error . message } ) ;
}
}
return { errors } ;
2021-08-13 17:22:28 -07:00
}
2023-08-04 12:53:11 +05:30
async function startSyncDnsRecords ( options ) {
assert . strictEqual ( typeof options , 'object' ) ;
const taskId = await tasks . add ( tasks . TASK _SYNC _DNS _RECORDS , [ options ] ) ;
tasks . startTask ( taskId , { } ) ;
return taskId ;
}