UNPKG

10.6 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 TRACER_NAME = 'dns_resolver';
30function trace(text) {
31 logging.trace(constants_2.LogVerbosity.DEBUG, TRACER_NAME, text);
32}
33/**
34 * The default TCP port to connect to if not explicitly specified in the target.
35 */
36const DEFAULT_PORT = 443;
37const resolveTxtPromise = util.promisify(dns.resolveTxt);
38const dnsLookupPromise = util.promisify(dns.lookup);
39/**
40 * Merge any number of arrays into a single alternating array
41 * @param arrays
42 */
43function mergeArrays(...arrays) {
44 const result = [];
45 for (let i = 0; i <
46 Math.max.apply(null, arrays.map((array) => array.length)); i++) {
47 for (const array of arrays) {
48 if (i < array.length) {
49 result.push(array[i]);
50 }
51 }
52 }
53 return result;
54}
55/**
56 * Resolver implementation that handles DNS names and IP addresses.
57 */
58class DnsResolver {
59 constructor(target, listener, channelOptions) {
60 var _a, _b;
61 this.target = target;
62 this.listener = listener;
63 this.pendingLookupPromise = null;
64 this.pendingTxtPromise = null;
65 this.latestLookupResult = null;
66 this.latestServiceConfig = null;
67 this.latestServiceConfigError = null;
68 trace('Resolver constructed for target ' + uri_parser_1.uriToString(target));
69 const hostPort = uri_parser_1.splitHostPort(target.path);
70 if (hostPort === null) {
71 this.ipResult = null;
72 this.dnsHostname = null;
73 this.port = null;
74 }
75 else {
76 if (net_1.isIPv4(hostPort.host) || net_1.isIPv6(hostPort.host)) {
77 this.ipResult = [
78 {
79 host: hostPort.host,
80 port: (_a = hostPort.port) !== null && _a !== void 0 ? _a : DEFAULT_PORT,
81 },
82 ];
83 this.dnsHostname = null;
84 this.port = null;
85 }
86 else {
87 this.ipResult = null;
88 this.dnsHostname = hostPort.host;
89 this.port = (_b = hostPort.port) !== null && _b !== void 0 ? _b : DEFAULT_PORT;
90 }
91 }
92 this.percentage = Math.random() * 100;
93 this.defaultResolutionError = {
94 code: constants_1.Status.UNAVAILABLE,
95 details: `Name resolution failed for target ${uri_parser_1.uriToString(this.target)}`,
96 metadata: new metadata_1.Metadata(),
97 };
98 }
99 /**
100 * If the target is an IP address, just provide that address as a result.
101 * Otherwise, initiate A, AAAA, and TXT lookups
102 */
103 startResolution() {
104 if (this.ipResult !== null) {
105 trace('Returning IP address for target ' + uri_parser_1.uriToString(this.target));
106 setImmediate(() => {
107 this.listener.onSuccessfulResolution(this.ipResult, null, null, null, {});
108 });
109 return;
110 }
111 if (this.dnsHostname === null) {
112 setImmediate(() => {
113 this.listener.onError({
114 code: constants_1.Status.UNAVAILABLE,
115 details: `Failed to parse DNS address ${uri_parser_1.uriToString(this.target)}`,
116 metadata: new metadata_1.Metadata(),
117 });
118 });
119 }
120 else {
121 /* We clear out latestLookupResult here to ensure that it contains the
122 * latest result since the last time we started resolving. That way, the
123 * TXT resolution handler can use it, but only if it finishes second. We
124 * don't clear out any previous service config results because it's
125 * better to use a service config that's slightly out of date than to
126 * revert to an effectively blank one. */
127 this.latestLookupResult = null;
128 const hostname = this.dnsHostname;
129 /* We lookup both address families here and then split them up later
130 * because when looking up a single family, dns.lookup outputs an error
131 * if the name exists but there are no records for that family, and that
132 * error is indistinguishable from other kinds of errors */
133 this.pendingLookupPromise = dnsLookupPromise(hostname, { all: true });
134 this.pendingLookupPromise.then((addressList) => {
135 this.pendingLookupPromise = null;
136 const ip4Addresses = addressList.filter((addr) => addr.family === 4);
137 const ip6Addresses = addressList.filter((addr) => addr.family === 6);
138 this.latestLookupResult = mergeArrays(ip6Addresses, ip4Addresses).map((addr) => ({ host: addr.address, port: +this.port }));
139 const allAddressesString = '[' +
140 this.latestLookupResult
141 .map((addr) => addr.host + ':' + addr.port)
142 .join(',') +
143 ']';
144 trace('Resolved addresses for target ' +
145 uri_parser_1.uriToString(this.target) +
146 ': ' +
147 allAddressesString);
148 if (this.latestLookupResult.length === 0) {
149 this.listener.onError(this.defaultResolutionError);
150 return;
151 }
152 /* If the TXT lookup has not yet finished, both of the last two
153 * arguments will be null, which is the equivalent of getting an
154 * empty TXT response. When the TXT lookup does finish, its handler
155 * can update the service config by using the same address list */
156 this.listener.onSuccessfulResolution(this.latestLookupResult, this.latestServiceConfig, this.latestServiceConfigError, null, {});
157 }, (err) => {
158 trace('Resolution error for target ' +
159 uri_parser_1.uriToString(this.target) +
160 ': ' +
161 err.message);
162 this.pendingLookupPromise = null;
163 this.listener.onError(this.defaultResolutionError);
164 });
165 /* If there already is a still-pending TXT resolution, we can just use
166 * that result when it comes in */
167 if (this.pendingTxtPromise === null) {
168 /* We handle the TXT query promise differently than the others because
169 * the name resolution attempt as a whole is a success even if the TXT
170 * lookup fails */
171 this.pendingTxtPromise = resolveTxtPromise(hostname);
172 this.pendingTxtPromise.then((txtRecord) => {
173 this.pendingTxtPromise = null;
174 try {
175 this.latestServiceConfig = service_config_1.extractAndSelectServiceConfig(txtRecord, this.percentage);
176 }
177 catch (err) {
178 this.latestServiceConfigError = {
179 code: constants_1.Status.UNAVAILABLE,
180 details: 'Parsing service config failed',
181 metadata: new metadata_1.Metadata(),
182 };
183 }
184 if (this.latestLookupResult !== null) {
185 /* We rely here on the assumption that calling this function with
186 * identical parameters will be essentialy idempotent, and calling
187 * it with the same address list and a different service config
188 * should result in a fast and seamless switchover. */
189 this.listener.onSuccessfulResolution(this.latestLookupResult, this.latestServiceConfig, this.latestServiceConfigError, null, {});
190 }
191 }, (err) => {
192 /* If TXT lookup fails we should do nothing, which means that we
193 * continue to use the result of the most recent successful lookup,
194 * or the default null config object if there has never been a
195 * successful lookup. We do not set the latestServiceConfigError
196 * here because that is specifically used for response validation
197 * errors. We still need to handle this error so that it does not
198 * bubble up as an unhandled promise rejection. */
199 });
200 }
201 }
202 }
203 updateResolution() {
204 trace('Resolution update requested for target ' + uri_parser_1.uriToString(this.target));
205 if (this.pendingLookupPromise === null) {
206 this.startResolution();
207 }
208 }
209 destroy() {
210 /* Do nothing. There is not a practical way to cancel in-flight DNS
211 * requests, and after this function is called we can expect that
212 * updateResolution will not be called again. */
213 }
214 /**
215 * Get the default authority for the given target. For IP targets, that is
216 * the IP address. For DNS targets, it is the hostname.
217 * @param target
218 */
219 static getDefaultAuthority(target) {
220 return target.path;
221 }
222}
223/**
224 * Set up the DNS resolver class by registering it as the handler for the
225 * "dns:" prefix and as the default resolver.
226 */
227function setup() {
228 resolver_1.registerResolver('dns', DnsResolver);
229 resolver_1.registerDefaultScheme('dns');
230}
231exports.setup = setup;
232//# sourceMappingURL=resolver-dns.js.map
\No newline at end of file