node/lib/internal/dns/utils.js
Joyee Cheung 12fb94c78c
dns: refactor default resolver
This patch refactors the DNS default resolver code to make it
easier to be included in a snapshot:

- The code specific for the callback-based DNS resolver are not
  in a separate module to make the dependency clearer (it's not
  actually needed if the user only ever loads `dns/promises`)
- The common part of the callback-based resolver and the promise-
  based resolver is now ResolverBase. The other two user-facing
  resolvers are now subclasses of ResolverBase. The default
  Resolver is constructed with just ResolverBase. This would
  be fine as the default resolver is never actually exposed
  to the user-land and it has been working using duck-typing anyway.
- Move the construction of Resolver subclasses into a common
  method `createResolverClass()` to reduce code duplication.
  The two subclasses now also share the same base constructor.
  This would make it possible for them to also share code
  for snapshot support later.
- `--dns-result-order` is now queried and refreshed during
  pre-execution. To avoid loading the cares_wrap binding unnecessarily
  the loading of the binding is also made lazy.

PR-URL: https://github.com/nodejs/node/pull/44541
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Zeyu "Alex" Yang <himself65@outlook.com>
Reviewed-By: Minwoo Jung <nodecorelab@gmail.com>
2022-09-13 13:36:08 +00:00

290 lines
7.5 KiB
JavaScript

'use strict';
const {
ArrayPrototypeForEach,
ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypePush,
FunctionPrototypeBind,
NumberParseInt,
RegExpPrototypeExec,
RegExpPrototypeSymbolReplace,
ObjectCreate,
} = primordials;
const errors = require('internal/errors');
const { isIP } = require('internal/net');
const { getOptionValue } = require('internal/options');
const {
validateArray,
validateInt32,
validateOneOf,
validateString,
} = require('internal/validators');
let binding;
function lazyBinding() {
binding ??= internalBinding('cares_wrap');
return binding;
}
const IANA_DNS_PORT = 53;
const IPv6RE = /^\[([^[\]]*)\]/;
const addrSplitRE = /(^.+?)(?::(\d+))?$/;
const {
ERR_DNS_SET_SERVERS_FAILED,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_IP_ADDRESS,
} = errors.codes;
function validateTimeout(options) {
const { timeout = -1 } = { ...options };
validateInt32(timeout, 'options.timeout', -1);
return timeout;
}
function validateTries(options) {
const { tries = 4 } = { ...options };
validateInt32(tries, 'options.tries', 1);
return tries;
}
// Resolver instances correspond 1:1 to c-ares channels.
class ResolverBase {
constructor(options = undefined) {
const timeout = validateTimeout(options);
const tries = validateTries(options);
const { ChannelWrap } = lazyBinding();
this._handle = new ChannelWrap(timeout, tries);
}
cancel() {
this._handle.cancel();
}
getServers() {
return ArrayPrototypeMap(this._handle.getServers() || [], (val) => {
if (!val[1] || val[1] === IANA_DNS_PORT)
return val[0];
const host = isIP(val[0]) === 6 ? `[${val[0]}]` : val[0];
return `${host}:${val[1]}`;
});
}
setServers(servers) {
validateArray(servers, 'servers');
// Cache the original servers because in the event of an error while
// setting the servers, c-ares won't have any servers available for
// resolution.
const orig = this._handle.getServers() || [];
const newSet = [];
ArrayPrototypeForEach(servers, (serv, index) => {
validateString(serv, `servers[${index}]`);
let ipVersion = isIP(serv);
if (ipVersion !== 0)
return ArrayPrototypePush(newSet, [ipVersion, serv, IANA_DNS_PORT]);
const match = RegExpPrototypeExec(IPv6RE, serv);
// Check for an IPv6 in brackets.
if (match) {
ipVersion = isIP(match[1]);
if (ipVersion !== 0) {
const port = NumberParseInt(
RegExpPrototypeSymbolReplace(addrSplitRE, serv, '$2')) || IANA_DNS_PORT;
return ArrayPrototypePush(newSet, [ipVersion, match[1], port]);
}
}
// addr::port
const addrSplitMatch = RegExpPrototypeExec(addrSplitRE, serv);
if (addrSplitMatch) {
const hostIP = addrSplitMatch[1];
const port = addrSplitMatch[2] || IANA_DNS_PORT;
ipVersion = isIP(hostIP);
if (ipVersion !== 0) {
return ArrayPrototypePush(
newSet, [ipVersion, hostIP, NumberParseInt(port)]);
}
}
throw new ERR_INVALID_IP_ADDRESS(serv);
});
const errorNumber = this._handle.setServers(newSet);
if (errorNumber !== 0) {
// Reset the servers to the old servers, because ares probably unset them.
this._handle.setServers(ArrayPrototypeJoin(orig, ','));
const { strerror } = lazyBinding();
const err = strerror(errorNumber);
throw new ERR_DNS_SET_SERVERS_FAILED(err, servers);
}
}
setLocalAddress(ipv4, ipv6) {
validateString(ipv4, 'ipv4');
if (ipv6 !== undefined) {
validateString(ipv6, 'ipv6');
}
this._handle.setLocalAddress(ipv4, ipv6);
}
}
let defaultResolver;
let dnsOrder;
function initializeDns() {
const orderFromCLI = getOptionValue('--dns-result-order');
if (!orderFromCLI) {
dnsOrder ??= 'verbatim';
} else {
// Allow the deserialized application to override order from CLI.
dnsOrder = orderFromCLI;
}
}
const resolverKeys = [
'getServers',
'resolve',
'resolve4',
'resolve6',
'resolveAny',
'resolveCaa',
'resolveCname',
'resolveMx',
'resolveNaptr',
'resolveNs',
'resolvePtr',
'resolveSoa',
'resolveSrv',
'resolveTxt',
'reverse',
];
function getDefaultResolver() {
// We do this here instead of pre-execution so that the default resolver is
// only ever created when the user loads any dns module.
if (defaultResolver === undefined) {
defaultResolver = new ResolverBase();
}
return defaultResolver;
}
function setDefaultResolver(resolver) {
defaultResolver = resolver;
}
function bindDefaultResolver(target, source) {
const defaultResolver = getDefaultResolver();
ArrayPrototypeForEach(resolverKeys, (key) => {
target[key] = FunctionPrototypeBind(source[key], defaultResolver);
});
}
function validateHints(hints) {
const { AI_ADDRCONFIG, AI_ALL, AI_V4MAPPED } = lazyBinding();
if ((hints & ~(AI_ADDRCONFIG | AI_ALL | AI_V4MAPPED)) !== 0) {
throw new ERR_INVALID_ARG_VALUE('hints', hints);
}
}
let invalidHostnameWarningEmitted = false;
function emitInvalidHostnameWarning(hostname) {
if (!invalidHostnameWarningEmitted) {
process.emitWarning(
`The provided hostname "${hostname}" is not a valid ` +
'hostname, and is supported in the dns module solely for compatibility.',
'DeprecationWarning',
'DEP0118'
);
invalidHostnameWarningEmitted = true;
}
}
function getDefaultVerbatim() {
return dnsOrder !== 'ipv4first';
}
function setDefaultResultOrder(value) {
validateOneOf(value, 'dnsOrder', ['verbatim', 'ipv4first']);
dnsOrder = value;
}
function createResolverClass(resolver) {
const resolveMap = ObjectCreate(null);
class Resolver extends ResolverBase {}
Resolver.prototype.resolveAny = resolveMap.ANY = resolver('queryAny');
Resolver.prototype.resolve4 = resolveMap.A = resolver('queryA');
Resolver.prototype.resolve6 = resolveMap.AAAA = resolver('queryAaaa');
Resolver.prototype.resolveCaa = resolveMap.CAA = resolver('queryCaa');
Resolver.prototype.resolveCname = resolveMap.CNAME = resolver('queryCname');
Resolver.prototype.resolveMx = resolveMap.MX = resolver('queryMx');
Resolver.prototype.resolveNs = resolveMap.NS = resolver('queryNs');
Resolver.prototype.resolveTxt = resolveMap.TXT = resolver('queryTxt');
Resolver.prototype.resolveSrv = resolveMap.SRV = resolver('querySrv');
Resolver.prototype.resolvePtr = resolveMap.PTR = resolver('queryPtr');
Resolver.prototype.resolveNaptr = resolveMap.NAPTR = resolver('queryNaptr');
Resolver.prototype.resolveSoa = resolveMap.SOA = resolver('querySoa');
Resolver.prototype.reverse = resolver('getHostByAddr');
return {
resolveMap,
Resolver
};
}
// ERROR CODES
const errorCodes = {
NODATA: 'ENODATA',
FORMERR: 'EFORMERR',
SERVFAIL: 'ESERVFAIL',
NOTFOUND: 'ENOTFOUND',
NOTIMP: 'ENOTIMP',
REFUSED: 'EREFUSED',
BADQUERY: 'EBADQUERY',
BADNAME: 'EBADNAME',
BADFAMILY: 'EBADFAMILY',
BADRESP: 'EBADRESP',
CONNREFUSED: 'ECONNREFUSED',
TIMEOUT: 'ETIMEOUT',
EOF: 'EOF',
FILE: 'EFILE',
NOMEM: 'ENOMEM',
DESTRUCTION: 'EDESTRUCTION',
BADSTR: 'EBADSTR',
BADFLAGS: 'EBADFLAGS',
NONAME: 'ENONAME',
BADHINTS: 'EBADHINTS',
NOTINITIALIZED: 'ENOTINITIALIZED',
LOADIPHLPAPI: 'ELOADIPHLPAPI',
ADDRGETNETWORKPARAMS: 'EADDRGETNETWORKPARAMS',
CANCELLED: 'ECANCELLED',
};
module.exports = {
bindDefaultResolver,
getDefaultResolver,
setDefaultResolver,
validateHints,
validateTimeout,
validateTries,
emitInvalidHostnameWarning,
getDefaultVerbatim,
setDefaultResultOrder,
errorCodes,
createResolverClass,
initializeDns
};