UNPKG

13.1 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 trace('Resolver constructed for target ' + uri_parser_1.uriToString(target));
73 const hostPort = uri_parser_1.splitHostPort(target.path);
74 if (hostPort === null) {
75 this.ipResult = null;
76 this.dnsHostname = null;
77 this.port = null;
78 }
79 else {
80 if (net_1.isIPv4(hostPort.host) || net_1.isIPv6(hostPort.host)) {
81 this.ipResult = [
82 {
83 host: hostPort.host,
84 port: (_a = hostPort.port) !== null && _a !== void 0 ? _a : DEFAULT_PORT,
85 },
86 ];
87 this.dnsHostname = null;
88 this.port = null;
89 }
90 else {
91 this.ipResult = null;
92 this.dnsHostname = hostPort.host;
93 this.port = (_b = hostPort.port) !== null && _b !== void 0 ? _b : DEFAULT_PORT;
94 }
95 }
96 this.percentage = Math.random() * 100;
97 this.defaultResolutionError = {
98 code: constants_1.Status.UNAVAILABLE,
99 details: `Name resolution failed for target ${uri_parser_1.uriToString(this.target)}`,
100 metadata: new metadata_1.Metadata(),
101 };
102 const backoffOptions = {
103 initialDelay: channelOptions['grpc.initial_reconnect_backoff_ms'],
104 maxDelay: channelOptions['grpc.max_reconnect_backoff_ms'],
105 };
106 this.backoff = new backoff_timeout_1.BackoffTimeout(() => {
107 if (this.continueResolving) {
108 this.startResolutionWithBackoff();
109 }
110 }, backoffOptions);
111 this.backoff.unref();
112 this.minTimeBetweenResolutionsMs = (_c = channelOptions['grpc.dns_min_time_between_resolutions_ms']) !== null && _c !== void 0 ? _c : DEFAULT_MIN_TIME_BETWEEN_RESOLUTIONS_MS;
113 this.nextResolutionTimer = setTimeout(() => { }, 0);
114 clearTimeout(this.nextResolutionTimer);
115 }
116 /**
117 * If the target is an IP address, just provide that address as a result.
118 * Otherwise, initiate A, AAAA, and TXT lookups
119 */
120 startResolution() {
121 if (this.ipResult !== null) {
122 trace('Returning IP address for target ' + uri_parser_1.uriToString(this.target));
123 setImmediate(() => {
124 this.listener.onSuccessfulResolution(this.ipResult, null, null, null, {});
125 });
126 this.backoff.stop();
127 this.backoff.reset();
128 return;
129 }
130 if (this.dnsHostname === null) {
131 trace('Failed to parse DNS address ' + uri_parser_1.uriToString(this.target));
132 setImmediate(() => {
133 this.listener.onError({
134 code: constants_1.Status.UNAVAILABLE,
135 details: `Failed to parse DNS address ${uri_parser_1.uriToString(this.target)}`,
136 metadata: new metadata_1.Metadata(),
137 });
138 });
139 this.stopNextResolutionTimer();
140 }
141 else {
142 if (this.pendingLookupPromise !== null) {
143 return;
144 }
145 trace('Looking up DNS hostname ' + this.dnsHostname);
146 /* We clear out latestLookupResult here to ensure that it contains the
147 * latest result since the last time we started resolving. That way, the
148 * TXT resolution handler can use it, but only if it finishes second. We
149 * don't clear out any previous service config results because it's
150 * better to use a service config that's slightly out of date than to
151 * revert to an effectively blank one. */
152 this.latestLookupResult = null;
153 const hostname = this.dnsHostname;
154 /* We lookup both address families here and then split them up later
155 * because when looking up a single family, dns.lookup outputs an error
156 * if the name exists but there are no records for that family, and that
157 * error is indistinguishable from other kinds of errors */
158 this.pendingLookupPromise = dnsLookupPromise(hostname, { all: true });
159 this.pendingLookupPromise.then((addressList) => {
160 this.pendingLookupPromise = null;
161 this.backoff.reset();
162 this.backoff.stop();
163 const ip4Addresses = addressList.filter((addr) => addr.family === 4);
164 const ip6Addresses = addressList.filter((addr) => addr.family === 6);
165 this.latestLookupResult = mergeArrays(ip6Addresses, ip4Addresses).map((addr) => ({ host: addr.address, port: +this.port }));
166 const allAddressesString = '[' +
167 this.latestLookupResult
168 .map((addr) => addr.host + ':' + addr.port)
169 .join(',') +
170 ']';
171 trace('Resolved addresses for target ' +
172 uri_parser_1.uriToString(this.target) +
173 ': ' +
174 allAddressesString);
175 if (this.latestLookupResult.length === 0) {
176 this.listener.onError(this.defaultResolutionError);
177 return;
178 }
179 /* If the TXT lookup has not yet finished, both of the last two
180 * arguments will be null, which is the equivalent of getting an
181 * empty TXT response. When the TXT lookup does finish, its handler
182 * can update the service config by using the same address list */
183 this.listener.onSuccessfulResolution(this.latestLookupResult, this.latestServiceConfig, this.latestServiceConfigError, null, {});
184 }, (err) => {
185 trace('Resolution error for target ' +
186 uri_parser_1.uriToString(this.target) +
187 ': ' +
188 err.message);
189 this.pendingLookupPromise = null;
190 this.stopNextResolutionTimer();
191 this.listener.onError(this.defaultResolutionError);
192 });
193 /* If there already is a still-pending TXT resolution, we can just use
194 * that result when it comes in */
195 if (this.pendingTxtPromise === null) {
196 /* We handle the TXT query promise differently than the others because
197 * the name resolution attempt as a whole is a success even if the TXT
198 * lookup fails */
199 this.pendingTxtPromise = resolveTxtPromise(hostname);
200 this.pendingTxtPromise.then((txtRecord) => {
201 this.pendingTxtPromise = null;
202 try {
203 this.latestServiceConfig = service_config_1.extractAndSelectServiceConfig(txtRecord, this.percentage);
204 }
205 catch (err) {
206 this.latestServiceConfigError = {
207 code: constants_1.Status.UNAVAILABLE,
208 details: 'Parsing service config failed',
209 metadata: new metadata_1.Metadata(),
210 };
211 }
212 if (this.latestLookupResult !== null) {
213 /* We rely here on the assumption that calling this function with
214 * identical parameters will be essentialy idempotent, and calling
215 * it with the same address list and a different service config
216 * should result in a fast and seamless switchover. */
217 this.listener.onSuccessfulResolution(this.latestLookupResult, this.latestServiceConfig, this.latestServiceConfigError, null, {});
218 }
219 }, (err) => {
220 /* If TXT lookup fails we should do nothing, which means that we
221 * continue to use the result of the most recent successful lookup,
222 * or the default null config object if there has never been a
223 * successful lookup. We do not set the latestServiceConfigError
224 * here because that is specifically used for response validation
225 * errors. We still need to handle this error so that it does not
226 * bubble up as an unhandled promise rejection. */
227 });
228 }
229 }
230 }
231 startNextResolutionTimer() {
232 var _a, _b;
233 clearTimeout(this.nextResolutionTimer);
234 this.nextResolutionTimer = (_b = (_a = setTimeout(() => {
235 this.stopNextResolutionTimer();
236 if (this.continueResolving) {
237 this.startResolutionWithBackoff();
238 }
239 }, this.minTimeBetweenResolutionsMs)).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
240 this.isNextResolutionTimerRunning = true;
241 }
242 stopNextResolutionTimer() {
243 clearTimeout(this.nextResolutionTimer);
244 this.isNextResolutionTimerRunning = false;
245 }
246 startResolutionWithBackoff() {
247 if (this.pendingLookupPromise === null) {
248 this.continueResolving = false;
249 this.startResolution();
250 this.backoff.runOnce();
251 this.startNextResolutionTimer();
252 }
253 }
254 updateResolution() {
255 /* If there is a pending lookup, just let it finish. Otherwise, if the
256 * nextResolutionTimer or backoff timer is running, set the
257 * continueResolving flag to resolve when whichever of those timers
258 * fires. Otherwise, start resolving immediately. */
259 if (this.pendingLookupPromise === null) {
260 if (this.isNextResolutionTimerRunning || this.backoff.isRunning()) {
261 this.continueResolving = true;
262 }
263 else {
264 this.startResolutionWithBackoff();
265 }
266 }
267 }
268 destroy() {
269 this.continueResolving = false;
270 this.backoff.stop();
271 this.stopNextResolutionTimer();
272 }
273 /**
274 * Get the default authority for the given target. For IP targets, that is
275 * the IP address. For DNS targets, it is the hostname.
276 * @param target
277 */
278 static getDefaultAuthority(target) {
279 return target.path;
280 }
281}
282/**
283 * Set up the DNS resolver class by registering it as the handler for the
284 * "dns:" prefix and as the default resolver.
285 */
286function setup() {
287 resolver_1.registerResolver('dns', DnsResolver);
288 resolver_1.registerDefaultScheme('dns');
289}
290exports.setup = setup;
291//# sourceMappingURL=resolver-dns.js.map
\No newline at end of file