12e073e8cf
mostly because code is being autogenerated by all the AI stuff using this prefix. it's also used in the stack trace.
169 lines
6.6 KiB
JavaScript
169 lines
6.6 KiB
JavaScript
'use strict';
|
|
|
|
exports = module.exports = {
|
|
removePrivateFields,
|
|
injectPrivateFields,
|
|
upsert,
|
|
get,
|
|
del,
|
|
wait,
|
|
verifyDomainConfig
|
|
};
|
|
|
|
const assert = require('node:assert'),
|
|
constants = require('../constants.js'),
|
|
BoxError = require('../boxerror.js'),
|
|
debug = require('debug')('box:dns/vultr'),
|
|
dig = require('../dig.js'),
|
|
dns = require('../dns.js'),
|
|
safe = require('safetydance'),
|
|
timers = require('timers/promises'),
|
|
superagent = require('@cloudron/superagent'),
|
|
waitForDns = require('./waitfordns.js');
|
|
|
|
const DESEC_ENDPOINT = 'https://desec.io/api/v1';
|
|
|
|
function formatError(response) {
|
|
return `deSEC DNS error [${response.status}] ${response.text}`;
|
|
}
|
|
|
|
function removePrivateFields(domainObject) {
|
|
domainObject.config.token = constants.SECRET_PLACEHOLDER;
|
|
return domainObject;
|
|
}
|
|
|
|
function injectPrivateFields(newConfig, currentConfig) {
|
|
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
|
}
|
|
|
|
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) || '@';
|
|
|
|
await timers.setTimeout(1000); // https://desec.readthedocs.io/en/latest/rate-limits.html
|
|
|
|
const [error, response] = await safe(superagent.get(`${DESEC_ENDPOINT}/domains/${zoneName}/rrsets/${name}/${type}`)
|
|
.set('Authorization', `Token ${domainConfig.token}`)
|
|
.timeout(30 * 1000)
|
|
.retry(5)
|
|
.ok(() => true));
|
|
|
|
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
|
|
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));
|
|
if (!Array.isArray(response.body.records)) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response));
|
|
|
|
return response.body.records;
|
|
}
|
|
|
|
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) || ''; // when upsert, we use '' and not '@'
|
|
|
|
await del(domainObject, location, type, values);
|
|
|
|
const data = {
|
|
subname: name,
|
|
type,
|
|
ttl: 3600, // min
|
|
records: values
|
|
};
|
|
|
|
await timers.setTimeout(1000); // https://desec.readthedocs.io/en/latest/rate-limits.html
|
|
const [error, response] = await safe(superagent.post(`${DESEC_ENDPOINT}/domains/${zoneName}/rrsets/`)
|
|
.set('Authorization', `Token ${domainConfig.token}`)
|
|
.send(data)
|
|
.timeout(30 * 1000)
|
|
.retry(5)
|
|
.ok(() => true));
|
|
|
|
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
|
|
if (response.status === 403 || response.status === 401) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response));
|
|
if (response.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response));
|
|
}
|
|
|
|
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) || '@';
|
|
|
|
await timers.setTimeout(1000); // https://desec.readthedocs.io/en/latest/rate-limits.html
|
|
const [error, response] = await safe(superagent.del(`${DESEC_ENDPOINT}/domains/${zoneName}/rrsets/${name}/${type}/`)
|
|
.set('Authorization', `Token ${domainConfig.token}`)
|
|
.timeout(30 * 1000)
|
|
.retry(5)
|
|
.ok(() => true));
|
|
|
|
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
|
|
if (response.status === 404) return;
|
|
if (response.status === 403 || response.status === 401) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response));
|
|
if (response.status !== 204) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response));
|
|
}
|
|
|
|
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.token || typeof domainConfig.token !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'token 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 = {
|
|
token: domainConfig.token,
|
|
customNameservers: !!domainConfig.customNameservers
|
|
};
|
|
|
|
if (constants.TEST) 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');
|
|
|
|
if (!nameservers.every(function (n) { return n.toLowerCase().indexOf('.desec.') !== -1; })) {
|
|
debug('verifyDomainConfig: %j does not contains deSEC NS', nameservers);
|
|
if (!domainConfig.customNameservers) throw new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to deSEC');
|
|
}
|
|
|
|
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;
|
|
}
|