12e073e8cf
mostly because code is being autogenerated by all the AI stuff using this prefix. it's also used in the stack trace.
237 lines
9.3 KiB
JavaScript
237 lines
9.3 KiB
JavaScript
'use strict';
|
|
|
|
exports = module.exports = {
|
|
removePrivateFields,
|
|
injectPrivateFields,
|
|
upsert,
|
|
get,
|
|
del,
|
|
wait,
|
|
verifyDomainConfig
|
|
};
|
|
|
|
const assert = require('node:assert'),
|
|
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) {
|
|
return `OVH DNS error ${error.error} ${error.message}`; // error.error is the status
|
|
}
|
|
|
|
function removePrivateFields(domainObject) {
|
|
domainObject.config.appSecret = constants.SECRET_PLACEHOLDER;
|
|
return domainObject;
|
|
}
|
|
|
|
function injectPrivateFields(newConfig, currentConfig) {
|
|
if (newConfig.appSecret === constants.SECRET_PLACEHOLDER) newConfig.appSecret = currentConfig.appSecret;
|
|
}
|
|
|
|
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;
|
|
|
|
for (const value of values) {
|
|
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');
|
|
if ('customNameservers' in domainConfig && typeof domainConfig.customNameservers !== 'boolean') throw new BoxError(BoxError.BAD_FIELD, 'customNameservers must be a boolean');
|
|
|
|
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,
|
|
customNameservers: !!domainConfig.customNameservers
|
|
};
|
|
|
|
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');
|
|
|
|
// ovh.net, ovh.ca or anycast.me
|
|
if (!nameservers.every(function (n) { return n.toLowerCase().search(/ovh|kimsufi|anycast/) !== -1; })) {
|
|
debug('verifyDomainConfig: %j does not contain OVH NS', nameservers);
|
|
if (!domainConfig.customNameservers) throw new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to OVH');
|
|
}
|
|
|
|
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;
|
|
}
|