diff --git a/packages/grpc-js/src/channel-credentials.ts b/packages/grpc-js/src/channel-credentials.ts index 60a553c39..dee6e06f6 100644 --- a/packages/grpc-js/src/channel-credentials.ts +++ b/packages/grpc-js/src/channel-credentials.ts @@ -20,11 +20,17 @@ import { createSecureContext, PeerCertificate, SecureContext, + checkServerIdentity, + connect as tlsConnect } from 'tls'; import { CallCredentials } from './call-credentials'; import { CIPHER_SUITES, getDefaultRootsData } from './tls-helpers'; import { CaCertificateUpdate, CaCertificateUpdateListener, CertificateProvider, IdentityCertificateUpdate, IdentityCertificateUpdateListener } from './certificate-provider'; +import { Socket } from 'net'; +import { ChannelOptions } from './channel-options'; +import { GrpcUri, parseUri, splitHostPort } from './uri-parser'; +import { getDefaultAuthority } from './resolver'; // eslint-disable-next-line @typescript-eslint/no-explicit-any function verifyIsBufferOrNull(obj: any, friendlyName: string): void { @@ -57,6 +63,11 @@ export interface VerifyOptions { rejectUnauthorized?: boolean; } +export interface SecureConnector { + connect(socket: Socket): Promise; + destroy(): void; +} + /** * A class that contains credentials for communicating over a channel, as well * as a set of per-call credentials, which are applied to every method call made @@ -83,13 +94,6 @@ export abstract class ChannelCredentials { return this.callCredentials; } - /** - * Gets a SecureContext object generated from input parameters if this - * instance was created with createSsl, or null if this instance was created - * with createInsecure. - */ - abstract _getConnectionOptions(): ConnectionOptions | null; - /** * Indicates whether this credentials object creates a secure channel. */ @@ -102,13 +106,7 @@ export abstract class ChannelCredentials { */ abstract _equals(other: ChannelCredentials): boolean; - _ref(): void { - // Do nothing by default - } - - _unref(): void { - // Do nothing by default - } + abstract _createSecureConnector(channelTarget: GrpcUri, options: ChannelOptions): SecureConnector; /** * Return a new ChannelCredentials instance with a given set of credentials. @@ -180,39 +178,104 @@ class InsecureChannelCredentialsImpl extends ChannelCredentials { compose(callCredentials: CallCredentials): never { throw new Error('Cannot compose insecure credentials'); } - - _getConnectionOptions(): ConnectionOptions | null { - return {}; - } _isSecure(): boolean { return false; } _equals(other: ChannelCredentials): boolean { return other instanceof InsecureChannelCredentialsImpl; } + _createSecureConnector(channelTarget: GrpcUri, options: ChannelOptions): SecureConnector { + return { + connect(socket) { + return Promise.resolve(socket); + }, + destroy() {} + } + } } -class SecureChannelCredentialsImpl extends ChannelCredentials { - connectionOptions: ConnectionOptions; +function getConnectionOptions(secureContext: SecureContext, verifyOptions: VerifyOptions, channelTarget: GrpcUri, options: ChannelOptions): ConnectionOptions { + const connectionOptions: ConnectionOptions = { + secureContext: secureContext + }; + if (verifyOptions.checkServerIdentity) { + connectionOptions.checkServerIdentity = verifyOptions.checkServerIdentity; + } + if (verifyOptions.rejectUnauthorized !== undefined) { + connectionOptions.rejectUnauthorized = verifyOptions.rejectUnauthorized; + } + connectionOptions.ALPNProtocols = ['h2']; + if (options['grpc.ssl_target_name_override']) { + const sslTargetNameOverride = options['grpc.ssl_target_name_override']!; + const originalCheckServerIdentity = + connectionOptions.checkServerIdentity ?? checkServerIdentity; + connectionOptions.checkServerIdentity = ( + host: string, + cert: PeerCertificate + ): Error | undefined => { + return originalCheckServerIdentity(sslTargetNameOverride, cert); + }; + connectionOptions.servername = sslTargetNameOverride; + } else { + if ('grpc.http_connect_target' in options) { + /* This is more or less how servername will be set in createSession + * if a connection is successfully established through the proxy. + * If the proxy is not used, these connectionOptions are discarded + * anyway */ + const targetPath = getDefaultAuthority( + parseUri(options['grpc.http_connect_target'] as string) ?? { + path: 'localhost', + } + ); + const hostPort = splitHostPort(targetPath); + connectionOptions.servername = hostPort?.host ?? targetPath; + } + } + if (options['grpc-node.tls_enable_trace']) { + connectionOptions.enableTrace = true; + } + let realTarget: GrpcUri = channelTarget; + if ('grpc.http_connect_target' in options) { + const parsedTarget = parseUri(options['grpc.http_connect_target']!); + if (parsedTarget) { + realTarget = parsedTarget; + } + } + const targetPath = getDefaultAuthority(realTarget); + const hostPort = splitHostPort(targetPath); + const remoteHost = hostPort?.host ?? targetPath; + connectionOptions.host = remoteHost; + connectionOptions.servername = remoteHost; + return connectionOptions; +} + +class SecureConnectorImpl implements SecureConnector { + constructor(private connectionOptions: ConnectionOptions) { + } + connect(socket: Socket): Promise { + const tlsConnectOptions: ConnectionOptions = { + socket: socket, + ...this.connectionOptions + }; + return new Promise((resolve, reject) => { + const tlsSocket = tlsConnect(tlsConnectOptions, () => { + resolve(tlsSocket) + }); + tlsSocket.on('error', (error: Error) => { + reject(error); + }); + }); + } + destroy() {} +} + +class SecureChannelCredentialsImpl extends ChannelCredentials { constructor( private secureContext: SecureContext, private verifyOptions: VerifyOptions ) { super(); - this.connectionOptions = { - secureContext, - }; - // Node asserts that this option is a function, so we cannot pass undefined - if (verifyOptions?.checkServerIdentity) { - this.connectionOptions.checkServerIdentity = - verifyOptions.checkServerIdentity; - } - - if (verifyOptions?.rejectUnauthorized !== undefined) { - this.connectionOptions.rejectUnauthorized = - verifyOptions.rejectUnauthorized; - } } compose(callCredentials: CallCredentials): ChannelCredentials { @@ -220,11 +283,6 @@ class SecureChannelCredentialsImpl extends ChannelCredentials { this.callCredentials.compose(callCredentials); return new ComposedChannelCredentialsImpl(this, combinedCallCredentials); } - - _getConnectionOptions(): ConnectionOptions | null { - // Copy to prevent callers from mutating this.connectionOptions - return { ...this.connectionOptions }; - } _isSecure(): boolean { return true; } @@ -242,6 +300,10 @@ class SecureChannelCredentialsImpl extends ChannelCredentials { return false; } } + _createSecureConnector(channelTarget: GrpcUri, options: ChannelOptions): SecureConnector { + const connectionOptions = getConnectionOptions(this.secureContext, this.verifyOptions, channelTarget, options); + return new SecureConnectorImpl(connectionOptions); + } } class CertificateProviderChannelCredentialsImpl extends ChannelCredentials { @@ -250,10 +312,38 @@ class CertificateProviderChannelCredentialsImpl extends ChannelCredentials { private latestIdentityUpdate: IdentityCertificateUpdate | null = null; private caCertificateUpdateListener: CaCertificateUpdateListener = this.handleCaCertificateUpdate.bind(this); private identityCertificateUpdateListener: IdentityCertificateUpdateListener = this.handleIdentityCertitificateUpdate.bind(this); + private static SecureConnectorImpl = class implements SecureConnector { + constructor(private parent: CertificateProviderChannelCredentialsImpl, private channelTarget: GrpcUri, private options: ChannelOptions) {} + + connect(socket: Socket): Promise { + return new Promise((resolve, reject) => { + const secureContext = this.parent.getLatestSecureContext(); + if (!secureContext) { + reject(new Error('Credentials not loaded')); + return; + } + const connnectionOptions = getConnectionOptions(secureContext, this.parent.verifyOptions, this.channelTarget, this.options); + const tlsConnectOptions: ConnectionOptions = { + socket: socket, + ...connnectionOptions + } + const tlsSocket = tlsConnect(tlsConnectOptions, () => { + resolve(tlsSocket) + }); + tlsSocket.on('error', (error: Error) => { + reject(error); + }); + }); + } + + destroy() { + this.parent.unref(); + } + } constructor( private caCertificateProvider: CertificateProvider, private identityCertificateProvider: CertificateProvider | null, - private verifyOptions: VerifyOptions | null + private verifyOptions: VerifyOptions ) { super(); } @@ -265,27 +355,6 @@ class CertificateProviderChannelCredentialsImpl extends ChannelCredentials { combinedCallCredentials ); } - _getConnectionOptions(): ConnectionOptions | null { - if (this.latestCaUpdate === null) { - return null; - } - if (this.identityCertificateProvider !== null && this.latestIdentityUpdate === null) { - return null; - } - const secureContext: SecureContext = createSecureContext({ - ca: this.latestCaUpdate.caCertificate, - key: this.latestIdentityUpdate?.privateKey, - cert: this.latestIdentityUpdate?.certificate, - ciphers: CIPHER_SUITES - }); - const options: ConnectionOptions = { - secureContext: secureContext - }; - if (this.verifyOptions?.checkServerIdentity) { - options.checkServerIdentity = this.verifyOptions.checkServerIdentity; - } - return options; - } _isSecure(): boolean { return true; } @@ -301,20 +370,24 @@ class CertificateProviderChannelCredentialsImpl extends ChannelCredentials { return false; } } - _ref(): void { + private ref(): void { if (this.refcount === 0) { this.caCertificateProvider.addCaCertificateListener(this.caCertificateUpdateListener); this.identityCertificateProvider?.addIdentityCertificateListener(this.identityCertificateUpdateListener); } this.refcount += 1; } - _unref(): void { + private unref(): void { this.refcount -= 1; if (this.refcount === 0) { this.caCertificateProvider.removeCaCertificateListener(this.caCertificateUpdateListener); this.identityCertificateProvider?.removeIdentityCertificateListener(this.identityCertificateUpdateListener); } } + _createSecureConnector(channelTarget: GrpcUri, options: ChannelOptions): SecureConnector { + this.ref(); + return new CertificateProviderChannelCredentialsImpl.SecureConnectorImpl(this, channelTarget, options); + } private handleCaCertificateUpdate(update: CaCertificateUpdate | null) { this.latestCaUpdate = update; @@ -323,10 +396,25 @@ class CertificateProviderChannelCredentialsImpl extends ChannelCredentials { private handleIdentityCertitificateUpdate(update: IdentityCertificateUpdate | null) { this.latestIdentityUpdate = update; } + + private getLatestSecureContext(): SecureContext | null { + if (this.latestCaUpdate === null) { + return null; + } + if (this.identityCertificateProvider !== null && this.latestIdentityUpdate === null) { + return null; + } + return createSecureContext({ + ca: this.latestCaUpdate.caCertificate, + key: this.latestIdentityUpdate?.privateKey, + cert: this.latestIdentityUpdate?.certificate, + ciphers: CIPHER_SUITES + }); + } } export function createCertificateProviderChannelCredentials(caCertificateProvider: CertificateProvider, identityCertificateProvider: CertificateProvider | null, verifyOptions?: VerifyOptions) { - return new CertificateProviderChannelCredentialsImpl(caCertificateProvider, identityCertificateProvider, verifyOptions ?? null); + return new CertificateProviderChannelCredentialsImpl(caCertificateProvider, identityCertificateProvider, verifyOptions ?? {}); } class ComposedChannelCredentialsImpl extends ChannelCredentials { @@ -347,10 +435,6 @@ class ComposedChannelCredentialsImpl extends ChannelCredentials { combinedCallCredentials ); } - - _getConnectionOptions(): ConnectionOptions | null { - return this.channelCredentials._getConnectionOptions(); - } _isSecure(): boolean { return true; } @@ -367,4 +451,7 @@ class ComposedChannelCredentialsImpl extends ChannelCredentials { return false; } } + _createSecureConnector(channelTarget: GrpcUri, options: ChannelOptions): SecureConnector { + return this.channelCredentials._createSecureConnector(channelTarget, options); + } } diff --git a/packages/grpc-js/src/http_proxy.ts b/packages/grpc-js/src/http_proxy.ts index 6fabf5025..88d621bd2 100644 --- a/packages/grpc-js/src/http_proxy.ts +++ b/packages/grpc-js/src/http_proxy.ts @@ -17,10 +17,8 @@ import { log } from './logging'; import { LogVerbosity } from './constants'; -import { getDefaultAuthority } from './resolver'; import { Socket } from 'net'; import * as http from 'http'; -import * as tls from 'tls'; import * as logging from './logging'; import { SubchannelAddress, @@ -172,27 +170,21 @@ export function mapProxyName( }; } -export interface ProxyConnectionResult { - socket?: Socket; - realTarget?: GrpcUri; -} - export function getProxiedConnection( address: SubchannelAddress, - channelOptions: ChannelOptions, - connectionOptions: tls.ConnectionOptions -): Promise { + channelOptions: ChannelOptions +): Promise { if (!('grpc.http_connect_target' in channelOptions)) { - return Promise.resolve({}); + return Promise.resolve(null); } const realTarget = channelOptions['grpc.http_connect_target'] as string; const parsedTarget = parseUri(realTarget); if (parsedTarget === null) { - return Promise.resolve({}); + return Promise.resolve(null); } const splitHostPost = splitHostPort(parsedTarget.path); if (splitHostPost === null) { - return Promise.resolve({}); + return Promise.resolve(null); } const hostPort = `${splitHostPost.host}:${ splitHostPost.port ?? DEFAULT_PORT @@ -221,7 +213,7 @@ export function getProxiedConnection( options.headers = headers; const proxyAddressString = subchannelAddressToString(address); trace('Using proxy ' + proxyAddressString + ' to connect to ' + options.path); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const request = http.request(options); request.once('connect', (res, socket, head) => { request.removeAllListeners(); @@ -239,55 +231,13 @@ export function getProxiedConnection( if (head.length > 0) { socket.unshift(head); } - if ('secureContext' in connectionOptions) { - /* The proxy is connecting to a TLS server, so upgrade this socket - * connection to a TLS connection. - * This is a workaround for https://github.com/nodejs/node/issues/32922 - * See https://github.com/grpc/grpc-node/pull/1369 for more info. */ - const targetPath = getDefaultAuthority(parsedTarget); - const hostPort = splitHostPort(targetPath); - const remoteHost = hostPort?.host ?? targetPath; - - const cts = tls.connect( - { - host: remoteHost, - servername: remoteHost, - socket: socket, - ...connectionOptions, - }, - () => { - trace( - 'Successfully established a TLS connection to ' + - options.path + - ' through proxy ' + - proxyAddressString - ); - resolve({ socket: cts, realTarget: parsedTarget }); - } - ); - cts.on('error', (error: Error) => { - trace( - 'Failed to establish a TLS connection to ' + - options.path + - ' through proxy ' + - proxyAddressString + - ' with error ' + - error.message - ); - reject(); - }); - } else { - trace( - 'Successfully established a plaintext connection to ' + - options.path + - ' through proxy ' + - proxyAddressString - ); - resolve({ - socket, - realTarget: parsedTarget, - }); - } + trace( + 'Successfully established a plaintext connection to ' + + options.path + + ' through proxy ' + + proxyAddressString + ); + resolve(socket); } else { log( LogVerbosity.ERROR, diff --git a/packages/grpc-js/src/subchannel.ts b/packages/grpc-js/src/subchannel.ts index 6a10a22f2..3074f63eb 100644 --- a/packages/grpc-js/src/subchannel.ts +++ b/packages/grpc-js/src/subchannel.ts @@ -15,7 +15,7 @@ * */ -import { ChannelCredentials } from './channel-credentials'; +import { ChannelCredentials, SecureConnector } from './channel-credentials'; import { Metadata } from './metadata'; import { ChannelOptions } from './channel-options'; import { ConnectivityState } from './connectivity-state'; @@ -102,6 +102,8 @@ export class Subchannel { // Channelz socket info private streamTracker: ChannelzCallTracker | ChannelzCallTrackerStub; + private secureConnector: SecureConnector; + /** * A class representing a connection to a single backend. * @param channelTarget The target string for the channel as a whole @@ -116,7 +118,7 @@ export class Subchannel { private channelTarget: GrpcUri, private subchannelAddress: SubchannelAddress, private options: ChannelOptions, - private credentials: ChannelCredentials, + credentials: ChannelCredentials, private connector: SubchannelConnector ) { const backoffOptions: BackoffOptions = { @@ -155,7 +157,7 @@ export class Subchannel { 'Subchannel constructed with options ' + JSON.stringify(options, undefined, 2) ); - credentials._ref(); + this.secureConnector = credentials._createSecureConnector(channelTarget, options); } private getChannelzInfo(): SubchannelInfo { @@ -230,7 +232,7 @@ export class Subchannel { options = { ...options, 'grpc.keepalive_time_ms': adjustedKeepaliveTime }; } this.connector - .connect(this.subchannelAddress, this.credentials, options) + .connect(this.subchannelAddress, this.secureConnector, options) .then( transport => { if ( @@ -365,7 +367,7 @@ export class Subchannel { if (this.refcount === 0) { this.channelzTrace.addTrace('CT_INFO', 'Shutting down'); unregisterChannelzRef(this.channelzRef); - this.credentials._unref(); + this.secureConnector.destroy(); process.nextTick(() => { this.transitionToState( [ConnectivityState.CONNECTING, ConnectivityState.READY], diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index 063fc86d9..97c2ffbcd 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -17,14 +17,11 @@ import * as http2 from 'http2'; import { - checkServerIdentity, CipherNameAndProtocol, - ConnectionOptions, - PeerCertificate, TLSSocket, } from 'tls'; import { PartialStatusObject } from './call-interface'; -import { ChannelCredentials } from './channel-credentials'; +import { SecureConnector } from './channel-credentials'; import { ChannelOptions } from './channel-options'; import { ChannelzCallTracker, @@ -36,7 +33,7 @@ import { unregisterChannelzRef, } from './channelz'; import { LogVerbosity } from './constants'; -import { getProxiedConnection, ProxyConnectionResult } from './http_proxy'; +import { getProxiedConnection } from './http_proxy'; import * as logging from './logging'; import { getDefaultAuthority } from './resolver'; import { @@ -44,7 +41,7 @@ import { SubchannelAddress, subchannelAddressToString, } from './subchannel-address'; -import { GrpcUri, parseUri, splitHostPort, uriToString } from './uri-parser'; +import { GrpcUri, parseUri, uriToString } from './uri-parser'; import * as net from 'net'; import { Http2SubchannelCall, @@ -53,6 +50,7 @@ import { } from './subchannel-call'; import { Metadata } from './metadata'; import { getNextCallNumber } from './call-number'; +import { Socket } from 'net'; const TRACER_NAME = 'transport'; const FLOW_CONTROL_TRACER_NAME = 'transport_flowctrl'; @@ -632,7 +630,7 @@ class Http2Transport implements Transport { export interface SubchannelConnector { connect( address: SubchannelAddress, - credentials: ChannelCredentials, + secureConnector: SecureConnector, options: ChannelOptions ): Promise; shutdown(): void; @@ -652,127 +650,30 @@ export class Http2SubchannelConnector implements SubchannelConnector { } private createSession( + underlyingConnection: Socket, address: SubchannelAddress, - credentials: ChannelCredentials, - options: ChannelOptions, - proxyConnectionResult: ProxyConnectionResult + options: ChannelOptions ): Promise { if (this.isShutdown) { return Promise.reject(); } return new Promise((resolve, reject) => { - let remoteName: string | null; - if (proxyConnectionResult.realTarget) { - remoteName = uriToString(proxyConnectionResult.realTarget); - this.trace( - 'creating HTTP/2 session through proxy to ' + - uriToString(proxyConnectionResult.realTarget) - ); - } else { - remoteName = null; - this.trace( - 'creating HTTP/2 session to ' + subchannelAddressToString(address) - ); - } - const targetAuthority = getDefaultAuthority( - proxyConnectionResult.realTarget ?? this.channelTarget - ); - let connectionOptions: http2.SecureClientSessionOptions | null = - credentials._getConnectionOptions(); - - if (!connectionOptions) { - reject('Credentials not loaded'); - return; - } - connectionOptions.maxSendHeaderBlockLength = Number.MAX_SAFE_INTEGER; - if ('grpc-node.max_session_memory' in options) { - connectionOptions.maxSessionMemory = - options['grpc-node.max_session_memory']; - } else { - /* By default, set a very large max session memory limit, to effectively - * disable enforcement of the limit. Some testing indicates that Node's - * behavior degrades badly when this limit is reached, so we solve that - * by disabling the check entirely. */ - connectionOptions.maxSessionMemory = Number.MAX_SAFE_INTEGER; - } - let addressScheme = 'http://'; - if ('secureContext' in connectionOptions) { - addressScheme = 'https://'; - // If provided, the value of grpc.ssl_target_name_override should be used - // to override the target hostname when checking server identity. - // This option is used for testing only. - if (options['grpc.ssl_target_name_override']) { - const sslTargetNameOverride = - options['grpc.ssl_target_name_override']!; - const originalCheckServerIdentity = - connectionOptions.checkServerIdentity ?? checkServerIdentity; - connectionOptions.checkServerIdentity = ( - host: string, - cert: PeerCertificate - ): Error | undefined => { - return originalCheckServerIdentity(sslTargetNameOverride, cert); - }; - connectionOptions.servername = sslTargetNameOverride; - } else { - const authorityHostname = - splitHostPort(targetAuthority)?.host ?? 'localhost'; - // We want to always set servername to support SNI - connectionOptions.servername = authorityHostname; - } - if (proxyConnectionResult.socket) { - /* This is part of the workaround for - * https://github.com/nodejs/node/issues/32922. Without that bug, - * proxyConnectionResult.socket would always be a plaintext socket and - * this would say - * connectionOptions.socket = proxyConnectionResult.socket; */ - connectionOptions.createConnection = (authority, option) => { - return proxyConnectionResult.socket!; - }; + let remoteName: string | null = null; + let realTarget: GrpcUri = this.channelTarget; + if ('grpc.http_connect_target' in options) { + const parsedTarget = parseUri(options['grpc.http_connect_target']!); + if (parsedTarget) { + realTarget = parsedTarget; + remoteName = uriToString(parsedTarget); } - } else { - /* In all but the most recent versions of Node, http2.connect does not use - * the options when establishing plaintext connections, so we need to - * establish that connection explicitly. */ - connectionOptions.createConnection = (authority, option) => { - if (proxyConnectionResult.socket) { - return proxyConnectionResult.socket; - } else { - /* net.NetConnectOpts is declared in a way that is more restrictive - * than what net.connect will actually accept, so we use the type - * assertion to work around that. */ - return net.connect(address); - } - }; } - - connectionOptions = { - ...connectionOptions, - ...address, - enableTrace: options['grpc-node.tls_enable_trace'] === 1, - }; - - /* http2.connect uses the options here: - * https://github.com/nodejs/node/blob/70c32a6d190e2b5d7b9ff9d5b6a459d14e8b7d59/lib/internal/http2/core.js#L3028-L3036 - * The spread operator overides earlier values with later ones, so any port - * or host values in the options will be used rather than any values extracted - * from the first argument. In addition, the path overrides the host and port, - * as documented for plaintext connections here: - * https://nodejs.org/api/net.html#net_socket_connect_options_connectlistener - * and for TLS connections here: - * https://nodejs.org/api/tls.html#tls_tls_connect_options_callback. In - * earlier versions of Node, http2.connect passes these options to - * tls.connect but not net.connect, so in the insecure case we still need - * to set the createConnection option above to create the connection - * explicitly. We cannot do that in the TLS case because http2.connect - * passes necessary additional options to tls.connect. - * The first argument just needs to be parseable as a URL and the scheme - * determines whether the connection will be established over TLS or not. - */ - const session = http2.connect( - addressScheme + targetAuthority, - connectionOptions - ); + const targetPath = getDefaultAuthority(realTarget); + const session = http2.connect(`http://${targetPath}`, { + createConnection: (authority, option) => { + return underlyingConnection; + } + }); this.session = session; let errorMessage = 'Failed to connect'; let reportedError = false; @@ -803,64 +704,34 @@ export class Http2SubchannelConnector implements SubchannelConnector { }); } - connect( + private tcpConnect(address: SubchannelAddress, options: ChannelOptions): Promise { + return getProxiedConnection(address, options).then(proxiedSocket => { + if (proxiedSocket) { + return proxiedSocket; + } else { + return new Promise((resolve, reject) => { + const socket = net.connect(address, () => { + resolve(socket); + }); + socket.once('error', (error) => { + reject(error); + }); + }); + } + }); + } + + async connect( address: SubchannelAddress, - credentials: ChannelCredentials, + secureConnector: SecureConnector, options: ChannelOptions ): Promise { if (this.isShutdown) { return Promise.reject(); } - /* Pass connection options through to the proxy so that it's able to - * upgrade it's connection to support tls if needed. - * This is a workaround for https://github.com/nodejs/node/issues/32922 - * See https://github.com/grpc/grpc-node/pull/1369 for more info. */ - const connectionOptions: ConnectionOptions | null = - credentials._getConnectionOptions(); - - if (!connectionOptions) { - return Promise.reject('Credentials not loaded'); - } - - if ('secureContext' in connectionOptions) { - connectionOptions.ALPNProtocols = ['h2']; - // If provided, the value of grpc.ssl_target_name_override should be used - // to override the target hostname when checking server identity. - // This option is used for testing only. - if (options['grpc.ssl_target_name_override']) { - const sslTargetNameOverride = options['grpc.ssl_target_name_override']!; - const originalCheckServerIdentity = - connectionOptions.checkServerIdentity ?? checkServerIdentity; - connectionOptions.checkServerIdentity = ( - host: string, - cert: PeerCertificate - ): Error | undefined => { - return originalCheckServerIdentity(sslTargetNameOverride, cert); - }; - connectionOptions.servername = sslTargetNameOverride; - } else { - if ('grpc.http_connect_target' in options) { - /* This is more or less how servername will be set in createSession - * if a connection is successfully established through the proxy. - * If the proxy is not used, these connectionOptions are discarded - * anyway */ - const targetPath = getDefaultAuthority( - parseUri(options['grpc.http_connect_target'] as string) ?? { - path: 'localhost', - } - ); - const hostPort = splitHostPort(targetPath); - connectionOptions.servername = hostPort?.host ?? targetPath; - } - } - if (options['grpc-node.tls_enable_trace']) { - connectionOptions.enableTrace = true; - } - } - - return getProxiedConnection(address, options, connectionOptions).then( - result => this.createSession(address, credentials, options, result) - ); + const tcpConnection = await this.tcpConnect(address, options); + const secureConnection = await secureConnector.connect(tcpConnection); + return this.createSession(secureConnection, address, options); } shutdown(): void { diff --git a/packages/grpc-js/test/test-channel-credentials.ts b/packages/grpc-js/test/test-channel-credentials.ts index dfd0cd378..f65e52e91 100644 --- a/packages/grpc-js/test/test-channel-credentials.ts +++ b/packages/grpc-js/test/test-channel-credentials.ts @@ -69,46 +69,7 @@ const pFixtures = Promise.all( }); describe('ChannelCredentials Implementation', () => { - describe('createInsecure', () => { - it('should return a ChannelCredentials object with no associated secure context', () => { - const creds = assert2.noThrowAndReturn(() => - ChannelCredentials.createInsecure() - ); - assert.ok(!creds._getConnectionOptions()?.secureContext); - }); - }); - describe('createSsl', () => { - it('should work when given no arguments', () => { - const creds: ChannelCredentials = assert2.noThrowAndReturn(() => - ChannelCredentials.createSsl() - ); - assert.ok(!!creds._getConnectionOptions()); - }); - - it('should work with just a CA override', async () => { - const { ca } = await pFixtures; - const creds = assert2.noThrowAndReturn(() => - ChannelCredentials.createSsl(ca) - ); - assert.ok(!!creds._getConnectionOptions()); - }); - - it('should work with just a private key and cert chain', async () => { - const { key, cert } = await pFixtures; - const creds = assert2.noThrowAndReturn(() => - ChannelCredentials.createSsl(null, key, cert) - ); - assert.ok(!!creds._getConnectionOptions()); - }); - - it('should work with three parameters specified', async () => { - const { ca, key, cert } = await pFixtures; - const creds = assert2.noThrowAndReturn(() => - ChannelCredentials.createSsl(ca, key, cert) - ); - assert.ok(!!creds._getConnectionOptions()); - }); it('should throw if just one of private key and cert chain are missing', async () => { const { ca, key, cert } = await pFixtures;