make waitForDns async

cloudflare is partly broken
This commit is contained in:
Girish Ramakrishnan
2022-02-03 16:15:14 -08:00
parent da5b5aadbc
commit 0373fb70d5
20 changed files with 219 additions and 249 deletions

View File

@@ -19,12 +19,6 @@ module.exports = exports = {
checkDnsRecords,
syncDnsRecords,
resolve,
promises: {
resolve: require('util').promisify(resolve)
}
};
const apps = require('./apps.js'),
@@ -32,7 +26,6 @@ const apps = require('./apps.js'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
debug = require('debug')('box:dns'),
dns = require('dns'),
domains = require('./domains.js'),
ipaddr = require('ipaddr.js'),
mail = require('./mail.js'),
@@ -41,8 +34,7 @@ const apps = require('./apps.js'),
settings = require('./settings.js'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
util = require('util'),
_ = require('underscore');
util = require('util');
// choose which subdomain backend we use for test purpose we use route53
function api(provider) {
@@ -67,30 +59,30 @@ function api(provider) {
}
}
function fqdn(location, domainObject) {
assert.strictEqual(typeof location, 'string');
function fqdn(subdomain, domainObject) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domainObject, 'object');
return location + (location ? '.' : '') + domainObject.domain;
return subdomain + (subdomain ? '.' : '') + domainObject.domain;
}
// Hostname validation comes from RFC 1123 (section 2.1)
// Domain name validation comes from RFC 2181 (Name syntax)
// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
// We are validating the validity of the location-fqdn as host name (and not dns name)
function validateHostname(location, domainObject) {
assert.strictEqual(typeof location, 'string');
function validateHostname(subdomain, domainObject) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domainObject, 'object');
const hostname = fqdn(location, domainObject);
const hostname = fqdn(subdomain, domainObject);
const RESERVED_LOCATIONS = [
constants.SMTP_LOCATION,
constants.IMAP_LOCATION
];
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new BoxError(BoxError.BAD_FIELD, location + ' is reserved', { field: 'location' });
if (RESERVED_LOCATIONS.indexOf(subdomain) !== -1) return new BoxError(BoxError.BAD_FIELD, subdomain + ' is reserved', { field: 'location' });
if (hostname === settings.dashboardFqdn()) return new BoxError(BoxError.BAD_FIELD, location + ' is reserved', { field: 'location' });
if (hostname === settings.dashboardFqdn()) return new BoxError(BoxError.BAD_FIELD, subdomain + ' is reserved', { field: 'location' });
// workaround https://github.com/oncletom/tld.js/issues/73
var tmp = hostname.replace('_', '-');
@@ -98,11 +90,11 @@ function validateHostname(location, domainObject) {
if (hostname.length > 253) return new BoxError(BoxError.BAD_FIELD, 'Hostname length exceeds 253 characters', { field: 'location' });
if (location) {
if (subdomain) {
// label validation
if (location.split('.').some(function (p) { return p.length > 63 || p.length < 1; })) return new BoxError(BoxError.BAD_FIELD, 'Invalid subdomain length', { field: 'location' });
if (location.match(/^[A-Za-z0-9-.]+$/) === null) return new BoxError(BoxError.BAD_FIELD, 'Subdomain can only contain alphanumeric, hyphen and dot', { field: 'location' });
if (/^[-.]/.test(location)) return new BoxError(BoxError.BAD_FIELD, 'Subdomain cannot start or end with hyphen or dot', { field: 'location' });
if (subdomain.split('.').some(function (p) { return p.length > 63 || p.length < 1; })) return new BoxError(BoxError.BAD_FIELD, 'Invalid subdomain length', { field: 'location' });
if (subdomain.match(/^[A-Za-z0-9-.]+$/) === null) return new BoxError(BoxError.BAD_FIELD, 'Subdomain can only contain alphanumeric, hyphen and dot', { field: 'location' });
if (/^[-.]/.test(subdomain)) return new BoxError(BoxError.BAD_FIELD, 'Subdomain cannot start or end with hyphen or dot', { field: 'location' });
}
return null;
@@ -110,12 +102,12 @@ function validateHostname(location, domainObject) {
// returns the 'name' that needs to be inserted into zone
// eslint-disable-next-line no-unused-vars
function getName(domain, location, type) {
function getName(domain, subdomain, type) {
const part = domain.domain.slice(0, -domain.zoneName.length - 1);
if (location === '') return part;
if (subdomain === '') return part;
return part ? `${location}.${part}` : location;
return part ? `${subdomain}.${part}` : subdomain;
}
function maybePromisify(func) {
@@ -123,20 +115,20 @@ function maybePromisify(func) {
return util.promisify(func);
}
async function getDnsRecords(location, domain, type) {
assert.strictEqual(typeof location, 'string');
async function getDnsRecords(subdomain, domain, type) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
const domainObject = await domains.get(domain);
return await maybePromisify(api(domainObject.provider).get)(domainObject, location, type);
return await maybePromisify(api(domainObject.provider).get)(domainObject, subdomain, type);
}
async function checkDnsRecords(location, domain) {
assert.strictEqual(typeof location, 'string');
async function checkDnsRecords(subdomain, domain) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
const ipv4Records = await getDnsRecords(location, domain, 'A');
const ipv4Records = await getDnsRecords(subdomain, domain, 'A');
const ipv4 = await sysinfo.getServerIPv4();
// if empty OR exactly one record with the ip, we don't need to overwrite
@@ -144,7 +136,7 @@ async function checkDnsRecords(location, domain) {
const ipv6Enabled = await settings.getIPv6Config();
if (ipv6Enabled) {
const ipv6Records = await getDnsRecords(location, domain, 'AAAA');
const ipv6Records = await getDnsRecords(subdomain, domain, 'AAAA');
const ipv6 = await sysinfo.getServerIPv6();
// if empty OR exactly one record with the ip, we don't need to overwrite
@@ -155,33 +147,33 @@ async function checkDnsRecords(location, domain) {
}
// note: for TXT records the values must be quoted
async function upsertDnsRecords(location, domain, type, values) {
assert.strictEqual(typeof location, 'string');
async function upsertDnsRecords(subdomain, domain, type, values) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
debug(`upsertDNSRecord: location ${location} on domain ${domain} of type ${type} with values ${JSON.stringify(values)}`);
debug(`upsertDNSRecord: location ${subdomain} on domain ${domain} of type ${type} with values ${JSON.stringify(values)}`);
const domainObject = await domains.get(domain);
await maybePromisify(api(domainObject.provider).upsert)(domainObject, location, type, values);
await maybePromisify(api(domainObject.provider).upsert)(domainObject, subdomain, type, values);
}
async function removeDnsRecords(location, domain, type, values) {
assert.strictEqual(typeof location, 'string');
async function removeDnsRecords(subdomain, domain, type, values) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
debug('removeDNSRecord: %s on %s type %s values', location, domain, type, values);
debug('removeDNSRecord: %s on %s type %s values', subdomain, domain, type, values);
const domainObject = await domains.get(domain);
const [error] = await safe(maybePromisify(api(domainObject.provider).del)(domainObject, location, type, values));
const [error] = await safe(maybePromisify(api(domainObject.provider).del)(domainObject, subdomain, type, values));
if (error && error.reason !== BoxError.NOT_FOUND) throw error;
}
async function waitForDnsRecord(location, domain, type, value, options) {
assert.strictEqual(typeof location, 'string');
async function waitForDnsRecord(subdomain, domain, type, value, options) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert(type === 'A' || type === 'AAAA' || type === 'TXT');
assert.strictEqual(typeof value, 'string');
@@ -192,7 +184,7 @@ async function waitForDnsRecord(location, domain, type, value, options) {
// linode DNS takes ~15mins
if (!options.interval) options.interval = domainObject.provider === 'linode' ? 20000 : 5000;
await maybePromisify(api(domainObject.provider).wait)(domainObject, location, type, value, options);
await api(domainObject.provider).wait(domainObject, subdomain, type, value, options);
}
function makeWildcard(vhost) {
@@ -318,35 +310,3 @@ async function syncDnsRecords(options, progressCallback) {
return { errors };
}
// a note on TXT records. It doesn't have quotes ("") at the DNS level. Those quotes
// are added for DNS server software to enclose spaces. Such quotes may also be returned
// by the DNS REST API of some providers
function resolve(hostname, rrtype, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof rrtype, 'string');
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
const defaultOptions = { server: '127.0.0.1', timeout: 5000 }; // unbound runs on 127.0.0.1
const resolver = new dns.Resolver();
options = _.extend({ }, defaultOptions, options);
// Only use unbound on a Cloudron
if (constants.CLOUDRON) resolver.setServers([ options.server ]);
// should callback with ECANCELLED but looks like we might hit https://github.com/nodejs/node/issues/14814
const timerId = setTimeout(resolver.cancel.bind(resolver), options.timeout || 5000);
resolver.resolve(hostname, rrtype, function (error, result) {
clearTimeout(timerId);
if (error && error.code === 'ECANCELLED') error.code = 'TIMEOUT';
// result is an empty array if there was no error but there is no record. when you query a random
// domain, it errors with ENOTFOUND. But if you query an existing domain (A record) but with different
// type (CNAME) it is not an error and empty array
// for TXT records, result is 2d array of strings
callback(error, result);
});
}