UNPKG

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