2023-11-05 18:38:30 +01:00
'use strict' ;
2025-10-08 20:11:55 +02:00
exports = module . exports = {
removePrivateFields ,
injectPrivateFields ,
upsert ,
get ,
del ,
wait ,
verifyDomainConfig
} ;
2025-08-14 11:17:38 +05:30
const assert = require ( 'node:assert' ) ,
2023-11-05 18:38:30 +01:00
BoxError = require ( '../boxerror.js' ) ,
constants = require ( '../constants.js' ) ,
debug = require ( 'debug' ) ( 'box:dns/ovh' ) ,
dig = require ( '../dig.js' ) ,
dns = require ( '../dns.js' ) ,
ovhClient = require ( 'ovh' ) ,
safe = require ( 'safetydance' ) ,
waitForDns = require ( './waitfordns.js' ) ;
function formatError ( error ) {
2025-02-14 17:26:54 +01:00
return ` OVH DNS error ${ error . error } ${ error . message } ` ; // error.error is the status
2023-11-05 18:38:30 +01:00
}
function removePrivateFields ( domainObject ) {
2025-10-08 12:04:31 +02:00
delete domainObject . config . appSecret ;
2023-11-05 18:38:30 +01:00
return domainObject ;
}
function injectPrivateFields ( newConfig , currentConfig ) {
2025-10-08 12:04:31 +02:00
if ( ! Object . hasOwn ( newConfig , 'appSecret' ) ) newConfig . appSecret = currentConfig . appSecret ;
2023-11-05 18:38:30 +01:00
}
function createClient ( domainConfig ) {
return ovhClient ( {
endpoint : domainConfig . endpoint ,
appKey : domainConfig . appKey ,
appSecret : domainConfig . appSecret ,
consumerKey : domainConfig . consumerKey ,
} ) ;
}
async function getDnsRecordIds ( domainConfig , zoneName , name , type ) {
assert . strictEqual ( typeof domainConfig , 'object' ) ;
assert . strictEqual ( typeof zoneName , 'string' ) ;
assert . strictEqual ( typeof name , 'string' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
debug ( ` get: ${ name } in zone ${ zoneName } of type ${ type } ` ) ;
const client = createClient ( domainConfig ) ;
const [ error , data ] = await safe ( client . requestPromised ( 'GET' , ` /domain/zone/ ${ zoneName } /record ` , { fieldType : type , subDomain : name } ) ) ;
if ( error ) {
if ( error . error === 401 || error . error === 403 ) throw new BoxError ( BoxError . ACCESS _DENIED , formatError ( error ) ) ;
throw new BoxError ( BoxError . EXTERNAL _ERROR , formatError ( error ) ) ;
}
return data || [ ] ; // array of numbers. data is undefined when no entries
}
async function refreshZone ( domainConfig , zoneName ) {
assert . strictEqual ( typeof domainConfig , 'object' ) ;
assert . strictEqual ( typeof zoneName , 'string' ) ;
debug ( ` refresh: zone ${ zoneName } ` ) ;
const client = createClient ( domainConfig ) ;
const [ error ] = await safe ( client . requestPromised ( 'POST' , ` /domain/zone/ ${ zoneName } /refresh ` ) ) ;
if ( error ) {
if ( error . error === 401 || error . error === 403 ) throw new BoxError ( BoxError . ACCESS _DENIED , formatError ( error ) ) ;
throw new BoxError ( BoxError . EXTERNAL _ERROR , formatError ( error ) ) ;
}
}
async function upsert ( domainObject , location , type , values ) {
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 ,
name = dns . getName ( domainObject , location , type ) || '' ;
debug ( ` upsert: ${ name } in zone ${ zoneName } of type ${ type } with values ${ JSON . stringify ( values ) } ` ) ;
const recordIds = await getDnsRecordIds ( domainConfig , zoneName , name , type ) ;
const client = createClient ( domainConfig ) ;
// used to track available records to update instead of create
let i = 0 ;
2025-03-02 07:27:09 +01:00
for ( const value of values ) {
2023-11-05 18:38:30 +01:00
const data = {
subDomain : name ,
target : value ,
ttl : 60
} ;
let error ;
if ( i >= recordIds . length ) {
data . fieldType = type ;
[ error ] = await safe ( client . requestPromised ( 'POST' , ` /domain/zone/ ${ zoneName } /record ` , data ) ) ;
} else {
[ error ] = await safe ( client . requestPromised ( 'PUT' , ` /domain/zone/ ${ zoneName } /record/ ${ recordIds [ i ] } ` , data ) ) ;
++ i ;
}
if ( error ) {
if ( error . error === 401 || error . error === 403 ) throw new BoxError ( BoxError . ACCESS _DENIED , formatError ( error ) ) ;
throw new BoxError ( BoxError . EXTERNAL _ERROR , formatError ( error ) ) ;
}
}
for ( let j = values . length + 1 ; j < recordIds . length ; j ++ ) {
const [ error ] = await safe ( client . requestPromised ( 'DELETE' , ` /domain/zone/ ${ zoneName } /record/ ${ recordIds [ j ] } ` ) ) ;
if ( error ) {
if ( error . error === 401 ) throw new BoxError ( BoxError . ACCESS _DENIED , formatError ( error ) ) ;
if ( error . error === 404 ) continue ; // not found
throw new BoxError ( BoxError . EXTERNAL _ERROR , formatError ( error ) ) ;
}
}
await refreshZone ( domainConfig , zoneName ) ;
debug ( 'upsert: completed' ) ;
}
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 ) || '' ;
const recordIds = await getDnsRecordIds ( domainConfig , zoneName , name , type ) ;
const client = createClient ( domainConfig ) ;
const result = [ ] ;
for ( const id of recordIds ) {
const [ error , data ] = await safe ( client . requestPromised ( 'GET' , ` /domain/zone/ ${ zoneName } /record/ ${ id } ` ) ) ;
if ( error ) {
if ( error . error === 401 ) throw new BoxError ( BoxError . ACCESS _DENIED , formatError ( error ) ) ;
throw new BoxError ( BoxError . EXTERNAL _ERROR , formatError ( error ) ) ;
}
result . push ( data . target ) ;
}
return result ;
}
async function del ( domainObject , location , type , values ) {
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 ,
name = dns . getName ( domainObject , location , type ) || '' ;
debug ( ` del: ${ name } in zone ${ zoneName } of type ${ type } with values ${ JSON . stringify ( values ) } ` ) ;
const recordIds = await getDnsRecordIds ( domainConfig , zoneName , name , type ) ;
const client = createClient ( domainConfig ) ;
for ( const id of recordIds ) {
const [ error ] = await safe ( client . requestPromised ( 'DELETE' , ` /domain/zone/ ${ zoneName } /record/ ${ id } ` ) ) ;
if ( error ) {
if ( error . error === 401 ) throw new BoxError ( BoxError . ACCESS _DENIED , formatError ( error ) ) ;
if ( error . error === 404 ) continue ; // not found
throw new BoxError ( BoxError . EXTERNAL _ERROR , formatError ( error ) ) ;
}
}
await refreshZone ( domainConfig , zoneName ) ;
}
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 . endpoint || typeof domainConfig . endpoint !== 'string' ) throw new BoxError ( BoxError . BAD _FIELD , 'endpoint must be a non-empty string' ) ;
if ( ! domainConfig . appKey || typeof domainConfig . appKey !== 'string' ) throw new BoxError ( BoxError . BAD _FIELD , 'appKey must be a non-empty string' ) ;
if ( ! domainConfig . appSecret || typeof domainConfig . appSecret !== 'string' ) throw new BoxError ( BoxError . BAD _FIELD , 'appSecret must be a non-empty string' ) ;
if ( ! domainConfig . consumerKey || typeof domainConfig . consumerKey !== 'string' ) throw new BoxError ( BoxError . BAD _FIELD , 'consumerKey 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' ) ;
2023-11-05 18:38:30 +01:00
const ip = '127.0.0.1' ;
const credentials = {
endpoint : domainConfig . endpoint , // https://github.com/ovh/node-ovh#2-authorize-your-application-to-access-to-a-customer-account
appKey : domainConfig . appKey ,
appSecret : domainConfig . appSecret ,
consumerKey : domainConfig . consumerKey ,
2025-03-02 07:27:09 +01:00
customNameservers : ! ! domainConfig . customNameservers
2023-11-05 18:38:30 +01:00
} ;
if ( constants . TEST ) return 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' ) ;
2023-12-05 14:03:59 +01:00
// ovh.net, ovh.ca or anycast.me
if ( ! nameservers . every ( function ( n ) { return n . toLowerCase ( ) . search ( /ovh|kimsufi|anycast/ ) !== - 1 ; } ) ) {
2023-11-05 18:38:30 +01:00
debug ( 'verifyDomainConfig: %j does not contain OVH 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 OVH' ) ;
2023-11-05 18:38:30 +01: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