mirror of https://github.com/grpc/grpc-node.git
grpc-js: Add security connector, rework connection establishment
This commit is contained in:
parent
f44b5e50e8
commit
f154954854
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue