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

266 lines
9.2 KiB
JavaScript
Raw Normal View History

2019-01-16 18:05:42 +02:00
'use strict';
exports = module.exports = {
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/namecheap'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
2019-01-16 18:05:42 +02:00
DomainsError = require('../domains.js').DomainsError,
Namecheap = require('namecheap'),
sysinfo = require('../sysinfo.js'),
util = require('util');
var namecheap;
function formatError(response) {
return util.format('NameCheap DNS error [%s] %j', response.code, response.message);
}
// The keys that NameCheap returns us and the keys we need to provide it differ, so we need to map them properly
function mapHosts(hosts) {
for (var i = 0; i < hosts.length; i++) {
let curHost = hosts[i];
if (curHost.Name && !curHost.HostName) {
curHost.HostName = curHost.Name;
delete curHost.Name;
}
if (curHost.Type && !curHost.RecordType) {
curHost.RecordType = curHost.Type;
delete curHost.Type;
}
}
return hosts;
}
function init(dnsConfig, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
if (namecheap) return callback();
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
// Note that for all NameCheap calls to go through properly, the public IP returned by the getPublicIp method below must be whitelisted on NameCheap's API dashboard
namecheap = new Namecheap(dnsConfig.username, dnsConfig.apiKey, ip);
namecheap.setUsername(dnsConfig.username);
callback();
});
}
2019-01-16 18:05:42 +02:00
function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
init(dnsConfig, function (error) {
if (error) return callback(error);
2019-01-16 18:05:42 +02:00
namecheap.domains.dns.getHosts(zoneName, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(error)));
2019-01-16 18:05:42 +02:00
debug('entire getInternal response: %j', error);
2019-01-16 18:05:42 +02:00
return callback(null, result['DomainDNSGetHostsResult']['host']);
});
2019-01-16 18:05:42 +02:00
});
}
function setInternal(zoneName, hosts, callback) {
let mappedHosts = mapHosts(hosts);
namecheap.domains.dns.setHosts(zoneName, mappedHosts, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(error)));
2019-01-16 18:05:42 +02:00
return callback(null, result);
2019-01-16 18:05:42 +02:00
});
}
function upsert(domainObject, subdomain, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
2019-01-16 18:05:42 +02:00
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
const zoneName = domainObject.zoneName;
subdomain = domains.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);
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
2019-01-16 18:05:42 +02:00
// Array to keep track of records that need to be inserted
let toInsert = [];
for (var i = 0; i < values.length; i++) {
let curValue = values[i];
let wasUpdate = false;
for (var j = 0; j < result.length; j++) {
let curHost = result[j];
if (curHost.Type === type && curHost.Name === subdomain) {
// Updating an already existing host
wasUpdate = true;
if (type === 'MX') {
2019-01-16 18:05:42 +02:00
curHost.MXPref = curValue.split(' ')[0];
curHost.Address = curValue.split(' ')[1];
} else {
curHost.Address = curValue;
}
}
}
// 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') {
2019-01-16 18:05:42 +02:00
newRecord.MXPref = curValue.split(' ')[0];
newRecord.Address = curValue.split(' ')[1];
}
toInsert.push(newRecord);
}
}
let toUpsert = result.concat(toInsert);
setInternal(zoneName, toUpsert, callback);
2019-01-16 18:05:42 +02:00
});
}
function get(domainObject, subdomain, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
2019-01-16 18:05:42 +02:00
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
const zoneName = domainObject.zoneName;
subdomain = domains.getName(domainObject, subdomain, type) || '@';
2019-01-16 18:05:42 +02:00
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
// We need to filter hosts to ones with this subdomain and type
let actualHosts = result.filter((host) => host.Type === type && host.Name === subdomain);
2019-01-16 18:05:42 +02:00
// We only return the value string
var tmp = actualHosts.map(function (record) { return record.Address; });
debug('get: %j', tmp);
return callback(null, tmp);
});
}
function del(domainObject, subdomain, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
2019-01-16 18:05:42 +02:00
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
const zoneName = domainObject.zoneName;
subdomain = domains.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);
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
if (result.length === 0) return callback();
2019-01-16 18:05:42 +02:00
let removed = false;
for (var i = 0; i < values.length; i++) {
let curValue = values[i];
for (var j = 0; j < result.length; j++) {
let curHost = result[i];
if (curHost.Type === type && curHost.Name === subdomain && curHost.Address === curValue) {
removed = true;
2019-01-16 18:05:42 +02:00
result.splice(i, 1); // Remove element from result array
}
}
}
// Only set hosts if we actually removed a host
if (removed) return setInternal(zoneName, result, callback);
callback();
2019-01-16 18:05:42 +02:00
});
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
2019-01-16 18:05:42 +02:00
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
const zoneName = domainObject.zoneName;
const ip = '127.0.0.1';
2019-01-16 18:05:42 +02:00
if (!dnsConfig.username || typeof dnsConfig.username !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'username must be a non-empty string'));
if (!dnsConfig.apiKey || typeof dnsConfig.apiKey !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'apiKey must be a non-empty string'));
var credentials = {
username: dnsConfig.username,
apKey: dnsConfig.apiKey
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
2019-01-22 11:56:56 +01:00
if (nameservers.some(function (n) { return n.toLowerCase().indexOf('.registrar-servers.com') === -1; })) {
2019-01-16 18:05:42 +02:00
debug('verifyDnsConfig: %j does not contains NC NS', nameservers);
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to NameCheap'));
}
const testSubdomain = 'cloudrontestdns';
upsert(domainObject, testSubdomain, 'A', [ip], function (error, changeId) {
2019-01-16 18:05:42 +02:00
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
del(dnsConfig, zoneName, testSubdomain, 'A', [ip], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
callback(null, dnsConfig);
});
});
});
}