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 { checkServerIdentity, CipherNameAndProtocol, ConnectionOptions, PeerCertificate, TLSSocket } from 'tls';
|
20 | import { StatusObject } from './call-interface';
|
21 | import { ChannelCredentials } from './channel-credentials';
|
22 | import { ChannelOptions } from './channel-options';
|
23 | import { ChannelzCallTracker, registerChannelzSocket, SocketInfo, SocketRef, TlsInfo, unregisterChannelzRef } from './channelz';
|
24 | import { LogVerbosity } from './constants';
|
25 | import { getProxiedConnection, ProxyConnectionResult } from './http_proxy';
|
26 | import * as logging from './logging';
|
27 | import { getDefaultAuthority } from './resolver';
|
28 | import { stringToSubchannelAddress, SubchannelAddress, subchannelAddressToString } from './subchannel-address';
|
29 | import { GrpcUri, parseUri, splitHostPort, uriToString } from './uri-parser';
|
30 | import * as net from 'net';
|
31 | import { Http2SubchannelCall, SubchannelCall, SubchannelCallInterceptingListener } from './subchannel-call';
|
32 | import { Metadata } from './metadata';
|
33 | import { getNextCallNumber } from './call-number';
|
34 |
|
35 | const TRACER_NAME = 'transport';
|
36 | const FLOW_CONTROL_TRACER_NAME = 'transport_flowctrl';
|
37 |
|
38 | const clientVersion = require('../../package.json').version;
|
39 |
|
40 | const {
|
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 |
|
50 |
|
51 |
|
52 | const KEEPALIVE_MAX_TIME_MS = ~(1 << 31);
|
53 | const KEEPALIVE_TIMEOUT_MS = 20000;
|
54 |
|
55 | export interface CallEventTracker {
|
56 | addMessageSent(): void;
|
57 | addMessageReceived(): void;
|
58 | onCallEnd(status: StatusObject): void;
|
59 | onStreamEnd(success: boolean): void;
|
60 | }
|
61 |
|
62 | export interface TransportDisconnectListener {
|
63 | (tooManyPings: boolean): void;
|
64 | }
|
65 |
|
66 | export 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 |
|
74 | const tooManyPingsData: Buffer = Buffer.from('too_many_pings', 'ascii');
|
75 |
|
76 | class Http2Transport implements Transport {
|
77 | |
78 |
|
79 |
|
80 | private keepaliveTimeMs: number = -1;
|
81 | |
82 |
|
83 |
|
84 | private keepaliveTimeoutMs: number = KEEPALIVE_TIMEOUT_MS;
|
85 | |
86 |
|
87 |
|
88 | private keepaliveIntervalId: NodeJS.Timer;
|
89 | |
90 |
|
91 |
|
92 | private keepaliveTimeoutId: NodeJS.Timer | null = null;
|
93 | |
94 |
|
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 |
|
109 | private channelzRef: SocketRef;
|
110 | private readonly channelzEnabled: boolean = true;
|
111 | |
112 |
|
113 |
|
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 |
|
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(' ');
|
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 | |
170 |
|
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 | |
185 |
|
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 |
|
269 |
|
270 |
|
271 |
|
272 |
|
273 |
|
274 |
|
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 |
|
286 |
|
287 | private handleDisconnect() {
|
288 | this.reportDisconnectToOwner(false);
|
289 | |
290 |
|
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 | |
331 |
|
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 | |
345 |
|
346 | }
|
347 |
|
348 | |
349 |
|
350 |
|
351 |
|
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 | |
388 |
|
389 |
|
390 |
|
391 |
|
392 |
|
393 |
|
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 |
|
478 | export interface SubchannelConnector {
|
479 | connect(address: SubchannelAddress, credentials: ChannelCredentials, options: ChannelOptions): Promise<Transport>;
|
480 | shutdown(): void;
|
481 | }
|
482 |
|
483 | export 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 | |
515 |
|
516 |
|
517 |
|
518 | connectionOptions.maxSessionMemory = Number.MAX_SAFE_INTEGER;
|
519 | }
|
520 | let addressScheme = 'http:
|
521 | if ('secureContext' in connectionOptions) {
|
522 | addressScheme = 'https:
|
523 |
|
524 |
|
525 |
|
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 |
|
541 | connectionOptions.servername = authorityHostname;
|
542 | }
|
543 | if (proxyConnectionResult.socket) {
|
544 | |
545 |
|
546 |
|
547 |
|
548 |
|
549 | connectionOptions.createConnection = (authority, option) => {
|
550 | return proxyConnectionResult.socket!;
|
551 | };
|
552 | }
|
553 | } else {
|
554 | |
555 |
|
556 |
|
557 | connectionOptions.createConnection = (authority, option) => {
|
558 | if (proxyConnectionResult.socket) {
|
559 | return proxyConnectionResult.socket;
|
560 | } else {
|
561 | |
562 |
|
563 |
|
564 | return net.connect(address);
|
565 | }
|
566 | };
|
567 | }
|
568 |
|
569 | connectionOptions = {
|
570 | ...connectionOptions,
|
571 | ...address,
|
572 | };
|
573 |
|
574 | |
575 |
|
576 |
|
577 |
|
578 |
|
579 |
|
580 |
|
581 |
|
582 |
|
583 |
|
584 |
|
585 |
|
586 |
|
587 |
|
588 |
|
589 |
|
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 | |
641 |
|
642 |
|
643 |
|
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 |