UNPKG

25.3 kBPlain TextView Raw
1/*
2 * Copyright 2023 gRPC authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 *
16 */
17
18import * as http2 from 'http2';
19import { checkServerIdentity, CipherNameAndProtocol, ConnectionOptions, PeerCertificate, TLSSocket } from 'tls';
20import { StatusObject } from './call-interface';
21import { ChannelCredentials } from './channel-credentials';
22import { ChannelOptions } from './channel-options';
23import { ChannelzCallTracker, registerChannelzSocket, SocketInfo, SocketRef, TlsInfo, unregisterChannelzRef } from './channelz';
24import { LogVerbosity } from './constants';
25import { getProxiedConnection, ProxyConnectionResult } from './http_proxy';
26import * as logging from './logging';
27import { getDefaultAuthority } from './resolver';
28import { stringToSubchannelAddress, SubchannelAddress, subchannelAddressToString } from './subchannel-address';
29import { GrpcUri, parseUri, splitHostPort, uriToString } from './uri-parser';
30import * as net from 'net';
31import { Http2SubchannelCall, SubchannelCall, SubchannelCallInterceptingListener } from './subchannel-call';
32import { Metadata } from './metadata';
33import { getNextCallNumber } from './call-number';
34
35const TRACER_NAME = 'transport';
36const FLOW_CONTROL_TRACER_NAME = 'transport_flowctrl';
37
38const clientVersion = require('../../package.json').version;
39
40const {
41 HTTP2_HEADER_AUTHORITY,
42 HTTP2_HEADER_CONTENT_TYPE,
43 HTTP2_HEADER_METHOD,
44 HTTP2_HEADER_PATH,
45 HTTP2_HEADER_TE,
46 HTTP2_HEADER_USER_AGENT,
47} = http2.constants;
48
49/* setInterval and setTimeout only accept signed 32 bit integers. JS doesn't
50 * have a constant for the max signed 32 bit integer, so this is a simple way
51 * to calculate it */
52const KEEPALIVE_MAX_TIME_MS = ~(1 << 31);
53const KEEPALIVE_TIMEOUT_MS = 20000;
54
55export interface CallEventTracker {
56 addMessageSent(): void;
57 addMessageReceived(): void;
58 onCallEnd(status: StatusObject): void;
59 onStreamEnd(success: boolean): void;
60}
61
62export interface TransportDisconnectListener {
63 (tooManyPings: boolean): void;
64}
65
66export interface Transport {
67 getChannelzRef(): SocketRef;
68 getPeerName(): string;
69 createCall(metadata: Metadata, host: string, method: string, listener: SubchannelCallInterceptingListener, subchannelCallStatsTracker: Partial<CallEventTracker>): SubchannelCall;
70 addDisconnectListener(listener: TransportDisconnectListener): void;
71 shutdown(): void;
72}
73
74const tooManyPingsData: Buffer = Buffer.from('too_many_pings', 'ascii');
75
76class Http2Transport implements Transport {
77 /**
78 * The amount of time in between sending pings
79 */
80 private keepaliveTimeMs: number = -1;
81 /**
82 * The amount of time to wait for an acknowledgement after sending a ping
83 */
84 private keepaliveTimeoutMs: number = KEEPALIVE_TIMEOUT_MS;
85 /**
86 * Timer reference for timeout that indicates when to send the next ping
87 */
88 private keepaliveIntervalId: NodeJS.Timer;
89 /**
90 * Timer reference tracking when the most recent ping will be considered lost
91 */
92 private keepaliveTimeoutId: NodeJS.Timer | null = null;
93 /**
94 * Indicates whether keepalive pings should be sent without any active calls
95 */
96 private keepaliveWithoutCalls = false;
97
98 private userAgent: string;
99
100 private activeCalls: Set<Http2SubchannelCall> = new Set();
101
102 private subchannelAddressString: string;
103
104 private disconnectListeners: TransportDisconnectListener[] = [];
105
106 private disconnectHandled = false;
107
108 // Channelz info
109 private channelzRef: SocketRef;
110 private readonly channelzEnabled: boolean = true;
111 /**
112 * Name of the remote server, if it is not the same as the subchannel
113 * address, i.e. if connecting through an HTTP CONNECT proxy.
114 */
115 private remoteName: string | null = null;
116 private streamTracker = new ChannelzCallTracker();
117 private keepalivesSent = 0;
118 private messagesSent = 0;
119 private messagesReceived = 0;
120 private lastMessageSentTimestamp: Date | null = null;
121 private lastMessageReceivedTimestamp: Date | null = null;
122
123 constructor(
124 private session: http2.ClientHttp2Session,
125 subchannelAddress: SubchannelAddress,
126 options: ChannelOptions
127 ) {
128 // Build user-agent string.
129 this.userAgent = [
130 options['grpc.primary_user_agent'],
131 `grpc-node-js/${clientVersion}`,
132 options['grpc.secondary_user_agent'],
133 ]
134 .filter((e) => e)
135 .join(' '); // remove falsey values first
136
137 if ('grpc.keepalive_time_ms' in options) {
138 this.keepaliveTimeMs = options['grpc.keepalive_time_ms']!;
139 }
140 if ('grpc.keepalive_timeout_ms' in options) {
141 this.keepaliveTimeoutMs = options['grpc.keepalive_timeout_ms']!;
142 }
143 if ('grpc.keepalive_permit_without_calls' in options) {
144 this.keepaliveWithoutCalls =
145 options['grpc.keepalive_permit_without_calls'] === 1;
146 } else {
147 this.keepaliveWithoutCalls = false;
148 }
149 this.keepaliveIntervalId = setTimeout(() => {}, 0);
150 clearTimeout(this.keepaliveIntervalId);
151 if (this.keepaliveWithoutCalls) {
152 this.startKeepalivePings();
153 }
154
155 this.subchannelAddressString = subchannelAddressToString(subchannelAddress);
156
157 if (options['grpc.enable_channelz'] === 0) {
158 this.channelzEnabled = false;
159 }
160 this.channelzRef = registerChannelzSocket(this.subchannelAddressString, () => this.getChannelzInfo(), this.channelzEnabled);
161
162 session.once('close', () => {
163 this.trace('session closed');
164 this.stopKeepalivePings();
165 this.handleDisconnect();
166 });
167 session.once('goaway', (errorCode: number, lastStreamID: number, opaqueData: Buffer) => {
168 let tooManyPings = false;
169 /* See the last paragraph of
170 * https://github.com/grpc/proposal/blob/master/A8-client-side-keepalive.md#basic-keepalive */
171 if (
172 errorCode === http2.constants.NGHTTP2_ENHANCE_YOUR_CALM &&
173 opaqueData.equals(tooManyPingsData)
174 ) {
175 tooManyPings = true;
176 }
177 this.trace(
178 'connection closed by GOAWAY with code ' +
179 errorCode
180 );
181 this.reportDisconnectToOwner(tooManyPings);
182 });
183 session.once('error', error => {
184 /* Do nothing here. Any error should also trigger a close event, which is
185 * where we want to handle that. */
186 this.trace(
187 'connection closed with error ' +
188 (error as Error).message
189 );
190 });
191 if (logging.isTracerEnabled(TRACER_NAME)) {
192 session.on('remoteSettings', (settings: http2.Settings) => {
193 this.trace(
194 'new settings received' +
195 (this.session !== session ? ' on the old connection' : '') +
196 ': ' +
197 JSON.stringify(settings)
198 );
199 });
200 session.on('localSettings', (settings: http2.Settings) => {
201 this.trace(
202 'local settings acknowledged by remote' +
203 (this.session !== session ? ' on the old connection' : '') +
204 ': ' +
205 JSON.stringify(settings)
206 );
207 });
208 }
209 }
210
211 private getChannelzInfo(): SocketInfo {
212 const sessionSocket = this.session.socket;
213 const remoteAddress = sessionSocket.remoteAddress ? stringToSubchannelAddress(sessionSocket.remoteAddress, sessionSocket.remotePort) : null;
214 const localAddress = sessionSocket.localAddress ? stringToSubchannelAddress(sessionSocket.localAddress, sessionSocket.localPort) : null;
215 let tlsInfo: TlsInfo | null;
216 if (this.session.encrypted) {
217 const tlsSocket: TLSSocket = sessionSocket as TLSSocket;
218 const cipherInfo: CipherNameAndProtocol & {standardName?: string} = tlsSocket.getCipher();
219 const certificate = tlsSocket.getCertificate();
220 const peerCertificate = tlsSocket.getPeerCertificate();
221 tlsInfo = {
222 cipherSuiteStandardName: cipherInfo.standardName ?? null,
223 cipherSuiteOtherName: cipherInfo.standardName ? null : cipherInfo.name,
224 localCertificate: (certificate && 'raw' in certificate) ? certificate.raw : null,
225 remoteCertificate: (peerCertificate && 'raw' in peerCertificate) ? peerCertificate.raw : null
226 };
227 } else {
228 tlsInfo = null;
229 }
230 const socketInfo: SocketInfo = {
231 remoteAddress: remoteAddress,
232 localAddress: localAddress,
233 security: tlsInfo,
234 remoteName: this.remoteName,
235 streamsStarted: this.streamTracker.callsStarted,
236 streamsSucceeded: this.streamTracker.callsSucceeded,
237 streamsFailed: this.streamTracker.callsFailed,
238 messagesSent: this.messagesSent,
239 messagesReceived: this.messagesReceived,
240 keepAlivesSent: this.keepalivesSent,
241 lastLocalStreamCreatedTimestamp: this.streamTracker.lastCallStartedTimestamp,
242 lastRemoteStreamCreatedTimestamp: null,
243 lastMessageSentTimestamp: this.lastMessageSentTimestamp,
244 lastMessageReceivedTimestamp: this.lastMessageReceivedTimestamp,
245 localFlowControlWindow: this.session.state.localWindowSize ?? null,
246 remoteFlowControlWindow: this.session.state.remoteWindowSize ?? null
247 };
248 return socketInfo;
249 }
250
251 private trace(text: string): void {
252 logging.trace(LogVerbosity.DEBUG, TRACER_NAME, '(' + this.channelzRef.id + ') ' + this.subchannelAddressString + ' ' + text);
253 }
254
255 private keepaliveTrace(text: string): void {
256 logging.trace(LogVerbosity.DEBUG, 'keepalive', '(' + this.channelzRef.id + ') ' + this.subchannelAddressString + ' ' + text);
257 }
258
259 private flowControlTrace(text: string): void {
260 logging.trace(LogVerbosity.DEBUG, FLOW_CONTROL_TRACER_NAME, '(' + this.channelzRef.id + ') ' + this.subchannelAddressString + ' ' + text);
261 }
262
263 private internalsTrace(text: string): void {
264 logging.trace(LogVerbosity.DEBUG, 'transport_internals', '(' + this.channelzRef.id + ') ' + this.subchannelAddressString + ' ' + text);
265 }
266
267 /**
268 * Indicate to the owner of this object that this transport should no longer
269 * be used. That happens if the connection drops, or if the server sends a
270 * GOAWAY.
271 * @param tooManyPings If true, this was triggered by a GOAWAY with data
272 * indicating that the session was closed becaues the client sent too many
273 * pings.
274 * @returns
275 */
276 private reportDisconnectToOwner(tooManyPings: boolean) {
277 if (this.disconnectHandled) {
278 return;
279 }
280 this.disconnectHandled = true;
281 this.disconnectListeners.forEach(listener => listener(tooManyPings));
282 }
283
284 /**
285 * Handle connection drops, but not GOAWAYs.
286 */
287 private handleDisconnect() {
288 this.reportDisconnectToOwner(false);
289 /* Give calls an event loop cycle to finish naturally before reporting the
290 * disconnnection to them. */
291 setImmediate(() => {
292 for (const call of this.activeCalls) {
293 call.onDisconnect();
294 }
295 });
296 }
297
298 addDisconnectListener(listener: TransportDisconnectListener): void {
299 this.disconnectListeners.push(listener);
300 }
301
302 private clearKeepaliveTimeout() {
303 if (!this.keepaliveTimeoutId) {
304 return;
305 }
306 clearTimeout(this.keepaliveTimeoutId);
307 this.keepaliveTimeoutId = null;
308 }
309
310 private sendPing() {
311 if (this.channelzEnabled) {
312 this.keepalivesSent += 1;
313 }
314 this.keepaliveTrace('Sending ping with timeout ' + this.keepaliveTimeoutMs + 'ms');
315 if (!this.keepaliveTimeoutId) {
316 this.keepaliveTimeoutId = setTimeout(() => {
317 this.keepaliveTrace('Ping timeout passed without response');
318 this.handleDisconnect();
319 }, this.keepaliveTimeoutMs);
320 this.keepaliveTimeoutId.unref?.();
321 }
322 try {
323 this.session!.ping(
324 (err: Error | null, duration: number, payload: Buffer) => {
325 this.keepaliveTrace('Received ping response');
326 this.clearKeepaliveTimeout();
327 }
328 );
329 } catch (e) {
330 /* If we fail to send a ping, the connection is no longer functional, so
331 * we should discard it. */
332 this.handleDisconnect();
333 }
334 }
335
336 private startKeepalivePings() {
337 if (this.keepaliveTimeMs < 0) {
338 return;
339 }
340 this.keepaliveIntervalId = setInterval(() => {
341 this.sendPing();
342 }, this.keepaliveTimeMs);
343 this.keepaliveIntervalId.unref?.();
344 /* Don't send a ping immediately because whatever caused us to start
345 * sending pings should also involve some network activity. */
346 }
347
348 /**
349 * Stop keepalive pings when terminating a connection. This discards the
350 * outstanding ping timeout, so it should not be called if the same
351 * connection will still be used.
352 */
353 private stopKeepalivePings() {
354 clearInterval(this.keepaliveIntervalId);
355 this.clearKeepaliveTimeout();
356 }
357
358 private removeActiveCall(call: Http2SubchannelCall) {
359 this.activeCalls.delete(call);
360 if (this.activeCalls.size === 0) {
361 this.session.unref();
362 if (!this.keepaliveWithoutCalls) {
363 this.stopKeepalivePings();
364 }
365 }
366 }
367
368 private addActiveCall(call: Http2SubchannelCall) {
369 if (this.activeCalls.size === 0) {
370 this.session.ref();
371 if (!this.keepaliveWithoutCalls) {
372 this.startKeepalivePings();
373 }
374 }
375 this.activeCalls.add(call);
376 }
377
378 createCall(metadata: Metadata, host: string, method: string, listener: SubchannelCallInterceptingListener, subchannelCallStatsTracker: Partial<CallEventTracker>): Http2SubchannelCall {
379 const headers = metadata.toHttp2Headers();
380 headers[HTTP2_HEADER_AUTHORITY] = host;
381 headers[HTTP2_HEADER_USER_AGENT] = this.userAgent;
382 headers[HTTP2_HEADER_CONTENT_TYPE] = 'application/grpc';
383 headers[HTTP2_HEADER_METHOD] = 'POST';
384 headers[HTTP2_HEADER_PATH] = method;
385 headers[HTTP2_HEADER_TE] = 'trailers';
386 let http2Stream: http2.ClientHttp2Stream;
387 /* In theory, if an error is thrown by session.request because session has
388 * become unusable (e.g. because it has received a goaway), this subchannel
389 * should soon see the corresponding close or goaway event anyway and leave
390 * READY. But we have seen reports that this does not happen
391 * (https://github.com/googleapis/nodejs-firestore/issues/1023#issuecomment-653204096)
392 * so for defense in depth, we just discard the session when we see an
393 * error here.
394 */
395 try {
396 http2Stream = this.session!.request(headers);
397 } catch (e) {
398 this.handleDisconnect();
399 throw e;
400 }
401 this.flowControlTrace(
402 'local window size: ' +
403 this.session.state.localWindowSize +
404 ' remote window size: ' +
405 this.session.state.remoteWindowSize
406 );
407 this.internalsTrace(
408 'session.closed=' +
409 this.session.closed +
410 ' session.destroyed=' +
411 this.session.destroyed +
412 ' session.socket.destroyed=' +
413 this.session.socket.destroyed);
414 let eventTracker: CallEventTracker;
415 let call: Http2SubchannelCall;
416 if (this.channelzEnabled) {
417 this.streamTracker.addCallStarted();
418 eventTracker = {
419 addMessageSent: () => {
420 this.messagesSent += 1;
421 this.lastMessageSentTimestamp = new Date();
422 subchannelCallStatsTracker.addMessageSent?.();
423 },
424 addMessageReceived: () => {
425 this.messagesReceived += 1;
426 this.lastMessageReceivedTimestamp = new Date();
427 subchannelCallStatsTracker.addMessageReceived?.();
428 },
429 onCallEnd: status => {
430 subchannelCallStatsTracker.onCallEnd?.(status);
431 this.removeActiveCall(call);
432 },
433 onStreamEnd: success => {
434 if (success) {
435 this.streamTracker.addCallSucceeded();
436 } else {
437 this.streamTracker.addCallFailed();
438 }
439 subchannelCallStatsTracker.onStreamEnd?.(success);
440 }
441 }
442 } else {
443 eventTracker = {
444 addMessageSent: () => {
445 subchannelCallStatsTracker.addMessageSent?.();
446 },
447 addMessageReceived: () => {
448 subchannelCallStatsTracker.addMessageReceived?.();
449 },
450 onCallEnd: (status) => {
451 subchannelCallStatsTracker.onCallEnd?.(status);
452 this.removeActiveCall(call);
453 },
454 onStreamEnd: (success) => {
455 subchannelCallStatsTracker.onStreamEnd?.(success);
456 }
457 }
458 }
459 call = new Http2SubchannelCall(http2Stream, eventTracker, listener, this, getNextCallNumber());
460 this.addActiveCall(call);
461 return call;
462 }
463
464 getChannelzRef(): SocketRef {
465 return this.channelzRef;
466 }
467
468 getPeerName() {
469 return this.subchannelAddressString;
470 }
471
472 shutdown() {
473 this.session.close();
474 unregisterChannelzRef(this.channelzRef);
475 }
476}
477
478export interface SubchannelConnector {
479 connect(address: SubchannelAddress, credentials: ChannelCredentials, options: ChannelOptions): Promise<Transport>;
480 shutdown(): void;
481}
482
483export class Http2SubchannelConnector implements SubchannelConnector {
484 private session: http2.ClientHttp2Session | null = null;
485 private isShutdown = false;
486 constructor(private channelTarget: GrpcUri) {}
487 private trace(text: string) {
488
489 }
490 private createSession(address: SubchannelAddress, credentials: ChannelCredentials, options: ChannelOptions, proxyConnectionResult: ProxyConnectionResult): Promise<Http2Transport> {
491 if (this.isShutdown) {
492 return Promise.reject();
493 }
494 return new Promise<Http2Transport>((resolve, reject) => {
495 let remoteName: string | null;
496 if (proxyConnectionResult.realTarget) {
497 remoteName = uriToString(proxyConnectionResult.realTarget);
498 this.trace('creating HTTP/2 session through proxy to ' + uriToString(proxyConnectionResult.realTarget));
499 } else {
500 remoteName = null;
501 this.trace('creating HTTP/2 session to ' + subchannelAddressToString(address));
502 }
503 const targetAuthority = getDefaultAuthority(
504 proxyConnectionResult.realTarget ?? this.channelTarget
505 );
506 let connectionOptions: http2.SecureClientSessionOptions =
507 credentials._getConnectionOptions() || {};
508 connectionOptions.maxSendHeaderBlockLength = Number.MAX_SAFE_INTEGER;
509 if ('grpc-node.max_session_memory' in options) {
510 connectionOptions.maxSessionMemory = options[
511 'grpc-node.max_session_memory'
512 ];
513 } else {
514 /* By default, set a very large max session memory limit, to effectively
515 * disable enforcement of the limit. Some testing indicates that Node's
516 * behavior degrades badly when this limit is reached, so we solve that
517 * by disabling the check entirely. */
518 connectionOptions.maxSessionMemory = Number.MAX_SAFE_INTEGER;
519 }
520 let addressScheme = 'http://';
521 if ('secureContext' in connectionOptions) {
522 addressScheme = 'https://';
523 // If provided, the value of grpc.ssl_target_name_override should be used
524 // to override the target hostname when checking server identity.
525 // This option is used for testing only.
526 if (options['grpc.ssl_target_name_override']) {
527 const sslTargetNameOverride = options[
528 'grpc.ssl_target_name_override'
529 ]!;
530 connectionOptions.checkServerIdentity = (
531 host: string,
532 cert: PeerCertificate
533 ): Error | undefined => {
534 return checkServerIdentity(sslTargetNameOverride, cert);
535 };
536 connectionOptions.servername = sslTargetNameOverride;
537 } else {
538 const authorityHostname =
539 splitHostPort(targetAuthority)?.host ?? 'localhost';
540 // We want to always set servername to support SNI
541 connectionOptions.servername = authorityHostname;
542 }
543 if (proxyConnectionResult.socket) {
544 /* This is part of the workaround for
545 * https://github.com/nodejs/node/issues/32922. Without that bug,
546 * proxyConnectionResult.socket would always be a plaintext socket and
547 * this would say
548 * connectionOptions.socket = proxyConnectionResult.socket; */
549 connectionOptions.createConnection = (authority, option) => {
550 return proxyConnectionResult.socket!;
551 };
552 }
553 } else {
554 /* In all but the most recent versions of Node, http2.connect does not use
555 * the options when establishing plaintext connections, so we need to
556 * establish that connection explicitly. */
557 connectionOptions.createConnection = (authority, option) => {
558 if (proxyConnectionResult.socket) {
559 return proxyConnectionResult.socket;
560 } else {
561 /* net.NetConnectOpts is declared in a way that is more restrictive
562 * than what net.connect will actually accept, so we use the type
563 * assertion to work around that. */
564 return net.connect(address);
565 }
566 };
567 }
568
569 connectionOptions = {
570 ...connectionOptions,
571 ...address,
572 };
573
574 /* http2.connect uses the options here:
575 * https://github.com/nodejs/node/blob/70c32a6d190e2b5d7b9ff9d5b6a459d14e8b7d59/lib/internal/http2/core.js#L3028-L3036
576 * The spread operator overides earlier values with later ones, so any port
577 * or host values in the options will be used rather than any values extracted
578 * from the first argument. In addition, the path overrides the host and port,
579 * as documented for plaintext connections here:
580 * https://nodejs.org/api/net.html#net_socket_connect_options_connectlistener
581 * and for TLS connections here:
582 * https://nodejs.org/api/tls.html#tls_tls_connect_options_callback. In
583 * earlier versions of Node, http2.connect passes these options to
584 * tls.connect but not net.connect, so in the insecure case we still need
585 * to set the createConnection option above to create the connection
586 * explicitly. We cannot do that in the TLS case because http2.connect
587 * passes necessary additional options to tls.connect.
588 * The first argument just needs to be parseable as a URL and the scheme
589 * determines whether the connection will be established over TLS or not.
590 */
591 const session = http2.connect(
592 addressScheme + targetAuthority,
593 connectionOptions
594 );
595 this.session = session;
596 session.unref();
597 session.once('connect', () => {
598 session.removeAllListeners();
599 resolve(new Http2Transport(session, address, options));
600 this.session = null;
601 });
602 session.once('close', () => {
603 this.session = null;
604 reject();
605 });
606 session.once('error', error => {
607 this.trace('connection failed with error ' + (error as Error).message)
608 });
609 });
610 }
611 connect(address: SubchannelAddress, credentials: ChannelCredentials, options: ChannelOptions): Promise<Http2Transport> {
612 if (this.isShutdown) {
613 return Promise.reject();
614 }
615 /* Pass connection options through to the proxy so that it's able to
616 * upgrade it's connection to support tls if needed.
617 * This is a workaround for https://github.com/nodejs/node/issues/32922
618 * See https://github.com/grpc/grpc-node/pull/1369 for more info. */
619 const connectionOptions: ConnectionOptions =
620 credentials._getConnectionOptions() || {};
621
622 if ('secureContext' in connectionOptions) {
623 connectionOptions.ALPNProtocols = ['h2'];
624 // If provided, the value of grpc.ssl_target_name_override should be used
625 // to override the target hostname when checking server identity.
626 // This option is used for testing only.
627 if (options['grpc.ssl_target_name_override']) {
628 const sslTargetNameOverride = options[
629 'grpc.ssl_target_name_override'
630 ]!;
631 connectionOptions.checkServerIdentity = (
632 host: string,
633 cert: PeerCertificate
634 ): Error | undefined => {
635 return checkServerIdentity(sslTargetNameOverride, cert);
636 };
637 connectionOptions.servername = sslTargetNameOverride;
638 } else {
639 if ('grpc.http_connect_target' in options) {
640 /* This is more or less how servername will be set in createSession
641 * if a connection is successfully established through the proxy.
642 * If the proxy is not used, these connectionOptions are discarded
643 * anyway */
644 const targetPath = getDefaultAuthority(
645 parseUri(options['grpc.http_connect_target'] as string) ?? {
646 path: 'localhost',
647 }
648 );
649 const hostPort = splitHostPort(targetPath);
650 connectionOptions.servername = hostPort?.host ?? targetPath;
651 }
652 }
653 }
654
655 return getProxiedConnection(
656 address,
657 options,
658 connectionOptions
659 ).then(
660 result => this.createSession(address, credentials, options, result)
661 );
662 }
663
664 shutdown(): void {
665 this.isShutdown = true;
666 this.session?.close();
667 this.session = null;
668 }
669}
\No newline at end of file