12e073e8cf
mostly because code is being autogenerated by all the AI stuff using this prefix. it's also used in the stack trace.
220 lines
8.9 KiB
JavaScript
220 lines
8.9 KiB
JavaScript
'use strict';
|
|
|
|
exports = module.exports = {
|
|
removePrivateFields,
|
|
injectPrivateFields,
|
|
upsert,
|
|
get,
|
|
del,
|
|
wait,
|
|
verifyDomainConfig
|
|
};
|
|
|
|
const { ApiClient, Language } = require('domrobot-client'),
|
|
assert = require('node:assert'),
|
|
BoxError = require('../boxerror.js'),
|
|
constants = require('../constants.js'),
|
|
debug = require('debug')('box:dns/inwx'),
|
|
dig = require('../dig.js'),
|
|
dns = require('../dns.js'),
|
|
safe = require('safetydance'),
|
|
waitForDns = require('./waitfordns.js');
|
|
|
|
function formatError(response) {
|
|
return `INWX Api error error [Code: [${response.code}] Message: ${response.msg}`;
|
|
}
|
|
|
|
function removePrivateFields(domainObject) {
|
|
domainObject.config.password = constants.SECRET_PLACEHOLDER;
|
|
return domainObject;
|
|
}
|
|
|
|
function injectPrivateFields(newConfig, currentConfig) {
|
|
if (newConfig.password === constants.SECRET_PLACEHOLDER) newConfig.password = currentConfig.password;
|
|
}
|
|
|
|
// https://www.inwx.com/en/help/apidoc/f/ch04.html
|
|
function translateError(response) {
|
|
if (response.code === 2200 || response.code === 2201 || response.code === 2202) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response));
|
|
if (response.code === 2003 || response.code === 2004 || response.code === 2005) throw new BoxError(BoxError.BAD_FIELD, formatError(response));
|
|
if (response.code !== 1000) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response));
|
|
}
|
|
|
|
async function login(domainConfig) {
|
|
const apiClient = new ApiClient(ApiClient.API_URL_LIVE, Language.EN, false /* debug mode */);
|
|
|
|
const sharedSecret = ''; // 2FA
|
|
const [error, response] = await safe(apiClient.login(domainConfig.username, domainConfig.password, sharedSecret));
|
|
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
|
|
if (response.code !== 1000) throw new BoxError(BoxError. ACCESS_DENIED, `Api login error. Code: ${response.code} Message: ${response.msg}`);
|
|
|
|
return apiClient;
|
|
}
|
|
|
|
async function getDnsRecords(domainConfig, apiClient, zoneName, fqdn, type) {
|
|
assert.strictEqual(typeof domainConfig, 'object');
|
|
assert.strictEqual(typeof apiClient, 'object');
|
|
assert.strictEqual(typeof zoneName, 'string');
|
|
assert.strictEqual(typeof fqdn, 'string');
|
|
assert.strictEqual(typeof type, 'string');
|
|
|
|
debug(`getDnsRecords: ${fqdn} in zone ${zoneName} of type ${type}`);
|
|
|
|
const [error, response] = await safe(apiClient.callApi('nameserver.info', { domain: zoneName, name: fqdn, type }));
|
|
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
|
|
if (response.code !== 1000) throw translateError(response);
|
|
|
|
return response.resData.record || []; // 'record' property will be missing if no 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;
|
|
|
|
debug(`upsert: ${location} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
|
|
|
const apiClient = await login(domainConfig);
|
|
const fqdn = dns.fqdn(location, domainObject.domain);
|
|
const records = await getDnsRecords(domainConfig, apiClient, zoneName, fqdn, type);
|
|
|
|
let i = 0; // // used to track available records to update instead of create
|
|
|
|
for (let value of values) {
|
|
let priority = 0;
|
|
|
|
if (type === 'MX') {
|
|
priority = parseInt(value.split(' ')[0], 10);
|
|
value = value.split(' ')[1];
|
|
}
|
|
|
|
if (i >= records.length) { // create a new record
|
|
const data = {
|
|
type,
|
|
name: fqdn,
|
|
domain: zoneName,
|
|
content: value,
|
|
prio: priority,
|
|
ttl: 300 // 300 to 2764800
|
|
};
|
|
const [error, response] = await safe(apiClient.callApi('nameserver.createRecord', data));
|
|
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
|
|
if (response.code !== 1000) throw translateError(response);
|
|
} else { // replace existing record
|
|
const data = {
|
|
id: records[i].id,
|
|
type,
|
|
name: fqdn,
|
|
content: value,
|
|
};
|
|
const [error, response] = await safe(apiClient.callApi('nameserver.updateRecord', data));
|
|
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
|
|
if (response.code !== 1000) throw translateError(response);
|
|
++i; // increment, as we have consumed the record
|
|
}
|
|
}
|
|
|
|
for (let j = values.length + 1; j < records.length; j++) {
|
|
const [error, response] = await safe(apiClient.callApi('nameserver.deleteRecord', { id: records[j].id }));
|
|
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
|
|
if (response.code !== 1000) throw translateError(response);
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
const apiClient = await login(domainConfig);
|
|
const fqdn = dns.fqdn(location, domainObject.domain);
|
|
const result = await getDnsRecords(domainConfig, apiClient, zoneName, fqdn, type);
|
|
const tmp = result.map(function (record) { return record.content; });
|
|
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;
|
|
|
|
debug(`del: ${location} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
|
|
|
const apiClient = await login(domainConfig);
|
|
const fqdn = dns.fqdn(location, domainObject.domain);
|
|
const result = await getDnsRecords(domainConfig, apiClient, zoneName, fqdn, type);
|
|
if (result.length === 0) return;
|
|
|
|
const tmp = result.filter(function (record) { return values.some(function (value) { return value === record.content; }); });
|
|
debug('del: %j', tmp);
|
|
|
|
for (const r of tmp) {
|
|
const [error, response] = await safe(apiClient.callApi('nameserver.deleteRecord', { id: r.id }));
|
|
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
|
|
if (response.code !== 1000) throw translateError(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.password !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'password 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,
|
|
password: domainConfig.password,
|
|
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().search(/inwx|xnameserver|domrobot/) !== -1; })) {
|
|
debug('verifyDomainConfig: %j does not contain INWX NS', nameservers);
|
|
if (!domainConfig.customNameservers) throw new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to INWX');
|
|
}
|
|
|
|
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;
|
|
}
|