1 | ;
|
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 | */
|
17 | Object.defineProperty(exports, "__esModule", { value: true });
|
18 | exports.setup = void 0;
|
19 | const resolver_1 = require("./resolver");
|
20 | const dns = require("dns");
|
21 | const util = require("util");
|
22 | const service_config_1 = require("./service-config");
|
23 | const constants_1 = require("./constants");
|
24 | const metadata_1 = require("./metadata");
|
25 | const logging = require("./logging");
|
26 | const constants_2 = require("./constants");
|
27 | const uri_parser_1 = require("./uri-parser");
|
28 | const net_1 = require("net");
|
29 | const TRACER_NAME = 'dns_resolver';
|
30 | function 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 | */
|
36 | const DEFAULT_PORT = 443;
|
37 | const resolveTxtPromise = util.promisify(dns.resolveTxt);
|
38 | const dnsLookupPromise = util.promisify(dns.lookup);
|
39 | /**
|
40 | * Merge any number of arrays into a single alternating array
|
41 | * @param arrays
|
42 | */
|
43 | function 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 | */
|
58 | class 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 | */
|
227 | function setup() {
|
228 | resolver_1.registerResolver('dns', DnsResolver);
|
229 | resolver_1.registerDefaultScheme('dns');
|
230 | }
|
231 | exports.setup = setup;
|
232 | //# sourceMappingURL=resolver-dns.js.map |
\ | No newline at end of file |