diff --git a/packages/grpc-js/src/server-interceptors.ts b/packages/grpc-js/src/server-interceptors.ts index 1c4a8f1b4..d6c6e2271 100644 --- a/packages/grpc-js/src/server-interceptors.ts +++ b/packages/grpc-js/src/server-interceptors.ts @@ -301,6 +301,13 @@ const defaultResponder: FullResponder = { }, }; +export interface ConnectionInfo { + localAddress?: string | undefined; + localPort?: number | undefined; + remoteAddress?: string | undefined; + remotePort?: number | undefined; +} + export interface ServerInterceptingCallInterface { /** * Register the listener to handle inbound events. @@ -338,6 +345,10 @@ export interface ServerInterceptingCallInterface { * Return the auth context of the connection the call is associated with. */ getAuthContext(): AuthContext; + /** + * Return information about the connection used to make the call. + */ + getConnectionInfo(): ConnectionInfo; } export class ServerInterceptingCall implements ServerInterceptingCallInterface { @@ -449,6 +460,9 @@ export class ServerInterceptingCall implements ServerInterceptingCallInterface { getAuthContext(): AuthContext { return this.nextCall.getAuthContext(); } + getConnectionInfo(): ConnectionInfo { + return this.nextCall.getConnectionInfo(); + } } export interface ServerInterceptor { @@ -519,6 +533,7 @@ export class BaseServerInterceptingCall private receivedHalfClose = false; private streamEnded = false; private host: string; + private connectionInfo: ConnectionInfo; constructor( private readonly stream: http2.ServerHttp2Stream, @@ -606,6 +621,14 @@ export class BaseServerInterceptingCall metadata.remove(http2.constants.HTTP2_HEADER_TE); metadata.remove(http2.constants.HTTP2_HEADER_CONTENT_TYPE); this.metadata = metadata; + + const socket = stream.session?.socket; + this.connectionInfo = { + localAddress: socket?.localAddress, + localPort: socket?.localPort, + remoteAddress: socket?.remoteAddress, + remotePort: socket?.remotePort + }; } private handleTimeoutHeader(timeoutHeader: string) { @@ -990,6 +1013,9 @@ export class BaseServerInterceptingCall return {}; } } + getConnectionInfo(): ConnectionInfo { + return this.connectionInfo; + } } export function getServerInterceptingCall( diff --git a/packages/grpc-js/test/common.ts b/packages/grpc-js/test/common.ts index f64cbcba9..d431f871d 100644 --- a/packages/grpc-js/test/common.ts +++ b/packages/grpc-js/test/common.ts @@ -150,15 +150,15 @@ export class TestClient { this.client.waitForReady(deadline, callback); } - sendRequest(callback: (error?: grpc.ServiceError) => void) { - this.client.echo({}, callback); + sendRequest(callback: (error?: grpc.ServiceError) => void): grpc.ClientUnaryCall { + return this.client.echo({}, callback); } sendRequestWithMetadata( metadata: grpc.Metadata, callback: (error?: grpc.ServiceError) => void - ) { - this.client.echo({}, metadata, callback); + ): grpc.ClientUnaryCall { + return this.client.echo({}, metadata, callback); } getChannelState() { diff --git a/packages/grpc-js/test/test-server-interceptors.ts b/packages/grpc-js/test/test-server-interceptors.ts index 4a4abb6fa..3fd4fd0bb 100644 --- a/packages/grpc-js/test/test-server-interceptors.ts +++ b/packages/grpc-js/test/test-server-interceptors.ts @@ -127,6 +127,22 @@ const testHeaderInjectionInterceptor: grpc.ServerInterceptor = ( }); }; +const callExaminationInterceptor: grpc.ServerInterceptor = ( + methodDescriptor, + call +) => { + const connectionInfo = call.getConnectionInfo(); + return new grpc.ServerInterceptingCall(call, { + sendMetadata: (metadata, next) => { + metadata.add('local-address', `${connectionInfo.localAddress}`); + metadata.add('local-port', `${connectionInfo.localPort}`); + metadata.add('remote-address', `${connectionInfo.remoteAddress}`); + metadata.add('remote-port', `${connectionInfo.remotePort}`); + next(metadata); + } + }) +} + describe('Server interceptors', () => { describe('Auth-type interceptor', () => { let server: grpc.Server; @@ -336,4 +352,45 @@ describe('Server interceptors', () => { }); }); }); + describe('Call properties', () => { + let server: grpc.Server; + let client: TestClient; + let portNum: number; + /* Tests that an interceptor can entirely prevent the handler from being + * invoked, based on the contents of the metadata. */ + before(done => { + server = new grpc.Server({ interceptors: [callExaminationInterceptor] }); + server.addService(echoService.service, { + echo: ( + call: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData + ) => { + callback(null, call.request); + }, + }); + server.bindAsync( + '[::1]:0', + grpc.ServerCredentials.createInsecure(), + (error, port) => { + assert.ifError(error); + client = new TestClient(`localhost:${port}`, false); + portNum = port; + done(); + } + ); + }); + after(done => { + client.close(); + server.tryShutdown(done); + }); + it('Should get valid connection information', done => { + const call = client.sendRequest(done); + call.on('metadata', metadata => { + assert.strictEqual(metadata.get('local-address')[0], '::1'); + assert.strictEqual(metadata.get('remote-address')[0], '::1'); + assert.strictEqual(metadata.get('local-port')[0], `${portNum}`); + assert.notStrictEqual(metadata.get('remote-port')[0], 'undefined'); + }); + }); + }); });