Skip to content

Commit 8f08bbe

Browse files
authored
Merge pull request #2855 from murgatroid99/grpc-js_credentials_secure_connector
grpc-js: Add security connector, rework connection establishment
2 parents f5133e4 + 1657324 commit 8f08bbe

File tree

5 files changed

+216
-345
lines changed

5 files changed

+216
-345
lines changed

packages/grpc-js/src/channel-credentials.ts

+154-67
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,17 @@ import {
2020
createSecureContext,
2121
PeerCertificate,
2222
SecureContext,
23+
checkServerIdentity,
24+
connect as tlsConnect
2325
} from 'tls';
2426

2527
import { CallCredentials } from './call-credentials';
2628
import { CIPHER_SUITES, getDefaultRootsData } from './tls-helpers';
2729
import { CaCertificateUpdate, CaCertificateUpdateListener, CertificateProvider, IdentityCertificateUpdate, IdentityCertificateUpdateListener } from './certificate-provider';
30+
import { Socket } from 'net';
31+
import { ChannelOptions } from './channel-options';
32+
import { GrpcUri, parseUri, splitHostPort } from './uri-parser';
33+
import { getDefaultAuthority } from './resolver';
2834

2935
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3036
function verifyIsBufferOrNull(obj: any, friendlyName: string): void {
@@ -57,6 +63,11 @@ export interface VerifyOptions {
5763
rejectUnauthorized?: boolean;
5864
}
5965

66+
export interface SecureConnector {
67+
connect(socket: Socket): Promise<Socket>;
68+
destroy(): void;
69+
}
70+
6071
/**
6172
* A class that contains credentials for communicating over a channel, as well
6273
* as a set of per-call credentials, which are applied to every method call made
@@ -83,13 +94,6 @@ export abstract class ChannelCredentials {
8394
return this.callCredentials;
8495
}
8596

86-
/**
87-
* Gets a SecureContext object generated from input parameters if this
88-
* instance was created with createSsl, or null if this instance was created
89-
* with createInsecure.
90-
*/
91-
abstract _getConnectionOptions(): ConnectionOptions | null;
92-
9397
/**
9498
* Indicates whether this credentials object creates a secure channel.
9599
*/
@@ -102,13 +106,7 @@ export abstract class ChannelCredentials {
102106
*/
103107
abstract _equals(other: ChannelCredentials): boolean;
104108

105-
_ref(): void {
106-
// Do nothing by default
107-
}
108-
109-
_unref(): void {
110-
// Do nothing by default
111-
}
109+
abstract _createSecureConnector(channelTarget: GrpcUri, options: ChannelOptions): SecureConnector;
112110

113111
/**
114112
* Return a new ChannelCredentials instance with a given set of credentials.
@@ -180,51 +178,111 @@ class InsecureChannelCredentialsImpl extends ChannelCredentials {
180178
compose(callCredentials: CallCredentials): never {
181179
throw new Error('Cannot compose insecure credentials');
182180
}
183-
184-
_getConnectionOptions(): ConnectionOptions | null {
185-
return {};
186-
}
187181
_isSecure(): boolean {
188182
return false;
189183
}
190184
_equals(other: ChannelCredentials): boolean {
191185
return other instanceof InsecureChannelCredentialsImpl;
192186
}
187+
_createSecureConnector(channelTarget: GrpcUri, options: ChannelOptions): SecureConnector {
188+
return {
189+
connect(socket) {
190+
return Promise.resolve(socket);
191+
},
192+
destroy() {}
193+
}
194+
}
193195
}
194196

195-
class SecureChannelCredentialsImpl extends ChannelCredentials {
196-
connectionOptions: ConnectionOptions;
197+
function getConnectionOptions(secureContext: SecureContext, verifyOptions: VerifyOptions, channelTarget: GrpcUri, options: ChannelOptions): ConnectionOptions {
198+
const connectionOptions: ConnectionOptions = {
199+
secureContext: secureContext
200+
};
201+
if (verifyOptions.checkServerIdentity) {
202+
connectionOptions.checkServerIdentity = verifyOptions.checkServerIdentity;
203+
}
204+
if (verifyOptions.rejectUnauthorized !== undefined) {
205+
connectionOptions.rejectUnauthorized = verifyOptions.rejectUnauthorized;
206+
}
207+
connectionOptions.ALPNProtocols = ['h2'];
208+
if (options['grpc.ssl_target_name_override']) {
209+
const sslTargetNameOverride = options['grpc.ssl_target_name_override']!;
210+
const originalCheckServerIdentity =
211+
connectionOptions.checkServerIdentity ?? checkServerIdentity;
212+
connectionOptions.checkServerIdentity = (
213+
host: string,
214+
cert: PeerCertificate
215+
): Error | undefined => {
216+
return originalCheckServerIdentity(sslTargetNameOverride, cert);
217+
};
218+
connectionOptions.servername = sslTargetNameOverride;
219+
} else {
220+
if ('grpc.http_connect_target' in options) {
221+
/* This is more or less how servername will be set in createSession
222+
* if a connection is successfully established through the proxy.
223+
* If the proxy is not used, these connectionOptions are discarded
224+
* anyway */
225+
const targetPath = getDefaultAuthority(
226+
parseUri(options['grpc.http_connect_target'] as string) ?? {
227+
path: 'localhost',
228+
}
229+
);
230+
const hostPort = splitHostPort(targetPath);
231+
connectionOptions.servername = hostPort?.host ?? targetPath;
232+
}
233+
}
234+
if (options['grpc-node.tls_enable_trace']) {
235+
connectionOptions.enableTrace = true;
236+
}
197237

238+
let realTarget: GrpcUri = channelTarget;
239+
if ('grpc.http_connect_target' in options) {
240+
const parsedTarget = parseUri(options['grpc.http_connect_target']!);
241+
if (parsedTarget) {
242+
realTarget = parsedTarget;
243+
}
244+
}
245+
const targetPath = getDefaultAuthority(realTarget);
246+
const hostPort = splitHostPort(targetPath);
247+
const remoteHost = hostPort?.host ?? targetPath;
248+
connectionOptions.host = remoteHost;
249+
connectionOptions.servername = remoteHost;
250+
return connectionOptions;
251+
}
252+
253+
class SecureConnectorImpl implements SecureConnector {
254+
constructor(private connectionOptions: ConnectionOptions) {
255+
}
256+
connect(socket: Socket): Promise<Socket> {
257+
const tlsConnectOptions: ConnectionOptions = {
258+
socket: socket,
259+
...this.connectionOptions
260+
};
261+
return new Promise<Socket>((resolve, reject) => {
262+
const tlsSocket = tlsConnect(tlsConnectOptions, () => {
263+
resolve(tlsSocket)
264+
});
265+
tlsSocket.on('error', (error: Error) => {
266+
reject(error);
267+
});
268+
});
269+
}
270+
destroy() {}
271+
}
272+
273+
class SecureChannelCredentialsImpl extends ChannelCredentials {
198274
constructor(
199275
private secureContext: SecureContext,
200276
private verifyOptions: VerifyOptions
201277
) {
202278
super();
203-
this.connectionOptions = {
204-
secureContext,
205-
};
206-
// Node asserts that this option is a function, so we cannot pass undefined
207-
if (verifyOptions?.checkServerIdentity) {
208-
this.connectionOptions.checkServerIdentity =
209-
verifyOptions.checkServerIdentity;
210-
}
211-
212-
if (verifyOptions?.rejectUnauthorized !== undefined) {
213-
this.connectionOptions.rejectUnauthorized =
214-
verifyOptions.rejectUnauthorized;
215-
}
216279
}
217280

218281
compose(callCredentials: CallCredentials): ChannelCredentials {
219282
const combinedCallCredentials =
220283
this.callCredentials.compose(callCredentials);
221284
return new ComposedChannelCredentialsImpl(this, combinedCallCredentials);
222285
}
223-
224-
_getConnectionOptions(): ConnectionOptions | null {
225-
// Copy to prevent callers from mutating this.connectionOptions
226-
return { ...this.connectionOptions };
227-
}
228286
_isSecure(): boolean {
229287
return true;
230288
}
@@ -242,6 +300,10 @@ class SecureChannelCredentialsImpl extends ChannelCredentials {
242300
return false;
243301
}
244302
}
303+
_createSecureConnector(channelTarget: GrpcUri, options: ChannelOptions): SecureConnector {
304+
const connectionOptions = getConnectionOptions(this.secureContext, this.verifyOptions, channelTarget, options);
305+
return new SecureConnectorImpl(connectionOptions);
306+
}
245307
}
246308

247309
class CertificateProviderChannelCredentialsImpl extends ChannelCredentials {
@@ -250,10 +312,38 @@ class CertificateProviderChannelCredentialsImpl extends ChannelCredentials {
250312
private latestIdentityUpdate: IdentityCertificateUpdate | null = null;
251313
private caCertificateUpdateListener: CaCertificateUpdateListener = this.handleCaCertificateUpdate.bind(this);
252314
private identityCertificateUpdateListener: IdentityCertificateUpdateListener = this.handleIdentityCertitificateUpdate.bind(this);
315+
private static SecureConnectorImpl = class implements SecureConnector {
316+
constructor(private parent: CertificateProviderChannelCredentialsImpl, private channelTarget: GrpcUri, private options: ChannelOptions) {}
317+
318+
connect(socket: Socket): Promise<Socket> {
319+
return new Promise((resolve, reject) => {
320+
const secureContext = this.parent.getLatestSecureContext();
321+
if (!secureContext) {
322+
reject(new Error('Credentials not loaded'));
323+
return;
324+
}
325+
const connnectionOptions = getConnectionOptions(secureContext, this.parent.verifyOptions, this.channelTarget, this.options);
326+
const tlsConnectOptions: ConnectionOptions = {
327+
socket: socket,
328+
...connnectionOptions
329+
}
330+
const tlsSocket = tlsConnect(tlsConnectOptions, () => {
331+
resolve(tlsSocket)
332+
});
333+
tlsSocket.on('error', (error: Error) => {
334+
reject(error);
335+
});
336+
});
337+
}
338+
339+
destroy() {
340+
this.parent.unref();
341+
}
342+
}
253343
constructor(
254344
private caCertificateProvider: CertificateProvider,
255345
private identityCertificateProvider: CertificateProvider | null,
256-
private verifyOptions: VerifyOptions | null
346+
private verifyOptions: VerifyOptions
257347
) {
258348
super();
259349
}
@@ -265,27 +355,6 @@ class CertificateProviderChannelCredentialsImpl extends ChannelCredentials {
265355
combinedCallCredentials
266356
);
267357
}
268-
_getConnectionOptions(): ConnectionOptions | null {
269-
if (this.latestCaUpdate === null) {
270-
return null;
271-
}
272-
if (this.identityCertificateProvider !== null && this.latestIdentityUpdate === null) {
273-
return null;
274-
}
275-
const secureContext: SecureContext = createSecureContext({
276-
ca: this.latestCaUpdate.caCertificate,
277-
key: this.latestIdentityUpdate?.privateKey,
278-
cert: this.latestIdentityUpdate?.certificate,
279-
ciphers: CIPHER_SUITES
280-
});
281-
const options: ConnectionOptions = {
282-
secureContext: secureContext
283-
};
284-
if (this.verifyOptions?.checkServerIdentity) {
285-
options.checkServerIdentity = this.verifyOptions.checkServerIdentity;
286-
}
287-
return options;
288-
}
289358
_isSecure(): boolean {
290359
return true;
291360
}
@@ -301,20 +370,24 @@ class CertificateProviderChannelCredentialsImpl extends ChannelCredentials {
301370
return false;
302371
}
303372
}
304-
_ref(): void {
373+
private ref(): void {
305374
if (this.refcount === 0) {
306375
this.caCertificateProvider.addCaCertificateListener(this.caCertificateUpdateListener);
307376
this.identityCertificateProvider?.addIdentityCertificateListener(this.identityCertificateUpdateListener);
308377
}
309378
this.refcount += 1;
310379
}
311-
_unref(): void {
380+
private unref(): void {
312381
this.refcount -= 1;
313382
if (this.refcount === 0) {
314383
this.caCertificateProvider.removeCaCertificateListener(this.caCertificateUpdateListener);
315384
this.identityCertificateProvider?.removeIdentityCertificateListener(this.identityCertificateUpdateListener);
316385
}
317386
}
387+
_createSecureConnector(channelTarget: GrpcUri, options: ChannelOptions): SecureConnector {
388+
this.ref();
389+
return new CertificateProviderChannelCredentialsImpl.SecureConnectorImpl(this, channelTarget, options);
390+
}
318391

319392
private handleCaCertificateUpdate(update: CaCertificateUpdate | null) {
320393
this.latestCaUpdate = update;
@@ -323,10 +396,25 @@ class CertificateProviderChannelCredentialsImpl extends ChannelCredentials {
323396
private handleIdentityCertitificateUpdate(update: IdentityCertificateUpdate | null) {
324397
this.latestIdentityUpdate = update;
325398
}
399+
400+
private getLatestSecureContext(): SecureContext | null {
401+
if (this.latestCaUpdate === null) {
402+
return null;
403+
}
404+
if (this.identityCertificateProvider !== null && this.latestIdentityUpdate === null) {
405+
return null;
406+
}
407+
return createSecureContext({
408+
ca: this.latestCaUpdate.caCertificate,
409+
key: this.latestIdentityUpdate?.privateKey,
410+
cert: this.latestIdentityUpdate?.certificate,
411+
ciphers: CIPHER_SUITES
412+
});
413+
}
326414
}
327415

328416
export function createCertificateProviderChannelCredentials(caCertificateProvider: CertificateProvider, identityCertificateProvider: CertificateProvider | null, verifyOptions?: VerifyOptions) {
329-
return new CertificateProviderChannelCredentialsImpl(caCertificateProvider, identityCertificateProvider, verifyOptions ?? null);
417+
return new CertificateProviderChannelCredentialsImpl(caCertificateProvider, identityCertificateProvider, verifyOptions ?? {});
330418
}
331419

332420
class ComposedChannelCredentialsImpl extends ChannelCredentials {
@@ -347,10 +435,6 @@ class ComposedChannelCredentialsImpl extends ChannelCredentials {
347435
combinedCallCredentials
348436
);
349437
}
350-
351-
_getConnectionOptions(): ConnectionOptions | null {
352-
return this.channelCredentials._getConnectionOptions();
353-
}
354438
_isSecure(): boolean {
355439
return true;
356440
}
@@ -367,4 +451,7 @@ class ComposedChannelCredentialsImpl extends ChannelCredentials {
367451
return false;
368452
}
369453
}
454+
_createSecureConnector(channelTarget: GrpcUri, options: ChannelOptions): SecureConnector {
455+
return this.channelCredentials._createSecureConnector(channelTarget, options);
456+
}
370457
}

0 commit comments

Comments
 (0)