UNPKG

13.4 kBJavaScriptView Raw
1"use strict";
2/*
3 * Copyright 2019 gRPC authors.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17Object.defineProperty(exports, "__esModule", { value: true });
18exports.setup = void 0;
19const resolver_1 = require("./resolver");
20const dns = require("dns");
21const util = require("util");
22const service_config_1 = require("./service-config");
23const constants_1 = require("./constants");
24const metadata_1 = require("./metadata");
25const logging = require("./logging");
26const constants_2 = require("./constants");
27const uri_parser_1 = require("./uri-parser");
28const net_1 = require("net");
29const backoff_timeout_1 = require("./backoff-timeout");
30const TRACER_NAME = 'dns_resolver';
31function trace(text) {
32 logging.trace(constants_2.LogVerbosity.DEBUG, TRACER_NAME, text);
33}
34/**
35 * The default TCP port to connect to if not explicitly specified in the target.
36 */
37const DEFAULT_PORT = 443;
38const DEFAULT_MIN_TIME_BETWEEN_RESOLUTIONS_MS = 30000;
39const resolveTxtPromise = util.promisify(dns.resolveTxt);
40const dnsLookupPromise = util.promisify(dns.lookup);
41/**
42 * Merge any number of arrays into a single alternating array
43 * @param arrays
44 */
45function mergeArrays(...arrays) {
46 const result = [];
47 for (let i = 0; i <
48 Math.max.apply(null, arrays.map((array) => array.length)); i++) {
49 for (const array of arrays) {
50 if (i < array.length) {
51 result.push(array[i]);
52 }
53 }
54 }
55 return result;
56}
57/**
58 * Resolver implementation that handles DNS names and IP addresses.
59 */
60class DnsResolver {
61 constructor(target, listener, channelOptions) {
62 var _a, _b, _c;
63 this.target = target;
64 this.listener = listener;
65 this.pendingLookupPromise = null;
66 this.pendingTxtPromise = null;
67 this.latestLookupResult = null;
68 this.latestServiceConfig = null;
69 this.latestServiceConfigError = null;
70 this.continueResolving = false;
71 this.isNextResolutionTimerRunning = false;
72 this.isServiceConfigEnabled = true;
73 trace('Resolver constructed for target ' + (0, uri_parser_1.uriToString)(target));
74 const hostPort = (0, uri_parser_1.splitHostPort)(target.path);
75 if (hostPort === null) {
76 this.ipResult = null;
77 this.dnsHostname = null;
78 this.port = null;
79 }
80 else {
81 if ((0, net_1.isIPv4)(hostPort.host) || (0, net_1.isIPv6)(hostPort.host)) {
82 this.ipResult = [
83 {
84 host: hostPort.host,
85 port: (_a = hostPort.port) !== null && _a !== void 0 ? _a : DEFAULT_PORT,
86 },
87 ];
88 this.dnsHostname = null;
89 this.port = null;
90 }
91 else {
92 this.ipResult = null;
93 this.dnsHostname = hostPort.host;
94 this.port = (_b = hostPort.port) !== null && _b !== void 0 ? _b : DEFAULT_PORT;
95 }
96 }
97 this.percentage = Math.random() * 100;
98 if (channelOptions['grpc.service_config_disable_resolution'] === 1) {
99 this.isServiceConfigEnabled = false;
100 }
101 this.defaultResolutionError = {
102 code: constants_1.Status.UNAVAILABLE,
103 details: `Name resolution failed for target ${(0, uri_parser_1.uriToString)(this.target)}`,
104 metadata: new metadata_1.Metadata(),
105 };
106 const backoffOptions = {
107 initialDelay: channelOptions['grpc.initial_reconnect_backoff_ms'],
108 maxDelay: channelOptions['grpc.max_reconnect_backoff_ms'],
109 };
110 this.backoff = new backoff_timeout_1.BackoffTimeout(() => {
111 if (this.continueResolving) {
112 this.startResolutionWithBackoff();
113 }
114 }, backoffOptions);
115 this.backoff.unref();
116 this.minTimeBetweenResolutionsMs = (_c = channelOptions['grpc.dns_min_time_between_resolutions_ms']) !== null && _c !== void 0 ? _c : DEFAULT_MIN_TIME_BETWEEN_RESOLUTIONS_MS;
117 this.nextResolutionTimer = setTimeout(() => { }, 0);
118 clearTimeout(this.nextResolutionTimer);
119 }
120 /**
121 * If the target is an IP address, just provide that address as a result.
122 * Otherwise, initiate A, AAAA, and TXT lookups
123 */
124 startResolution() {
125 if (this.ipResult !== null) {
126 trace('Returning IP address for target ' + (0, uri_parser_1.uriToString)(this.target));
127 setImmediate(() => {
128 this.listener.onSuccessfulResolution(this.ipResult, null, null, null, {});
129 });
130 this.backoff.stop();
131 this.backoff.reset();
132 return;
133 }
134 if (this.dnsHostname === null) {
135 trace('Failed to parse DNS address ' + (0, uri_parser_1.uriToString)(this.target));
136 setImmediate(() => {
137 this.listener.onError({
138 code: constants_1.Status.UNAVAILABLE,
139 details: `Failed to parse DNS address ${(0, uri_parser_1.uriToString)(this.target)}`,
140 metadata: new metadata_1.Metadata(),
141 });
142 });
143 this.stopNextResolutionTimer();
144 }
145 else {
146 if (this.pendingLookupPromise !== null) {
147 return;
148 }
149 trace('Looking up DNS hostname ' + this.dnsHostname);
150 /* We clear out latestLookupResult here to ensure that it contains the
151 * latest result since the last time we started resolving. That way, the
152 * TXT resolution handler can use it, but only if it finishes second. We
153 * don't clear out any previous service config results because it's
154 * better to use a service config that's slightly out of date than to
155 * revert to an effectively blank one. */
156 this.latestLookupResult = null;
157 const hostname = this.dnsHostname;
158 /* We lookup both address families here and then split them up later
159 * because when looking up a single family, dns.lookup outputs an error
160 * if the name exists but there are no records for that family, and that
161 * error is indistinguishable from other kinds of errors */
162 this.pendingLookupPromise = dnsLookupPromise(hostname, { all: true });
163 this.pendingLookupPromise.then((addressList) => {
164 this.pendingLookupPromise = null;
165 this.backoff.reset();
166 this.backoff.stop();
167 const ip4Addresses = addressList.filter((addr) => addr.family === 4);
168 const ip6Addresses = addressList.filter((addr) => addr.family === 6);
169 this.latestLookupResult = mergeArrays(ip6Addresses, ip4Addresses).map((addr) => ({ host: addr.address, port: +this.port }));
170 const allAddressesString = '[' +
171 this.latestLookupResult
172 .map((addr) => addr.host + ':' + addr.port)
173 .join(',') +
174 ']';
175 trace('Resolved addresses for target ' +
176 (0, uri_parser_1.uriToString)(this.target) +
177 ': ' +
178 allAddressesString);
179 if (this.latestLookupResult.length === 0) {
180 this.listener.onError(this.defaultResolutionError);
181 return;
182 }
183 /* If the TXT lookup has not yet finished, both of the last two
184 * arguments will be null, which is the equivalent of getting an
185 * empty TXT response. When the TXT lookup does finish, its handler
186 * can update the service config by using the same address list */
187 this.listener.onSuccessfulResolution(this.latestLookupResult, this.latestServiceConfig, this.latestServiceConfigError, null, {});
188 }, (err) => {
189 trace('Resolution error for target ' +
190 (0, uri_parser_1.uriToString)(this.target) +
191 ': ' +
192 err.message);
193 this.pendingLookupPromise = null;
194 this.stopNextResolutionTimer();
195 this.listener.onError(this.defaultResolutionError);
196 });
197 /* If there already is a still-pending TXT resolution, we can just use
198 * that result when it comes in */
199 if (this.isServiceConfigEnabled && this.pendingTxtPromise === null) {
200 /* We handle the TXT query promise differently than the others because
201 * the name resolution attempt as a whole is a success even if the TXT
202 * lookup fails */
203 this.pendingTxtPromise = resolveTxtPromise(hostname);
204 this.pendingTxtPromise.then((txtRecord) => {
205 this.pendingTxtPromise = null;
206 try {
207 this.latestServiceConfig = (0, service_config_1.extractAndSelectServiceConfig)(txtRecord, this.percentage);
208 }
209 catch (err) {
210 this.latestServiceConfigError = {
211 code: constants_1.Status.UNAVAILABLE,
212 details: 'Parsing service config failed',
213 metadata: new metadata_1.Metadata(),
214 };
215 }
216 if (this.latestLookupResult !== null) {
217 /* We rely here on the assumption that calling this function with
218 * identical parameters will be essentialy idempotent, and calling
219 * it with the same address list and a different service config
220 * should result in a fast and seamless switchover. */
221 this.listener.onSuccessfulResolution(this.latestLookupResult, this.latestServiceConfig, this.latestServiceConfigError, null, {});
222 }
223 }, (err) => {
224 /* If TXT lookup fails we should do nothing, which means that we
225 * continue to use the result of the most recent successful lookup,
226 * or the default null config object if there has never been a
227 * successful lookup. We do not set the latestServiceConfigError
228 * here because that is specifically used for response validation
229 * errors. We still need to handle this error so that it does not
230 * bubble up as an unhandled promise rejection. */
231 });
232 }
233 }
234 }
235 startNextResolutionTimer() {
236 var _a, _b;
237 clearTimeout(this.nextResolutionTimer);
238 this.nextResolutionTimer = (_b = (_a = setTimeout(() => {
239 this.stopNextResolutionTimer();
240 if (this.continueResolving) {
241 this.startResolutionWithBackoff();
242 }
243 }, this.minTimeBetweenResolutionsMs)).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
244 this.isNextResolutionTimerRunning = true;
245 }
246 stopNextResolutionTimer() {
247 clearTimeout(this.nextResolutionTimer);
248 this.isNextResolutionTimerRunning = false;
249 }
250 startResolutionWithBackoff() {
251 if (this.pendingLookupPromise === null) {
252 this.continueResolving = false;
253 this.startResolution();
254 this.backoff.runOnce();
255 this.startNextResolutionTimer();
256 }
257 }
258 updateResolution() {
259 /* If there is a pending lookup, just let it finish. Otherwise, if the
260 * nextResolutionTimer or backoff timer is running, set the
261 * continueResolving flag to resolve when whichever of those timers
262 * fires. Otherwise, start resolving immediately. */
263 if (this.pendingLookupPromise === null) {
264 if (this.isNextResolutionTimerRunning || this.backoff.isRunning()) {
265 this.continueResolving = true;
266 }
267 else {
268 this.startResolutionWithBackoff();
269 }
270 }
271 }
272 destroy() {
273 this.continueResolving = false;
274 this.backoff.stop();
275 this.stopNextResolutionTimer();
276 }
277 /**
278 * Get the default authority for the given target. For IP targets, that is
279 * the IP address. For DNS targets, it is the hostname.
280 * @param target
281 */
282 static getDefaultAuthority(target) {
283 return target.path;
284 }
285}
286/**
287 * Set up the DNS resolver class by registering it as the handler for the
288 * "dns:" prefix and as the default resolver.
289 */
290function setup() {
291 (0, resolver_1.registerResolver)('dns', DnsResolver);
292 (0, resolver_1.registerDefaultScheme)('dns');
293}
294exports.setup = setup;
295//# sourceMappingURL=resolver-dns.js.map
\No newline at end of file