diff --git a/examples/interceptors/README.md b/examples/interceptors/README.md new file mode 100644 index 000000000..fa5a746c7 --- /dev/null +++ b/examples/interceptors/README.md @@ -0,0 +1,120 @@ +# Interceptor + +Node gRPC provides simple APIs to implement and install interceptors on clients +and servers. An interceptor intercepts the execution of each incoming/outgoing +RPC call on the client or server where it is installed. Users can use +interceptors to do logging, authentication/authorization, metrics collection, +and many other functions that can be shared across RPCs. + +## Run the server + +``` +node server.js +``` + +## Run the client + +``` +node client.js +``` + +# Explanation + +In Node gRPC, clients and servers each have their own types of interceptors. + +## Client + +Node gRPC client interceptors are formally specified in [gRFC L5](https://github.com/grpc/proposal/blob/master/L5-node-client-interceptors.md). +An interceptor is a function that can wrap a call object with an +`InterceptingCall`, with intercepting functions for individual call operations. +To illustrate, the following is a trivial interceptor with all interception +methods: + +```js +const interceptor = function(options, nextCall) { + const requester = { + start: function(metadata, listener, next) { + const listener = { + onReceiveMetadata: function(metadata, next) { + next(metadata); + }, + onReceiveMessage: function(message, next) { + next(message); + }, + onReceiveStatus: function(status, next) { + next(status); + } + }; + next(metadata, listener); + }, + sendMessage: function(message, next) { + next(messasge); + }, + halfClose: function(next) { + next(); + }, + cancel: function(message, next) { + next(); + } + }; + return new InterceptingCall(nextCall(options), requester); +}; +``` + +The requester intercepts outgoing operations, and the listener intercepts +incoming operations. Each intercepting method can read or modify the data for +that operation before passing it along to the `next` callback. + +The `RequesterBuilder` and `ListenerBuilder` utility classes provide an +alternative way to construct requester and listener objects + +## Server + +Node gRPC server interceptors are formally specified in [gRFC L112](https://github.com/grpc/proposal/blob/master/L112-node-server-interceptors.md). +Similar to client interceptors, a server interceptor is a function that can +wrap a call object with a `ServerInterceptingCall`, with intercepting functions +for individual call operations. Server intercepting functions broadly mirror +the client intercepting functions, with sending and receiving switched. To +illustrate, the following is a trivial server interceptor with all interception +methods: + +```js +const interceptor = function(methodDescriptor, call) { + const responder = { + start: function(next) { + const listener = { + onReceiveMetadata: function(metadata, next) { + next(metadata); + }, + onReceiveMessage: function(message, next) { + next(message); + }, + onReceiveHalfClose: function(next) { + next(); + }, + onCancel: function() { + } + }; + next(listener); + }, + sendMetadata: function(metadata, next) { + next(metadata); + }, + sendMessage: function(message, next) { + next(message); + }, + sendStatus: function(status, next) { + next(status); + } + }; + return new ServerInterceptingCall(call, responder); +} +``` + +As with client interceptors, the responder intercepts outgoing operations and +the listener intercepts incoming operations. Each intercepting method can read +or modify the data for that operation before passing it along to the `next` +callback. + +The `ResponderBuilder` and `ServerListenerBuilder` utility classes provide an +alternative way to build responder and server listener objects. diff --git a/examples/interceptors/client.js b/examples/interceptors/client.js new file mode 100644 index 000000000..0b33b0fff --- /dev/null +++ b/examples/interceptors/client.js @@ -0,0 +1,113 @@ +/* + * + * 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. + * + */ + +const grpc = require('@grpc/grpc-js'); +const protoLoader = require('@grpc/proto-loader'); +const parseArgs = require('minimist'); + +const PROTO_PATH = __dirname + '/../protos/echo.proto'; + +const packageDefinition = protoLoader.loadSync( + PROTO_PATH, + {keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true + }); +const echoProto = grpc.loadPackageDefinition(packageDefinition).grpc.examples.echo; + +function authInterceptor(options, nextCall) { + const requester = (new grpc.RequesterBuilder()) + .withStart((metadata, listener, next) => { + metadata.set('authorization', 'some-secret-token'); + next(metadata, listener); + }).build(); + return new grpc.InterceptingCall(nextCall(options), requester); +} + +// logger is to mock a sophisticated logging system. To simplify the example, we just print out the content. +function logger(format, ...args) { + console.log(`LOG (client):\t${format}\n`, ...args); +} + +function loggingInterceptor(options, nextCall) { + const listener = (new grpc.ListenerBuilder()) + .withOnReceiveMessage((message, next) => { + logger(`Receive a message ${JSON.stringify(message)} at ${(new Date()).toISOString()}`); + next(message); + }).build(); + const requester = (new grpc.RequesterBuilder()) + .withSendMessage((message, next) => { + logger(`Send a message ${JSON.stringify(message)} at ${(new Date()).toISOString()}`); + next(message); + }).build(); + return new grpc.InterceptingCall(nextCall(options), requester); +} + +function callUnaryEcho(client, message) { + return new Promise((resolve, reject) => { + const deadline = new Date(); + deadline.setSeconds(deadline.getSeconds() + 10); + client.unaryEcho({message: message}, {deadline}, (error, value) => { + if (error) { + reject(error); + return; + } + console.log(`UnaryEcho: ${JSON.stringify(value)}`); + resolve(); + }); + }); +} + +function callBidiStreamingEcho(client) { + return new Promise((resolve, reject) => { + const deadline = new Date(); + deadline.setSeconds(deadline.getSeconds() + 10); + const call = client.bidirectionalStreamingEcho({deadline}); + call.on('data', value => { + console.log(`BidiStreamingEcho: ${JSON.stringify(value)}`); + }); + call.on('status', status => { + if (status.code === grpc.status.OK) { + resolve(); + } else { + reject(status); + } + }); + call.on('error', () => { + // Ignore error event + }); + for (let i = 0; i < 5; i++) { + call.write({message: `Request ${i + 1}`}); + } + call.end(); + }); +} + +async function main() { + let argv = parseArgs(process.argv.slice(2), { + string: 'target', + default: {target: 'localhost:50051'} + }); + const client = new echoProto.Echo(argv.target, grpc.credentials.createInsecure(), {interceptors: [authInterceptor, loggingInterceptor]}); + await callUnaryEcho(client, 'hello world'); + await callBidiStreamingEcho(client); +} + +main(); diff --git a/examples/interceptors/server.js b/examples/interceptors/server.js new file mode 100644 index 000000000..b6f5a5eaa --- /dev/null +++ b/examples/interceptors/server.js @@ -0,0 +1,120 @@ +/* + * + * 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. + * + */ + +const grpc = require('@grpc/grpc-js'); +const protoLoader = require('@grpc/proto-loader'); +const parseArgs = require('minimist'); + +const PROTO_PATH = __dirname + '/../protos/echo.proto'; + +const packageDefinition = protoLoader.loadSync( + PROTO_PATH, + {keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true + }); +const echoProto = grpc.loadPackageDefinition(packageDefinition).grpc.examples.echo; + +function unaryEcho(call, callback) { + console.log(`unary echoing message ${call.request.message}`); + callback(null, call.request); +} + +function bidirectionalStreamingEcho(call) { + call.on('data', request => { + console.log(`bidi echoing message ${request.message}`); + call.write(request); + }); + call.on('end', () => { + call.end(); + }); +} + +const serviceImplementation = { + unaryEcho, + bidirectionalStreamingEcho +} + +function validateAuthorizationMetadata(metadata) { + const authorization = metadata.get('authorization'); + if (authorization.length < 1) { + return false; + } + return authorization[0] === 'some-secret-token'; +} + +function authInterceptor(methodDescriptor, call) { +const listener = (new grpc.ServerListenerBuilder()) + .withOnReceiveMetadata((metadata, next) => { + if (validateAuthorizationMetadata(metadata)) { + next(metadata); + } else { + call.sendStatus({ + code: grpc.status.UNAUTHENTICATED, + details: 'Auth metadata not correct' + }); + } + }).build(); + const responder = (new grpc.ResponderBuilder()) + .withStart(next => { + next(listener); + }).build(); + return new grpc.ServerInterceptingCall(call, responder); +} + +// logger is to mock a sophisticated logging system. To simplify the example, we just print out the content. +function logger(format, ...args) { + console.log(`LOG (server):\t${format}\n`, ...args); +} + +function loggingInterceptor(methodDescriptor, call) { + const listener = new grpc.ServerListenerBuilder() + .withOnReceiveMessage((message, next) => { + logger(`Receive a message ${JSON.stringify(message)} at ${(new Date()).toISOString()}`); + next(message); + }).build(); + const responder = new grpc.ResponderBuilder() + .withStart(next => { + next(listener); + }) + .withSendMessage((message, next) => { + logger(`Send a message ${JSON.stringify(message)} at ${(new Date()).toISOString()}`); + next(message); + }).build(); + return new grpc.ServerInterceptingCall(call, responder); +} + +function main() { + const argv = parseArgs(process.argv.slice(2), { + string: 'port', + default: {port: '50051'} + }); + const server = new grpc.Server({interceptors: [authInterceptor, loggingInterceptor]}); + server.addService(echoProto.Echo.service, serviceImplementation); + server.bindAsync(`0.0.0.0:${argv.port}`, grpc.ServerCredentials.createInsecure(), (err, port) => { + if (err != null) { + return console.error(err); + } + console.log(`gRPC listening on ${port}`) + }); + client = new echoProto.Echo(`localhost:${argv.port}`, grpc.credentials.createInsecure()); +} + +main();