Files
cloudron-box/src/dns/namecheap.js

285 lines
11 KiB
JavaScript
Raw Normal View History

2019-01-16 18:05:42 +02:00
'use strict';
exports = module.exports = {
2021-08-13 17:22:28 -07:00
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
verifyDomainConfig,
2021-08-13 17:22:28 -07:00
wait
2019-01-16 18:05:42 +02:00
};
2021-08-13 17:22:28 -07:00
const assert = require('assert'),
2019-10-23 10:02:04 -07:00
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
2019-01-16 18:05:42 +02:00
debug = require('debug')('box:dns/namecheap'),
2022-02-04 15:34:02 -08:00
dig = require('../dig.js'),
2021-08-13 17:22:28 -07:00
dns = require('../dns.js'),
network = require('../network.js'),
2019-05-16 17:00:17 +02:00
safe = require('safetydance'),
superagent = require('superagent'),
timers = require('timers/promises'),
util = require('util'),
waitForDns = require('./waitfordns.js'),
xml2js = require('xml2js');
2019-01-16 18:05:42 +02:00
const ENDPOINT = 'https://api.namecheap.com/xml.response';
2019-01-16 18:05:42 +02:00
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 getQuery(domainConfig) {
assert.strictEqual(typeof domainConfig, 'object');
const ip = await network.getIPv4(); // only supports ipv4
// https://www.namecheap.com/support/knowledgebase/article.aspx/9739/63/api-faq/#z . 50 / minute
await timers.setTimeout(5000); // limits to 12req/min for this process. we can have 3 apptasks in parallel
return {
ApiUser: domainConfig.username,
ApiKey: domainConfig.token,
UserName: domainConfig.username,
ClientIp: ip
};
}
2022-02-04 15:34:02 -08:00
async function getZone(domainConfig, zoneName) {
assert.strictEqual(typeof domainConfig, 'object');
2019-01-16 18:05:42 +02:00
assert.strictEqual(typeof zoneName, 'string');
2022-02-04 15:34:02 -08:00
const query = await getQuery(domainConfig);
query.Command = 'namecheap.domains.dns.getHosts';
query.SLD = zoneName.split('.', 1)[0];
query.TLD = zoneName.slice(query.SLD.length + 1);
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
const [error, response] = await safe(superagent.get(ENDPOINT).query(query).ok(() => true));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
const parser = new xml2js.Parser();
const [parserError, result] = await safe(util.promisify(parser.parseString)(response.text));
if (parserError) throw new BoxError(BoxError.EXTERNAL_ERROR, parserError);
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
const tmp = result.ApiResponse;
if (tmp['$'].Status !== 'OK') {
const errorMessage = safe.query(tmp, 'Errors[0].Error[0]._', 'Invalid response');
if (errorMessage === 'API Key is invalid or API access has not been enabled') throw new BoxError(BoxError.ACCESS_DENIED, errorMessage);
2022-02-04 15:34:02 -08:00
throw new BoxError(BoxError.EXTERNAL_ERROR, errorMessage);
}
const host = safe.query(tmp, 'CommandResponse[0].DomainDNSGetHostsResult[0].host', []);
2022-02-04 15:34:02 -08:00
if (!host) throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response: ${JSON.stringify(tmp)}`);
if (!Array.isArray(host)) throw new BoxError(BoxError.EXTERNAL_ERROR, `host is not an array: ${JSON.stringify(tmp)}`);
2019-09-23 20:00:49 +02:00
2022-02-04 15:34:02 -08:00
const hosts = host.map(h => h['$']);
return hosts;
2019-01-16 18:05:42 +02:00
}
2022-02-04 15:34:02 -08:00
async function setZone(domainConfig, zoneName, hosts) {
assert.strictEqual(typeof domainConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert(Array.isArray(hosts));
2022-02-04 15:34:02 -08:00
const query = await getQuery(domainConfig);
query.Command = 'namecheap.domains.dns.setHosts';
query.SLD = zoneName.split('.', 1)[0];
query.TLD = zoneName.slice(query.SLD.length + 1);
2022-02-04 15:34:02 -08:00
// Map to query params https://www.namecheap.com/support/api/methods/domains-dns/set-hosts.aspx
hosts.forEach(function (host, i) {
2022-04-14 17:41:41 -05:00
const n = i+1; // api starts with 1 not 0
2022-02-04 15:34:02 -08:00
query['TTL' + n] = '300'; // keep it low
query['HostName' + n] = host.HostName || host.Name;
query['RecordType' + n] = host.RecordType || host.Type;
query['Address' + n] = host.Address;
if (host.Type === 'MX') {
query['EmailType' + n] = 'MX';
if (host.MXPref) query['MXPref' + n] = host.MXPref;
}
});
2021-04-13 21:36:05 -07:00
2022-02-04 15:34:02 -08:00
// namecheap recommends sending as POSTDATA with > 10 records
const qs = new URLSearchParams(query).toString();
2022-02-04 15:34:02 -08:00
const [error, response] = await safe(superagent.post(ENDPOINT).set('Content-Type', 'application/x-www-form-urlencoded').send(qs).ok(() => true));
if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, error);
2022-02-04 15:34:02 -08:00
const parser = new xml2js.Parser();
2022-02-06 10:58:49 -08:00
const [parserError, result] = await safe(util.promisify(parser.parseString)(response.text));
2022-02-04 15:34:02 -08:00
if (parserError) throw new BoxError(BoxError.EXTERNAL_ERROR, parserError.message);
2019-09-23 20:00:49 +02:00
2022-02-04 15:34:02 -08:00
const tmp = result.ApiResponse;
if (tmp['$'].Status !== 'OK') {
const errorMessage = safe.query(tmp, 'Errors[0].Error[0]._', 'Invalid response');
if (errorMessage === 'API Key is invalid or API access has not been enabled') throw new BoxError(BoxError.ACCESS_DENIED, errorMessage);
2022-02-04 15:34:02 -08:00
throw new BoxError(BoxError.EXTERNAL_ERROR, errorMessage);
}
if (!tmp.CommandResponse[0]) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response');
if (!tmp.CommandResponse[0].DomainDNSSetHostsResult[0]) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response');
if (tmp.CommandResponse[0].DomainDNSSetHostsResult[0]['$'].IsSuccess !== 'true') throw new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response');
2019-01-16 18:05:42 +02:00
}
2022-02-04 15:34:02 -08:00
async function upsert(domainObject, subdomain, type, values) {
assert.strictEqual(typeof domainObject, 'object');
2019-01-16 18:05:42 +02:00
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
2021-04-13 15:10:24 -07:00
assert(Array.isArray(values));
2019-01-16 18:05:42 +02:00
const domainConfig = domainObject.config;
const zoneName = domainObject.zoneName;
2021-08-13 17:22:28 -07:00
subdomain = dns.getName(domainObject, subdomain, type) || '@';
2019-01-16 18:05:42 +02:00
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
2022-02-04 15:34:02 -08:00
const result = await getZone(domainConfig, zoneName);
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
// Array to keep track of records that need to be inserted
let toInsert = [];
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
for (let i = 0; i < values.length; i++) {
let curValue = values[i];
let wasUpdate = false;
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
for (let j = 0; j < result.length; j++) {
let curHost = result[j];
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
if (curHost.Type === type && curHost.Name === subdomain) {
// Updating an already existing host
wasUpdate = true;
if (type === 'MX') {
curHost.MXPref = curValue.split(' ')[0];
curHost.Address = curValue.split(' ')[1];
} else {
curHost.Address = curValue;
2019-01-16 18:05:42 +02:00
}
}
2022-02-04 15:34:02 -08:00
}
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
// We don't have this host at all yet, let's push to toInsert array
if (!wasUpdate) {
let newRecord = {
RecordType: type,
HostName: subdomain,
Address: curValue
};
// Special case for MX records
if (type === 'MX') {
newRecord.MXPref = curValue.split(' ')[0];
newRecord.Address = curValue.split(' ')[1];
}
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
toInsert.push(newRecord);
2019-01-16 18:05:42 +02:00
}
2022-02-04 15:34:02 -08:00
}
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
const hosts = result.concat(toInsert);
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
return await setZone(domainConfig, zoneName, hosts);
2019-01-16 18:05:42 +02:00
}
2022-02-04 15:34:02 -08:00
async function get(domainObject, subdomain, type) {
assert.strictEqual(typeof domainObject, 'object');
2019-01-16 18:05:42 +02:00
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
const domainConfig = domainObject.config;
const zoneName = domainObject.zoneName;
2021-08-13 17:22:28 -07:00
subdomain = dns.getName(domainObject, subdomain, type) || '@';
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
const result = await getZone(domainConfig, zoneName);
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
// We need to filter hosts to ones with this subdomain and type
const actualHosts = result.filter((host) => host.Type === type && host.Name === subdomain);
2022-02-04 15:34:02 -08:00
const tmp = actualHosts.map(function (record) { return record.Address; });
return tmp;
2019-01-16 18:05:42 +02:00
}
2022-02-04 15:34:02 -08:00
async function del(domainObject, subdomain, type, values) {
assert.strictEqual(typeof domainObject, 'object');
2019-01-16 18:05:42 +02:00
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
2021-04-13 15:10:24 -07:00
assert(Array.isArray(values));
2019-01-16 18:05:42 +02:00
const domainConfig = domainObject.config;
const zoneName = domainObject.zoneName;
2021-08-13 17:22:28 -07:00
subdomain = dns.getName(domainObject, subdomain, type) || '@';
2019-01-16 18:05:42 +02:00
debug('del: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
2022-02-04 15:34:02 -08:00
let result = await getZone(domainConfig, zoneName);
if (result.length === 0) return;
const originalLength = result.length;
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
for (let i = 0; i < values.length; i++) {
let curValue = values[i];
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
result = result.filter(curHost => curHost.Type !== type || curHost.Name !== subdomain || curHost.Address !== curValue);
}
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
if (result.length !== originalLength) return await setZone(domainConfig, zoneName, result);
}
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 }
2019-01-16 18:05:42 +02:00
const fqdn = dns.fqdn(subdomain, domainObject.domain);
2022-02-04 15:34:02 -08:00
await waitForDns(fqdn, domainObject.zoneName, type, value, options);
2019-01-16 18:05:42 +02:00
}
2022-02-04 15:34:02 -08:00
async function verifyDomainConfig(domainObject) {
assert.strictEqual(typeof domainObject, 'object');
2019-01-16 18:05:42 +02:00
const domainConfig = domainObject.config;
const zoneName = domainObject.zoneName;
const ip = '127.0.0.1';
2022-02-04 15:34:02 -08:00
if (!domainConfig.username || typeof domainConfig.username !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'username must be a non-empty string');
if (!domainConfig.token || typeof domainConfig.token !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'token must be a non-empty string');
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
const credentials = {
username: domainConfig.username,
token: domainConfig.token
2019-01-16 18:05:42 +02:00
};
2023-10-01 13:52:19 +05:30
if (constants.TEST) return credentials; // this shouldn't be here
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
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');
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
if (nameservers.some(function (n) { return n.toLowerCase().indexOf('.registrar-servers.com') === -1; })) {
debug('verifyDomainConfig: %j does not contains NC NS', nameservers);
throw new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to NameCheap');
}
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
const testSubdomain = 'cloudrontestdns';
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
await upsert(domainObject, testSubdomain, 'A', [ip]);
debug('verifyDomainConfig: Test A record added');
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
await del(domainObject, testSubdomain, 'A', [ip]);
debug('verifyDomainConfig: Test A record removed again');
2019-01-16 18:05:42 +02:00
2022-02-04 15:34:02 -08:00
return credentials;
}