2017-04-26 08:45:20 +04:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
exports = module.exports = {
|
|
|
|
|
upsert: upsert,
|
|
|
|
|
get: get,
|
|
|
|
|
del: del,
|
2017-04-26 11:48:31 +02:00
|
|
|
waitForDns: require('./waitfordns.js'),
|
2017-04-26 08:45:20 +04:00
|
|
|
verifyDnsConfig: verifyDnsConfig
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var assert = require('assert'),
|
2017-04-26 09:59:08 +02:00
|
|
|
async = require('async'),
|
2017-10-29 01:48:55 +02:00
|
|
|
debug = require('debug')('box:dns/cloudflare'),
|
2017-04-26 09:59:08 +02:00
|
|
|
dns = require('dns'),
|
2017-10-29 01:48:55 +02:00
|
|
|
DomainError = require('../domains.js').DomainError,
|
2017-04-26 08:45:20 +04:00
|
|
|
superagent = require('superagent'),
|
2017-10-29 01:48:55 +02:00
|
|
|
util = require('util'),
|
|
|
|
|
_ = require('underscore');
|
2017-04-26 08:45:20 +04:00
|
|
|
|
2017-04-26 11:48:31 +02:00
|
|
|
// we are using latest v4 stable API https://api.cloudflare.com/#getting-started-endpoints
|
2017-07-28 16:12:41 +02:00
|
|
|
var CLOUDFLARE_ENDPOINT = 'https://api.cloudflare.com/client/v4';
|
2017-04-26 08:45:20 +04:00
|
|
|
|
2017-07-31 11:25:17 +02:00
|
|
|
function translateRequestError(result, callback) {
|
|
|
|
|
assert.strictEqual(typeof result, 'object');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2017-10-29 01:48:55 +02:00
|
|
|
if (result.statusCode === 404) return callback(new DomainError(DomainError.NOT_FOUND, util.format('%s %j', result.statusCode, 'API does not exist')));
|
|
|
|
|
if (result.statusCode === 422) return callback(new DomainError(DomainError.BAD_FIELD, result.body.message));
|
2017-07-31 11:25:17 +02:00
|
|
|
if ((result.statusCode === 400 || result.statusCode === 401 || result.statusCode === 403) && result.body.errors.length > 0) {
|
|
|
|
|
let error = result.body.errors[0];
|
|
|
|
|
let message = error.message;
|
|
|
|
|
if (error.code === 6003) {
|
|
|
|
|
if (error.error_chain[0] && error.error_chain[0].code === 6103) message = 'Invalid API Key';
|
|
|
|
|
else message = 'Invalid credentials';
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-29 01:48:55 +02:00
|
|
|
return callback(new DomainError(DomainError.ACCESS_DENIED, message));
|
2017-07-31 11:25:17 +02:00
|
|
|
}
|
|
|
|
|
|
2017-10-29 01:48:55 +02:00
|
|
|
callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
2017-07-31 11:25:17 +02:00
|
|
|
}
|
|
|
|
|
|
2017-04-26 08:45:20 +04:00
|
|
|
function getZoneByName(dnsConfig, zoneName, callback) {
|
|
|
|
|
assert.strictEqual(typeof dnsConfig, 'object');
|
|
|
|
|
assert.strictEqual(typeof zoneName, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
2017-04-26 11:48:31 +02:00
|
|
|
|
2017-04-26 08:45:20 +04:00
|
|
|
superagent.get(CLOUDFLARE_ENDPOINT + '/zones?name=' + zoneName + '&status=active')
|
2017-04-26 11:48:31 +02:00
|
|
|
.set('X-Auth-Key', dnsConfig.token)
|
|
|
|
|
.set('X-Auth-Email', dnsConfig.email)
|
2017-04-26 08:45:20 +04:00
|
|
|
.timeout(30 * 1000)
|
|
|
|
|
.end(function (error, result) {
|
|
|
|
|
if (error && !error.response) return callback(error);
|
2017-07-31 11:25:17 +02:00
|
|
|
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
2017-10-29 01:48:55 +02:00
|
|
|
if (!result.body.result.length) return callback(new DomainError(DomainError.NOT_FOUND, util.format('%s %j', result.statusCode, result.body)));
|
2017-04-26 08:45:20 +04:00
|
|
|
|
2017-04-26 11:48:31 +02:00
|
|
|
callback(null, result.body.result[0]);
|
2017-04-26 08:45:20 +04:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2017-07-28 16:41:00 +02:00
|
|
|
function getDNSRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, callback) {
|
2017-04-26 08:45:20 +04:00
|
|
|
assert.strictEqual(typeof dnsConfig, 'object');
|
2017-07-28 16:41:00 +02:00
|
|
|
assert.strictEqual(typeof zoneId, 'string');
|
2017-04-26 08:45:20 +04:00
|
|
|
assert.strictEqual(typeof zoneName, 'string');
|
|
|
|
|
assert.strictEqual(typeof subdomain, 'string');
|
|
|
|
|
assert.strictEqual(typeof type, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
2017-04-26 11:48:31 +02:00
|
|
|
|
2017-09-05 16:07:14 -07:00
|
|
|
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
|
|
|
|
|
|
2017-07-28 16:41:00 +02:00
|
|
|
superagent.get(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records')
|
|
|
|
|
.set('X-Auth-Key',dnsConfig.token)
|
|
|
|
|
.set('X-Auth-Email',dnsConfig.email)
|
2017-09-05 16:07:14 -07:00
|
|
|
.query({ type: type, name: fqdn })
|
2017-07-28 16:41:00 +02:00
|
|
|
.timeout(30 * 1000)
|
|
|
|
|
.end(function (error, result) {
|
2017-04-26 08:45:20 +04:00
|
|
|
if (error && !error.response) return callback(error);
|
2017-07-31 11:25:17 +02:00
|
|
|
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
2017-07-28 16:41:00 +02:00
|
|
|
|
2017-09-05 16:07:14 -07:00
|
|
|
var tmp = result.body.result;
|
2017-04-26 08:45:20 +04:00
|
|
|
|
2017-07-28 16:41:00 +02:00
|
|
|
return callback(null, tmp);
|
2017-04-26 08:45:20 +04:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
|
|
|
|
assert.strictEqual(typeof dnsConfig, 'object');
|
|
|
|
|
assert.strictEqual(typeof zoneName, 'string');
|
|
|
|
|
assert.strictEqual(typeof subdomain, 'string');
|
|
|
|
|
assert.strictEqual(typeof type, 'string');
|
|
|
|
|
assert(util.isArray(values));
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
|
|
|
|
|
|
|
|
|
|
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
|
|
|
|
|
|
2017-07-28 16:41:00 +02:00
|
|
|
getZoneByName(dnsConfig, zoneName, function(error, result){
|
2017-04-26 08:45:20 +04:00
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
2017-07-28 16:41:00 +02:00
|
|
|
var zoneId = result.id;
|
2017-04-26 08:45:20 +04:00
|
|
|
|
2017-07-28 16:41:00 +02:00
|
|
|
getDNSRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, function (error, result) {
|
2017-04-26 08:45:20 +04:00
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
2017-07-28 16:41:00 +02:00
|
|
|
var dnsRecords = result;
|
|
|
|
|
|
|
|
|
|
// used to track available records to update instead of create
|
|
|
|
|
var i = 0;
|
|
|
|
|
|
|
|
|
|
async.eachSeries(values, function (value, callback) {
|
2017-08-26 15:54:34 -07:00
|
|
|
var priority = null;
|
|
|
|
|
|
|
|
|
|
if (type === 'MX') {
|
|
|
|
|
priority = value.split(' ')[0];
|
|
|
|
|
value = value.split(' ')[1];
|
|
|
|
|
}
|
|
|
|
|
|
2017-07-28 16:41:00 +02:00
|
|
|
var data = {
|
|
|
|
|
type: type,
|
|
|
|
|
name: fqdn,
|
|
|
|
|
content: value,
|
2017-08-26 15:54:34 -07:00
|
|
|
priority: priority,
|
2017-07-28 16:41:00 +02:00
|
|
|
ttl: 120 // 1 means "automatic" (meaning 300ms) and 120 is the lowest supported
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (i >= dnsRecords.length) {
|
|
|
|
|
superagent.post(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records')
|
|
|
|
|
.set('X-Auth-Key',dnsConfig.token)
|
|
|
|
|
.set('X-Auth-Email',dnsConfig.email)
|
|
|
|
|
.send(data)
|
|
|
|
|
.timeout(30 * 1000)
|
|
|
|
|
.end(function (error, result) {
|
|
|
|
|
if (error && !error.response) return callback(error);
|
2017-07-31 11:25:17 +02:00
|
|
|
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
2017-07-28 16:41:00 +02:00
|
|
|
|
|
|
|
|
callback(null);
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
superagent.put(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records/' + dnsRecords[i].id)
|
|
|
|
|
.set('X-Auth-Key',dnsConfig.token)
|
|
|
|
|
.set('X-Auth-Email',dnsConfig.email)
|
|
|
|
|
.send(data)
|
|
|
|
|
.timeout(30 * 1000)
|
|
|
|
|
.end(function (error, result) {
|
|
|
|
|
// increment, as we have consumed the record
|
|
|
|
|
++i;
|
|
|
|
|
|
|
|
|
|
if (error && !error.response) return callback(error);
|
2017-07-31 11:25:17 +02:00
|
|
|
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
2017-07-28 16:41:00 +02:00
|
|
|
|
|
|
|
|
callback(null);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, function (error) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
callback(null, 'unused');
|
|
|
|
|
});
|
2017-04-26 08:45:20 +04:00
|
|
|
});
|
2017-04-26 09:59:08 +02:00
|
|
|
});
|
2017-04-26 08:45:20 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function get(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');
|
|
|
|
|
|
2017-07-28 16:41:00 +02:00
|
|
|
getZoneByName(dnsConfig, zoneName, function(error, result){
|
2017-04-26 08:45:20 +04:00
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
2017-07-28 16:41:00 +02:00
|
|
|
getDNSRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
var tmp = result.map(function (record) { return record.content; });
|
|
|
|
|
debug('get: %j', tmp);
|
2017-04-26 08:45:20 +04:00
|
|
|
|
2017-07-28 16:41:00 +02:00
|
|
|
callback(null, tmp);
|
|
|
|
|
});
|
2017-04-26 08:45:20 +04:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
|
|
|
|
assert.strictEqual(typeof dnsConfig, 'object');
|
|
|
|
|
assert.strictEqual(typeof zoneName, 'string');
|
|
|
|
|
assert.strictEqual(typeof subdomain, 'string');
|
|
|
|
|
assert.strictEqual(typeof type, 'string');
|
|
|
|
|
assert(util.isArray(values));
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
2017-04-26 09:59:08 +02:00
|
|
|
|
2017-07-28 16:41:00 +02:00
|
|
|
getZoneByName(dnsConfig, zoneName, function(error, result){
|
2017-04-26 08:45:20 +04:00
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
2017-07-28 16:41:00 +02:00
|
|
|
getDNSRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
if (result.length === 0) return callback(null);
|
|
|
|
|
|
|
|
|
|
var zoneId = result[0].zone_id;
|
|
|
|
|
|
|
|
|
|
var tmp = result.filter(function (record) { return values.some(function (value) { return value === record.content; }); });
|
|
|
|
|
debug('del: %j', tmp);
|
2017-04-26 08:45:20 +04:00
|
|
|
|
2017-07-28 16:41:00 +02:00
|
|
|
if (tmp.length === 0) return callback(null);
|
2017-04-26 08:45:20 +04:00
|
|
|
|
2017-07-28 16:41:00 +02:00
|
|
|
async.eachSeries(tmp, function (record, callback) {
|
|
|
|
|
superagent.del(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records/' + record.id)
|
|
|
|
|
.set('X-Auth-Key',dnsConfig.token)
|
|
|
|
|
.set('X-Auth-Email',dnsConfig.email)
|
|
|
|
|
.timeout(30 * 1000)
|
|
|
|
|
.end(function (error, result) {
|
|
|
|
|
if (error && !error.response) return callback(error);
|
2017-07-31 11:25:17 +02:00
|
|
|
if (result.statusCode !== 204 || result.body.success !== true) return translateRequestError(result, callback);
|
2017-04-26 08:45:20 +04:00
|
|
|
|
2017-07-28 16:41:00 +02:00
|
|
|
debug('del: done');
|
2017-04-26 08:45:20 +04:00
|
|
|
|
2017-07-28 16:41:00 +02:00
|
|
|
callback(null);
|
|
|
|
|
});
|
|
|
|
|
}, function (error) {
|
|
|
|
|
if (error) return callback(error);
|
2017-04-26 08:45:20 +04:00
|
|
|
|
2017-07-28 16:41:00 +02:00
|
|
|
callback(null, 'unused');
|
2017-04-26 08:45:20 +04:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2017-07-28 16:33:19 +02:00
|
|
|
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
2017-04-26 08:45:20 +04:00
|
|
|
assert.strictEqual(typeof dnsConfig, 'object');
|
2017-07-28 16:33:19 +02:00
|
|
|
assert.strictEqual(typeof fqdn, 'string');
|
|
|
|
|
assert.strictEqual(typeof zoneName, 'string');
|
2017-04-26 08:45:20 +04:00
|
|
|
assert.strictEqual(typeof ip, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2017-10-29 01:48:55 +02:00
|
|
|
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainError(DomainError.BAD_FIELD, 'token must be a non-empty string'));
|
|
|
|
|
if (!dnsConfig.email || typeof dnsConfig.email !== 'string') return callback(new DomainError(DomainError.BAD_FIELD, 'email must be a non-empty string'));
|
2017-04-26 11:06:33 +02:00
|
|
|
|
2017-04-26 08:45:20 +04:00
|
|
|
var credentials = {
|
2017-04-26 11:06:33 +02:00
|
|
|
token: dnsConfig.token,
|
|
|
|
|
email: dnsConfig.email
|
2017-04-26 08:45:20 +04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
|
|
|
|
|
|
2017-07-28 16:33:19 +02:00
|
|
|
dns.resolveNs(zoneName, function (error, nameservers) {
|
2017-10-29 01:48:55 +02:00
|
|
|
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
|
|
|
|
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
2017-04-26 08:45:20 +04:00
|
|
|
|
2017-07-28 16:33:19 +02:00
|
|
|
getZoneByName(dnsConfig, zoneName, function(error, result) {
|
2017-04-26 08:45:20 +04:00
|
|
|
if (error) return callback(error);
|
2017-07-28 16:41:00 +02:00
|
|
|
|
2017-04-26 08:45:20 +04:00
|
|
|
if (!_.isEqual(result.name_servers.sort(), nameservers.sort())) {
|
|
|
|
|
debug('verifyDnsConfig: %j and %j do not match', nameservers, result.name_servers);
|
2017-10-29 01:48:55 +02:00
|
|
|
return callback(new DomainError(DomainError.BAD_FIELD, 'Domain nameservers are not set to Cloudflare'));
|
2017-04-26 08:45:20 +04:00
|
|
|
}
|
2017-04-26 11:48:31 +02:00
|
|
|
|
2017-11-07 23:13:58 +01:00
|
|
|
const testSubdomain = 'cloudrontestdns';
|
|
|
|
|
|
|
|
|
|
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
|
2017-04-26 08:45:20 +04:00
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
2017-11-07 23:13:58 +01:00
|
|
|
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
|
|
|
|
|
|
|
|
|
|
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
|
|
|
|
|
if (error) return callback(error);
|
2017-04-26 08:45:20 +04:00
|
|
|
|
2017-11-07 23:13:58 +01:00
|
|
|
debug('verifyDnsConfig: Test A record removed again');
|
|
|
|
|
|
|
|
|
|
callback(null, credentials);
|
|
|
|
|
});
|
2017-04-26 08:45:20 +04:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|