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

311 lines
13 KiB
JavaScript
Raw Normal View History

'use strict';
exports = module.exports = {
upsert: upsert,
get: get,
del: del,
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
2017-04-26 09:59:08 +02:00
async = require('async'),
debug = require('debug')('box:dns/cloudflare'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
2018-04-29 11:20:12 -07:00
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js'),
_ = require('underscore');
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-07-31 11:25:17 +02:00
function translateRequestError(result, callback) {
assert.strictEqual(typeof result, 'object');
assert.strictEqual(typeof callback, 'function');
2018-04-29 11:20:12 -07:00
if (result.statusCode === 404) return callback(new DomainsError(DomainsError.NOT_FOUND, util.format('%s %j', result.statusCode, 'API does not exist')));
if (result.statusCode === 422) return callback(new DomainsError(DomainsError.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 = `message: ${error.message} statusCode: ${result.statusCode} code:${error.code}`;
2018-04-29 11:20:12 -07:00
return callback(new DomainsError(DomainsError.ACCESS_DENIED, message));
2017-07-31 11:25:17 +02:00
}
2018-04-29 11:20:12 -07:00
callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
2017-07-31 11:25:17 +02: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
superagent.get(CLOUDFLARE_ENDPOINT + '/zones?name=' + zoneName + '&status=active')
2018-01-19 09:55:27 -08:00
.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);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
2018-04-29 11:20:12 -07:00
if (!result.body.result.length) return callback(new DomainsError(DomainsError.NOT_FOUND, util.format('%s %j', result.statusCode, result.body)));
2018-01-19 09:55:27 -08:00
callback(null, result.body.result[0]);
});
}
// gets records filtered by zone, type and fqdn
function getDnsRecords(dnsConfig, zoneId, fqdn, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
2017-07-28 16:41:00 +02:00
assert.strictEqual(typeof zoneId, 'string');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
2017-04-26 11:48:31 +02:00
2017-07-28 16:41:00 +02:00
superagent.get(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records')
2018-01-19 09:55:27 -08:00
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.query({ type: type, name: fqdn })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
2017-07-28 16:41:00 +02:00
2018-01-19 09:55:27 -08:00
var tmp = result.body.result;
2018-01-19 09:55:27 -08:00
return callback(null, tmp);
});
}
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
debug('upsert: %s for zone %s of type %s with values %j', fqdn, zoneName, type, values);
getZoneByName(dnsConfig, zoneName, function(error, result) {
if (error) return callback(error);
let zoneId = result.id;
getDnsRecords(dnsConfig, zoneId, fqdn, type, function (error, dnsRecords) {
if (error) return callback(error);
let i = 0; // // used to track available records to update instead of create
2017-07-28 16:41:00 +02:00
async.eachSeries(values, function (value, iteratorCallback) {
2017-08-26 15:54:34 -07:00
var priority = null;
if (type === 'MX') {
2018-09-07 11:44:31 -07:00
priority = parseInt(value.split(' ')[0], 10);
2017-08-26 15:54:34 -07:00
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,
2019-01-03 10:41:35 -08:00
proxied: false,
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) { // create a new record
debug(`upsert: Adding new record fqdn: ${fqdn}, zoneName: ${zoneName} proxied: false`);
2019-01-03 10:41:35 -08:00
superagent.post(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records')
.set('X-Auth-Key', dnsConfig.token)
.set('X-Auth-Email', dnsConfig.email)
2018-01-19 09:55:27 -08:00
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return iteratorCallback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, iteratorCallback);
2018-01-19 09:55:27 -08:00
iteratorCallback(null);
2018-01-19 09:55:27 -08:00
});
} else { // replace existing record
2019-01-03 10:41:35 -08:00
data.proxied = dnsRecords[i].proxied; // preserve proxied parameter
debug(`upsert: Updating existing record fqdn: ${fqdn}, zoneName: ${zoneName} proxied: ${data.proxied}`);
2019-01-03 10:41:35 -08:00
superagent.put(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records/' + dnsRecords[i].id)
.set('X-Auth-Key', dnsConfig.token)
.set('X-Auth-Email', dnsConfig.email)
2018-01-19 09:55:27 -08:00
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
++i; // increment, as we have consumed the record
2017-07-28 16:41:00 +02:00
if (error && !error.response) return iteratorCallback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, iteratorCallback);
2017-07-28 16:41:00 +02:00
iteratorCallback(null);
2018-01-19 09:55:27 -08:00
});
2017-07-28 16:41:00 +02:00
}
2018-06-29 22:25:34 +02:00
}, callback);
});
2017-04-26 09:59:08 +02:00
});
}
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
getZoneByName(dnsConfig, zoneName, function(error, zone) {
if (error) return callback(error);
getDnsRecords(dnsConfig, zone.id, fqdn, type, function (error, result) {
2017-07-28 16:41:00 +02:00
if (error) return callback(error);
var tmp = result.map(function (record) { return record.content; });
debug('get: %j', tmp);
2017-07-28 16:41:00 +02:00
callback(null, tmp);
});
});
}
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
2017-04-26 09:59:08 +02:00
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
getZoneByName(dnsConfig, zoneName, function(error, zone) {
if (error) return callback(error);
getDnsRecords(dnsConfig, zone.id, fqdn, type, function(error, result) {
2017-07-28 16:41:00 +02:00
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-07-28 16:41:00 +02:00
if (tmp.length === 0) return callback(null);
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)
2018-01-19 09:55:27 -08:00
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
2018-01-23 14:52:39 -08:00
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
2018-01-19 09:55:27 -08:00
debug('del: done');
2018-01-19 09:55:27 -08:00
callback(null);
});
2017-07-28 16:41:00 +02:00
}, function (error) {
if (error) return callback(error);
2017-07-28 16:41:00 +02:00
callback(null, 'unused');
});
});
});
}
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, '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 dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
debug('wait: %s for zone %s of type %s', fqdn, zoneName, type);
getZoneByName(dnsConfig, zoneName, function(error, result) {
if (error) return callback(error);
let zoneId = result.id;
getDnsRecords(dnsConfig, zoneId, fqdn, type, function (error, dnsRecords) {
if (error) return callback(error);
if (dnsRecords.length === 0) return callback(new DomainsError(DomainsError.NOT_FOUND, 'Domain not found'));
if (!dnsRecords[0].proxied) return waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
debug('wait: skipping wait of proxied domain');
callback(null); // maybe we can check for dns to be cloudflare IPs? https://api.cloudflare.com/#cloudflare-ips-cloudflare-ip-details
});
});
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
2018-04-29 11:20:12 -07:00
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
if (!dnsConfig.email || typeof dnsConfig.email !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'email must be a non-empty string'));
2017-04-26 11:06:33 +02:00
const ip = '127.0.0.1';
var credentials = {
2017-04-26 11:06:33 +02:00
token: dnsConfig.token,
email: dnsConfig.email
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
2018-02-08 14:39:35 -08:00
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
2018-04-29 11:20:12 -07:00
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'));
getZoneByName(dnsConfig, zoneName, function(error, zone) {
if (error) return callback(error);
2017-07-28 16:41:00 +02:00
if (!_.isEqual(zone.name_servers.sort(), nameservers.sort())) {
debug('verifyDnsConfig: %j and %j do not match', nameservers, zone.name_servers);
2018-04-29 11:20:12 -07:00
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Cloudflare'));
}
2017-04-26 11:48:31 +02:00
const location = 'cloudrontestdns';
upsert(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added');
del(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
callback(null, credentials);
});
});
});
});
}