1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | import * as http2 from 'http2';
|
19 | import {
|
20 | checkServerIdentity,
|
21 | CipherNameAndProtocol,
|
22 | ConnectionOptions,
|
23 | PeerCertificate,
|
24 | TLSSocket,
|
25 | } from 'tls';
|
26 | import { StatusObject } from './call-interface';
|
27 | import { ChannelCredentials } from './channel-credentials';
|
28 | import { ChannelOptions } from './channel-options';
|
29 | import {
|
30 | ChannelzCallTracker,
|
31 | registerChannelzSocket,
|
32 | SocketInfo,
|
33 | SocketRef,
|
34 | TlsInfo,
|
35 | unregisterChannelzRef,
|
36 | } from './channelz';
|
37 | import { LogVerbosity } from './constants';
|
38 | import { getProxiedConnection, ProxyConnectionResult } from './http_proxy';
|
39 | import * as logging from './logging';
|
40 | import { getDefaultAuthority } from './resolver';
|
41 | import {
|
42 | stringToSubchannelAddress,
|
43 | SubchannelAddress,
|
44 | subchannelAddressToString,
|
45 | } from './subchannel-address';
|
46 | import { GrpcUri, parseUri, splitHostPort, uriToString } from './uri-parser';
|
47 | import * as net from 'net';
|
48 | import {
|
49 | Http2SubchannelCall,
|
50 | SubchannelCall,
|
51 | SubchannelCallInterceptingListener,
|
52 | } from './subchannel-call';
|
53 | import { Metadata } from './metadata';
|
54 | import { getNextCallNumber } from './call-number';
|
55 |
|
56 | const TRACER_NAME = 'transport';
|
57 | const FLOW_CONTROL_TRACER_NAME = 'transport_flowctrl';
|
58 |
|
59 | const clientVersion = require('../../package.json').version;
|
60 |
|
61 | const {
|
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 |
|
70 | const KEEPALIVE_TIMEOUT_MS = 20000;
|
71 |
|
72 | export interface CallEventTracker {
|
73 | addMessageSent(): void;
|
74 | addMessageReceived(): void;
|
75 | onCallEnd(status: StatusObject): void;
|
76 | onStreamEnd(success: boolean): void;
|
77 | }
|
78 |
|
79 | export interface TransportDisconnectListener {
|
80 | (tooManyPings: boolean): void;
|
81 | }
|
82 |
|
83 | export 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 |
|
97 | const tooManyPingsData: Buffer = Buffer.from('too_many_pings', 'ascii');
|
98 |
|
99 | class Http2Transport implements Transport {
|
100 | |
101 |
|
102 |
|
103 | private keepaliveTimeMs = -1;
|
104 | |
105 |
|
106 |
|
107 | private keepaliveTimeoutMs: number = KEEPALIVE_TIMEOUT_MS;
|
108 | |
109 |
|
110 |
|
111 | private keepaliveTimerId: NodeJS.Timeout | null = null;
|
112 | |
113 |
|
114 |
|
115 |
|
116 | private pendingSendKeepalivePing = false;
|
117 | |
118 |
|
119 |
|
120 | private keepaliveTimeoutId: NodeJS.Timeout | null = null;
|
121 | |
122 |
|
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 |
|
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 |
|
152 |
|
153 |
|
154 | private remoteName: string | null
|
155 | ) {
|
156 | |
157 |
|
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 |
|
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(' ');
|
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 | |
200 |
|
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 | |
213 |
|
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 | |
235 |
|
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 |
|
351 |
|
352 |
|
353 |
|
354 |
|
355 |
|
356 |
|
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 |
|
368 |
|
369 | private handleDisconnect() {
|
370 | this.reportDisconnectToOwner(false);
|
371 | |
372 |
|
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 | |
440 |
|
441 | this.handleDisconnect();
|
442 | }
|
443 | }
|
444 |
|
445 | |
446 |
|
447 |
|
448 |
|
449 |
|
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 | |
467 |
|
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 | |
511 |
|
512 |
|
513 |
|
514 |
|
515 |
|
516 |
|
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 |
|
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 |
|
609 | export interface SubchannelConnector {
|
610 | connect(
|
611 | address: SubchannelAddress,
|
612 | credentials: ChannelCredentials,
|
613 | options: ChannelOptions
|
614 | ): Promise<Transport>;
|
615 | shutdown(): void;
|
616 | }
|
617 |
|
618 | export 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 | |
663 |
|
664 |
|
665 |
|
666 | connectionOptions.maxSessionMemory = Number.MAX_SAFE_INTEGER;
|
667 | }
|
668 | let addressScheme = 'http:
|
669 | if ('secureContext' in connectionOptions) {
|
670 | addressScheme = 'https:
|
671 |
|
672 |
|
673 |
|
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 |
|
688 | connectionOptions.servername = authorityHostname;
|
689 | }
|
690 | if (proxyConnectionResult.socket) {
|
691 | |
692 |
|
693 |
|
694 |
|
695 |
|
696 | connectionOptions.createConnection = (authority, option) => {
|
697 | return proxyConnectionResult.socket!;
|
698 | };
|
699 | }
|
700 | } else {
|
701 | |
702 |
|
703 |
|
704 | connectionOptions.createConnection = (authority, option) => {
|
705 | if (proxyConnectionResult.socket) {
|
706 | return proxyConnectionResult.socket;
|
707 | } else {
|
708 | |
709 |
|
710 |
|
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 | |
723 |
|
724 |
|
725 |
|
726 |
|
727 |
|
728 |
|
729 |
|
730 |
|
731 |
|
732 |
|
733 |
|
734 |
|
735 |
|
736 |
|
737 |
|
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 | |
791 |
|
792 |
|
793 |
|
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 | }
|