2019-01-16 18:05:42 +02:00
'use strict' ;
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' ) ,
2019-01-16 18:05:42 +02:00
debug = require ( 'debug' ) ( 'box:dns/namecheap' ) ,
2022-02-04 15:34:02 -08:00
dig = require ( '../dig.js' ) ,
2021-08-13 17:22:28 -07:00
dns = require ( '../dns.js' ) ,
2023-08-03 13:38:42 +05:30
network = require ( '../network.js' ) ,
2019-05-16 17:00:17 +02:00
safe = require ( 'safetydance' ) ,
2025-07-10 10:55:52 +02:00
superagent = require ( '@cloudron/superagent' ) ,
2024-01-07 22:01:36 +01:00
timers = require ( 'timers/promises' ) ,
2025-08-14 11:17:38 +05:30
util = require ( 'node:util' ) ,
2019-05-16 16:53:36 +02:00
waitForDns = require ( './waitfordns.js' ) ,
xml2js = require ( 'xml2js' ) ;
2019-01-16 18:05:42 +02:00
2019-05-16 16:53:36 +02:00
const ENDPOINT = 'https://api.namecheap.com/xml.response' ;
2019-01-16 18:05:42 +02:00
2019-02-08 11:11:49 +01:00
function removePrivateFields ( domainObject ) {
2025-10-08 12:04:31 +02:00
delete domainObject . config . token ;
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 , 'token' ) ) newConfig . token = currentConfig . token ;
2019-02-09 19:08:15 +01:00
}
2022-01-05 22:41:41 -08:00
async function getQuery ( domainConfig ) {
assert . strictEqual ( typeof domainConfig , 'object' ) ;
2019-01-21 14:36:21 +01:00
2023-08-03 13:38:42 +05:30
const ip = await network . getIPv4 ( ) ; // only supports ipv4
2019-01-21 14:36:21 +01:00
2024-01-07 22:01:36 +01:00
// https://www.namecheap.com/support/knowledgebase/article.aspx/9739/63/api-faq/#z . 50 / minute
await timers . setTimeout ( 5000 ) ; // limits to 12req/min for this process. we can have 3 apptasks in parallel
2021-08-27 09:52:24 -07:00
return {
2022-01-05 22:41:41 -08:00
ApiUser : domainConfig . username ,
ApiKey : domainConfig . token ,
UserName : domainConfig . username ,
2021-08-27 09:52:24 -07:00
ClientIp : ip
} ;
2019-01-21 14:36:21 +01:00
}
2022-02-04 15:34:02 -08:00
async function getZone ( domainConfig , zoneName ) {
2022-01-05 22:41:41 -08:00
assert . strictEqual ( typeof domainConfig , 'object' ) ;
2019-01-16 18:05:42 +02:00
assert . strictEqual ( typeof zoneName , 'string' ) ;
2022-02-04 15:34:02 -08:00
const query = await getQuery ( domainConfig ) ;
query . Command = 'namecheap.domains.dns.getHosts' ;
2024-01-08 11:54:37 +01:00
query . SLD = zoneName . split ( '.' , 1 ) [ 0 ] ;
query . TLD = zoneName . slice ( query . SLD . length + 1 ) ;
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
const [ error , response ] = await safe ( superagent . get ( ENDPOINT ) . query ( query ) . ok ( ( ) => true ) ) ;
2024-11-19 17:08:19 +05:30
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
const parser = new xml2js . Parser ( ) ;
const [ parserError , result ] = await safe ( util . promisify ( parser . parseString ) ( response . text ) ) ;
if ( parserError ) throw new BoxError ( BoxError . EXTERNAL _ERROR , parserError ) ;
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
const tmp = result . ApiResponse ;
if ( tmp [ '$' ] . Status !== 'OK' ) {
const errorMessage = safe . query ( tmp , 'Errors[0].Error[0]._' , 'Invalid response' ) ;
if ( errorMessage === 'API Key is invalid or API access has not been enabled' ) throw new BoxError ( BoxError . ACCESS _DENIED , errorMessage ) ;
2019-05-16 16:53:36 +02:00
2022-02-04 15:34:02 -08:00
throw new BoxError ( BoxError . EXTERNAL _ERROR , errorMessage ) ;
}
2023-07-19 14:51:42 +02:00
const host = safe . query ( tmp , 'CommandResponse[0].DomainDNSGetHostsResult[0].host' , [ ] ) ;
2022-02-04 15:34:02 -08:00
if ( ! host ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Invalid response: ${ JSON . stringify ( tmp ) } ` ) ;
if ( ! Array . isArray ( host ) ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` host is not an array: ${ JSON . stringify ( tmp ) } ` ) ;
2019-09-23 20:00:49 +02:00
2022-02-04 15:34:02 -08:00
const hosts = host . map ( h => h [ '$' ] ) ;
return hosts ;
2019-01-16 18:05:42 +02:00
}
2022-02-04 15:34:02 -08:00
async function setZone ( domainConfig , zoneName , hosts ) {
2022-01-05 22:41:41 -08:00
assert . strictEqual ( typeof domainConfig , 'object' ) ;
2019-02-08 20:21:16 -08:00
assert . strictEqual ( typeof zoneName , 'string' ) ;
assert ( Array . isArray ( hosts ) ) ;
2019-05-16 16:53:36 +02:00
2022-02-04 15:34:02 -08:00
const query = await getQuery ( domainConfig ) ;
query . Command = 'namecheap.domains.dns.setHosts' ;
2024-01-09 09:44:22 +01:00
query . SLD = zoneName . split ( '.' , 1 ) [ 0 ] ;
query . TLD = zoneName . slice ( query . SLD . length + 1 ) ;
2022-02-04 15:34:02 -08:00
// Map to query params https://www.namecheap.com/support/api/methods/domains-dns/set-hosts.aspx
hosts . forEach ( function ( host , i ) {
2022-04-14 17:41:41 -05:00
const n = i + 1 ; // api starts with 1 not 0
2022-02-04 15:34:02 -08:00
query [ 'TTL' + n ] = '300' ; // keep it low
query [ 'HostName' + n ] = host . HostName || host . Name ;
query [ 'RecordType' + n ] = host . RecordType || host . Type ;
query [ 'Address' + n ] = host . Address ;
if ( host . Type === 'MX' ) {
query [ 'EmailType' + n ] = 'MX' ;
if ( host . MXPref ) query [ 'MXPref' + n ] = host . MXPref ;
}
} ) ;
2021-04-13 21:36:05 -07:00
2022-02-04 15:34:02 -08:00
// namecheap recommends sending as POSTDATA with > 10 records
const qs = new URLSearchParams ( query ) . toString ( ) ;
2019-05-16 16:53:36 +02:00
2022-02-04 15:34:02 -08:00
const [ error , response ] = await safe ( superagent . post ( ENDPOINT ) . set ( 'Content-Type' , 'application/x-www-form-urlencoded' ) . send ( qs ) . ok ( ( ) => true ) ) ;
if ( error ) throw new BoxError ( BoxError . EXTERNAL _ERROR , error ) ;
2019-02-08 20:21:16 -08:00
2022-02-04 15:34:02 -08:00
const parser = new xml2js . Parser ( ) ;
2022-02-06 10:58:49 -08:00
const [ parserError , result ] = await safe ( util . promisify ( parser . parseString ) ( response . text ) ) ;
2022-02-04 15:34:02 -08:00
if ( parserError ) throw new BoxError ( BoxError . EXTERNAL _ERROR , parserError . message ) ;
2019-09-23 20:00:49 +02:00
2022-02-04 15:34:02 -08:00
const tmp = result . ApiResponse ;
if ( tmp [ '$' ] . Status !== 'OK' ) {
const errorMessage = safe . query ( tmp , 'Errors[0].Error[0]._' , 'Invalid response' ) ;
if ( errorMessage === 'API Key is invalid or API access has not been enabled' ) throw new BoxError ( BoxError . ACCESS _DENIED , errorMessage ) ;
2019-05-16 16:53:36 +02:00
2022-02-04 15:34:02 -08:00
throw new BoxError ( BoxError . EXTERNAL _ERROR , errorMessage ) ;
}
if ( ! tmp . CommandResponse [ 0 ] ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'Invalid response' ) ;
if ( ! tmp . CommandResponse [ 0 ] . DomainDNSSetHostsResult [ 0 ] ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'Invalid response' ) ;
if ( tmp . CommandResponse [ 0 ] . DomainDNSSetHostsResult [ 0 ] [ '$' ] . IsSuccess !== 'true' ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'Invalid response' ) ;
2019-01-16 18:05:42 +02:00
}
2022-02-04 15:34:02 -08:00
async function upsert ( domainObject , subdomain , type , values ) {
2019-01-21 14:36:21 +01:00
assert . strictEqual ( typeof domainObject , 'object' ) ;
2019-01-16 18:05:42 +02:00
assert . strictEqual ( typeof subdomain , 'string' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
2021-04-13 15:10:24 -07:00
assert ( Array . isArray ( values ) ) ;
2019-01-16 18:05:42 +02:00
2022-01-05 22:41:41 -08:00
const domainConfig = domainObject . config ;
2019-01-21 14:36:21 +01:00
const zoneName = domainObject . zoneName ;
2021-08-13 17:22:28 -07:00
subdomain = dns . getName ( domainObject , subdomain , type ) || '@' ;
2019-01-16 18:05:42 +02:00
debug ( 'upsert: %s for zone %s of type %s with values %j' , subdomain , zoneName , type , values ) ;
2022-02-04 15:34:02 -08:00
const result = await getZone ( domainConfig , zoneName ) ;
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
// Array to keep track of records that need to be inserted
2025-01-31 09:47:31 +01:00
const toInsert = [ ] ;
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
for ( let i = 0 ; i < values . length ; i ++ ) {
2025-01-31 09:47:31 +01:00
const curValue = values [ i ] ;
2022-02-04 15:34:02 -08:00
let wasUpdate = false ;
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
for ( let j = 0 ; j < result . length ; j ++ ) {
2025-01-31 09:47:31 +01:00
const curHost = result [ j ] ;
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
if ( curHost . Type === type && curHost . Name === subdomain ) {
// Updating an already existing host
wasUpdate = true ;
if ( type === 'MX' ) {
curHost . MXPref = curValue . split ( ' ' ) [ 0 ] ;
curHost . Address = curValue . split ( ' ' ) [ 1 ] ;
} else {
curHost . Address = curValue ;
2019-01-16 18:05:42 +02:00
}
}
2022-02-04 15:34:02 -08:00
}
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
// We don't have this host at all yet, let's push to toInsert array
if ( ! wasUpdate ) {
2025-01-31 09:47:31 +01:00
const newRecord = {
2022-02-04 15:34:02 -08:00
RecordType : type ,
HostName : subdomain ,
Address : curValue
} ;
// Special case for MX records
if ( type === 'MX' ) {
newRecord . MXPref = curValue . split ( ' ' ) [ 0 ] ;
newRecord . Address = curValue . split ( ' ' ) [ 1 ] ;
}
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
toInsert . push ( newRecord ) ;
2019-01-16 18:05:42 +02:00
}
2022-02-04 15:34:02 -08:00
}
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
const hosts = result . concat ( toInsert ) ;
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
return await setZone ( domainConfig , zoneName , hosts ) ;
2019-01-16 18:05:42 +02:00
}
2022-02-04 15:34:02 -08:00
async function get ( domainObject , subdomain , type ) {
2019-01-21 14:36:21 +01:00
assert . strictEqual ( typeof domainObject , 'object' ) ;
2019-01-16 18:05:42 +02:00
assert . strictEqual ( typeof subdomain , 'string' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
2022-01-05 22:41:41 -08:00
const domainConfig = domainObject . config ;
2019-01-21 14:36:21 +01:00
const zoneName = domainObject . zoneName ;
2021-08-13 17:22:28 -07:00
subdomain = dns . getName ( domainObject , subdomain , type ) || '@' ;
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
const result = await getZone ( domainConfig , zoneName ) ;
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
// We need to filter hosts to ones with this subdomain and type
const actualHosts = result . filter ( ( host ) => host . Type === type && host . Name === subdomain ) ;
2019-01-21 14:36:21 +01:00
2022-02-04 15:34:02 -08:00
const tmp = actualHosts . map ( function ( record ) { return record . Address ; } ) ;
return tmp ;
2019-01-16 18:05:42 +02:00
}
2022-02-04 15:34:02 -08:00
async function del ( domainObject , subdomain , type , values ) {
2019-01-21 14:36:21 +01:00
assert . strictEqual ( typeof domainObject , 'object' ) ;
2019-01-16 18:05:42 +02:00
assert . strictEqual ( typeof subdomain , 'string' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
2021-04-13 15:10:24 -07:00
assert ( Array . isArray ( values ) ) ;
2019-01-16 18:05:42 +02:00
2022-01-05 22:41:41 -08:00
const domainConfig = domainObject . config ;
2019-01-21 14:36:21 +01:00
const zoneName = domainObject . zoneName ;
2021-08-13 17:22:28 -07:00
subdomain = dns . getName ( domainObject , subdomain , type ) || '@' ;
2019-01-21 14:36:21 +01:00
2019-01-16 18:05:42 +02:00
debug ( 'del: %s for zone %s of type %s with values %j' , subdomain , zoneName , type , values ) ;
2022-02-04 15:34:02 -08:00
let result = await getZone ( domainConfig , zoneName ) ;
if ( result . length === 0 ) return ;
const originalLength = result . length ;
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
for ( let i = 0 ; i < values . length ; i ++ ) {
2025-01-31 09:47:31 +01:00
const curValue = values [ i ] ;
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
result = result . filter ( curHost => curHost . Type !== type || curHost . Name !== subdomain || curHost . Address !== curValue ) ;
}
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
if ( result . length !== originalLength ) return await setZone ( domainConfig , zoneName , result ) ;
}
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 }
2019-01-16 18:05:42 +02:00
2022-11-28 21:23:06 +01:00
const fqdn = dns . fqdn ( subdomain , domainObject . domain ) ;
2019-01-21 14:36:21 +01:00
2022-02-04 15:34:02 -08:00
await waitForDns ( fqdn , domainObject . zoneName , type , value , options ) ;
2019-01-16 18:05:42 +02:00
}
2022-02-04 15:34:02 -08:00
async function verifyDomainConfig ( domainObject ) {
2019-01-21 14:36:21 +01:00
assert . strictEqual ( typeof domainObject , 'object' ) ;
2019-01-16 18:05:42 +02:00
2022-01-05 22:41:41 -08:00
const domainConfig = domainObject . config ;
2019-01-21 14:36:21 +01:00
const zoneName = domainObject . zoneName ;
const ip = '127.0.0.1' ;
2022-02-04 15:34:02 -08:00
if ( ! domainConfig . username || typeof domainConfig . username !== 'string' ) throw new BoxError ( BoxError . BAD _FIELD , 'username must be a non-empty string' ) ;
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' ) ;
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -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
2019-01-16 18:05:42 +02:00
} ;
2023-10-01 13:52:19 +05:30
if ( constants . TEST ) return credentials ; // this shouldn't be here
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -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' ) ;
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
if ( nameservers . some ( function ( n ) { return n . toLowerCase ( ) . indexOf ( '.registrar-servers.com' ) === - 1 ; } ) ) {
debug ( 'verifyDomainConfig: %j does not contains NC 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 NameCheap' ) ;
2022-02-04 15:34:02 -08:00
}
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
const testSubdomain = 'cloudrontestdns' ;
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
await upsert ( domainObject , testSubdomain , 'A' , [ ip ] ) ;
debug ( 'verifyDomainConfig: Test A record added' ) ;
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
await del ( domainObject , testSubdomain , 'A' , [ ip ] ) ;
debug ( 'verifyDomainConfig: Test A record removed again' ) ;
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
return credentials ;
2019-01-22 12:12:46 +01:00
}
2025-10-08 12:04:31 +02:00
exports = module . exports = {
removePrivateFields ,
injectPrivateFields ,
upsert ,
get ,
del ,
verifyDomainConfig ,
wait
} ;