|
"use strict"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Object.defineProperty(exports, "__esModule", { value: true }); |
|
exports.DEFAULT_PORT = void 0; |
|
exports.setup = setup; |
|
const resolver_1 = require("./resolver"); |
|
const dns_1 = require("dns"); |
|
const service_config_1 = require("./service-config"); |
|
const constants_1 = require("./constants"); |
|
const metadata_1 = require("./metadata"); |
|
const logging = require("./logging"); |
|
const constants_2 = require("./constants"); |
|
const uri_parser_1 = require("./uri-parser"); |
|
const net_1 = require("net"); |
|
const backoff_timeout_1 = require("./backoff-timeout"); |
|
const environment_1 = require("./environment"); |
|
const TRACER_NAME = 'dns_resolver'; |
|
function trace(text) { |
|
logging.trace(constants_2.LogVerbosity.DEBUG, TRACER_NAME, text); |
|
} |
|
|
|
|
|
|
|
exports.DEFAULT_PORT = 443; |
|
const DEFAULT_MIN_TIME_BETWEEN_RESOLUTIONS_MS = 30000; |
|
|
|
|
|
|
|
class DnsResolver { |
|
constructor(target, listener, channelOptions) { |
|
var _a, _b, _c; |
|
this.target = target; |
|
this.listener = listener; |
|
this.pendingLookupPromise = null; |
|
this.pendingTxtPromise = null; |
|
this.latestLookupResult = null; |
|
this.latestServiceConfig = null; |
|
this.latestServiceConfigError = null; |
|
this.continueResolving = false; |
|
this.isNextResolutionTimerRunning = false; |
|
this.isServiceConfigEnabled = true; |
|
this.returnedIpResult = false; |
|
this.alternativeResolver = new dns_1.promises.Resolver(); |
|
trace('Resolver constructed for target ' + (0, uri_parser_1.uriToString)(target)); |
|
if (target.authority) { |
|
this.alternativeResolver.setServers([target.authority]); |
|
} |
|
const hostPort = (0, uri_parser_1.splitHostPort)(target.path); |
|
if (hostPort === null) { |
|
this.ipResult = null; |
|
this.dnsHostname = null; |
|
this.port = null; |
|
} |
|
else { |
|
if ((0, net_1.isIPv4)(hostPort.host) || (0, net_1.isIPv6)(hostPort.host)) { |
|
this.ipResult = [ |
|
{ |
|
addresses: [ |
|
{ |
|
host: hostPort.host, |
|
port: (_a = hostPort.port) !== null && _a !== void 0 ? _a : exports.DEFAULT_PORT, |
|
}, |
|
], |
|
}, |
|
]; |
|
this.dnsHostname = null; |
|
this.port = null; |
|
} |
|
else { |
|
this.ipResult = null; |
|
this.dnsHostname = hostPort.host; |
|
this.port = (_b = hostPort.port) !== null && _b !== void 0 ? _b : exports.DEFAULT_PORT; |
|
} |
|
} |
|
this.percentage = Math.random() * 100; |
|
if (channelOptions['grpc.service_config_disable_resolution'] === 1) { |
|
this.isServiceConfigEnabled = false; |
|
} |
|
this.defaultResolutionError = { |
|
code: constants_1.Status.UNAVAILABLE, |
|
details: `Name resolution failed for target ${(0, uri_parser_1.uriToString)(this.target)}`, |
|
metadata: new metadata_1.Metadata(), |
|
}; |
|
const backoffOptions = { |
|
initialDelay: channelOptions['grpc.initial_reconnect_backoff_ms'], |
|
maxDelay: channelOptions['grpc.max_reconnect_backoff_ms'], |
|
}; |
|
this.backoff = new backoff_timeout_1.BackoffTimeout(() => { |
|
if (this.continueResolving) { |
|
this.startResolutionWithBackoff(); |
|
} |
|
}, backoffOptions); |
|
this.backoff.unref(); |
|
this.minTimeBetweenResolutionsMs = |
|
(_c = channelOptions['grpc.dns_min_time_between_resolutions_ms']) !== null && _c !== void 0 ? _c : DEFAULT_MIN_TIME_BETWEEN_RESOLUTIONS_MS; |
|
this.nextResolutionTimer = setTimeout(() => { }, 0); |
|
clearTimeout(this.nextResolutionTimer); |
|
} |
|
|
|
|
|
|
|
|
|
startResolution() { |
|
if (this.ipResult !== null) { |
|
if (!this.returnedIpResult) { |
|
trace('Returning IP address for target ' + (0, uri_parser_1.uriToString)(this.target)); |
|
setImmediate(() => { |
|
this.listener.onSuccessfulResolution(this.ipResult, null, null, null, {}); |
|
}); |
|
this.returnedIpResult = true; |
|
} |
|
this.backoff.stop(); |
|
this.backoff.reset(); |
|
this.stopNextResolutionTimer(); |
|
return; |
|
} |
|
if (this.dnsHostname === null) { |
|
trace('Failed to parse DNS address ' + (0, uri_parser_1.uriToString)(this.target)); |
|
setImmediate(() => { |
|
this.listener.onError({ |
|
code: constants_1.Status.UNAVAILABLE, |
|
details: `Failed to parse DNS address ${(0, uri_parser_1.uriToString)(this.target)}`, |
|
metadata: new metadata_1.Metadata(), |
|
}); |
|
}); |
|
this.stopNextResolutionTimer(); |
|
} |
|
else { |
|
if (this.pendingLookupPromise !== null) { |
|
return; |
|
} |
|
trace('Looking up DNS hostname ' + this.dnsHostname); |
|
|
|
|
|
|
|
|
|
|
|
|
|
this.latestLookupResult = null; |
|
const hostname = this.dnsHostname; |
|
this.pendingLookupPromise = this.lookup(hostname); |
|
this.pendingLookupPromise.then(addressList => { |
|
if (this.pendingLookupPromise === null) { |
|
return; |
|
} |
|
this.pendingLookupPromise = null; |
|
this.backoff.reset(); |
|
this.backoff.stop(); |
|
this.latestLookupResult = addressList.map(address => ({ |
|
addresses: [address], |
|
})); |
|
const allAddressesString = '[' + |
|
addressList.map(addr => addr.host + ':' + addr.port).join(',') + |
|
']'; |
|
trace('Resolved addresses for target ' + |
|
(0, uri_parser_1.uriToString)(this.target) + |
|
': ' + |
|
allAddressesString); |
|
if (this.latestLookupResult.length === 0) { |
|
this.listener.onError(this.defaultResolutionError); |
|
return; |
|
} |
|
|
|
|
|
|
|
|
|
this.listener.onSuccessfulResolution(this.latestLookupResult, this.latestServiceConfig, this.latestServiceConfigError, null, {}); |
|
}, err => { |
|
if (this.pendingLookupPromise === null) { |
|
return; |
|
} |
|
trace('Resolution error for target ' + |
|
(0, uri_parser_1.uriToString)(this.target) + |
|
': ' + |
|
err.message); |
|
this.pendingLookupPromise = null; |
|
this.stopNextResolutionTimer(); |
|
this.listener.onError(this.defaultResolutionError); |
|
}); |
|
|
|
|
|
if (this.isServiceConfigEnabled && this.pendingTxtPromise === null) { |
|
|
|
|
|
|
|
this.pendingTxtPromise = this.resolveTxt(hostname); |
|
this.pendingTxtPromise.then(txtRecord => { |
|
if (this.pendingTxtPromise === null) { |
|
return; |
|
} |
|
this.pendingTxtPromise = null; |
|
try { |
|
this.latestServiceConfig = (0, service_config_1.extractAndSelectServiceConfig)(txtRecord, this.percentage); |
|
} |
|
catch (err) { |
|
this.latestServiceConfigError = { |
|
code: constants_1.Status.UNAVAILABLE, |
|
details: `Parsing service config failed with error ${err.message}`, |
|
metadata: new metadata_1.Metadata(), |
|
}; |
|
} |
|
if (this.latestLookupResult !== null) { |
|
|
|
|
|
|
|
|
|
this.listener.onSuccessfulResolution(this.latestLookupResult, this.latestServiceConfig, this.latestServiceConfigError, null, {}); |
|
} |
|
}, err => { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}); |
|
} |
|
} |
|
} |
|
async lookup(hostname) { |
|
if (environment_1.GRPC_NODE_USE_ALTERNATIVE_RESOLVER) { |
|
trace('Using alternative DNS resolver.'); |
|
const records = await Promise.allSettled([ |
|
this.alternativeResolver.resolve4(hostname), |
|
this.alternativeResolver.resolve6(hostname), |
|
]); |
|
if (records.every(result => result.status === 'rejected')) { |
|
throw new Error(records[0].reason); |
|
} |
|
return records |
|
.reduce((acc, result) => { |
|
return result.status === 'fulfilled' |
|
? [...acc, ...result.value] |
|
: acc; |
|
}, []) |
|
.map(addr => ({ |
|
host: addr, |
|
port: +this.port, |
|
})); |
|
} |
|
|
|
|
|
|
|
|
|
const addressList = await dns_1.promises.lookup(hostname, { all: true }); |
|
return addressList.map(addr => ({ host: addr.address, port: +this.port })); |
|
} |
|
async resolveTxt(hostname) { |
|
if (environment_1.GRPC_NODE_USE_ALTERNATIVE_RESOLVER) { |
|
trace('Using alternative DNS resolver.'); |
|
return this.alternativeResolver.resolveTxt(hostname); |
|
} |
|
return dns_1.promises.resolveTxt(hostname); |
|
} |
|
startNextResolutionTimer() { |
|
var _a, _b; |
|
clearTimeout(this.nextResolutionTimer); |
|
this.nextResolutionTimer = setTimeout(() => { |
|
this.stopNextResolutionTimer(); |
|
if (this.continueResolving) { |
|
this.startResolutionWithBackoff(); |
|
} |
|
}, this.minTimeBetweenResolutionsMs); |
|
(_b = (_a = this.nextResolutionTimer).unref) === null || _b === void 0 ? void 0 : _b.call(_a); |
|
this.isNextResolutionTimerRunning = true; |
|
} |
|
stopNextResolutionTimer() { |
|
clearTimeout(this.nextResolutionTimer); |
|
this.isNextResolutionTimerRunning = false; |
|
} |
|
startResolutionWithBackoff() { |
|
if (this.pendingLookupPromise === null) { |
|
this.continueResolving = false; |
|
this.backoff.runOnce(); |
|
this.startNextResolutionTimer(); |
|
this.startResolution(); |
|
} |
|
} |
|
updateResolution() { |
|
|
|
|
|
|
|
|
|
if (this.pendingLookupPromise === null) { |
|
if (this.isNextResolutionTimerRunning || this.backoff.isRunning()) { |
|
if (this.isNextResolutionTimerRunning) { |
|
trace('resolution update delayed by "min time between resolutions" rate limit'); |
|
} |
|
else { |
|
trace('resolution update delayed by backoff timer until ' + |
|
this.backoff.getEndTime().toISOString()); |
|
} |
|
this.continueResolving = true; |
|
} |
|
else { |
|
this.startResolutionWithBackoff(); |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
destroy() { |
|
this.continueResolving = false; |
|
this.backoff.reset(); |
|
this.backoff.stop(); |
|
this.stopNextResolutionTimer(); |
|
this.pendingLookupPromise = null; |
|
this.pendingTxtPromise = null; |
|
this.latestLookupResult = null; |
|
this.latestServiceConfig = null; |
|
this.latestServiceConfigError = null; |
|
this.returnedIpResult = false; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
static getDefaultAuthority(target) { |
|
return target.path; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
function setup() { |
|
(0, resolver_1.registerResolver)('dns', DnsResolver); |
|
(0, resolver_1.registerDefaultScheme)('dns'); |
|
} |
|
|