Skip to content

Commit a1fde62

Browse files
committed
grpc-js: Expand ServerCredentials API to support watchers
1 parent 5b44a44 commit a1fde62

File tree

3 files changed

+161
-8
lines changed

3 files changed

+161
-8
lines changed

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,40 @@
1717

1818
import { SecureServerOptions } from 'http2';
1919
import { CIPHER_SUITES, getDefaultRootsData } from './tls-helpers';
20+
import { SecureContextOptions } from 'tls';
2021

2122
export interface KeyCertPair {
2223
private_key: Buffer;
2324
cert_chain: Buffer;
2425
}
2526

27+
export interface SecureContextWatcher {
28+
(context: SecureContextOptions | null): void;
29+
}
30+
2631
export abstract class ServerCredentials {
32+
private watchers: Set<SecureContextWatcher> = new Set();
33+
private latestContextOptions: SecureServerOptions | null = null;
34+
_addWatcher(watcher: SecureContextWatcher) {
35+
this.watchers.add(watcher);
36+
}
37+
_removeWatcher(watcher: SecureContextWatcher) {
38+
this.watchers.delete(watcher);
39+
}
40+
protected updateSecureContextOptions(options: SecureServerOptions | null) {
41+
if (options) {
42+
this.latestContextOptions = options;
43+
} else {
44+
this.latestContextOptions = null;
45+
}
46+
for (const watcher of this.watchers) {
47+
watcher(this.latestContextOptions);
48+
}
49+
}
2750
abstract _isSecure(): boolean;
28-
abstract _getSettings(): SecureServerOptions | null;
51+
_getSettings(): SecureServerOptions | null {
52+
return this.latestContextOptions;
53+
}
2954
abstract _equals(other: ServerCredentials): boolean;
3055

3156
static createInsecure(): ServerCredentials {

packages/grpc-js/src/server.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import {
4141
ServerStatusResponse,
4242
serverErrorToStatus,
4343
} from './server-call';
44-
import { ServerCredentials } from './server-credentials';
44+
import { SecureContextWatcher, ServerCredentials } from './server-credentials';
4545
import { ChannelOptions } from './channel-options';
4646
import {
4747
createResolver,
@@ -73,6 +73,7 @@ import { CipherNameAndProtocol, TLSSocket } from 'tls';
7373
import { ServerInterceptingCallInterface, ServerInterceptor, getServerInterceptingCall } from './server-interceptors';
7474
import { PartialStatusObject } from './call-interface';
7575
import { CallEventTracker } from './transport';
76+
import { Socket } from 'net';
7677

7778
const UNLIMITED_CONNECTION_AGE_MS = ~(1 << 31);
7879
const KEEPALIVE_MAX_TIME_MS = ~(1 << 31);
@@ -501,13 +502,19 @@ export class Server {
501502
private createHttp2Server(credentials: ServerCredentials) {
502503
let http2Server: http2.Http2Server | http2.Http2SecureServer;
503504
if (credentials._isSecure()) {
504-
const secureServerOptions = Object.assign(
505-
this.commonServerOptions,
506-
credentials._getSettings()!
507-
);
508-
secureServerOptions.enableTrace =
509-
this.options['grpc-node.tls_enable_trace'] === 1;
505+
const credentialsSettings = credentials._getSettings();
506+
const secureServerOptions: http2.SecureServerOptions = {
507+
...this.commonServerOptions,
508+
...credentialsSettings,
509+
enableTrace: this.options['grpc-node.tls_enable_trace'] === 1
510+
};
511+
let areCredentialsValid = credentialsSettings !== null;
510512
http2Server = http2.createSecureServer(secureServerOptions);
513+
http2Server.on('connection', (socket: Socket) => {
514+
if (!areCredentialsValid) {
515+
socket.destroy();
516+
}
517+
});
511518
http2Server.on('secureConnection', (socket: TLSSocket) => {
512519
/* These errors need to be handled by the user of Http2SecureServer,
513520
* according to https://github.com/nodejs/node/issues/35824 */
@@ -517,6 +524,16 @@ export class Server {
517524
);
518525
});
519526
});
527+
const credsWatcher: SecureContextWatcher = options => {
528+
if (options) {
529+
(http2Server as http2.Http2SecureServer).setSecureContext(options);
530+
}
531+
areCredentialsValid = options !== null;
532+
}
533+
credentials._addWatcher(credsWatcher);
534+
http2Server.on('close', () => {
535+
credentials._removeWatcher(credsWatcher);
536+
});
520537
} else {
521538
http2Server = http2.createServer(this.commonServerOptions);
522539
}

packages/grpc-js/test/test-server.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
import { ProtoGrpcType as TestServiceGrpcType } from './generated/test_service';
4242
import { Request__Output } from './generated/Request';
4343
import { CompressionAlgorithms } from '../src/compression-algorithms';
44+
import { SecureContextOptions } from 'tls';
4445

4546
const loadedTestServiceProto = protoLoader.loadSync(
4647
path.join(__dirname, 'fixtures/test_service.proto'),
@@ -746,6 +747,116 @@ describe('Echo service', () => {
746747
);
747748
});
748749

750+
describe('ServerCredentials watcher', () => {
751+
let server: Server;
752+
let serverPort: number;
753+
const protoFile = path.join(__dirname, 'fixtures', 'echo_service.proto');
754+
const echoService = loadProtoFile(protoFile)
755+
.EchoService as ServiceClientConstructor;
756+
757+
class ToggleableSecureServerCredentials extends ServerCredentials {
758+
private contextOptions: SecureContextOptions;
759+
constructor(key: Buffer, cert: Buffer) {
760+
super();
761+
this.contextOptions = {key, cert};
762+
this.enable();
763+
}
764+
enable() {
765+
this.updateSecureContextOptions(this.contextOptions);
766+
}
767+
disable() {
768+
this.updateSecureContextOptions(null);
769+
}
770+
_isSecure(): boolean {
771+
return true;
772+
}
773+
_equals(other: grpc.ServerCredentials): boolean {
774+
return this === other;
775+
}
776+
}
777+
778+
const serverCredentials = new ToggleableSecureServerCredentials(key, cert);
779+
780+
const serviceImplementation = {
781+
echo(call: ServerUnaryCall<any, any>, callback: sendUnaryData<any>) {
782+
callback(null, call.request);
783+
},
784+
echoBidiStream(call: ServerDuplexStream<any, any>) {
785+
call.on('data', data => {
786+
call.write(data);
787+
});
788+
call.on('end', () => {
789+
call.end();
790+
});
791+
},
792+
};
793+
794+
before(done => {
795+
server = new Server();
796+
server.addService(echoService.service, serviceImplementation);
797+
798+
server.bindAsync(
799+
'localhost:0',
800+
serverCredentials,
801+
(err, port) => {
802+
assert.ifError(err);
803+
serverPort = port;
804+
done();
805+
}
806+
);
807+
});
808+
809+
after(done => {
810+
client.close();
811+
server.tryShutdown(done);
812+
});
813+
814+
it('should make successful requests only when the credentials are enabled', done => {
815+
const client1 = new echoService(
816+
`localhost:${serverPort}`,
817+
grpc.credentials.createSsl(ca),
818+
{
819+
'grpc.ssl_target_name_override': 'foo.test.google.fr',
820+
'grpc.default_authority': 'foo.test.google.fr',
821+
'grpc.use_local_subchannel_pool': 1
822+
}
823+
);
824+
const testMessage = { value: 'test value', value2: 3 };
825+
client1.echo(testMessage, (error: ServiceError, response: any) => {
826+
assert.ifError(error);
827+
assert.deepStrictEqual(response, testMessage);
828+
serverCredentials.disable();
829+
const client2 = new echoService(
830+
`localhost:${serverPort}`,
831+
grpc.credentials.createSsl(ca),
832+
{
833+
'grpc.ssl_target_name_override': 'foo.test.google.fr',
834+
'grpc.default_authority': 'foo.test.google.fr',
835+
'grpc.use_local_subchannel_pool': 1
836+
}
837+
);
838+
client2.echo(testMessage, (error: ServiceError, response: any) => {
839+
assert(error);
840+
assert.strictEqual(error.code, grpc.status.UNAVAILABLE);
841+
serverCredentials.enable();
842+
const client3 = new echoService(
843+
`localhost:${serverPort}`,
844+
grpc.credentials.createSsl(ca),
845+
{
846+
'grpc.ssl_target_name_override': 'foo.test.google.fr',
847+
'grpc.default_authority': 'foo.test.google.fr',
848+
'grpc.use_local_subchannel_pool': 1
849+
}
850+
);
851+
client3.echo(testMessage, (error: ServiceError, response: any) => {
852+
assert.ifError(error);
853+
done();
854+
});
855+
});
856+
});
857+
});
858+
});
859+
749860
/* This test passes on Node 18 but fails on Node 16. The failure appears to
750861
* be caused by https://github.com/nodejs/node/issues/42713 */
751862
it.skip('should continue a stream after server shutdown', done => {

0 commit comments

Comments
 (0)