Merge pull request #2802 from murgatroid99/grpc-js_certificate_provider

grpc-js: Add file watcher certificate provider, and credentials that use them
This commit is contained in:
Michael Lumish 2024-09-06 10:30:44 -07:00 committed by GitHub
commit 21f470824c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 638 additions and 18 deletions

View File

@ -0,0 +1,171 @@
/*
* Copyright 2024 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import * as fs from 'fs/promises';
import * as logging from './logging';
import { LogVerbosity } from './constants';
const TRACER_NAME = 'certificate_provider';
function trace(text: string) {
logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text);
}
export interface CaCertificateUpdate {
caCertificate: Buffer;
}
export interface IdentityCertificateUpdate {
certificate: Buffer;
privateKey: Buffer;
}
export interface CaCertificateUpdateListener {
(update: CaCertificateUpdate | null): void;
}
export interface IdentityCertificateUpdateListener {
(update: IdentityCertificateUpdate | null) : void;
}
export interface CertificateProvider {
addCaCertificateListener(listener: CaCertificateUpdateListener): void;
removeCaCertificateListener(listener: CaCertificateUpdateListener): void;
addIdentityCertificateListener(listener: IdentityCertificateUpdateListener): void;
removeIdentityCertificateListener(listener: IdentityCertificateUpdateListener): void;
}
export interface CertificateProviderProvider<Provider> {
getInstance(): Provider;
}
export interface FileWatcherCertificateProviderConfig {
certificateFile?: string | undefined;
privateKeyFile?: string | undefined;
caCertificateFile?: string | undefined;
refreshIntervalMs: number;
}
export class FileWatcherCertificateProvider implements CertificateProvider {
private refreshTimer: NodeJS.Timeout | null = null;
private fileResultPromise: Promise<[PromiseSettledResult<Buffer>, PromiseSettledResult<Buffer>, PromiseSettledResult<Buffer>]> | null = null;
private latestCaUpdate: CaCertificateUpdate | null = null;
private caListeners: Set<CaCertificateUpdateListener> = new Set();
private latestIdentityUpdate: IdentityCertificateUpdate | null = null;
private identityListeners: Set<IdentityCertificateUpdateListener> = new Set();
private lastUpdateTime: Date | null = null;
constructor(
private config: FileWatcherCertificateProviderConfig
) {
if ((config.certificateFile === undefined) !== (config.privateKeyFile === undefined)) {
throw new Error('certificateFile and privateKeyFile must be set or unset together');
}
if (config.certificateFile === undefined && config.caCertificateFile === undefined) {
throw new Error('At least one of certificateFile and caCertificateFile must be set');
}
trace('File watcher constructed with config ' + JSON.stringify(config));
}
private updateCertificates() {
if (this.fileResultPromise) {
return;
}
this.fileResultPromise = Promise.allSettled([
this.config.certificateFile ? fs.readFile(this.config.certificateFile) : Promise.reject<Buffer>(),
this.config.privateKeyFile ? fs.readFile(this.config.privateKeyFile) : Promise.reject<Buffer>(),
this.config.caCertificateFile ? fs.readFile(this.config.caCertificateFile) : Promise.reject<Buffer>()
]);
this.fileResultPromise.then(([certificateResult, privateKeyResult, caCertificateResult]) => {
if (!this.refreshTimer) {
return;
}
trace('File watcher read certificates certificate' + (certificateResult ? '!=' : '==') + 'null, privateKey' + (privateKeyResult ? '!=' : '==') + 'null, CA certificate' + (caCertificateResult ? '!=' : '==') + 'null');
this.lastUpdateTime = new Date();
this.fileResultPromise = null;
if (certificateResult.status === 'fulfilled' && privateKeyResult.status === 'fulfilled') {
this.latestIdentityUpdate = {
certificate: certificateResult.value,
privateKey: privateKeyResult.value
};
} else {
this.latestIdentityUpdate = null;
}
if (caCertificateResult.status === 'fulfilled') {
this.latestCaUpdate = {
caCertificate: caCertificateResult.value
};
}
for (const listener of this.identityListeners) {
listener(this.latestIdentityUpdate);
}
for (const listener of this.caListeners) {
listener(this.latestCaUpdate);
}
});
trace('File watcher initiated certificate update');
}
private maybeStartWatchingFiles() {
if (!this.refreshTimer) {
/* Perform the first read immediately, but only if there was not already
* a recent read, to avoid reading from the filesystem significantly more
* frequently than configured if the provider quickly switches between
* used and unused. */
const timeSinceLastUpdate = this.lastUpdateTime ? (new Date()).getTime() - this.lastUpdateTime.getTime() : Infinity;
if (timeSinceLastUpdate > this.config.refreshIntervalMs) {
this.updateCertificates();
}
if (timeSinceLastUpdate > this.config.refreshIntervalMs * 2) {
// Clear out old updates if they are definitely stale
this.latestCaUpdate = null;
this.latestIdentityUpdate = null;
}
this.refreshTimer = setInterval(() => this.updateCertificates(), this.config.refreshIntervalMs);
trace('File watcher started watching');
}
}
private maybeStopWatchingFiles() {
if (this.caListeners.size === 0 && this.identityListeners.size === 0) {
this.fileResultPromise = null;
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
this.refreshTimer = null;
}
}
}
addCaCertificateListener(listener: CaCertificateUpdateListener): void {
this.caListeners.add(listener);
this.maybeStartWatchingFiles();
process.nextTick(listener, this.latestCaUpdate);
}
removeCaCertificateListener(listener: CaCertificateUpdateListener): void {
this.caListeners.delete(listener);
this.maybeStopWatchingFiles();
}
addIdentityCertificateListener(listener: IdentityCertificateUpdateListener): void {
this.identityListeners.add(listener);
this.maybeStartWatchingFiles();
process.nextTick(listener, this.latestIdentityUpdate);
}
removeIdentityCertificateListener(listener: IdentityCertificateUpdateListener): void {
this.identityListeners.delete(listener);
this.maybeStopWatchingFiles();
}
}

View File

@ -24,6 +24,7 @@ import {
import { CallCredentials } from './call-credentials';
import { CIPHER_SUITES, getDefaultRootsData } from './tls-helpers';
import { CaCertificateUpdate, CaCertificateUpdateListener, CertificateProvider, IdentityCertificateUpdate, IdentityCertificateUpdateListener } from './certificate-provider';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function verifyIsBufferOrNull(obj: any, friendlyName: string): void {
@ -100,6 +101,14 @@ export abstract class ChannelCredentials {
*/
abstract _equals(other: ChannelCredentials): boolean;
_ref(): void {
// Do nothing by default
}
_unref(): void {
// Do nothing by default
}
/**
* Return a new ChannelCredentials instance with a given set of credentials.
* The resulting instance can be used to construct a Channel that communicates
@ -172,7 +181,7 @@ class InsecureChannelCredentialsImpl extends ChannelCredentials {
}
_getConnectionOptions(): ConnectionOptions | null {
return null;
return {};
}
_isSecure(): boolean {
return false;
@ -229,12 +238,100 @@ class SecureChannelCredentialsImpl extends ChannelCredentials {
}
}
class CertificateProviderChannelCredentialsImpl extends ChannelCredentials {
private refcount: number = 0;
private latestCaUpdate: CaCertificateUpdate | null = null;
private latestIdentityUpdate: IdentityCertificateUpdate | null = null;
private caCertificateUpdateListener: CaCertificateUpdateListener = this.handleCaCertificateUpdate.bind(this);
private identityCertificateUpdateListener: IdentityCertificateUpdateListener = this.handleIdentityCertitificateUpdate.bind(this);
constructor(
private caCertificateProvider: CertificateProvider,
private identityCertificateProvider: CertificateProvider | null,
private verifyOptions: VerifyOptions | null
) {
super();
}
compose(callCredentials: CallCredentials): ChannelCredentials {
const combinedCallCredentials =
this.callCredentials.compose(callCredentials);
return new ComposedChannelCredentialsImpl(
this,
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;
}
_equals(other: ChannelCredentials): boolean {
if (this === other) {
return true;
}
if (other instanceof CertificateProviderChannelCredentialsImpl) {
return this.caCertificateProvider === other.caCertificateProvider &&
this.identityCertificateProvider === other.identityCertificateProvider &&
this.verifyOptions?.checkServerIdentity === other.verifyOptions?.checkServerIdentity;
} else {
return false;
}
}
_ref(): void {
if (this.refcount === 0) {
this.caCertificateProvider.addCaCertificateListener(this.caCertificateUpdateListener);
this.identityCertificateProvider?.addIdentityCertificateListener(this.identityCertificateUpdateListener);
}
this.refcount += 1;
}
_unref(): void {
this.refcount -= 1;
if (this.refcount === 0) {
this.caCertificateProvider.removeCaCertificateListener(this.caCertificateUpdateListener);
this.identityCertificateProvider?.removeIdentityCertificateListener(this.identityCertificateUpdateListener);
}
}
private handleCaCertificateUpdate(update: CaCertificateUpdate | null) {
this.latestCaUpdate = update;
}
private handleIdentityCertitificateUpdate(update: IdentityCertificateUpdate | null) {
this.latestIdentityUpdate = update;
}
}
export function createCertificateProviderChannelCredentials(caCertificateProvider: CertificateProvider, identityCertificateProvider: CertificateProvider | null, verifyOptions?: VerifyOptions) {
return new CertificateProviderChannelCredentialsImpl(caCertificateProvider, identityCertificateProvider, verifyOptions ?? null);
}
class ComposedChannelCredentialsImpl extends ChannelCredentials {
constructor(
private channelCredentials: SecureChannelCredentialsImpl,
private channelCredentials: ChannelCredentials,
callCreds: CallCredentials
) {
super(callCreds);
if (!channelCredentials._isSecure()) {
throw new Error('Cannot compose insecure credentials');
}
}
compose(callCredentials: CallCredentials) {
const combinedCallCredentials =

View File

@ -53,4 +53,14 @@ export {
FailurePercentageEjectionConfig,
} from './load-balancer-outlier-detection';
export { createServerCredentialsWithInterceptors } from './server-credentials';
export { createServerCredentialsWithInterceptors, createCertificateProviderServerCredentials } from './server-credentials';
export {
CaCertificateUpdate,
CaCertificateUpdateListener,
IdentityCertificateUpdate,
IdentityCertificateUpdateListener,
CertificateProvider,
FileWatcherCertificateProvider,
FileWatcherCertificateProviderConfig
} from './certificate-provider';
export { createCertificateProviderChannelCredentials } from './channel-credentials';

View File

@ -309,6 +309,7 @@ export class PickFirstLoadBalancer implements LoadBalancer {
this.requestReresolution();
}
if (this.stickyTransientFailureMode) {
this.calculateAndReportNewState();
return;
}
this.stickyTransientFailureMode = true;

View File

@ -19,6 +19,7 @@ import { SecureServerOptions } from 'http2';
import { CIPHER_SUITES, getDefaultRootsData } from './tls-helpers';
import { SecureContextOptions } from 'tls';
import { ServerInterceptor } from '.';
import { CaCertificateUpdate, CaCertificateUpdateListener, CertificateProvider, IdentityCertificateUpdate, IdentityCertificateUpdateListener } from './certificate-provider';
export interface KeyCertPair {
private_key: Buffer;
@ -38,12 +39,11 @@ export abstract class ServerCredentials {
_removeWatcher(watcher: SecureContextWatcher) {
this.watchers.delete(watcher);
}
protected getWatcherCount() {
return this.watchers.size;
}
protected updateSecureContextOptions(options: SecureServerOptions | null) {
if (options) {
this.latestContextOptions = options;
} else {
this.latestContextOptions = null;
}
this.latestContextOptions = options;
for (const watcher of this.watchers) {
watcher(this.latestContextOptions);
}
@ -219,6 +219,91 @@ class SecureServerCredentials extends ServerCredentials {
}
}
class CertificateProviderServerCredentials extends ServerCredentials {
private latestCaUpdate: CaCertificateUpdate | null = null;
private latestIdentityUpdate: IdentityCertificateUpdate | null = null;
private caCertificateUpdateListener: CaCertificateUpdateListener = this.handleCaCertificateUpdate.bind(this);
private identityCertificateUpdateListener: IdentityCertificateUpdateListener = this.handleIdentityCertitificateUpdate.bind(this);
constructor(
private identityCertificateProvider: CertificateProvider,
private caCertificateProvider: CertificateProvider | null,
private requireClientCertificate: boolean
) {
super();
}
_addWatcher(watcher: SecureContextWatcher): void {
if (this.getWatcherCount() === 0) {
this.caCertificateProvider?.addCaCertificateListener(this.caCertificateUpdateListener);
this.identityCertificateProvider.addIdentityCertificateListener(this.identityCertificateUpdateListener);
}
super._addWatcher(watcher);
}
_removeWatcher(watcher: SecureContextWatcher): void {
super._removeWatcher(watcher);
if (this.getWatcherCount() === 0) {
this.caCertificateProvider?.removeCaCertificateListener(this.caCertificateUpdateListener);
this.identityCertificateProvider.removeIdentityCertificateListener(this.identityCertificateUpdateListener);
}
}
_isSecure(): boolean {
return true;
}
_equals(other: ServerCredentials): boolean {
if (this === other) {
return true;
}
if (!(other instanceof CertificateProviderServerCredentials)) {
return false;
}
return (
this.caCertificateProvider === other.caCertificateProvider &&
this.identityCertificateProvider === other.identityCertificateProvider &&
this.requireClientCertificate === other.requireClientCertificate
)
}
private calculateSecureContextOptions(): SecureServerOptions | null {
if (this.latestIdentityUpdate === null) {
return null;
}
if (this.caCertificateProvider !== null && this.latestCaUpdate === null) {
return null;
}
return {
ca: this.latestCaUpdate?.caCertificate,
cert: this.latestIdentityUpdate.certificate,
key: this.latestIdentityUpdate.privateKey,
requestCert: this.latestIdentityUpdate !== null,
rejectUnauthorized: this.requireClientCertificate
};
}
private finalizeUpdate() {
this.updateSecureContextOptions(this.calculateSecureContextOptions());
}
private handleCaCertificateUpdate(update: CaCertificateUpdate | null) {
this.latestCaUpdate = update;
this.finalizeUpdate();
}
private handleIdentityCertitificateUpdate(update: IdentityCertificateUpdate | null) {
this.latestIdentityUpdate = update;
this.finalizeUpdate();
}
}
export function createCertificateProviderServerCredentials(
caCertificateProvider: CertificateProvider,
identityCertificateProvider: CertificateProvider | null,
requireClientCertificate: boolean
) {
return new CertificateProviderServerCredentials(
caCertificateProvider,
identityCertificateProvider,
requireClientCertificate);
}
class InterceptorServerCredentials extends ServerCredentials {
constructor(private readonly childCredentials: ServerCredentials, private readonly interceptors: ServerInterceptor[]) {
super();

View File

@ -155,6 +155,7 @@ export class Subchannel {
'Subchannel constructed with options ' +
JSON.stringify(options, undefined, 2)
);
credentials._ref();
}
private getChannelzInfo(): SubchannelInfo {
@ -290,11 +291,21 @@ export class Subchannel {
if (oldStates.indexOf(this.connectivityState) === -1) {
return false;
}
this.trace(
ConnectivityState[this.connectivityState] +
' -> ' +
ConnectivityState[newState]
);
if (errorMessage) {
this.trace(
ConnectivityState[this.connectivityState] +
' -> ' +
ConnectivityState[newState] +
' with error "' + errorMessage + '"'
);
} else {
this.trace(
ConnectivityState[this.connectivityState] +
' -> ' +
ConnectivityState[newState]
);
}
if (this.channelzEnabled) {
this.channelzTrace.addTrace(
'CT_INFO',
@ -354,6 +365,7 @@ export class Subchannel {
if (this.refcount === 0) {
this.channelzTrace.addTrace('CT_INFO', 'Shutting down');
unregisterChannelzRef(this.channelzRef);
this.credentials._unref();
process.nextTick(() => {
this.transitionToState(
[ConnectivityState.CONNECTING, ConnectivityState.READY],

View File

@ -676,8 +676,13 @@ export class Http2SubchannelConnector implements SubchannelConnector {
const targetAuthority = getDefaultAuthority(
proxyConnectionResult.realTarget ?? this.channelTarget
);
let connectionOptions: http2.SecureClientSessionOptions =
credentials._getConnectionOptions() || {};
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 =
@ -800,8 +805,12 @@ export class Http2SubchannelConnector implements SubchannelConnector {
* 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 =
credentials._getConnectionOptions() || {};
const connectionOptions: ConnectionOptions | null =
credentials._getConnectionOptions();
if (!connectionOptions) {
return Promise.reject('Credentials not loaded');
}
if ('secureContext' in connectionOptions) {
connectionOptions.ALPNProtocols = ['h2'];

View File

@ -0,0 +1,155 @@
/*
* Copyright 2024 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import * as assert from 'assert';
import * as path from 'path';
import * as fs from 'fs/promises';
import { experimental } from '../src';
describe('Certificate providers', () => {
describe('File watcher', () => {
const [caPath, keyPath, certPath] = ['ca.pem', 'server1.key', 'server1.pem'].map(file => path.join(__dirname, 'fixtures', file));
let caData: Buffer, keyData: Buffer, certData: Buffer;
before(async () => {
[caData, keyData, certData] = await Promise.all([caPath, keyPath, certPath].map(filePath => fs.readFile(filePath)));
});
it('Should reject a config with no files', () => {
const config: experimental.FileWatcherCertificateProviderConfig = {
refreshIntervalMs: 1000
};
assert.throws(() => {
new experimental.FileWatcherCertificateProvider(config);
});
});
it('Should accept a config with just a CA certificate', () => {
const config: experimental.FileWatcherCertificateProviderConfig = {
caCertificateFile: caPath,
refreshIntervalMs: 1000
};
assert.doesNotThrow(() => {
new experimental.FileWatcherCertificateProvider(config);
});
});
it('Should accept a config with just a key and certificate', () => {
const config: experimental.FileWatcherCertificateProviderConfig = {
certificateFile: certPath,
privateKeyFile: keyPath,
refreshIntervalMs: 1000
};
assert.doesNotThrow(() => {
new experimental.FileWatcherCertificateProvider(config);
});
});
it('Should accept a config with all files', () => {
const config: experimental.FileWatcherCertificateProviderConfig = {
caCertificateFile: caPath,
certificateFile: certPath,
privateKeyFile: keyPath,
refreshIntervalMs: 1000
};
assert.doesNotThrow(() => {
new experimental.FileWatcherCertificateProvider(config);
});
});
it('Should reject a config with a key but no certificate', () => {
const config: experimental.FileWatcherCertificateProviderConfig = {
caCertificateFile: caPath,
privateKeyFile: keyPath,
refreshIntervalMs: 1000
};
assert.throws(() => {
new experimental.FileWatcherCertificateProvider(config);
});
});
it('Should reject a config with a certificate but no key', () => {
const config: experimental.FileWatcherCertificateProviderConfig = {
caCertificateFile: caPath,
privateKeyFile: keyPath,
refreshIntervalMs: 1000
};
assert.throws(() => {
new experimental.FileWatcherCertificateProvider(config);
});
});
it('Should find the CA file when configured for it', done => {
const config: experimental.FileWatcherCertificateProviderConfig = {
caCertificateFile: caPath,
refreshIntervalMs: 1000
};
const provider = new experimental.FileWatcherCertificateProvider(config);
const listener: experimental.CaCertificateUpdateListener = update => {
if (update) {
provider.removeCaCertificateListener(listener);
assert(update.caCertificate.equals(caData));
done();
}
};
provider.addCaCertificateListener(listener);
});
it('Should find the identity certificate files when configured for it', done => {
const config: experimental.FileWatcherCertificateProviderConfig = {
certificateFile: certPath,
privateKeyFile: keyPath,
refreshIntervalMs: 1000
};
const provider = new experimental.FileWatcherCertificateProvider(config);
const listener: experimental.IdentityCertificateUpdateListener = update => {
if (update) {
provider.removeIdentityCertificateListener(listener);
assert(update.certificate.equals(certData));
assert(update.privateKey.equals(keyData));
done();
}
};
provider.addIdentityCertificateListener(listener);
});
it('Should find all files when configured for it', done => {
const config: experimental.FileWatcherCertificateProviderConfig = {
caCertificateFile: caPath,
certificateFile: certPath,
privateKeyFile: keyPath,
refreshIntervalMs: 1000
};
const provider = new experimental.FileWatcherCertificateProvider(config);
let seenCaUpdate = false;
let seenIdentityUpdate = false;
const caListener: experimental.CaCertificateUpdateListener = update => {
if (update) {
provider.removeCaCertificateListener(caListener);
assert(update.caCertificate.equals(caData));
seenCaUpdate = true;
if (seenIdentityUpdate) {
done();
}
}
};
const identityListener: experimental.IdentityCertificateUpdateListener = update => {
if (update) {
provider.removeIdentityCertificateListener(identityListener);
assert(update.certificate.equals(certData));
assert(update.privateKey.equals(keyData));
seenIdentityUpdate = true;
if (seenCaUpdate) {
done();
}
}
};
provider.addCaCertificateListener(caListener);
provider.addIdentityCertificateListener(identityListener);
});
});
});

View File

@ -74,7 +74,7 @@ describe('ChannelCredentials Implementation', () => {
const creds = assert2.noThrowAndReturn(() =>
ChannelCredentials.createInsecure()
);
assert.ok(!creds._getConnectionOptions());
assert.ok(!creds._getConnectionOptions()?.secureContext);
});
});

View File

@ -0,0 +1,80 @@
/*
* Copyright 2024 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import * as assert from 'assert';
import * as path from 'path';
import { loadProtoFile } from './common';
import { Metadata, Server, ServerDuplexStream, ServerUnaryCall, ServiceClientConstructor, ServiceError, experimental, sendUnaryData } from '../src';
import { ServiceClient } from '../src/make-client';
const protoFile = path.join(__dirname, 'fixtures', 'echo_service.proto');
const EchoService = loadProtoFile(protoFile)
.EchoService as ServiceClientConstructor;
const echoServiceImplementation = {
echo(call: ServerUnaryCall<any, any>, callback: sendUnaryData<any>) {
callback(null, call.request);
},
echoBidiStream(call: ServerDuplexStream<any, any>) {
call.on('data', data => {
call.write(data);
});
call.on('end', () => {
call.end();
});
},
};
describe('Client should successfully communicate with server', () => {
let server: Server | null = null;
let client: ServiceClient | null = null;
afterEach(() => {
client?.close();
client = null;
server?.forceShutdown();
server = null;
})
it('With file watcher credentials', done => {
const [caPath, keyPath, certPath] = ['ca.pem', 'server1.key', 'server1.pem'].map(file => path.join(__dirname, 'fixtures', file));
const fileWatcherConfig: experimental.FileWatcherCertificateProviderConfig = {
caCertificateFile: caPath,
certificateFile: certPath,
privateKeyFile: keyPath,
refreshIntervalMs: 1000
};
const certificateProvider: experimental.CertificateProvider = new experimental.FileWatcherCertificateProvider(fileWatcherConfig);
const serverCreds = experimental.createCertificateProviderServerCredentials(certificateProvider, certificateProvider, true);
const clientCreds = experimental.createCertificateProviderChannelCredentials(certificateProvider, certificateProvider);
server = new Server();
server.addService(EchoService.service, echoServiceImplementation);
server.bindAsync('localhost:0', serverCreds, (error, port) => {
assert.ifError(error);
client = new EchoService(`localhost:${port}`, clientCreds, {
'grpc.ssl_target_name_override': 'foo.test.google.fr',
'grpc.default_authority': 'foo.test.google.fr'
});
const metadata = new Metadata({waitForReady: true});
const deadline = new Date();
deadline.setSeconds(deadline.getSeconds() + 3);
const testMessage = { value: 'test value', value2: 3 };
client.echo(testMessage, metadata, { deadline }, (error: ServiceError, value: any) => {
assert.ifError(error);
assert.deepStrictEqual(value, testMessage);
done();
});
});
}).timeout(5000);
});