12e073e8cf
mostly because code is being autogenerated by all the AI stuff using this prefix. it's also used in the stack trace.
253 lines
9.9 KiB
JavaScript
253 lines
9.9 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/namecom'),
|
|
dig = require('../dig.js'),
|
|
dns = require('../dns.js'),
|
|
safe = require('safetydance'),
|
|
superagent = require('@cloudron/superagent'),
|
|
waitForDns = require('./waitfordns.js');
|
|
|
|
const NAMECOM_API = 'https://api.name.com/v4';
|
|
|
|
function formatError(response) {
|
|
return `name.com 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 addRecord(domainConfig, zoneName, name, type, values) {
|
|
assert.strictEqual(typeof domainConfig, 'object');
|
|
assert.strictEqual(typeof zoneName, 'string');
|
|
assert.strictEqual(typeof name, 'string');
|
|
assert.strictEqual(typeof type, 'string');
|
|
assert(Array.isArray(values));
|
|
|
|
debug(`add: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
|
|
|
const data = {
|
|
host: name,
|
|
type: type,
|
|
ttl: 300 // 300 is the lowest
|
|
};
|
|
|
|
if (type === 'MX') {
|
|
data.priority = parseInt(values[0].split(' ')[0], 10);
|
|
data.answer = values[0].split(' ')[1];
|
|
} else if (type === 'TXT') {
|
|
// we have to strip the quoting for some odd reason for name.com! If you change that also change updateRecord
|
|
const tmp = values[0];
|
|
data.answer = tmp.indexOf('"') === 0 && tmp.lastIndexOf('"') === tmp.length-1 ? tmp.slice(1, tmp.length-1) : tmp;
|
|
} else {
|
|
data.answer = values[0];
|
|
}
|
|
|
|
const [error, response] = await safe(superagent.post(`${NAMECOM_API}/domains/${zoneName}/records`)
|
|
.auth(domainConfig.username, domainConfig.token)
|
|
.timeout(30 * 1000)
|
|
.send(data)
|
|
.ok(() => true));
|
|
|
|
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
|
|
if (response.status === 403) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response));
|
|
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response));
|
|
}
|
|
|
|
async function updateRecord(domainConfig, zoneName, recordId, name, type, values) {
|
|
assert.strictEqual(typeof domainConfig, 'object');
|
|
assert.strictEqual(typeof zoneName, 'string');
|
|
assert.strictEqual(typeof recordId, 'number');
|
|
assert.strictEqual(typeof name, 'string');
|
|
assert.strictEqual(typeof type, 'string');
|
|
assert(Array.isArray(values));
|
|
|
|
debug(`update:${recordId} on ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
|
|
|
const data = {
|
|
host: name,
|
|
type: type,
|
|
ttl: 300 // 300 is the lowest
|
|
};
|
|
|
|
if (type === 'MX') {
|
|
data.priority = parseInt(values[0].split(' ')[0], 10);
|
|
data.answer = values[0].split(' ')[1];
|
|
} else if (type === 'TXT') {
|
|
// we have to strip the quoting for some odd reason for name.com! If you change that also change addRecord
|
|
const tmp = values[0];
|
|
data.answer = tmp.indexOf('"') === 0 && tmp.lastIndexOf('"') === tmp.length-1 ? tmp.slice(1, tmp.length-1) : tmp;
|
|
} else {
|
|
data.answer = values[0];
|
|
}
|
|
|
|
const [error, response] = await safe(superagent.put(`${NAMECOM_API}/domains/${zoneName}/records/${recordId}`)
|
|
.auth(domainConfig.username, domainConfig.token)
|
|
.timeout(30 * 1000)
|
|
.send(data)
|
|
.ok(() => true));
|
|
|
|
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
|
|
if (response.status === 403) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response));
|
|
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response));
|
|
}
|
|
|
|
async function getInternal(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(`getInternal: ${name} in zone ${zoneName} of type ${type}`);
|
|
|
|
const [error, response] = await safe(superagent.get(`${NAMECOM_API}/domains/${zoneName}/records`)
|
|
.auth(domainConfig.username, domainConfig.token)
|
|
.timeout(30 * 1000)
|
|
.ok(() => true));
|
|
|
|
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
|
|
if (response.status === 403) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response));
|
|
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response));
|
|
|
|
// name.com does not return the correct content-type
|
|
response.body = safe.JSON.parse(response.text);
|
|
if (!response.body.records) response.body.records = [];
|
|
|
|
response.body.records.forEach(function (r) {
|
|
// name.com api simply strips empty properties
|
|
r.host = r.host || '';
|
|
});
|
|
|
|
const results = response.body.records.filter(function (r) {
|
|
return (r.host === name && r.type === type);
|
|
});
|
|
|
|
return results;
|
|
}
|
|
|
|
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 result = await getInternal(domainConfig, zoneName, name, type);
|
|
if (result.length === 0) return await addRecord(domainConfig, zoneName, name, type, values);
|
|
|
|
return await updateRecord(domainConfig, zoneName, result[0].id, name, type, values);
|
|
}
|
|
|
|
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 result = await getInternal(domainConfig, zoneName, name, type);
|
|
const tmp = result.map(function (record) { return record.answer; });
|
|
return tmp;
|
|
}
|
|
|
|
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 result = await getInternal(domainConfig, zoneName, name, type);
|
|
if (result.length === 0) return;
|
|
|
|
const [error, response] = await safe(superagent.del(`${NAMECOM_API}/domains/${zoneName}/records/${result[0].id}`)
|
|
.auth(domainConfig.username, domainConfig.token)
|
|
.timeout(30 * 1000)
|
|
.ok(() => true));
|
|
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
|
|
if (response.status === 403) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response));
|
|
if (response.status !== 200) 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 (typeof domainConfig.username !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'username must be a string');
|
|
if (typeof domainConfig.token !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'token must be a string');
|
|
if ('customNameservers' in domainConfig && typeof domainConfig.customNameservers !== 'boolean') throw new BoxError(BoxError.BAD_FIELD, 'customNameservers must be a boolean');
|
|
|
|
const credentials = {
|
|
username: domainConfig.username,
|
|
token: domainConfig.token,
|
|
customNameservers: !!domainConfig.customNameservers
|
|
};
|
|
|
|
const ip = '127.0.0.1';
|
|
|
|
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');
|
|
|
|
if (!nameservers.every(function (n) { return n.toLowerCase().indexOf('.name.com') !== -1; })) {
|
|
debug('verifyDomainConfig: %j does not contain Name.com NS', nameservers);
|
|
if (!domainConfig.customNameservers) throw new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to name.com');
|
|
}
|
|
|
|
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;
|
|
}
|