diff --git a/src/dns/namecheap.js b/src/dns/namecheap.js index 498b5c769..e7214ab39 100644 --- a/src/dns/namecheap.js +++ b/src/dns/namecheap.js @@ -14,8 +14,8 @@ const assert = require('assert'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), debug = require('debug')('box:dns/namecheap'), + dig = require('../dig.js'), dns = require('../dns.js'), - querystring = require('querystring'), safe = require('safetydance'), superagent = require('superagent'), sysinfo = require('../sysinfo.js'), @@ -47,103 +47,88 @@ async function getQuery(domainConfig) { }; } -function getZone(domainConfig, zoneName, callback) { +async function getZone(domainConfig, zoneName) { assert.strictEqual(typeof domainConfig, 'object'); assert.strictEqual(typeof zoneName, 'string'); - assert.strictEqual(typeof callback, 'function'); - util.callbackify(getQuery)(domainConfig, function (error, query) { - if (error) return callback(error); + const query = await getQuery(domainConfig); + query.Command = 'namecheap.domains.dns.getHosts'; + query.SLD = zoneName.split('.')[0]; + query.TLD = zoneName.split('.')[1]; - query.Command = 'namecheap.domains.dns.getHosts'; - query.SLD = zoneName.split('.')[0]; - query.TLD = zoneName.split('.')[1]; + const [error, response] = await safe(superagent.get(ENDPOINT).query(query).ok(() => true)); + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); - superagent.get(ENDPOINT).query(query).end(function (error, result) { - if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error)); + 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); - var parser = new xml2js.Parser(); - parser.parseString(result.text, function (error, result) { - if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error)); + 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); - var tmp = result.ApiResponse; - if (tmp['$'].Status !== 'OK') { - var errorMessage = safe.query(tmp, 'Errors[0].Error[0]._', 'Invalid response'); - if (errorMessage === 'API Key is invalid or API access has not been enabled') return callback(new BoxError(BoxError.ACCESS_DENIED, errorMessage)); + throw new BoxError(BoxError.EXTERNAL_ERROR, errorMessage); + } + const host = safe.query(tmp, 'CommandResponse[0].DomainDNSGetHostsResult[0].host'); + 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)}`); - return callback(new BoxError(BoxError.EXTERNAL_ERROR, errorMessage)); - } - const host = safe.query(tmp, 'CommandResponse[0].DomainDNSGetHostsResult[0].host'); - if (!host) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response: ${JSON.stringify(tmp)}`)); - if (!Array.isArray(host)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `host is not an array: ${JSON.stringify(tmp)}`)); - - const hosts = host.map(h => h['$']); - callback(null, hosts); - }); - }); - }); + const hosts = host.map(h => h['$']); + return hosts; } -function setZone(domainConfig, zoneName, hosts, callback) { +async function setZone(domainConfig, zoneName, hosts) { assert.strictEqual(typeof domainConfig, 'object'); assert.strictEqual(typeof zoneName, 'string'); assert(Array.isArray(hosts)); - assert.strictEqual(typeof callback, 'function'); - util.callbackify(getQuery)(domainConfig, function (error, query) { - if (error) return callback(error); + const query = await getQuery(domainConfig); + query.Command = 'namecheap.domains.dns.setHosts'; + query.SLD = zoneName.split('.')[0]; + query.TLD = zoneName.split('.')[1]; - query.Command = 'namecheap.domains.dns.setHosts'; - query.SLD = zoneName.split('.')[0]; - query.TLD = zoneName.split('.')[1]; + // Map to query params https://www.namecheap.com/support/api/methods/domains-dns/set-hosts.aspx + hosts.forEach(function (host, i) { + var n = i+1; // api starts with 1 not 0 + 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; - // Map to query params https://www.namecheap.com/support/api/methods/domains-dns/set-hosts.aspx - hosts.forEach(function (host, i) { - var n = i+1; // api starts with 1 not 0 - 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; - } - }); - - // namecheap recommends sending as POSTDATA with > 10 records - const qs = querystring.stringify(query); - - superagent.post(ENDPOINT).set('Content-Type', 'application/x-www-form-urlencoded').send(qs).end(function (error, result) { - if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error)); - - var parser = new xml2js.Parser(); - parser.parseString(result.text, function (error, result) { - if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error)); - - var tmp = result.ApiResponse; - if (tmp['$'].Status !== 'OK') { - var errorMessage = safe.query(tmp, 'Errors[0].Error[0]._', 'Invalid response'); - if (errorMessage === 'API Key is invalid or API access has not been enabled') return callback(new BoxError(BoxError.ACCESS_DENIED, errorMessage)); - - return callback(new BoxError(BoxError.EXTERNAL_ERROR, errorMessage)); - } - if (!tmp.CommandResponse[0]) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response')); - if (!tmp.CommandResponse[0].DomainDNSSetHostsResult[0]) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response')); - if (tmp.CommandResponse[0].DomainDNSSetHostsResult[0]['$'].IsSuccess !== 'true') return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response')); - - callback(null); - }); - }); + if (host.Type === 'MX') { + query['EmailType' + n] = 'MX'; + if (host.MXPref) query['MXPref' + n] = host.MXPref; + } }); + + // namecheap recommends sending as POSTDATA with > 10 records + const qs = new URLSearchParams(query).toString(); + + 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); + + 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.message); + + 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); + + 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'); } -function upsert(domainObject, subdomain, type, values, callback) { +async function upsert(domainObject, subdomain, type, values) { assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof type, 'string'); assert(Array.isArray(values)); - assert.strictEqual(typeof callback, 'function'); const domainConfig = domainObject.config; const zoneName = domainObject.zoneName; @@ -152,88 +137,78 @@ function upsert(domainObject, subdomain, type, values, callback) { debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values); - getZone(domainConfig, zoneName, function (error, result) { - if (error) return callback(error); + const result = await getZone(domainConfig, zoneName); - // Array to keep track of records that need to be inserted - let toInsert = []; + // Array to keep track of records that need to be inserted + let toInsert = []; - for (let i = 0; i < values.length; i++) { - let curValue = values[i]; - let wasUpdate = false; + for (let i = 0; i < values.length; i++) { + let curValue = values[i]; + let wasUpdate = false; - for (let j = 0; j < result.length; j++) { - let curHost = result[j]; + for (let 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 (curHost.Type === type && curHost.Name === subdomain) { + // Updating an already existing host + wasUpdate = true; if (type === 'MX') { - newRecord.MXPref = curValue.split(' ')[0]; - newRecord.Address = curValue.split(' ')[1]; + curHost.MXPref = curValue.split(' ')[0]; + curHost.Address = curValue.split(' ')[1]; + } else { + curHost.Address = curValue; } - - toInsert.push(newRecord); - } } - const hosts = result.concat(toInsert); + // 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 + }; - setZone(domainConfig, zoneName, hosts, callback); - }); + // Special case for MX records + if (type === 'MX') { + newRecord.MXPref = curValue.split(' ')[0]; + newRecord.Address = curValue.split(' ')[1]; + } + + toInsert.push(newRecord); + + } + } + + const hosts = result.concat(toInsert); + + return await setZone(domainConfig, zoneName, hosts); } -function get(domainObject, subdomain, type, callback) { +async function get(domainObject, subdomain, type) { assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof type, 'string'); - assert.strictEqual(typeof callback, 'function'); const domainConfig = domainObject.config; const zoneName = domainObject.zoneName; subdomain = dns.getName(domainObject, subdomain, type) || '@'; - getZone(domainConfig, zoneName, function (error, result) { - if (error) return callback(error); + const result = await getZone(domainConfig, zoneName); - // We need to filter hosts to ones with this subdomain and type - const actualHosts = result.filter((host) => host.Type === type && host.Name === subdomain); + // We need to filter hosts to ones with this subdomain and type + const actualHosts = result.filter((host) => host.Type === type && host.Name === subdomain); - // We only return the value string - const tmp = actualHosts.map(function (record) { return record.Address; }); - - debug(`get: subdomain: ${subdomain} type:${type} value:${JSON.stringify(tmp)}`); - - return callback(null, tmp); - }); + const tmp = actualHosts.map(function (record) { return record.Address; }); + return tmp; } -function del(domainObject, subdomain, type, values, callback) { +async function del(domainObject, subdomain, type, values) { assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof type, 'string'); assert(Array.isArray(values)); - assert.strictEqual(typeof callback, 'function'); const domainConfig = domainObject.config; const zoneName = domainObject.zoneName; @@ -242,67 +217,17 @@ function del(domainObject, subdomain, type, values, callback) { debug('del: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values); - getZone(domainConfig, zoneName, function (error, result) { - if (error) return callback(error); + let result = await getZone(domainConfig, zoneName); + if (result.length === 0) return; + const originalLength = result.length; - if (result.length === 0) return callback(); - const originalLength = result.length; + for (let i = 0; i < values.length; i++) { + let curValue = values[i]; - for (let i = 0; i < values.length; i++) { - let curValue = values[i]; + result = result.filter(curHost => curHost.Type !== type || curHost.Name !== subdomain || curHost.Address !== curValue); + } - result = result.filter(curHost => curHost.Type !== type || curHost.Name !== subdomain || curHost.Address !== curValue); - } - - if (result.length !== originalLength) return setZone(domainConfig, zoneName, result, callback); - - callback(); - }); -} - -function verifyDomainConfig(domainObject, callback) { - assert.strictEqual(typeof domainObject, 'object'); - assert.strictEqual(typeof callback, 'function'); - - const domainConfig = domainObject.config; - const zoneName = domainObject.zoneName; - const ip = '127.0.0.1'; - - if (!domainConfig.username || typeof domainConfig.username !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'username must be a non-empty string', { field: 'username' })); - if (!domainConfig.token || typeof domainConfig.token !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'token must be a non-empty string', { field: 'token' })); - - let credentials = { - username: domainConfig.username, - token: domainConfig.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 BoxError(BoxError.BAD_FIELD, 'Unable to resolve nameservers for this domain', { field: 'nameservers' })); - if (error || !nameservers) return callback(new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' })); - - if (nameservers.some(function (n) { return n.toLowerCase().indexOf('.registrar-servers.com') === -1; })) { - debug('verifyDomainConfig: %j does not contains NC NS', nameservers); - return callback(new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to NameCheap', { field: 'nameservers' })); - } - - const testSubdomain = 'cloudrontestdns'; - - upsert(domainObject, testSubdomain, 'A', [ip], function (error, changeId) { - if (error) return callback(error); - - debug('verifyDomainConfig: Test A record added with change id %s', changeId); - - del(domainObject, testSubdomain, 'A', [ip], function (error) { - if (error) return callback(error); - - debug('verifyDomainConfig: Test A record removed again'); - - callback(null, credentials); - }); - }); - }); + if (result.length !== originalLength) return await setZone(domainConfig, zoneName, result); } async function wait(domainObject, subdomain, type, value, options) { @@ -316,3 +241,40 @@ async function wait(domainObject, subdomain, type, value, options) { await waitForDns(fqdn, domainObject.zoneName, type, value, options); } + +async function verifyDomainConfig(domainObject) { + assert.strictEqual(typeof domainObject, 'object'); + + const domainConfig = domainObject.config; + const zoneName = domainObject.zoneName; + const ip = '127.0.0.1'; + + 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'); + + const credentials = { + username: domainConfig.username, + token: domainConfig.token + }; + + if (process.env.BOX_ENV === '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.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'); + } + + const testSubdomain = 'cloudrontestdns'; + + await upsert(domainObject, testSubdomain, 'A', [ip]); + debug('verifyDomainConfig: Test A record added'); + + await del(domainObject, testSubdomain, 'A', [ip]); + debug('verifyDomainConfig: Test A record removed again'); + + return credentials; +}