grpc-js: Add security connector, rework connection establishment

This commit is contained in:
Michael Lumish 2024-11-22 13:52:57 -08:00
parent f44b5e50e8
commit f154954854
5 changed files with 216 additions and 345 deletions

View File

@ -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<Socket>;
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() {}
}
}
}
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<Socket> {
const tlsConnectOptions: ConnectionOptions = {
socket: socket,
...this.connectionOptions
};
return new Promise<Socket>((resolve, reject) => {
const tlsSocket = tlsConnect(tlsConnectOptions, () => {
resolve(tlsSocket)
});
tlsSocket.on('error', (error: Error) => {
reject(error);
});
});
}
destroy() {}
}
class SecureChannelCredentialsImpl extends ChannelCredentials {
connectionOptions: ConnectionOptions;
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<Socket> {
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);
}
}

View File

@ -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<ProxyConnectionResult> {
channelOptions: ChannelOptions
): Promise<Socket | null> {
if (!('grpc.http_connect_target' in channelOptions)) {
return Promise.resolve<ProxyConnectionResult>({});
return Promise.resolve(null);
}
const realTarget = channelOptions['grpc.http_connect_target'] as string;
const parsedTarget = parseUri(realTarget);
if (parsedTarget === null) {
return Promise.resolve<ProxyConnectionResult>({});
return Promise.resolve(null);
}
const splitHostPost = splitHostPort(parsedTarget.path);
if (splitHostPost === null) {
return Promise.resolve<ProxyConnectionResult>({});
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<ProxyConnectionResult>((resolve, reject) => {
return new Promise<Socket | null>((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,

View File

@ -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],

View File

@ -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<Transport>;
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<Http2Transport> {
if (this.isShutdown) {
return Promise.reject();
}
return new Promise<Http2Transport>((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;
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);
}
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!;
};
}
} 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<Socket> {
return getProxiedConnection(address, options).then(proxiedSocket => {
if (proxiedSocket) {
return proxiedSocket;
} else {
return new Promise<Socket>((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<Http2Transport> {
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 {

View File

@ -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;