298 lines
10 KiB
JavaScript
298 lines
10 KiB
JavaScript
'use strict';
|
|
|
|
exports = module.exports = {
|
|
removePrivateFields: removePrivateFields,
|
|
injectPrivateFields: injectPrivateFields,
|
|
upsert: upsert,
|
|
get: get,
|
|
del: del,
|
|
verifyDnsConfig: verifyDnsConfig,
|
|
wait: wait
|
|
};
|
|
|
|
var assert = require('assert'),
|
|
debug = require('debug')('box:dns/namecheap'),
|
|
dns = require('../native-dns.js'),
|
|
domains = require('../domains.js'),
|
|
DomainsError = require('../domains.js').DomainsError,
|
|
Namecheap = require('namecheap'),
|
|
sysinfo = require('../sysinfo.js'),
|
|
util = require('util'),
|
|
waitForDns = require('./waitfordns.js');
|
|
|
|
function formatError(response) {
|
|
return util.format('NameCheap DNS error [%s] %j', response.code, response.message);
|
|
}
|
|
|
|
function removePrivateFields(domainObject) {
|
|
domainObject.config.token = domains.SECRET_PLACEHOLDER;
|
|
return domainObject;
|
|
}
|
|
|
|
function injectPrivateFields(newConfig, currentConfig) {
|
|
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
|
}
|
|
|
|
// Only send required fields - https://www.namecheap.com/support/api/methods/domains-dns/set-hosts.aspx
|
|
function mapHosts(hosts) {
|
|
return hosts.map(function (host) {
|
|
let tmp = {};
|
|
|
|
tmp.TTL = '300';
|
|
tmp.RecordType = host.RecordType || host.Type;
|
|
tmp.HostName = host.HostName || host.Name;
|
|
tmp.Address = host.Address;
|
|
|
|
if (tmp.RecordType === 'MX') {
|
|
tmp.EmailType = 'MX';
|
|
if (host.MXPref) tmp.MXPref = host.MXPref;
|
|
}
|
|
|
|
return tmp;
|
|
});
|
|
}
|
|
|
|
function getApi(dnsConfig, callback) {
|
|
assert.strictEqual(typeof dnsConfig, 'object');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
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
|
|
let namecheap = new Namecheap(dnsConfig.username, dnsConfig.token, ip);
|
|
namecheap.setUsername(dnsConfig.username);
|
|
|
|
callback(null, namecheap);
|
|
});
|
|
}
|
|
|
|
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');
|
|
|
|
getApi(dnsConfig, function (error, namecheap) {
|
|
if (error) return callback(error);
|
|
|
|
namecheap.domains.dns.getHosts(zoneName, function (error, result) {
|
|
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(error)));
|
|
|
|
debug('entire getInternal response: %j', result);
|
|
|
|
return callback(null, result['DomainDNSGetHostsResult']['host']);
|
|
});
|
|
});
|
|
}
|
|
|
|
function setInternal(dnsConfig, zoneName, hosts, callback) {
|
|
assert.strictEqual(typeof dnsConfig, 'object');
|
|
assert.strictEqual(typeof zoneName, 'string');
|
|
assert(Array.isArray(hosts));
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
let mappedHosts = mapHosts(hosts);
|
|
|
|
getApi(dnsConfig, function (error, namecheap) {
|
|
if (error) return callback(error);
|
|
|
|
namecheap.domains.dns.setHosts(zoneName, mappedHosts, function (error, result) {
|
|
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(error)));
|
|
|
|
return callback(null, result);
|
|
});
|
|
});
|
|
}
|
|
|
|
function upsert(domainObject, subdomain, type, values, callback) {
|
|
assert.strictEqual(typeof domainObject, 'object');
|
|
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) || '@';
|
|
|
|
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);
|
|
|
|
// 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') {
|
|
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') {
|
|
newRecord.MXPref = curValue.split(' ')[0];
|
|
newRecord.Address = curValue.split(' ')[1];
|
|
}
|
|
|
|
toInsert.push(newRecord);
|
|
|
|
}
|
|
}
|
|
|
|
let toUpsert = result.concat(toInsert);
|
|
|
|
setInternal(dnsConfig, zoneName, toUpsert, callback);
|
|
});
|
|
}
|
|
|
|
function get(domainObject, subdomain, type, callback) {
|
|
assert.strictEqual(typeof domainObject, 'object');
|
|
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) || '@';
|
|
|
|
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);
|
|
|
|
// 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');
|
|
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) || '@';
|
|
|
|
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();
|
|
|
|
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;
|
|
|
|
result.splice(i, 1); // Remove element from result array
|
|
}
|
|
}
|
|
}
|
|
|
|
// Only set hosts if we actually removed a host
|
|
if (removed) return setInternal(dnsConfig, zoneName, result, callback);
|
|
|
|
callback();
|
|
});
|
|
}
|
|
|
|
function verifyDnsConfig(domainObject, callback) {
|
|
assert.strictEqual(typeof domainObject, 'object');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
const dnsConfig = domainObject.config;
|
|
const zoneName = domainObject.zoneName;
|
|
const ip = '127.0.0.1';
|
|
|
|
if (!dnsConfig.username || typeof dnsConfig.username !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'username must be a non-empty string'));
|
|
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
|
|
|
|
let credentials = {
|
|
username: dnsConfig.username,
|
|
token: dnsConfig.token
|
|
};
|
|
|
|
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'));
|
|
|
|
if (nameservers.some(function (n) { return n.toLowerCase().indexOf('.registrar-servers.com') === -1; })) {
|
|
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) {
|
|
if (error) return callback(error);
|
|
|
|
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
|
|
|
|
del(domainObject, testSubdomain, 'A', [ip], function (error) {
|
|
if (error) return callback(error);
|
|
|
|
debug('verifyDnsConfig: Test A record removed again');
|
|
|
|
callback(null, credentials);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function wait(domainObject, subdomain, type, value, options, callback) {
|
|
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 }
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
const fqdn = domains.fqdn(subdomain, domainObject);
|
|
|
|
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
|
|
}
|