├── .github
├── FUNDING.yml
└── workflows
│ └── CI.yml
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── lib
├── compression-filter.js
├── handler.js
├── index.d.ts
├── index.js
├── logging.js
├── metadata.js
├── options.js
├── server-call.js
├── server-credentials.js
├── server-resolver.js
├── server-session.js
├── server.js
├── status.js
├── stream-decoder.js
└── utils.js
├── package.json
└── test
├── common.js
├── compression.js
├── deadline.js
├── errors.js
├── fixtures
├── README
├── ca.pem
├── server1.key
└── server1.pem
├── logging.js
├── metadata.js
├── options.js
├── proto
├── echo_service.proto
├── math.proto
├── test_messages.proto
└── test_service.proto
├── server-credentials.js
├── server-resolver.js
├── server.js
├── stream-decoder.js
└── utils.js
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [cjihrig]
4 | patreon: cjihrig
5 | custom: https://www.paypal.me/cjihrig/5
6 |
--------------------------------------------------------------------------------
/.github/workflows/CI.yml:
--------------------------------------------------------------------------------
1 | name: grpc-server-js CI
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | branches: [master]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ${{ matrix.os }}
13 |
14 | strategy:
15 | matrix:
16 | node-version: [10.x, 12.x, 13.x, 14.x, 15.x]
17 | os: [ubuntu-latest, macos-latest, windows-latest]
18 |
19 | steps:
20 | - uses: actions/checkout@v2
21 | - name: Use Node.js ${{ matrix.node-version }}
22 | uses: actions/setup-node@v1
23 | with:
24 | node-version: ${{ matrix.node-version }}
25 | - run: npm install
26 | - run: npm test
27 | env:
28 | CI: true
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | .vscode
61 |
62 | dev-client.js
63 | dev-server.js
64 | dev-service.proto
65 |
66 | .node_bash_completion
67 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *
2 | !lib/**
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Colin J. Ihrig
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # grpc-server-js
2 |
3 | [](https://www.npmjs.org/package/grpc-server-js)
4 | 
5 | 
6 | [](https://github.com/cjihrig/belly-button)
7 |
8 | Pure JavaScript gRPC Server
9 |
10 | ## Documentation
11 |
12 | The goal is to be largely compatible with the existing [`Server`](https://grpc.io/grpc/node/grpc.Server.html) implementation.
13 |
14 | ## Features
15 |
16 | - [Unary calls](https://grpc.github.io/grpc/node/grpc-ServerUnaryCall.html).
17 | - [Streaming client request calls](https://grpc.github.io/grpc/node/grpc-ServerReadableStream.html).
18 | - [Streaming server response calls](https://grpc.github.io/grpc/node/grpc-ServerWritableStream.html).
19 | - [Bidirectional streaming calls](https://grpc.github.io/grpc/node/grpc-ServerDuplexStream.html).
20 | - Deadline and cancellation support.
21 | - Support for gzip and deflate compression, as well as uncompressed messages.
22 | - [Server credentials](https://grpc.github.io/grpc/node/grpc.ServerCredentials.html) for handling both secure and insecure calls.
23 | - [gRPC Metadata](https://grpc.github.io/grpc/node/grpc.Metadata.html).
24 | - gRPC logging.
25 | - No production dependencies.
26 | - No C++ dependencies. This implementation relies on Node's [`http2`](https://nodejs.org/api/http2.html) module.
27 | - Supports the following gRPC server options:
28 | - `grpc.http2.max_frame_size`
29 | - `grpc.keepalive_time_ms`
30 | - `grpc.keepalive_timeout_ms`
31 | - `grpc.max_concurrent_streams`
32 | - `grpc.max_receive_message_length`
33 | - `grpc.max_send_message_length`
34 | - All possible options and their descriptions are available [here](https://github.com/grpc/grpc/blob/master/include/grpc/impl/codegen/grpc_types.h).
35 | - Supports the following gRPC environment variables:
36 | - `GRPC_DEFAULT_SSL_ROOTS_FILE_PATH`
37 | - `GRPC_SSL_CIPHER_SUITES`
38 | - `GRPC_VERBOSITY`
39 | - All possible environment variables and their descriptions are available [here](https://github.com/grpc/grpc/blob/master/doc/environment_variables.md).
40 |
41 | ## Public API Deviations from the Existing `grpc.Server`
42 |
43 | - `Server.prototype.bind()` is an `async` function.
44 | - The deprecated `Server.prototype.addProtoService()` is not implemented.
45 | - `Server.prototype.addHttp2Port()` is not implemented.
46 |
47 | ## Useful References
48 |
49 | - [What is gRPC?](https://grpc.io/docs/guides/index.html)
50 | - [gRPC over HTTP2](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md)
51 | - [gRPC Compression](https://github.com/grpc/grpc/blob/master/doc/compression.md)
52 | - [gRPC Environment Variables](https://github.com/grpc/grpc/blob/master/doc/environment_variables.md)
53 | - [gRPC Keepalive](https://github.com/grpc/grpc/blob/master/doc/keepalive.md)
54 | - [gRPC Name Resolution](https://github.com/grpc/grpc/blob/master/doc/naming.md)
55 | - [gRPC Status Codes](https://github.com/grpc/grpc/blob/master/doc/statuscodes.md)
56 |
57 | ## Acknowledgement
58 |
59 | This module is heavily inspired by the [`grpc`](https://www.npmjs.com/package/grpc) native module. Some of the source code is adapted from the [`@grpc/grpc-js`](https://www.npmjs.com/package/@grpc/grpc-js) module.
60 |
--------------------------------------------------------------------------------
/lib/compression-filter.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const Zlib = require('zlib');
3 | const kGrpcEncodingHeader = 'grpc-encoding';
4 | const kGrpcAcceptEncodingHeader = 'grpc-accept-encoding';
5 |
6 |
7 | class CompressionHandler {
8 | async writeMessage (message, compress) {
9 | if (compress) {
10 | message = await this.compressMessage(message);
11 | }
12 |
13 | const output = Buffer.allocUnsafe(message.byteLength + 5);
14 |
15 | output.writeUInt8(compress ? 1 : 0, 0);
16 | output.writeUInt32BE(message.byteLength, 1);
17 | message.copy(output, 5);
18 |
19 | return output;
20 | }
21 |
22 | async readMessage (data) {
23 | const compressed = data.readUInt8(0) === 1;
24 | let message = data.slice(5);
25 |
26 | if (compressed) {
27 | message = await this.decompressMessage(message);
28 | }
29 |
30 | return message;
31 | }
32 | }
33 |
34 |
35 | class IdentityHandler extends CompressionHandler {
36 | constructor () {
37 | super();
38 | this.name = 'identity';
39 | }
40 |
41 | compressMessage (message) { // eslint-disable-line class-methods-use-this
42 | throw new Error('Identity encoding does not support compression');
43 | }
44 |
45 | decompressMessage (message) { // eslint-disable-line class-methods-use-this
46 | throw new Error('Identity encoding does not support compression');
47 | }
48 |
49 | // eslint-disable-next-line class-methods-use-this
50 | writeMessage (message, compress) {
51 | const output = Buffer.allocUnsafe(message.byteLength + 5);
52 |
53 | // Identity compression messages should be marked as uncompressed.
54 | output.writeUInt8(0, 0);
55 | output.writeUInt32BE(message.length, 1);
56 | message.copy(output, 5);
57 |
58 | return output;
59 | }
60 | }
61 |
62 |
63 | class GzipHandler extends CompressionHandler {
64 | constructor () {
65 | super();
66 | this.name = 'gzip';
67 | }
68 |
69 | compressMessage (message) { // eslint-disable-line class-methods-use-this
70 | return new Promise((resolve, reject) => {
71 | Zlib.gzip(message, (err, output) => {
72 | if (err) {
73 | reject(err);
74 | } else {
75 | resolve(output);
76 | }
77 | });
78 | });
79 | }
80 |
81 | decompressMessage (message) { // eslint-disable-line class-methods-use-this
82 | return new Promise((resolve, reject) => {
83 | Zlib.unzip(message, (err, output) => {
84 | if (err) {
85 | reject(err);
86 | } else {
87 | resolve(output);
88 | }
89 | });
90 | });
91 | }
92 | }
93 |
94 |
95 | class DeflateHandler extends CompressionHandler {
96 | constructor () {
97 | super();
98 | this.name = 'deflate';
99 | }
100 |
101 | compressMessage (message) { // eslint-disable-line class-methods-use-this
102 | return new Promise((resolve, reject) => {
103 | Zlib.deflate(message, (err, output) => {
104 | if (err) {
105 | reject(err);
106 | } else {
107 | resolve(output);
108 | }
109 | });
110 | });
111 | }
112 |
113 | decompressMessage (message) { // eslint-disable-line class-methods-use-this
114 | return new Promise((resolve, reject) => {
115 | Zlib.inflate(message, (err, output) => {
116 | if (err) {
117 | reject(err);
118 | } else {
119 | resolve(output);
120 | }
121 | });
122 | });
123 | }
124 | }
125 |
126 |
127 | // This class tracks all compression methods supported by a server.
128 | // TODO: Export this class and make it configurable by the Server class.
129 | class CompressionMethodMap {
130 | constructor () {
131 | this.default = null;
132 | this.accepts = null;
133 | this.map = new Map();
134 | this.register('identity', IdentityHandler);
135 | this.register('deflate', DeflateHandler);
136 | this.register('gzip', GzipHandler);
137 | this.setDefault('identity');
138 | }
139 |
140 | register (compressionName, compressionMethodConstructor) {
141 | if (typeof compressionName !== 'string') {
142 | throw new TypeError('Compression method must be a string');
143 | }
144 |
145 | if (typeof compressionMethodConstructor !== 'function') {
146 | throw new TypeError('Compression method constructor must be a function');
147 | }
148 |
149 | this.map.set(compressionName, compressionMethodConstructor);
150 | this.accepts = Array.from(this.map.keys());
151 | }
152 |
153 | setDefault (compressionName) {
154 | if (typeof compressionName !== 'string') {
155 | throw new TypeError('Compression method must be a string');
156 | }
157 |
158 | if (!this.map.has(compressionName)) {
159 | // TODO: This error code must be UNIMPLEMENTED.
160 | throw new Error(`Compression method not supported: ${compressionName}`);
161 | }
162 |
163 | this.default = compressionName;
164 | }
165 |
166 | getDefaultInstance () {
167 | return this.getInstance(this.default);
168 | }
169 |
170 | getInstance (compressionName) {
171 | if (typeof compressionName !== 'string') {
172 | throw new TypeError('Compression method must be a string');
173 | }
174 |
175 | const Ctor = this.map.get(compressionName);
176 |
177 | if (Ctor === undefined) {
178 | // TODO: This error code must be UNIMPLEMENTED.
179 | throw new Error(`Compression method not supported: ${compressionName}`);
180 | }
181 |
182 | return new Ctor();
183 | }
184 | }
185 |
186 |
187 | const compressionMethods = new CompressionMethodMap();
188 | const defaultCompression = compressionMethods.getDefaultInstance();
189 | const defaultAcceptedEncoding = compressionMethods.accepts;
190 |
191 |
192 | class CompressionFilter {
193 | constructor () {
194 | this.supportedMethods = compressionMethods;
195 | this.send = defaultCompression;
196 | this.receive = defaultCompression;
197 | this.accepts = defaultAcceptedEncoding;
198 | }
199 |
200 | receiveMetadata (metadata) {
201 | const receiveEncoding = metadata.get(kGrpcEncodingHeader);
202 |
203 | if (receiveEncoding.length > 0) {
204 | const encoding = receiveEncoding[0];
205 |
206 | if (encoding !== this.receive.name) {
207 | this.receive = this.supportedMethods.getInstance(encoding);
208 | }
209 | }
210 |
211 | const acceptedEncoding = metadata.get(kGrpcAcceptEncodingHeader);
212 |
213 | if (acceptedEncoding.length > 0) {
214 | this.accepts = acceptedEncoding;
215 | }
216 |
217 | // Check that the client supports the incoming compression type.
218 | if (this.accepts.includes(this.receive.name)) {
219 | if (this.send.name !== this.receive.name) {
220 | this.send = this.supportedMethods.getInstance(this.receive.name);
221 | }
222 | } else {
223 | // The client does not support this compression type, so send
224 | // back uncompressed data.
225 | if (this.send.name !== 'identity') {
226 | this.send = this.supportedMethods.getInstance(this.receive.name);
227 | }
228 | }
229 |
230 | metadata.remove(kGrpcEncodingHeader);
231 | metadata.remove(kGrpcAcceptEncodingHeader);
232 |
233 | return metadata;
234 | }
235 |
236 | serializeMessage (message) {
237 | // TODO: Add support for flags (compression) later.
238 | return this.send.writeMessage(message, false);
239 | }
240 |
241 | deserializeMessage (message) {
242 | return this.receive.readMessage(message);
243 | }
244 | }
245 |
246 | module.exports = {
247 | CompressionFilter,
248 | CompressionMethodMap,
249 | DeflateHandler,
250 | GzipHandler,
251 | IdentityHandler
252 | };
253 |
--------------------------------------------------------------------------------
/lib/handler.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const EventEmitter = require('events');
3 | const { Duplex, Readable, Writable } = require('stream');
4 | const Status = require('./status');
5 | const { StreamDecoder } = require('./stream-decoder');
6 | const { hasGrpcStatusCode } = require('./utils');
7 | const kCall = Symbol('call');
8 | const kReadableState = Symbol('readableState');
9 | const kReadablePushOrBufferMessage = Symbol('readablePushOrBufferMessage');
10 | const kReadablePushMessage = Symbol('readablePushMessage');
11 |
12 |
13 | class ServerUnaryCall extends EventEmitter {
14 | constructor (call, metadata) {
15 | super();
16 | setUpHandler(this, call, metadata);
17 | this.request = undefined;
18 | }
19 | }
20 |
21 | ServerUnaryCall.prototype.sendMetadata = sendMetadata;
22 | ServerUnaryCall.prototype.getPeer = getPeer;
23 | ServerUnaryCall.prototype.getDeadline = getDeadline;
24 |
25 |
26 | class ServerReadableStream extends Readable {
27 | constructor (call, metadata) {
28 | super({ objectMode: true });
29 | setUpHandler(this, call, metadata);
30 | setUpReadable(this);
31 | }
32 |
33 | _read (size) {
34 | this[kReadableState].canPush = true;
35 | const { messagesToPush } = this[kReadableState];
36 |
37 | while (messagesToPush.length > 0) {
38 | const nextMessage = messagesToPush.shift();
39 | const canPush = this.push(nextMessage);
40 |
41 | if (nextMessage === null || canPush === false) {
42 | this[kReadableState].canPush = false;
43 | return;
44 | }
45 | }
46 |
47 | this.call.resume();
48 | }
49 |
50 | deserialize (input) {
51 | if (input === null || input === undefined) {
52 | return null;
53 | }
54 |
55 | return this[kCall].handler.deserialize(input);
56 | }
57 | }
58 |
59 | ServerReadableStream.prototype.sendMetadata = sendMetadata;
60 | ServerReadableStream.prototype.getPeer = getPeer;
61 | ServerReadableStream.prototype.getDeadline = getDeadline;
62 | ServerReadableStream.prototype[kReadablePushOrBufferMessage] =
63 | readablePushOrBufferMessage;
64 | ServerReadableStream.prototype[kReadablePushMessage] = readablePushMessage;
65 |
66 |
67 | class ServerWritableStream extends Writable {
68 | constructor (call, metadata) {
69 | super({ objectMode: true });
70 | setUpHandler(this, call, metadata);
71 | setUpWritable(this);
72 | this.request = undefined;
73 | }
74 |
75 | async _write (chunk, encoding, callback) {
76 | // This function is asynchronous in order to support async compression.
77 | // The following code does not work with `write()` being asynchronous, but
78 | // such code should be utilizing the `write()` callback.
79 | // stream.write(data);
80 | // stream.write(data);
81 | // stream.emit('error', err);
82 | try {
83 | const response = await this[kCall].serializeMessage(chunk);
84 |
85 | if (this[kCall].write(response) === false) {
86 | this[kCall].once('drain', callback);
87 | return;
88 | }
89 |
90 | callback(null);
91 | } catch (err) {
92 | err.code = Status.INTERNAL;
93 | callback(err);
94 | }
95 | }
96 |
97 | _final (callback) {
98 | this[kCall].end();
99 | callback(null);
100 | }
101 |
102 | end (metadata) {
103 | if (metadata) {
104 | this[kCall].status.metadata = metadata;
105 | }
106 |
107 | Writable.prototype.end.call(this);
108 | }
109 |
110 | serialize (input) {
111 | if (input === null || input === undefined) {
112 | return null;
113 | }
114 |
115 | return this[kCall].handler.serialize(input);
116 | }
117 | }
118 |
119 | ServerWritableStream.prototype.sendMetadata = sendMetadata;
120 | ServerWritableStream.prototype.getPeer = getPeer;
121 | ServerWritableStream.prototype.getDeadline = getDeadline;
122 |
123 |
124 | class ServerDuplexStream extends Duplex {
125 | constructor (call, metadata) {
126 | super({ objectMode: true });
127 | setUpHandler(this, call, metadata);
128 | setUpReadable(this);
129 | setUpWritable(this);
130 | }
131 | }
132 |
133 | ServerDuplexStream.prototype.sendMetadata = sendMetadata;
134 | ServerDuplexStream.prototype.getPeer = getPeer;
135 | ServerDuplexStream.prototype.getDeadline = getDeadline;
136 | ServerDuplexStream.prototype._read = ServerReadableStream.prototype._read;
137 | ServerDuplexStream.prototype._write = ServerWritableStream.prototype._write;
138 | ServerDuplexStream.prototype._final = ServerWritableStream.prototype._final;
139 | ServerDuplexStream.prototype.end = ServerWritableStream.prototype.end;
140 | ServerDuplexStream.prototype.serialize =
141 | ServerWritableStream.prototype.serialize;
142 | ServerDuplexStream.prototype.deserialize =
143 | ServerReadableStream.prototype.deserialize;
144 | ServerDuplexStream.prototype[kReadablePushOrBufferMessage] =
145 | ServerReadableStream.prototype[kReadablePushOrBufferMessage];
146 | ServerDuplexStream.prototype[kReadablePushMessage] =
147 | ServerReadableStream.prototype[kReadablePushMessage];
148 |
149 |
150 | function sendMetadata (responseMetadata) {
151 | return this[kCall].sendMetadata(responseMetadata);
152 | }
153 |
154 |
155 | function getPeer () {
156 | const { socket } = this.call.session;
157 |
158 | if (!(socket && socket.remoteAddress)) {
159 | return 'unknown';
160 | }
161 |
162 | if (socket.remotePort) {
163 | return `${socket.remoteAddress}:${socket.remotePort}`;
164 | }
165 |
166 | return socket.remoteAddress;
167 | }
168 |
169 |
170 | function getDeadline () {
171 | return this[kCall].deadline;
172 | }
173 |
174 |
175 | function setUpHandler (handler, call, metadata) {
176 | handler[kCall] = call;
177 | handler.call = call.stream;
178 | handler.metadata = metadata;
179 | handler.cancelled = false;
180 | handler.cancelledReason = null;
181 |
182 | call.once('cancelled', (reason) => {
183 | handler.cancelled = true;
184 | handler.cancelledReason = reason;
185 | handler.emit('cancelled', reason);
186 | });
187 | }
188 |
189 |
190 | function setUpReadable (stream) {
191 | const decoder = new StreamDecoder();
192 | const { maxReceiveMessageLength } = stream[kCall];
193 |
194 | stream[kReadableState] = {
195 | canPush: false, // Can data be pushed to the readable stream.
196 | isPushPending: false, // Is an asynchronous push operation in progress.
197 | bufferedMessages: [], // Messages that have not been deserialized yet.
198 | messagesToPush: [] // Deserialized messages not yet pushed to the stream.
199 | };
200 |
201 | stream.once('cancelled', () => {
202 | stream.destroy();
203 | });
204 |
205 | stream.call.on('data', (data) => {
206 | // It's possible that more than one message arrives in a single 'data'
207 | // event. pushOrBufferMessage() ensures that only a single message is
208 | // actually processed at a time, because the deserialization process is
209 | // asynchronous, and can lead to out of order messages.
210 | const messages = decoder.write(data);
211 |
212 | for (let i = 0; i < messages.length; i++) {
213 | if (messages[i].length > maxReceiveMessageLength) {
214 | const err = new Error('Received message larger than max ' +
215 | `(${messages[i].length} vs. ${maxReceiveMessageLength})`);
216 | stream[kCall].sendError(err, Status.RESOURCE_EXHAUSTED);
217 | return;
218 | }
219 |
220 | stream[kReadablePushOrBufferMessage](messages[i]);
221 | }
222 | });
223 |
224 | stream.call.once('end', () => {
225 | // If the HTTP2 stream was destroyed, an error happened so this stream
226 | // should not emit an 'end' event.
227 | if (stream.call.destroyed) {
228 | stream.destroy();
229 | return;
230 | }
231 |
232 | stream[kReadablePushOrBufferMessage](null);
233 | });
234 | }
235 |
236 |
237 | function readablePushOrBufferMessage (messageBytes) {
238 | const { bufferedMessages, isPushPending } = this[kReadableState];
239 |
240 | if (isPushPending === true) {
241 | bufferedMessages.push(messageBytes);
242 | } else {
243 | this[kReadablePushMessage](messageBytes);
244 | }
245 | }
246 |
247 |
248 | async function readablePushMessage (messageBytes) {
249 | const { bufferedMessages, messagesToPush } = this[kReadableState];
250 |
251 | if (messageBytes === null) {
252 | if (this[kReadableState].canPush === true) {
253 | this.push(null);
254 | } else {
255 | messagesToPush.push(null);
256 | }
257 |
258 | return;
259 | }
260 |
261 | this[kReadableState].isPushPending = true;
262 |
263 | try {
264 | const deserialized = await this[kCall].deserializeMessage(messageBytes);
265 |
266 | if (this[kReadableState].canPush === true) {
267 | if (!this.push(deserialized)) {
268 | this[kReadableState].canPush = false;
269 | this.call.pause();
270 | }
271 | } else {
272 | messagesToPush.push(deserialized);
273 | }
274 | } catch (err) {
275 | // Ignore any remaining messages when errors occur.
276 | bufferedMessages.length = 0;
277 |
278 | if (!hasGrpcStatusCode(err)) {
279 | err.code = Status.INTERNAL;
280 | }
281 |
282 | this.emit('error', err);
283 | }
284 |
285 | this[kReadableState].isPushPending = false;
286 |
287 | if (bufferedMessages.length > 0) {
288 | this[kReadablePushMessage](bufferedMessages.shift());
289 | }
290 | }
291 |
292 |
293 | function setUpWritable (stream) {
294 | stream.on('error', (err) => {
295 | stream[kCall].sendError(err);
296 | stream.destroy();
297 | });
298 | }
299 |
300 |
301 | module.exports = {
302 | ServerDuplexStream,
303 | ServerReadableStream,
304 | ServerUnaryCall,
305 | ServerWritableStream
306 | };
307 |
--------------------------------------------------------------------------------
/lib/index.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import * as http2 from 'http2';
3 | import { Duplex, Readable, Writable } from 'stream';
4 |
5 |
6 | export type Deadline = Date | number;
7 |
8 |
9 | export interface Serialize {
10 | (value: T): Buffer;
11 | }
12 |
13 | export interface Deserialize {
14 | (bytes: Buffer): T;
15 | }
16 |
17 | export interface MethodDefinition {
18 | path: string;
19 | requestStream: boolean;
20 | responseStream: boolean;
21 | requestSerialize: Serialize;
22 | responseSerialize: Serialize;
23 | requestDeserialize: Deserialize;
24 | responseDeserialize: Deserialize;
25 | originalName?: string;
26 | }
27 |
28 |
29 | export declare type KeyCertPair = {
30 | private_key: Buffer;
31 | cert_chain: Buffer;
32 | };
33 |
34 | export declare abstract class ServerCredentials {
35 | abstract _isSecure(): boolean;
36 | abstract _getSettings(): http2.SecureServerOptions | null;
37 | static createInsecure(): ServerCredentials;
38 | static createSsl(rootCerts: Buffer | null,
39 | keyCertPairs: KeyCertPair[],
40 | checkClientCertificate?: boolean): ServerCredentials;
41 | }
42 |
43 |
44 | export interface MetadataOptions {
45 | // Signal that the request is idempotent. Defaults to false.
46 | idempotentRequest?: boolean;
47 | // Signal that the call should not return UNAVAILABLE before it has started.
48 | // Defaults to true.
49 | waitForReady?: boolean;
50 | // Signal that the call is cacheable. gRPC is free to use the GET verb.
51 | // Defaults to false.
52 | cacheableRequest?: boolean;
53 | // Signal that the initial metadata should be corked. Defaults to false.
54 | corked?: boolean;
55 | }
56 |
57 | export declare type MetadataValue = string | Buffer;
58 | export declare type MetadataObject = Map;
59 | export declare class Metadata {
60 | protected internalRepr: MetadataObject;
61 | private options: MetadataOptions;
62 | constructor(options?: MetadataOptions);
63 | set(key: string, value: MetadataValue): void;
64 | add(key: string, value: MetadataValue): void;
65 | remove(key: string): void;
66 | get(key: string): MetadataValue[];
67 | getMap(): { [key: string]: MetadataValue; };
68 | clone(): Metadata;
69 | merge(other: Metadata): void;
70 | toHttp2Headers(): http2.OutgoingHttpHeaders;
71 | setOptions(options: MetadataOptions): void;
72 | getOptions(): MetadataOptions;
73 | static fromHttp2Headers(headers: http2.IncomingHttpHeaders): Metadata;
74 | }
75 |
76 |
77 | export declare enum LogVerbosity {
78 | DEBUG = 0,
79 | INFO = 1,
80 | ERROR = 2
81 | }
82 |
83 | export declare const setLogger: (logger: Partial) => void;
84 | export declare const setLogVerbosity: (verbosity: LogVerbosity) => void;
85 |
86 |
87 | export declare enum Status {
88 | OK = 0,
89 | CANCELLED = 1,
90 | UNKNOWN = 2,
91 | INVALID_ARGUMENT = 3,
92 | DEADLINE_EXCEEDED = 4,
93 | NOT_FOUND = 5,
94 | ALREADY_EXISTS = 6,
95 | PERMISSION_DENIED = 7,
96 | RESOURCE_EXHAUSTED = 8,
97 | FAILED_PRECONDITION = 9,
98 | ABORTED = 10,
99 | OUT_OF_RANGE = 11,
100 | UNIMPLEMENTED = 12,
101 | INTERNAL = 13,
102 | UNAVAILABLE = 14,
103 | DATA_LOSS = 15,
104 | UNAUTHENTICATED = 16
105 | }
106 |
107 | export interface StatusObject {
108 | code: Status;
109 | details: string;
110 | metadata: Metadata;
111 | }
112 |
113 | export declare type ServiceError = StatusObject & Error;
114 | export declare type ServerStatusResponse = Partial;
115 | export declare type ServerErrorResponse = ServerStatusResponse & Error;
116 |
117 |
118 | declare type ServerSurfaceCall = {
119 | cancelled: boolean;
120 | readonly metadata: Metadata;
121 | getPeer(): string;
122 | sendMetadata(responseMetadata: Metadata): void;
123 | getDeadline(): Deadline;
124 | };
125 | export declare type ServerUnaryCall =
126 | ServerSurfaceCall & { request: RequestType | null; };
127 | export declare type ServerReadableStream =
128 | ServerSurfaceCall & Readable;
129 | export declare type ServerWritableStream =
130 | ServerSurfaceCall & Writable & {
131 | request: RequestType | null;
132 | end: (metadata?: Metadata) => void;
133 | };
134 | export declare type ServerDuplexStream =
135 | ServerSurfaceCall & Duplex & { end: (metadata?: Metadata) => void; };
136 |
137 |
138 | export declare type sendUnaryData =
139 | (error: ServerErrorResponse | ServerStatusResponse | null,
140 | value: ResponseType | null,
141 | trailer?: Metadata,
142 | flags?: number) => void;
143 | export declare type handleUnaryCall =
144 | (call: ServerUnaryCall,
145 | callback: sendUnaryData) => void;
146 | export declare type handleClientStreamingCall =
147 | (call: ServerReadableStream,
148 | callback: sendUnaryData) => void;
149 | export declare type handleServerStreamingCall =
150 | (call: ServerWritableStream) => void;
151 | export declare type handleBidiStreamingCall =
152 | (call: ServerDuplexStream) => void;
153 |
154 |
155 | export declare type HandleCall =
156 | handleUnaryCall |
157 | handleClientStreamingCall |
158 | handleServerStreamingCall |
159 | handleBidiStreamingCall;
160 |
161 |
162 | export declare type UntypedHandleCall = HandleCall;
163 | export interface UntypedServiceImplementation {
164 | [name: string]: UntypedHandleCall;
165 | }
166 |
167 | export declare type ServiceDefinition = {
168 | readonly [index in keyof ImplementationType]: MethodDefinition;
169 | }
170 |
171 |
172 | export interface ChannelOptions {
173 | 'grpc.http2.max_frame_size'?: string;
174 | 'grpc.ssl_target_name_override'?: string;
175 | 'grpc.primary_user_agent'?: string;
176 | 'grpc.secondary_user_agent'?: string;
177 | 'grpc.default_authority'?: string;
178 | 'grpc.keepalive_time_ms'?: number;
179 | 'grpc.keepalive_timeout_ms'?: number;
180 | 'grpc.service_config'?: string;
181 | 'grpc.max_concurrent_streams'?: number;
182 | 'grpc.initial_reconnect_backoff_ms'?: number;
183 | 'grpc.max_reconnect_backoff_ms'?: number;
184 | 'grpc.use_local_subchannel_pool'?: number;
185 | 'grpc.max_send_message_length'?: number;
186 | 'grpc.max_receive_message_length'?: number;
187 | [key: string]: string | number | undefined;
188 | }
189 |
190 |
191 | export declare class Server {
192 | constructor(options?: ChannelOptions);
193 | addProtoService(): void;
194 | addService(service: ServiceDefinition,
195 | implementation: UntypedServiceImplementation): void;
196 | removeService(service: ServiceDefinition): void;
197 | bind(port: string, creds: ServerCredentials): Promise;
198 | bindAsync(port: string,
199 | creds: ServerCredentials,
200 | callback: (error: Error | null, port: number) => void): void;
201 | forceShutdown(): void;
202 | register(
203 | name: string,
204 | handler: HandleCall,
205 | serialize: Serialize,
206 | deserialize: Deserialize,
207 | type: string
208 | ): boolean;
209 | unregister(name: string): boolean;
210 | start(): void;
211 | tryShutdown(callback: (error?: Error) => void): void;
212 | addHttp2Port(): void;
213 | }
214 |
215 | export {
216 | LogVerbosity as logVerbosity,
217 | Status as status
218 | };
219 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const { LogVerbosity, setLogger, setLogVerbosity } = require('./logging');
3 | const { Metadata } = require('./metadata');
4 | const { Server } = require('./server');
5 | const { ServerCredentials } = require('./server-credentials');
6 | const Status = require('./status');
7 |
8 |
9 | module.exports = {
10 | logVerbosity: { ...LogVerbosity },
11 | Metadata,
12 | Server,
13 | ServerCredentials,
14 | setLogger,
15 | setLogVerbosity,
16 | status: { ...Status }
17 | };
18 |
--------------------------------------------------------------------------------
/lib/logging.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const LogVerbosity = {
3 | DEBUG: 0,
4 | INFO: 1,
5 | ERROR: 2
6 | };
7 | const envVerbosity = LogVerbosity[process.env.GRPC_VERBOSITY];
8 | let _logger = console;
9 | let _logVerbosity = envVerbosity !== undefined ? envVerbosity :
10 | LogVerbosity.ERROR;
11 |
12 |
13 | function getLogger () {
14 | return _logger;
15 | }
16 |
17 |
18 | function setLogger (logger) {
19 | _logger = logger;
20 | }
21 |
22 |
23 | function setLogVerbosity (verbosity) {
24 | _logVerbosity = verbosity;
25 | }
26 |
27 |
28 | function log (severity, ...args) {
29 | if (severity >= _logVerbosity && typeof _logger.error === 'function') {
30 | _logger.error(...args);
31 | }
32 | }
33 |
34 |
35 | module.exports = {
36 | getLogger,
37 | log,
38 | LogVerbosity,
39 | setLogger,
40 | setLogVerbosity
41 | };
42 |
--------------------------------------------------------------------------------
/lib/metadata.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const { log, LogVerbosity } = require('./logging');
3 | const kLegalKeyRegex = /^[0-9a-z_.-]+$/;
4 | const kLegalNonBinaryValueRegex = /^[ -~]*$/;
5 |
6 |
7 | function isBinaryKey (key) {
8 | return key.endsWith('-bin');
9 | }
10 |
11 |
12 | function isCustomMetadata (key) {
13 | return !key.startsWith('grpc-');
14 | }
15 |
16 |
17 | function normalizeKey (key) {
18 | return key.toLowerCase();
19 | }
20 |
21 |
22 | function validate (key, value = null) {
23 | if (!kLegalKeyRegex.test(key)) {
24 | throw new Error(`Metadata key "${key}" contains illegal characters`);
25 | }
26 |
27 | if (value === null) {
28 | return;
29 | }
30 |
31 | if (isBinaryKey(key)) {
32 | if (!(value instanceof Buffer)) {
33 | throw new Error('keys that end with \'-bin\' must have Buffer values');
34 | }
35 | } else {
36 | if (value instanceof Buffer) {
37 | throw new Error(
38 | 'keys that don\'t end with \'-bin\' must have String values'
39 | );
40 | }
41 |
42 | if (!kLegalNonBinaryValueRegex.test(value)) {
43 | throw new Error(
44 | `Metadata string value "${value}" contains illegal characters`
45 | );
46 | }
47 | }
48 | }
49 |
50 |
51 | class Metadata {
52 | constructor (options = {}) {
53 | this.options = options;
54 | this.internalRepr = new Map();
55 | }
56 |
57 | set (key, value) {
58 | key = normalizeKey(key);
59 | validate(key, value);
60 | this.internalRepr.set(key, [value]);
61 | }
62 |
63 | add (key, value) {
64 | key = normalizeKey(key);
65 | validate(key, value);
66 |
67 | const existingValue = this.internalRepr.get(key);
68 |
69 | if (existingValue === undefined) {
70 | this.internalRepr.set(key, [value]);
71 | } else {
72 | existingValue.push(value);
73 | }
74 | }
75 |
76 | remove (key) {
77 | key = normalizeKey(key);
78 | validate(key);
79 | this.internalRepr.delete(key);
80 | }
81 |
82 | get (key) {
83 | key = normalizeKey(key);
84 | validate(key);
85 | return this.internalRepr.get(key) || [];
86 | }
87 |
88 | getMap () {
89 | const result = {};
90 |
91 | this.internalRepr.forEach((values, key) => {
92 | if (values.length > 0) {
93 | const v = values[0];
94 |
95 | result[key] = v instanceof Buffer ? v.slice() : v;
96 | }
97 | });
98 |
99 | return result;
100 | }
101 |
102 | clone () {
103 | const newMetadata = new Metadata(this.options);
104 | const newInternalRepr = newMetadata.internalRepr;
105 |
106 | this.internalRepr.forEach((value, key) => {
107 | const clonedValue = value.map((v) => {
108 | return v instanceof Buffer ? Buffer.from(v) : v;
109 | });
110 |
111 | newInternalRepr.set(key, clonedValue);
112 | });
113 |
114 | return newMetadata;
115 | }
116 |
117 | merge (other) {
118 | other.internalRepr.forEach((values, key) => {
119 | const mergedValue = (this.internalRepr.get(key) || []).concat(values);
120 |
121 | this.internalRepr.set(key, mergedValue);
122 | });
123 | }
124 |
125 | setOptions (options) {
126 | this.options = options;
127 | }
128 |
129 | getOptions () {
130 | return this.options;
131 | }
132 |
133 | toHttp2Headers () {
134 | const result = {};
135 |
136 | this.internalRepr.forEach((values, key) => {
137 | result[key] = values.map((value) => {
138 | return value instanceof Buffer ? value.toString('base64') : value;
139 | });
140 | });
141 |
142 | return result;
143 | }
144 |
145 | static fromHttp2Headers (headers) {
146 | const result = new Metadata();
147 |
148 | Object.keys(headers).forEach((key) => {
149 | // Reserved headers (beginning with `:`) are not valid keys.
150 | if (key.charAt(0) === ':') {
151 | return;
152 | }
153 |
154 | const values = headers[key];
155 |
156 | try {
157 | if (isBinaryKey(key)) {
158 | if (Array.isArray(values)) {
159 | values.forEach((value) => {
160 | result.add(key, Buffer.from(value, 'base64'));
161 | });
162 | } else if (values !== undefined) {
163 | if (isCustomMetadata(key)) {
164 | values.split(',').forEach((v) => {
165 | result.add(key, Buffer.from(v.trim(), 'base64'));
166 | });
167 | } else {
168 | result.add(key, Buffer.from(values, 'base64'));
169 | }
170 | }
171 | } else {
172 | if (Array.isArray(values)) {
173 | values.forEach((value) => {
174 | result.add(key, value);
175 | });
176 | } else if (values !== undefined) {
177 | result.add(key, values);
178 | }
179 | }
180 | } catch (err) {
181 | log(
182 | LogVerbosity.ERROR,
183 | `Failed to add metadata entry ${key}: ${values}. ${err.message}.`
184 | );
185 | }
186 | });
187 |
188 | return result;
189 | }
190 | }
191 |
192 | module.exports = { Metadata };
193 |
--------------------------------------------------------------------------------
/lib/options.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const Http2 = require('http2');
3 | const defaultHttp2Settings = Http2.getDefaultSettings();
4 | const defaultServerOptions = {
5 | 'grpc.max_concurrent_streams': undefined,
6 | 'grpc.http2.max_frame_size': defaultHttp2Settings.maxFrameSize,
7 | 'grpc.keepalive_time_ms': 7200000, // 2 hours in ms (spec default).
8 | 'grpc.keepalive_timeout_ms': 20000, // 20 seconds in ms (spec default).
9 | 'grpc.max_send_message_length': Infinity,
10 | 'grpc.max_receive_message_length': 4 * 1024 * 1024 // 4 MB
11 | };
12 |
13 |
14 | function parseOptions (inputOptions) {
15 | const mergedOptions = { ...defaultServerOptions, ...inputOptions };
16 |
17 | // Check for unsupported options.
18 | for (const prop in mergedOptions) {
19 | if (!(prop in defaultServerOptions)) {
20 | throw new Error(`unknown option: ${prop}`);
21 | }
22 | }
23 |
24 | // Map the gRPC option names to normal camelCase property names.
25 | const options = {
26 | maxConcurrentStreams: mergedOptions['grpc.max_concurrent_streams'],
27 | maxFrameSize: mergedOptions['grpc.http2.max_frame_size'],
28 | keepaliveTimeMs: mergedOptions['grpc.keepalive_time_ms'],
29 | keepaliveTimeoutMs: mergedOptions['grpc.keepalive_timeout_ms'],
30 | maxSendMessageLength: mergedOptions['grpc.max_send_message_length'],
31 | maxReceiveMessageLength: mergedOptions['grpc.max_receive_message_length']
32 | };
33 |
34 | // grpc.max_send_message_length uses -1 to represent no max size.
35 | if (options.maxSendMessageLength === -1) {
36 | options.maxSendMessageLength = Infinity;
37 | }
38 |
39 | // grpc.max_receive_message_length uses -1 to represent no max size.
40 | if (options.maxReceiveMessageLength === -1) {
41 | options.maxReceiveMessageLength = Infinity;
42 | }
43 |
44 | return options;
45 | }
46 |
47 | module.exports = { parseOptions };
48 |
--------------------------------------------------------------------------------
/lib/server-call.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const EventEmitter = require('events');
3 | const Http2 = require('http2');
4 | const { CompressionFilter } = require('./compression-filter');
5 | const { Metadata } = require('./metadata');
6 | const Status = require('./status');
7 | const kGrpcMessageHeader = 'grpc-message';
8 | const kGrpcStatusHeader = 'grpc-status';
9 | const kGrpcTimeoutHeader = 'grpc-timeout';
10 | const kGrpcEncodingHeader = 'grpc-encoding';
11 | const kGrpcAcceptEncodingHeader = 'grpc-accept-encoding';
12 | const kDeadlineRegex = /(\d{1,8})\s*([HMSmun])/;
13 | const deadlineUnitsToMs = {
14 | H: 3600000,
15 | M: 60000,
16 | S: 1000,
17 | m: 1,
18 | u: 0.001,
19 | n: 0.000001
20 | };
21 | const defaultResponseOptions = { waitForTrailers: true };
22 | const {
23 | HTTP2_HEADER_CONTENT_TYPE,
24 | HTTP2_HEADER_STATUS,
25 | HTTP_STATUS_OK,
26 | NGHTTP2_CANCEL
27 | } = Http2.constants;
28 |
29 |
30 | class ServerCall extends EventEmitter {
31 | constructor (stream, options) {
32 | super();
33 | this.handler = null;
34 | this.stream = stream;
35 | this.cancelled = false;
36 | this.deadline = Infinity;
37 | this.deadlineTimer = null;
38 | this.compression = new CompressionFilter();
39 | this.metadataSent = false;
40 | this.status = { code: Status.OK, details: 'OK', metadata: null };
41 | this.maxSendMessageLength = options.maxSendMessageLength;
42 | this.maxReceiveMessageLength = options.maxReceiveMessageLength;
43 | this.stream.on('drain', onStreamDrain.bind(this));
44 | this.stream.once('error', onStreamError.bind(this));
45 | this.stream.once('close', onStreamClose.bind(this));
46 | }
47 |
48 | sendMetadata (customMetadata) {
49 | if (this.metadataSent === true || this.cancelled === true ||
50 | this.stream.destroyed === true) {
51 | return;
52 | }
53 |
54 | this.metadataSent = true;
55 |
56 | const headers = {
57 | [kGrpcEncodingHeader]: this.compression.send.name,
58 | [kGrpcAcceptEncodingHeader]: this.compression.accepts.join(','),
59 | [HTTP2_HEADER_STATUS]: HTTP_STATUS_OK,
60 | [HTTP2_HEADER_CONTENT_TYPE]: 'application/grpc+proto'
61 | };
62 |
63 | this.stream.once('wantTrailers', onWantTrailers.bind(this));
64 |
65 | if (customMetadata === undefined || customMetadata === null) {
66 | this.stream.respond(headers, defaultResponseOptions);
67 | } else {
68 | this.stream.respond({
69 | ...headers,
70 | ...customMetadata.toHttp2Headers()
71 | }, defaultResponseOptions);
72 | }
73 | }
74 |
75 | receiveMetadata (headers) {
76 | let metadata = Metadata.fromHttp2Headers(headers);
77 |
78 | metadata = this.compression.receiveMetadata(metadata);
79 |
80 | const timeoutHeader = metadata.get(kGrpcTimeoutHeader);
81 |
82 | if (timeoutHeader.length > 0) {
83 | const match = timeoutHeader[0].match(kDeadlineRegex);
84 |
85 | if (match === null) {
86 | this.sendError(new Error('Invalid deadline'), Status.OUT_OF_RANGE);
87 | return;
88 | }
89 |
90 | const timeout = (+match[1] * deadlineUnitsToMs[match[2]]) | 0;
91 |
92 | this.deadline = Date.now() + timeout;
93 | this.deadlineTimer = setTimeout(handleExpiredDeadline, timeout, this);
94 | metadata.remove(kGrpcTimeoutHeader);
95 | }
96 |
97 | return metadata;
98 | }
99 |
100 | receiveUnaryMessage (callback) {
101 | const stream = this.stream;
102 | const chunks = [];
103 | let totalLength = 0;
104 |
105 | stream.on('data', (data) => {
106 | chunks.push(data);
107 | totalLength += data.byteLength;
108 | });
109 |
110 | stream.once('end', async () => {
111 | if (totalLength > this.maxReceiveMessageLength) {
112 | const err = new Error('Received message larger than max ' +
113 | `(${totalLength} vs. ${this.maxReceiveMessageLength})`);
114 | this.sendError(err, Status.RESOURCE_EXHAUSTED);
115 | return;
116 | }
117 |
118 | try {
119 | const requestBytes = Buffer.concat(chunks, totalLength);
120 |
121 | callback(null, await this.deserializeMessage(requestBytes));
122 | } catch (err) {
123 | this.sendError(err, Status.INTERNAL);
124 | callback(err, null);
125 | }
126 | });
127 | }
128 |
129 | serializeMessage (value) {
130 | const messageBuffer = this.handler.serialize(value);
131 |
132 | return this.compression.serializeMessage(messageBuffer);
133 | }
134 |
135 | async deserializeMessage (bytes) {
136 | const receivedMessage = await this.compression.deserializeMessage(bytes);
137 |
138 | return this.handler.deserialize(receivedMessage);
139 | }
140 |
141 | async sendUnaryMessage (err, value, metadata, flags) {
142 | if (err) {
143 | if (metadata && !err.hasOwnProperty('metadata')) {
144 | err.metadata = metadata;
145 | }
146 |
147 | this.sendError(err);
148 | return;
149 | }
150 |
151 | try {
152 | const response = await this.serializeMessage(value);
153 |
154 | if (metadata) {
155 | this.status.metadata = metadata;
156 | }
157 |
158 | this.write(response);
159 | this.stream.end();
160 | } catch (err) {
161 | this.sendError(err, Status.INTERNAL);
162 | }
163 | }
164 |
165 | sendError (error, code = Status.UNKNOWN) {
166 | const { status } = this;
167 |
168 | if ('message' in error) {
169 | status.details = error.message;
170 | } else {
171 | status.details = 'Unknown Error';
172 | }
173 |
174 | if ('code' in error && Number.isInteger(error.code)) {
175 | status.code = error.code;
176 |
177 | if ('details' in error && typeof error.details === 'string') {
178 | status.details = error.details;
179 | }
180 | } else {
181 | status.code = code;
182 | }
183 |
184 | if ('metadata' in error && error.metadata !== undefined) {
185 | status.metadata = error.metadata;
186 | }
187 |
188 | this.end();
189 | }
190 |
191 | write (chunk) {
192 | if (this.cancelled === true || this.stream.destroyed === true) {
193 | return;
194 | }
195 |
196 | if (chunk.length > this.maxSendMessageLength) {
197 | const err = new Error('Sent message larger than max ' +
198 | `(${chunk.length} vs. ${this.maxSendMessageLength})`);
199 | this.sendError(err, Status.RESOURCE_EXHAUSTED);
200 | return;
201 | }
202 |
203 | this.sendMetadata();
204 | return this.stream.write(chunk);
205 | }
206 |
207 | end () {
208 | if (this.cancelled === true || this.stream.destroyed === true) {
209 | return;
210 | }
211 |
212 | this.sendMetadata();
213 | return this.stream.end();
214 | }
215 | }
216 |
217 | module.exports = { ServerCall };
218 |
219 |
220 | function onStreamDrain () {
221 | // `this` is bound to the Call instance, not the stream itself.
222 | this.emit('drain');
223 | }
224 |
225 | function onStreamError (err) {
226 | // `this` is bound to the Call instance, not the stream itself.
227 | this.sendError(err, Status.INTERNAL);
228 | }
229 |
230 |
231 | function onStreamClose () {
232 | // `this` is bound to the Call instance, not the stream itself.
233 | if (this.stream.rstCode === NGHTTP2_CANCEL) {
234 | this.cancelled = true;
235 | this.emit('cancelled', 'cancelled');
236 | }
237 | }
238 |
239 |
240 | function onWantTrailers () {
241 | // `this` is bound to the Call instance, not the stream itself.
242 | let trailersToSend = {
243 | [kGrpcStatusHeader]: this.status.code,
244 | [kGrpcMessageHeader]: encodeURI(this.status.details)
245 | };
246 | const metadata = this.status.metadata;
247 |
248 | if (this.status.metadata !== null) {
249 | trailersToSend = { ...trailersToSend, ...metadata.toHttp2Headers() };
250 | }
251 |
252 | clearTimeout(this.deadlineTimer);
253 | this.stream.sendTrailers(trailersToSend);
254 | }
255 |
256 |
257 | function handleExpiredDeadline (call) {
258 | call.sendError(new Error('Deadline exceeded'), Status.DEADLINE_EXCEEDED);
259 | call.cancelled = true;
260 | call.emit('cancelled', 'deadline');
261 | }
262 |
--------------------------------------------------------------------------------
/lib/server-credentials.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const { readFileSync } = require('fs');
3 | const cipherSuites = process.env.GRPC_SSL_CIPHER_SUITES;
4 | const defaultRootsFilePath = process.env.GRPC_DEFAULT_SSL_ROOTS_FILE_PATH;
5 | let defaultRootsData = null;
6 |
7 |
8 | class InsecureServerCredentials {
9 | _isSecure () { // eslint-disable-line class-methods-use-this
10 | return false;
11 | }
12 |
13 | _getSettings () { // eslint-disable-line class-methods-use-this
14 | return null;
15 | }
16 | }
17 |
18 |
19 | class SecureServerCredentials {
20 | constructor (options = {}) {
21 | this.options = options;
22 | }
23 |
24 | _isSecure () { // eslint-disable-line class-methods-use-this
25 | return true;
26 | }
27 |
28 | _getSettings () {
29 | return this.options;
30 | }
31 | }
32 |
33 |
34 | class ServerCredentials {
35 | static createInsecure () {
36 | return new InsecureServerCredentials();
37 | }
38 |
39 | static createSsl (rootCerts, keyCertPairs, checkClientCertificate = false) {
40 | if (rootCerts !== null && !Buffer.isBuffer(rootCerts)) {
41 | throw new TypeError('rootCerts must be null or a Buffer');
42 | }
43 |
44 | if (!Array.isArray(keyCertPairs)) {
45 | throw new TypeError('keyCertPairs must be an array');
46 | }
47 |
48 | if (typeof checkClientCertificate !== 'boolean') {
49 | throw new TypeError('checkClientCertificate must be a boolean');
50 | }
51 |
52 | const cert = [];
53 | const key = [];
54 |
55 | for (let i = 0; i < keyCertPairs.length; i++) {
56 | const pair = keyCertPairs[i];
57 |
58 | if (pair === null || typeof pair !== 'object') {
59 | throw new TypeError(`keyCertPair[${i}] must be an object`);
60 | }
61 |
62 | if (!Buffer.isBuffer(pair.private_key)) {
63 | throw new TypeError(`keyCertPair[${i}].private_key must be a Buffer`);
64 | }
65 |
66 | if (!Buffer.isBuffer(pair.cert_chain)) {
67 | throw new TypeError(`keyCertPair[${i}].cert_chain must be a Buffer`);
68 | }
69 |
70 | cert.push(pair.cert_chain);
71 | key.push(pair.private_key);
72 | }
73 |
74 | return new SecureServerCredentials({
75 | ca: rootCerts || getDefaultRootsData() || undefined,
76 | cert,
77 | key,
78 | requestCert: checkClientCertificate,
79 | ciphers: cipherSuites
80 | });
81 | }
82 | }
83 |
84 | module.exports = { ServerCredentials };
85 |
86 |
87 | function getDefaultRootsData () {
88 | if (!defaultRootsFilePath) {
89 | return null;
90 | }
91 |
92 | if (defaultRootsData === null) {
93 | defaultRootsData = readFileSync(defaultRootsFilePath);
94 | }
95 |
96 | return defaultRootsData;
97 | }
98 |
--------------------------------------------------------------------------------
/lib/server-resolver.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const { isAbsolute, resolve } = require('path');
3 | const { URL } = require('url');
4 |
5 |
6 | function resolveToListenOptions (target, secure) {
7 | if (target.startsWith('unix:')) {
8 | if (target.startsWith('unix://')) {
9 | const path = target.substring(7);
10 |
11 | // The path following 'unix://' must be absolute.
12 | if (!isAbsolute(path)) {
13 | throw new Error(`'${target}' must specify an absolute path`);
14 | }
15 |
16 | return { path };
17 | }
18 |
19 | // The path following 'unix:' can be relative or absolute.
20 | return { path: resolve(target.substring(5)) };
21 | }
22 |
23 | if (target.startsWith('dns:')) {
24 | target = target.substring(4);
25 | }
26 |
27 | const url = new URL(`http://${target}`);
28 | const defaultPort = secure === true ? 443 : 80;
29 | let port = String(+url.port) === url.port ? +url.port : defaultPort;
30 |
31 | // Handle an edge case. WHATWG URLs don't set their port to 80, so a manual
32 | // check is required here.
33 | if (secure && url.port === '' && target.includes(`${url.hostname}:80`)) {
34 | port = 80;
35 | }
36 |
37 | return { host: url.hostname, port };
38 | }
39 |
40 |
41 | module.exports = { resolveToListenOptions };
42 |
--------------------------------------------------------------------------------
/lib/server-session.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const EventEmitter = require('events');
3 |
4 | class ServerSession extends EventEmitter {
5 | constructor (http2Session, options) {
6 | super();
7 | this.http2Session = http2Session;
8 | this.options = options;
9 | this.keepaliveInterval = null;
10 | this.keepaliveTimeout = null;
11 |
12 | const teardown = onSessionClose.bind(this);
13 | this.http2Session.on('close', teardown);
14 | this.http2Session.on('error', teardown);
15 | }
16 |
17 | startKeepalivePings () {
18 | const sendPing = this.sendPing.bind(this);
19 | const intervalLength = this.options.keepaliveTimeMs;
20 |
21 | this.keepaliveInterval = setInterval(sendPing, intervalLength);
22 | }
23 |
24 | stopKeepalivePings () {
25 | clearInterval(this.keepaliveInterval);
26 | clearTimeout(this.keepaliveTimeout);
27 | this.keepaliveInterval = null;
28 | this.keepaliveTimeout = null;
29 | }
30 |
31 | sendPing () {
32 | this.keepaliveTimeout = setTimeout(() => {
33 | // The ping timed out.
34 | this.stopKeepalivePings();
35 | this.http2Session.destroy();
36 | }, this.options.keepaliveTimeoutMs);
37 |
38 | this.http2Session.ping((err, duration, payload) => {
39 | clearTimeout(this.keepaliveTimeout);
40 |
41 | if (err) {
42 | // The ping errored.
43 | this.stopKeepalivePings();
44 | this.http2Session.destroy();
45 | }
46 | });
47 | }
48 | }
49 |
50 | module.exports = { ServerSession };
51 |
52 |
53 | function onSessionClose () {
54 | this.stopKeepalivePings();
55 | this.emit('close');
56 | }
57 |
--------------------------------------------------------------------------------
/lib/server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const Http2 = require('http2');
3 | const {
4 | ServerDuplexStream,
5 | ServerReadableStream,
6 | ServerUnaryCall,
7 | ServerWritableStream
8 | } = require('./handler');
9 | const { parseOptions } = require('./options');
10 | const { ServerCall } = require('./server-call');
11 | const { ServerCredentials } = require('./server-credentials');
12 | const { resolveToListenOptions } = require('./server-resolver');
13 | const { ServerSession } = require('./server-session');
14 | const Status = require('./status');
15 | const kHandlers = Symbol('handlers');
16 | const kServers = Symbol('servers');
17 | const kStarted = Symbol('started');
18 | const kOptions = Symbol('options');
19 | const kSessions = Symbol('sessions');
20 | const kUnaryHandlerType = 0;
21 | const kClientStreamHandlerType = 1;
22 | const kServerStreamHandlerType = 2;
23 | const kBidiHandlerType = 3;
24 | const kValidContentTypePrefix = 'application/grpc';
25 | const {
26 | HTTP2_HEADER_CONTENT_TYPE,
27 | HTTP2_HEADER_STATUS,
28 | HTTP2_HEADER_PATH,
29 | HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE,
30 | NGHTTP2_CANCEL
31 | } = Http2.constants;
32 | const defaultHttp2Settings = {
33 | ...Http2.getDefaultSettings(),
34 | maxSendHeaderBlockLength: Number.MAX_SAFE_INTEGER
35 | };
36 |
37 | const unsuportedMediaTypeResponse = {
38 | [HTTP2_HEADER_STATUS]: HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE
39 | };
40 | const unsuportedMediaTypeResponseOptions = { endStream: true };
41 |
42 | function noop () {}
43 |
44 |
45 | class Server {
46 | constructor (options = {}) {
47 | if (options === null || typeof options !== 'object') {
48 | throw new TypeError('options must be an object');
49 | }
50 |
51 | this[kServers] = [];
52 | this[kHandlers] = new Map();
53 | this[kSessions] = new Set();
54 | this[kStarted] = false;
55 | this[kOptions] = parseOptions(options);
56 | }
57 |
58 | bind (port, creds) {
59 | if (this[kStarted] === true) {
60 | throw new Error('server is already started');
61 | }
62 |
63 | if (typeof port === 'number') {
64 | port = `localhost:${port}`;
65 | }
66 |
67 | if (creds === null || typeof creds !== 'object') {
68 | creds = ServerCredentials.createInsecure();
69 | }
70 |
71 | return new Promise((resolve, reject) => {
72 | this.bindAsync(port, creds, (err, boundPort) => {
73 | if (err) {
74 | reject(err);
75 | }
76 |
77 | resolve(boundPort);
78 | });
79 | });
80 | }
81 |
82 | bindAsync (port, creds, callback) {
83 | if (this[kStarted] === true) {
84 | throw new Error('server is already started');
85 | }
86 |
87 | if (typeof port !== 'string') {
88 | throw new TypeError('port must be a string');
89 | }
90 |
91 | if (creds === null || typeof creds !== 'object') {
92 | throw new TypeError('creds must be an object');
93 | }
94 |
95 | if (typeof callback !== 'function') {
96 | throw new TypeError('callback must be a function');
97 | }
98 |
99 | const listenOptions = resolveToListenOptions(port, creds._isSecure());
100 | const http2ServerOptions = {
101 | allowHTTP1: false,
102 | settings: {
103 | ...defaultHttp2Settings,
104 | enablePush: false,
105 | maxFrameSize: this[kOptions].maxFrameSize,
106 | maxConcurrentStreams: this[kOptions].maxConcurrentStreams
107 | }
108 | };
109 |
110 | let server;
111 |
112 | if (creds._isSecure()) {
113 | server = Http2.createSecureServer({
114 | ...http2ServerOptions,
115 | ...creds._getSettings()
116 | });
117 | } else {
118 | server = Http2.createServer(http2ServerOptions);
119 | }
120 |
121 | server.timeout = 0;
122 | setupHandlers(this, server);
123 |
124 | function onError (err) {
125 | callback(err, -1);
126 | }
127 |
128 | server.once('error', onError);
129 | server.listen(listenOptions, () => {
130 | const port = server.address().port;
131 |
132 | server.removeListener('error', onError);
133 | this[kServers].push(server);
134 | callback(null, port);
135 | });
136 | }
137 |
138 | start () {
139 | const servers = this[kServers];
140 | const ready = servers.length > 0 && servers.every((server) => {
141 | return server.listening === true;
142 | });
143 |
144 | if (!ready) {
145 | throw new Error('server must be bound in order to start');
146 | }
147 |
148 | if (this[kStarted] === true) {
149 | throw new Error('server is already started');
150 | }
151 |
152 | this[kStarted] = true;
153 | }
154 |
155 | addService (service, implementation) {
156 | if (service === null || typeof service !== 'object' ||
157 | implementation === null || typeof implementation !== 'object') {
158 | throw new Error('addService requires two objects as arguments');
159 | }
160 |
161 | const serviceKeys = Object.keys(service);
162 |
163 | if (serviceKeys.length === 0) {
164 | throw new Error('Cannot add an empty service to a server');
165 | }
166 |
167 | serviceKeys.forEach((name) => {
168 | const attrs = service[name];
169 | let methodType;
170 |
171 | if (attrs.requestStream) {
172 | if (attrs.responseStream) {
173 | methodType = kBidiHandlerType;
174 | } else {
175 | methodType = kClientStreamHandlerType;
176 | }
177 | } else {
178 | if (attrs.responseStream) {
179 | methodType = kServerStreamHandlerType;
180 | } else {
181 | methodType = kUnaryHandlerType;
182 | }
183 | }
184 |
185 | const implFn = implementation[name] || implementation[attrs.originalName];
186 | let impl;
187 |
188 | if (implFn !== undefined) {
189 | impl = implFn.bind(implementation);
190 | } else {
191 | impl = getDefaultHandler(methodType, name);
192 | }
193 |
194 | const success = this.register(attrs.path, impl, attrs.responseSerialize,
195 | attrs.requestDeserialize, methodType);
196 |
197 | if (success === false) {
198 | throw new Error(`Method handler for ${attrs.path} already provided.`);
199 | }
200 | });
201 | }
202 |
203 | removeService (service) {
204 | if (service === null || typeof service !== 'object') {
205 | throw new Error('removeService requires an object argument');
206 | }
207 |
208 | Object.keys(service).forEach((name) => {
209 | this.unregister(service[name].path);
210 | });
211 | }
212 |
213 | register (name, handler, serialize, deserialize, type) {
214 | if (this[kHandlers].has(name)) {
215 | return false;
216 | }
217 |
218 | this[kHandlers].set(name, {
219 | func: handler,
220 | serialize,
221 | deserialize,
222 | type,
223 | path: name
224 | });
225 |
226 | return true;
227 | }
228 |
229 | unregister (name) {
230 | return this[kHandlers].delete(name);
231 | }
232 |
233 | tryShutdown (callback) {
234 | callback = typeof callback === 'function' ? callback : noop;
235 |
236 | let pendingChecks = 0;
237 | let callbackError = null;
238 |
239 | function maybeCallback (err) {
240 | if (err) {
241 | callbackError = err;
242 | }
243 |
244 | pendingChecks--;
245 |
246 | if (pendingChecks === 0) {
247 | callback(callbackError);
248 | }
249 | }
250 |
251 | // Close the server if necessary.
252 | this[kStarted] = false;
253 | this[kServers].forEach((server) => {
254 | if (server.listening === true) {
255 | pendingChecks++;
256 | server.close(maybeCallback);
257 | }
258 | });
259 |
260 | // If any sessions are active, close them gracefully.
261 | this[kSessions].forEach((session) => {
262 | if (!session.closed) {
263 | session.close(maybeCallback);
264 | pendingChecks++;
265 | }
266 | });
267 |
268 | // If the server is closed and there are no active sessions, just call back.
269 | if (pendingChecks === 0) {
270 | callback(null);
271 | }
272 | }
273 |
274 | forceShutdown () {
275 | // Close the server if it is still running.
276 | this[kServers].forEach((server) => {
277 | if (server.listening === true) {
278 | server.close();
279 | }
280 | });
281 |
282 | this[kStarted] = false;
283 |
284 | // Always destroy any available sessions. It's possible that one or more
285 | // tryShutdown() calls are in progress. Don't wait on them to finish.
286 | this[kSessions].forEach((session) => {
287 | session.destroy(NGHTTP2_CANCEL);
288 | });
289 |
290 | this[kSessions].clear();
291 | }
292 |
293 | addHttp2Port () { // eslint-disable-line class-methods-use-this
294 | throw new Error('not implemented');
295 | }
296 |
297 | addProtoService () { // eslint-disable-line class-methods-use-this
298 | throw new Error('not implemented. use addService() instead');
299 | }
300 | }
301 |
302 | module.exports = { Server };
303 |
304 |
305 | const handlerTypes = [
306 | handleUnary,
307 | handleClientStreaming,
308 | handleServerStreaming,
309 | handleBidiStreaming
310 | ];
311 |
312 |
313 | function setupHandlers (grpcServer, http2Server) {
314 | http2Server.on('stream', (stream, headers) => {
315 | const contentType = headers[HTTP2_HEADER_CONTENT_TYPE];
316 |
317 | if (typeof contentType !== 'string' ||
318 | !contentType.startsWith(kValidContentTypePrefix)) {
319 | stream.respond(unsuportedMediaTypeResponse,
320 | unsuportedMediaTypeResponseOptions);
321 | return;
322 | }
323 |
324 | const call = new ServerCall(stream, grpcServer[kOptions]);
325 |
326 | try {
327 | const path = headers[HTTP2_HEADER_PATH];
328 | const handler = grpcServer[kHandlers].get(path);
329 |
330 | if (handler === undefined) {
331 | return call.sendError(getUnimplementedStatusResponse(path));
332 | }
333 |
334 | const metadata = call.receiveMetadata(headers);
335 |
336 | call.handler = handler;
337 | handlerTypes[handler.type](call, handler, metadata);
338 | } catch (err) {
339 | call.sendError(err, Status.INTERNAL);
340 | }
341 | });
342 |
343 | http2Server.on('session', (session) => {
344 | if (grpcServer[kStarted] !== true) {
345 | session.destroy();
346 | return;
347 | }
348 |
349 | const grpcSession = new ServerSession(session, grpcServer[kOptions]);
350 |
351 | // The client has connected, so begin sending keepalive pings.
352 | grpcSession.startKeepalivePings();
353 |
354 | grpcServer[kSessions].add(session);
355 | grpcSession.once('close', () => {
356 | grpcServer[kSessions].delete(session);
357 | });
358 | });
359 | }
360 |
361 |
362 | function handleUnary (call, handler, metadata) {
363 | call.receiveUnaryMessage((err, request) => {
364 | if (err !== null || call.cancelled === true) {
365 | return;
366 | }
367 |
368 | const emitter = new ServerUnaryCall(call, metadata);
369 |
370 | emitter.request = request;
371 | handler.func(emitter, call.sendUnaryMessage.bind(call));
372 | });
373 | }
374 |
375 |
376 | function handleClientStreaming (call, handler, metadata) {
377 | const stream = new ServerReadableStream(call, metadata);
378 |
379 | function respond (err, value, trailer, flags) {
380 | stream.destroy();
381 | call.sendUnaryMessage(err, value, trailer, flags);
382 | }
383 |
384 | if (call.cancelled === true) {
385 | return;
386 | }
387 |
388 | stream.on('error', respond);
389 | handler.func(stream, respond);
390 | }
391 |
392 |
393 | function handleServerStreaming (call, handler, metadata) {
394 | call.receiveUnaryMessage((err, request) => {
395 | if (err !== null || call.cancelled === true) {
396 | return;
397 | }
398 |
399 | const stream = new ServerWritableStream(call, metadata);
400 |
401 | stream.request = request;
402 | handler.func(stream);
403 | });
404 | }
405 |
406 |
407 | function handleBidiStreaming (call, handler, metadata) {
408 | const stream = new ServerDuplexStream(call, metadata);
409 |
410 | if (call.cancelled === true) {
411 | return;
412 | }
413 |
414 | handler.func(stream);
415 | }
416 |
417 |
418 | function getUnimplementedStatusResponse (path) {
419 | return {
420 | code: Status.UNIMPLEMENTED,
421 | details: `The server does not implement the method ${path}`
422 | };
423 | }
424 |
425 |
426 | function getDefaultHandler (handlerType, callName) {
427 | const unimplementedStatusResponse = getUnimplementedStatusResponse(callName);
428 |
429 | switch (handlerType) {
430 | case 0 : // Unary
431 | return function unary (call, callback) {
432 | callback(unimplementedStatusResponse);
433 | };
434 | case 1 : // Client stream
435 | return function clientStream (call, callback) {
436 | callback(unimplementedStatusResponse);
437 | };
438 | case 2 : // Server stream
439 | return function serverStream (call) {
440 | call.emit('error', unimplementedStatusResponse);
441 | };
442 | case 3 : // Bidi stream
443 | return function bidi (call) {
444 | call.emit('error', unimplementedStatusResponse);
445 | };
446 | default :
447 | throw new Error(`Invalid handler type ${handlerType}`);
448 | }
449 | }
450 |
--------------------------------------------------------------------------------
/lib/status.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | OK: 0,
5 | CANCELLED: 1,
6 | UNKNOWN: 2,
7 | INVALID_ARGUMENT: 3,
8 | DEADLINE_EXCEEDED: 4,
9 | NOT_FOUND: 5,
10 | ALREADY_EXISTS: 6,
11 | PERMISSION_DENIED: 7,
12 | RESOURCE_EXHAUSTED: 8,
13 | FAILED_PRECONDITION: 9,
14 | ABORTED: 10,
15 | OUT_OF_RANGE: 11,
16 | UNIMPLEMENTED: 12,
17 | INTERNAL: 13,
18 | UNAVAILABLE: 14,
19 | DATA_LOSS: 15,
20 | UNAUTHENTICATED: 16
21 | };
22 |
--------------------------------------------------------------------------------
/lib/stream-decoder.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const kNoData = 1;
3 | const kReadingSize = 2;
4 | const kReadingMessage = 3;
5 |
6 |
7 | class StreamDecoder {
8 | constructor () {
9 | this.readState = kNoData;
10 | this.readCompressFlag = Buffer.alloc(1);
11 | this.readPartialSize = Buffer.alloc(4);
12 | this.readSizeRemaining = 4;
13 | this.readMessageSize = 0;
14 | this.readMessageRemaining = 0;
15 | this.readPartialMessage = [];
16 | }
17 |
18 | write (data) {
19 | const result = [];
20 | let readHead = 0;
21 | let toRead;
22 |
23 | while (readHead < data.length) {
24 | switch (this.readState) {
25 | case kNoData :
26 | this.readCompressFlag = data.slice(readHead, readHead + 1);
27 | readHead += 1;
28 | this.readState = kReadingSize;
29 | this.readPartialSize.fill(0);
30 | this.readSizeRemaining = 4;
31 | this.readMessageSize = 0;
32 | this.readMessageRemaining = 0;
33 | this.readPartialMessage = [];
34 | break;
35 | case kReadingSize :
36 | toRead = Math.min(data.length - readHead, this.readSizeRemaining);
37 | data.copy(
38 | this.readPartialSize, 4 - this.readSizeRemaining, readHead,
39 | readHead + toRead);
40 | this.readSizeRemaining -= toRead;
41 | readHead += toRead;
42 | // readSizeRemaining >=0 here
43 | if (this.readSizeRemaining === 0) {
44 | this.readMessageSize = this.readPartialSize.readUInt32BE(0);
45 | this.readMessageRemaining = this.readMessageSize;
46 | if (this.readMessageRemaining > 0) {
47 | this.readState = kReadingMessage;
48 | } else {
49 | const message = Buffer.concat(
50 | [this.readCompressFlag, this.readPartialSize], 5);
51 |
52 | this.readState = kNoData;
53 | result.push(message);
54 | }
55 | }
56 | break;
57 | case kReadingMessage :
58 | toRead =
59 | Math.min(data.length - readHead, this.readMessageRemaining);
60 | this.readPartialMessage.push(
61 | data.slice(readHead, readHead + toRead));
62 | this.readMessageRemaining -= toRead;
63 | readHead += toRead;
64 | // readMessageRemaining >=0 here
65 | if (this.readMessageRemaining === 0) {
66 | // At this point, we have read a full message
67 | const framedMessageBuffers = [
68 | this.readCompressFlag, this.readPartialSize
69 | ].concat(this.readPartialMessage);
70 | const framedMessage = Buffer.concat(
71 | framedMessageBuffers, this.readMessageSize + 5);
72 |
73 | this.readState = kNoData;
74 | result.push(framedMessage);
75 | }
76 | break;
77 | default :
78 | throw new Error('Unexpected read state');
79 | }
80 | }
81 |
82 | return result;
83 | }
84 | }
85 |
86 | module.exports = { StreamDecoder };
87 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const Status = require('./status');
3 |
4 |
5 | function hasGrpcStatusCode (obj) {
6 | return 'code' in obj &&
7 | Number.isInteger(obj.code) &&
8 | obj.code >= Status.OK &&
9 | obj.code <= Status.UNAUTHENTICATED;
10 | }
11 |
12 |
13 | module.exports = { hasGrpcStatusCode };
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "grpc-server-js",
3 | "version": "0.5.0",
4 | "description": "Pure JavaScript gRPC Server",
5 | "author": "Colin J. Ihrig (http://www.cjihrig.com/)",
6 | "main": "lib/index.js",
7 | "types": "lib/index.d.ts",
8 | "homepage": "https://github.com/cjihrig/grpc-server-js",
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/cjihrig/grpc-server-js.git"
12 | },
13 | "bugs": {
14 | "url": "https://github.com/cjihrig/grpc-server-js/issues"
15 | },
16 | "license": "MIT",
17 | "scripts": {
18 | "lint": "belly-button -f",
19 | "pretest": "npm run lint",
20 | "test": "lab -v -t 94"
21 | },
22 | "engines": {
23 | "node": ">=10.10.0"
24 | },
25 | "devDependencies": {
26 | "@grpc/grpc-js": "1.x.x",
27 | "@grpc/proto-loader": "0.5.x",
28 | "belly-button": "7.x.x",
29 | "cb-barrier": "1.x.x",
30 | "@hapi/lab": "24.x.x"
31 | },
32 | "keywords": [
33 | "grpc"
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/test/common.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const Grpc = require('@grpc/grpc-js');
3 | const Loader = require('@grpc/proto-loader');
4 | const protoLoaderOptions = {
5 | keepCase: true,
6 | longs: String,
7 | enums: String,
8 | defaults: true,
9 | oneofs: true
10 | };
11 |
12 |
13 | function loadProtoFile (file) {
14 | const packageDefinition = Loader.loadSync(file, protoLoaderOptions);
15 | const pkg = Grpc.loadPackageDefinition(packageDefinition);
16 |
17 | return pkg;
18 | }
19 |
20 |
21 | module.exports = { loadProtoFile };
22 |
--------------------------------------------------------------------------------
/test/compression.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const Assert = require('assert');
3 | const Lab = require('@hapi/lab');
4 | const Compression = require('../lib/compression-filter');
5 |
6 | // Test shortcuts
7 | const lab = exports.lab = Lab.script();
8 | const { describe, it } = lab;
9 |
10 |
11 | describe('Compression', () => {
12 | describe('IdentityHandler', () => {
13 | it('constructs an IdentityHandler instance', () => {
14 | const handler = new Compression.IdentityHandler();
15 |
16 | Assert(handler instanceof Compression.IdentityHandler);
17 | Assert.strictEqual(handler.name, 'identity');
18 | });
19 |
20 | it('throws when trying to compress', () => {
21 | const handler = new Compression.IdentityHandler();
22 |
23 | Assert.throws(() => {
24 | handler.compressMessage();
25 | }, /Error: Identity encoding does not support compression/);
26 | });
27 |
28 | it('throws when trying to decompress', () => {
29 | const handler = new Compression.IdentityHandler();
30 |
31 | Assert.throws(() => {
32 | handler.decompressMessage();
33 | }, /Error: Identity encoding does not support compression/);
34 | });
35 |
36 | it('frames and unframes a message', async () => {
37 | const handler = new Compression.IdentityHandler();
38 | const data = Buffer.from('abc');
39 | const processed = handler.writeMessage(data);
40 |
41 | Assert(Buffer.isBuffer(processed));
42 | Assert.strictEqual(processed.byteLength, 8);
43 | Assert.deepStrictEqual(await handler.readMessage(processed), data);
44 | });
45 |
46 | it('throws during reading if the message is compressed', async () => {
47 | const handler = new Compression.IdentityHandler();
48 | const data = Buffer.from('abc');
49 | const processed = handler.writeMessage(data);
50 |
51 | processed.writeUInt8(1, 0);
52 | await Assert.rejects(async () => {
53 | await handler.readMessage(processed);
54 | }, /Error: Identity encoding does not support compression/);
55 | });
56 | });
57 |
58 | describe('GzipHandler', () => {
59 | it('constructs a GzipHandler instance', () => {
60 | const handler = new Compression.GzipHandler();
61 |
62 | Assert(handler instanceof Compression.GzipHandler);
63 | Assert.strictEqual(handler.name, 'gzip');
64 | });
65 |
66 | it('frames and unframes a message', async () => {
67 | const handler = new Compression.GzipHandler();
68 | const data = Buffer.from('abc');
69 | const processed = await handler.writeMessage(data, true);
70 |
71 | Assert(Buffer.isBuffer(processed));
72 | Assert(processed.byteLength > 8);
73 | Assert.deepStrictEqual(await handler.readMessage(processed), data);
74 | });
75 |
76 | it('frames and unframes a message without compressing', async () => {
77 | const handler = new Compression.GzipHandler();
78 | const data = Buffer.from('abc');
79 | const processed = await handler.writeMessage(data, false);
80 |
81 | Assert(Buffer.isBuffer(processed));
82 | Assert.strictEqual(processed.byteLength, 8);
83 | Assert.deepStrictEqual(await handler.readMessage(processed), data);
84 | });
85 | });
86 |
87 | describe('DeflateHandler', () => {
88 | it('constructs a DeflateHandler instance', () => {
89 | const handler = new Compression.DeflateHandler();
90 |
91 | Assert(handler instanceof Compression.DeflateHandler);
92 | Assert.strictEqual(handler.name, 'deflate');
93 | });
94 |
95 | it('frames and unframes a message', async () => {
96 | const handler = new Compression.DeflateHandler();
97 | const data = Buffer.from('abc');
98 | const processed = await handler.writeMessage(data, true);
99 |
100 | Assert(Buffer.isBuffer(processed));
101 | Assert(processed.byteLength > 8);
102 | Assert.deepStrictEqual(await handler.readMessage(processed), data);
103 | });
104 |
105 | it('frames and unframes a message without compressing', async () => {
106 | const handler = new Compression.DeflateHandler();
107 | const data = Buffer.from('abc');
108 | const processed = await handler.writeMessage(data, false);
109 |
110 | Assert(Buffer.isBuffer(processed));
111 | Assert.strictEqual(processed.byteLength, 8);
112 | Assert.deepStrictEqual(await handler.readMessage(processed), data);
113 | });
114 | });
115 | });
116 |
--------------------------------------------------------------------------------
/test/deadline.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const Assert = require('assert');
3 | const Path = require('path');
4 | const Barrier = require('cb-barrier');
5 | const Lab = require('@hapi/lab');
6 | const Grpc = require('@grpc/grpc-js');
7 | const { Server, ServerCredentials } = require('../lib');
8 | const { loadProtoFile } = require('./common');
9 |
10 | // Test shortcuts
11 | const lab = exports.lab = Lab.script();
12 | const { describe, it, before, after } = lab;
13 |
14 |
15 | const clientInsecureCreds = Grpc.credentials.createInsecure();
16 | const serverInsecureCreds = ServerCredentials.createInsecure();
17 |
18 |
19 | describe('Deadlines', () => {
20 | let server;
21 | let client;
22 |
23 | before(async () => {
24 | const proto = loadProtoFile(Path.join(__dirname, 'proto', 'test_service.proto'));
25 | const TestServiceClient = proto.TestService;
26 |
27 | server = new Server();
28 | server.addService(proto.TestService.service, {
29 | unary (call, cb) {
30 | call.on('cancelled', (reason) => {
31 | Assert.strictEqual(reason, 'deadline');
32 | });
33 |
34 | setTimeout(() => {
35 | cb(null, {});
36 | }, 2000);
37 | }
38 | });
39 |
40 | const port = await server.bind('localhost:0', serverInsecureCreds);
41 | client = new TestServiceClient(`localhost:${port}`, clientInsecureCreds);
42 | server.start();
43 | });
44 |
45 | after(() => {
46 | client.close();
47 | server.forceShutdown();
48 | });
49 |
50 | it('works with deadlines', () => {
51 | const barrier = new Barrier();
52 | const metadata = new Grpc.Metadata();
53 | const {
54 | path,
55 | requestSerialize: serialize,
56 | responseDeserialize: deserialize
57 | } = client.unary;
58 |
59 | metadata.set('grpc-timeout', '100m');
60 | client.makeUnaryRequest(path, serialize, deserialize, {}, metadata, {}, (error, response) => {
61 | Assert.strictEqual(error.code, Grpc.status.DEADLINE_EXCEEDED);
62 | Assert.strictEqual(error.details, 'Deadline exceeded');
63 | barrier.pass();
64 | });
65 |
66 | return barrier;
67 | });
68 |
69 | it('rejects invalid deadline', () => {
70 | const barrier = new Barrier();
71 | const metadata = new Grpc.Metadata();
72 | const {
73 | path,
74 | requestSerialize: serialize,
75 | responseDeserialize: deserialize
76 | } = client.unary;
77 |
78 | metadata.set('grpc-timeout', 'Infinity');
79 | client.makeUnaryRequest(path, serialize, deserialize, {}, metadata, {}, (error, response) => {
80 | Assert.strictEqual(error.code, Grpc.status.OUT_OF_RANGE);
81 | Assert.strictEqual(error.details, 'Invalid deadline');
82 | barrier.pass();
83 | });
84 |
85 | return barrier;
86 | });
87 | });
88 |
89 |
90 | describe('Cancellation', () => {
91 | let server;
92 | let client;
93 | let inHandler = false;
94 | let cancelledInServer = false;
95 |
96 | before(async () => {
97 | const proto = loadProtoFile(Path.join(__dirname, 'proto', 'test_service.proto'));
98 | const TestServiceClient = proto.TestService;
99 |
100 | server = new Server();
101 | server.addService(proto.TestService.service, {
102 | serverStream (stream) {
103 | inHandler = true;
104 | stream.on('cancelled', (reason) => {
105 | Assert.strictEqual(reason, 'cancelled');
106 | stream.write({});
107 | stream.end();
108 | cancelledInServer = true;
109 | });
110 | }
111 | });
112 |
113 | const port = await server.bind('localhost:0', serverInsecureCreds);
114 | client = new TestServiceClient(`localhost:${port}`, clientInsecureCreds);
115 | server.start();
116 | });
117 |
118 | after(() => {
119 | client.close();
120 | server.forceShutdown();
121 | });
122 |
123 | it('handles requests cancelled by the client', () => {
124 | const barrier = new Barrier();
125 | const call = client.serverStream({});
126 |
127 | call.on('data', Assert.ifError);
128 | call.on('error', (error) => {
129 | Assert.strictEqual(error.code, Grpc.status.CANCELLED);
130 | Assert.strictEqual(error.details, 'Cancelled on client');
131 | waitForServerCancel();
132 | });
133 |
134 | function waitForHandler () {
135 | if (inHandler === true) {
136 | call.cancel();
137 | return;
138 | }
139 |
140 | setImmediate(waitForHandler);
141 | }
142 |
143 | function waitForServerCancel () {
144 | if (cancelledInServer === true) {
145 | barrier.pass();
146 | return;
147 | }
148 |
149 | setImmediate(waitForServerCancel);
150 | }
151 |
152 | waitForHandler();
153 | return barrier;
154 | });
155 | });
156 |
--------------------------------------------------------------------------------
/test/errors.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const Assert = require('assert');
3 | const Net = require('net');
4 | const Path = require('path');
5 | const Barrier = require('cb-barrier');
6 | const Lab = require('@hapi/lab');
7 | const Grpc = require('@grpc/grpc-js');
8 | const { Server, ServerCredentials } = require('../lib');
9 | const { loadProtoFile } = require('./common');
10 |
11 | // Test shortcuts
12 | const lab = exports.lab = Lab.script();
13 | const { describe, it, before, after } = lab;
14 |
15 |
16 | const protoFile = Path.join(__dirname, 'proto', 'test_service.proto');
17 | const testServiceDef = loadProtoFile(protoFile);
18 | const TestServiceClient = testServiceDef.TestService;
19 | const clientInsecureCreds = Grpc.credentials.createInsecure();
20 | const serverInsecureCreds = ServerCredentials.createInsecure();
21 |
22 |
23 | describe('Client malformed response handling', () => {
24 | let server;
25 | let client;
26 | const badArg = Buffer.from([0xFF]);
27 |
28 | before(async () => {
29 | const malformedTestService = {
30 | unary: {
31 | path: '/TestService/Unary',
32 | requestStream: false,
33 | responseStream: false,
34 | requestDeserialize: identity,
35 | responseSerialize: identity
36 | },
37 | clientStream: {
38 | path: '/TestService/ClientStream',
39 | requestStream: true,
40 | responseStream: false,
41 | requestDeserialize: identity,
42 | responseSerialize: identity
43 | },
44 | serverStream: {
45 | path: '/TestService/ServerStream',
46 | requestStream: false,
47 | responseStream: true,
48 | requestDeserialize: identity,
49 | responseSerialize: identity
50 | },
51 | bidiStream: {
52 | path: '/TestService/BidiStream',
53 | requestStream: true,
54 | responseStream: true,
55 | requestDeserialize: identity,
56 | responseSerialize: identity
57 | }
58 | };
59 |
60 | server = new Server();
61 |
62 | server.addService(malformedTestService, {
63 | unary (call, cb) {
64 | cb(null, badArg);
65 | },
66 |
67 | clientStream (stream, cb) {
68 | stream.on('data', noop);
69 | stream.on('end', () => {
70 | cb(null, badArg);
71 | });
72 | },
73 |
74 | serverStream (stream) {
75 | stream.write(badArg);
76 | stream.end();
77 | },
78 |
79 | bidiStream (stream) {
80 | stream.on('data', () => {
81 | // Ignore requests
82 | stream.write(badArg);
83 | });
84 |
85 | stream.on('end', () => {
86 | stream.end();
87 | });
88 | }
89 | });
90 |
91 | const port = await server.bind('localhost:0', serverInsecureCreds);
92 | client = new TestServiceClient(`localhost:${port}`, clientInsecureCreds);
93 | server.start();
94 | });
95 |
96 | after(() => {
97 | client.close();
98 | server.forceShutdown();
99 | });
100 |
101 | it('should get an INTERNAL status with a unary call', () => {
102 | const barrier = new Barrier();
103 |
104 | client.unary({}, (err, data) => {
105 | Assert(err);
106 | Assert.strictEqual(err.code, Grpc.status.INTERNAL);
107 | barrier.pass();
108 | });
109 |
110 | return barrier;
111 | });
112 |
113 | it('should get an INTERNAL status with a client stream call', () => {
114 | const barrier = new Barrier();
115 | const call = client.clientStream((err, data) => {
116 | Assert(err);
117 | Assert.strictEqual(err.code, Grpc.status.INTERNAL);
118 | barrier.pass();
119 | });
120 |
121 | call.write({});
122 | call.end();
123 |
124 | return barrier;
125 | });
126 |
127 | it('should get an INTERNAL status with a server stream call', () => {
128 | const barrier = new Barrier();
129 | const call = client.serverStream({});
130 |
131 | call.on('data', noop);
132 | call.on('error', (err) => {
133 | Assert(err);
134 | Assert.strictEqual(err.code, Grpc.status.INTERNAL);
135 | barrier.pass();
136 | });
137 |
138 | return barrier;
139 | });
140 |
141 | it('should get an INTERNAL status with a bidi stream call', () => {
142 | const barrier = new Barrier();
143 | const call = client.bidiStream();
144 |
145 | call.on('data', noop);
146 | call.on('error', (err) => {
147 | Assert(err);
148 | Assert.strictEqual(err.code, Grpc.status.INTERNAL);
149 | barrier.pass();
150 | });
151 |
152 | call.write({});
153 | call.end();
154 |
155 | return barrier;
156 | });
157 | });
158 |
159 | describe('Server serialization failure handling', () => {
160 | let client;
161 | let server;
162 |
163 | before(async () => {
164 | function serializeFail (obj) {
165 | throw new Error('Serialization failed');
166 | }
167 |
168 | const malformedTestService = {
169 | unary: {
170 | path: '/TestService/Unary',
171 | requestStream: false,
172 | responseStream: false,
173 | requestDeserialize: identity,
174 | responseSerialize: serializeFail
175 | },
176 | clientStream: {
177 | path: '/TestService/ClientStream',
178 | requestStream: true,
179 | responseStream: false,
180 | requestDeserialize: identity,
181 | responseSerialize: serializeFail
182 | },
183 | serverStream: {
184 | path: '/TestService/ServerStream',
185 | requestStream: false,
186 | responseStream: true,
187 | requestDeserialize: identity,
188 | responseSerialize: serializeFail
189 | },
190 | bidiStream: {
191 | path: '/TestService/BidiStream',
192 | requestStream: true,
193 | responseStream: true,
194 | requestDeserialize: identity,
195 | responseSerialize: serializeFail
196 | }
197 | };
198 |
199 | server = new Server();
200 | server.addService(malformedTestService, {
201 | unary (call, cb) {
202 | cb(null, {});
203 | },
204 |
205 | clientStream (stream, cb) {
206 | stream.on('data', noop);
207 | stream.on('end', () => {
208 | cb(null, {});
209 | });
210 | },
211 |
212 | serverStream (stream) {
213 | stream.write({});
214 | stream.end();
215 | },
216 |
217 | bidiStream (stream) {
218 | stream.on('data', () => {
219 | // Ignore requests
220 | stream.write({});
221 | });
222 | stream.on('end', () => {
223 | stream.end();
224 | });
225 | }
226 | });
227 |
228 | const port = await server.bind('localhost:0', serverInsecureCreds);
229 |
230 | client = new TestServiceClient(`localhost:${port}`, clientInsecureCreds);
231 | server.start();
232 | });
233 |
234 | after(() => {
235 | client.close();
236 | server.forceShutdown();
237 | });
238 |
239 | it('should get an INTERNAL status with a unary call', () => {
240 | const barrier = new Barrier();
241 |
242 | client.unary({}, (err, data) => {
243 | Assert(err);
244 | Assert.strictEqual(err.code, Grpc.status.INTERNAL);
245 | barrier.pass();
246 | });
247 |
248 | return barrier;
249 | });
250 |
251 | it('should get an INTERNAL status with a client stream call', () => {
252 | const barrier = new Barrier();
253 | const call = client.clientStream((err, data) => {
254 | Assert(err);
255 | Assert.strictEqual(err.code, Grpc.status.INTERNAL);
256 | barrier.pass();
257 | });
258 |
259 | call.write({});
260 | call.end();
261 |
262 | return barrier;
263 | });
264 |
265 | it('should get an INTERNAL status with a server stream call', () => {
266 | const barrier = new Barrier();
267 | const call = client.serverStream({});
268 |
269 | call.on('data', noop);
270 | call.on('error', (err) => {
271 | Assert(err);
272 | Assert.strictEqual(err.code, Grpc.status.INTERNAL);
273 | barrier.pass();
274 | });
275 |
276 | return barrier;
277 | });
278 |
279 | it('should get an INTERNAL status with a bidi stream call', () => {
280 | const barrier = new Barrier();
281 | const call = client.bidiStream();
282 |
283 | call.on('data', noop);
284 | call.on('error', (err) => {
285 | Assert(err);
286 | Assert.strictEqual(err.code, Grpc.status.INTERNAL);
287 | barrier.pass();
288 | });
289 |
290 | call.write({});
291 | call.end();
292 |
293 | return barrier;
294 | });
295 | });
296 |
297 | describe('Other conditions', () => {
298 | let client;
299 | let server;
300 | let port;
301 |
302 | before(async () => {
303 | const trailerMetadata = new Grpc.Metadata();
304 | const existingMetadata = new Grpc.Metadata();
305 |
306 | server = new Server();
307 | trailerMetadata.add('trailer-present', 'yes');
308 | existingMetadata.add('existing-present', 'yes');
309 |
310 | function validatePeer (call) {
311 | const [peerAddress, peerPort] = call.getPeer().split(':');
312 |
313 | Assert(Net.isIP(peerAddress));
314 | Assert.strictEqual(String(Number(peerPort)), peerPort);
315 | Assert(Number.isSafeInteger(Number(peerPort)));
316 | }
317 |
318 | server.addService(TestServiceClient.service, {
319 | unary (call, cb) {
320 | const req = call.request;
321 |
322 | validatePeer(call);
323 |
324 | if (req.error) {
325 | const details = req.message || 'Requested error';
326 | const response = {
327 | code: Grpc.status.UNKNOWN,
328 | details
329 | };
330 |
331 | if (req.message === 'existing-metadata') {
332 | response.metadata = existingMetadata;
333 | }
334 |
335 | cb(response, null, trailerMetadata);
336 | } else {
337 | cb(null, { count: 1 }, trailerMetadata);
338 | }
339 | },
340 |
341 | clientStream (stream, cb) {
342 | let count = 0;
343 | let errored;
344 |
345 | validatePeer(stream);
346 |
347 | stream.on('data', (data) => {
348 | if (data.error) {
349 | const message = data.message || 'Requested error';
350 | errored = true;
351 | cb(new Error(message), null, trailerMetadata);
352 | } else {
353 | count++;
354 | }
355 | });
356 |
357 | stream.on('end', () => {
358 | if (!errored) {
359 | cb(null, { count }, trailerMetadata);
360 | }
361 | });
362 | },
363 |
364 | serverStream (stream) {
365 | const req = stream.request;
366 |
367 | validatePeer(stream);
368 |
369 | if (req.error) {
370 | stream.emit('error', {
371 | code: Grpc.status.UNKNOWN,
372 | details: req.message || 'Requested error',
373 | metadata: trailerMetadata
374 | });
375 | } else {
376 | for (let i = 0; i < 5; i++) {
377 | stream.write({ count: i });
378 | }
379 |
380 | stream.end(trailerMetadata);
381 | }
382 | },
383 |
384 | bidiStream (stream) {
385 | validatePeer(stream);
386 |
387 | let count = 0;
388 | stream.on('data', (data) => {
389 | if (data.error) {
390 | const message = data.message || 'Requested error';
391 | const err = new Error(message);
392 |
393 | err.metadata = trailerMetadata.clone();
394 | err.metadata.add('count', '' + count);
395 | stream.emit('error', err);
396 | } else {
397 | stream.write({ count });
398 | count++;
399 | }
400 | });
401 |
402 | stream.on('end', () => {
403 | stream.end(trailerMetadata);
404 | });
405 | }
406 | });
407 |
408 | port = await server.bind('localhost:0', serverInsecureCreds);
409 | client = new TestServiceClient(`localhost:${port}`, clientInsecureCreds);
410 | server.start();
411 | });
412 |
413 | after(function () {
414 | client.close();
415 | server.forceShutdown();
416 | });
417 |
418 | describe('Server receiving bad input', () => {
419 | let misbehavingClient;
420 | const badArg = Buffer.from([0xFF]);
421 |
422 | before(() => {
423 | const testServiceAttrs = {
424 | unary: {
425 | path: '/TestService/Unary',
426 | requestStream: false,
427 | responseStream: false,
428 | requestSerialize: identity,
429 | responseDeserialize: identity
430 | },
431 | clientStream: {
432 | path: '/TestService/ClientStream',
433 | requestStream: true,
434 | responseStream: false,
435 | requestSerialize: identity,
436 | responseDeserialize: identity
437 | },
438 | serverStream: {
439 | path: '/TestService/ServerStream',
440 | requestStream: false,
441 | responseStream: true,
442 | requestSerialize: identity,
443 | responseDeserialize: identity
444 | },
445 | bidiStream: {
446 | path: '/TestService/BidiStream',
447 | requestStream: true,
448 | responseStream: true,
449 | requestSerialize: identity,
450 | responseDeserialize: identity
451 | }
452 | };
453 |
454 | const Client = Grpc.makeGenericClientConstructor(testServiceAttrs, 'TestService');
455 |
456 | misbehavingClient = new Client(`localhost:${port}`, clientInsecureCreds);
457 | });
458 |
459 | after(() => {
460 | misbehavingClient.close();
461 | });
462 |
463 | it('should respond correctly to a unary call', () => {
464 | const barrier = new Barrier();
465 |
466 | misbehavingClient.unary(badArg, (err, data) => {
467 | Assert(err);
468 | Assert.strictEqual(err.code, Grpc.status.INTERNAL);
469 | barrier.pass();
470 | });
471 |
472 | return barrier;
473 | });
474 |
475 | it('should respond correctly to a client stream', () => {
476 | const barrier = new Barrier();
477 | const call = misbehavingClient.clientStream((err, data) => {
478 | Assert(err);
479 | Assert.strictEqual(err.code, Grpc.status.INTERNAL);
480 | barrier.pass();
481 | });
482 |
483 | call.write(badArg);
484 | call.end();
485 |
486 | return barrier;
487 | });
488 |
489 | it('should respond correctly to a server stream', () => {
490 | const barrier = new Barrier();
491 | const call = misbehavingClient.serverStream(badArg);
492 |
493 | call.on('data', (data) => {
494 | Assert.fail(data);
495 | });
496 |
497 | call.on('error', (err) => {
498 | Assert(err);
499 | Assert.strictEqual(err.code, Grpc.status.INTERNAL);
500 | barrier.pass();
501 | });
502 |
503 | return barrier;
504 | });
505 |
506 | it('should respond correctly to a bidi stream', () => {
507 | const barrier = new Barrier();
508 | const call = misbehavingClient.bidiStream();
509 |
510 | call.on('data', (data) => {
511 | Assert.fail(data);
512 | });
513 |
514 | call.on('error', (err) => {
515 | Assert(err);
516 | Assert.strictEqual(err.code, Grpc.status.INTERNAL);
517 | barrier.pass();
518 | });
519 |
520 | call.write(badArg);
521 | call.end();
522 | return barrier;
523 | });
524 | });
525 |
526 | describe('Trailing metadata', () => {
527 | it('should be present when a unary call succeeds', () => {
528 | const barrier = new Barrier(2);
529 | const call = client.unary({ error: false }, (err, data) => {
530 | Assert.ifError(err);
531 | barrier.pass();
532 | });
533 |
534 | call.on('status', (status) => {
535 | Assert.deepStrictEqual(status.metadata.get('trailer-present'), ['yes']);
536 | barrier.pass();
537 | });
538 |
539 | return barrier;
540 | });
541 |
542 | it('should be present when a unary call fails', () => {
543 | const barrier = new Barrier(2);
544 | const call = client.unary({ error: true }, (err, data) => {
545 | Assert(err);
546 | barrier.pass();
547 | });
548 |
549 | call.on('status', (status) => {
550 | Assert.deepStrictEqual(status.metadata.get('trailer-present'), ['yes']);
551 | barrier.pass();
552 | });
553 |
554 | return barrier;
555 | });
556 |
557 | it('should be present when a client stream call succeeds', () => {
558 | const barrier = new Barrier(2);
559 | const call = client.clientStream((err, data) => {
560 | Assert.ifError(err);
561 | barrier.pass();
562 | });
563 |
564 | call.write({ error: false });
565 | call.write({ error: false });
566 | call.end();
567 |
568 | call.on('status', (status) => {
569 | Assert.deepStrictEqual(status.metadata.get('trailer-present'), ['yes']);
570 | barrier.pass();
571 | });
572 |
573 | return barrier;
574 | });
575 |
576 | it('should be present when a client stream call fails', () => {
577 | const barrier = new Barrier(2);
578 | const call = client.clientStream((err, data) => {
579 | Assert(err);
580 | barrier.pass();
581 | });
582 |
583 | call.write({ error: false });
584 | call.write({ error: true });
585 | call.end();
586 |
587 | call.on('status', (status) => {
588 | Assert.deepStrictEqual(status.metadata.get('trailer-present'), ['yes']);
589 | barrier.pass();
590 | });
591 |
592 | return barrier;
593 | });
594 |
595 | it('should be present when a server stream call succeeds', () => {
596 | const barrier = new Barrier();
597 | const call = client.serverStream({ error: false });
598 |
599 | call.on('data', noop);
600 | call.on('status', (status) => {
601 | Assert.strictEqual(status.code, Grpc.status.OK);
602 | Assert.deepStrictEqual(status.metadata.get('trailer-present'), ['yes']);
603 | barrier.pass();
604 | });
605 |
606 | return barrier;
607 | });
608 |
609 | it('should be present when a server stream call fails', () => {
610 | const barrier = new Barrier();
611 | const call = client.serverStream({ error: true });
612 |
613 | call.on('data', noop);
614 | call.on('error', (error) => {
615 | Assert.deepStrictEqual(error.metadata.get('trailer-present'), ['yes']);
616 | barrier.pass();
617 | });
618 |
619 | return barrier;
620 | });
621 |
622 | it('should be present when a bidi stream succeeds', () => {
623 | const barrier = new Barrier();
624 | const call = client.bidiStream();
625 |
626 | call.write({ error: false });
627 | call.write({ error: false });
628 | call.end();
629 | call.on('data', noop);
630 | call.on('status', (status) => {
631 | Assert.strictEqual(status.code, Grpc.status.OK);
632 | Assert.deepStrictEqual(status.metadata.get('trailer-present'), ['yes']);
633 | barrier.pass();
634 | });
635 |
636 | return barrier;
637 | });
638 |
639 | it('should be present when a bidi stream fails', () => {
640 | const barrier = new Barrier();
641 | const call = client.bidiStream();
642 |
643 | call.write({ error: false });
644 | call.write({ error: true });
645 | call.end();
646 | call.on('data', noop);
647 | call.on('error', (error) => {
648 | Assert.deepStrictEqual(error.metadata.get('trailer-present'), ['yes']);
649 | barrier.pass();
650 | });
651 |
652 | return barrier;
653 | });
654 | });
655 |
656 | it('existing metadata is not overwritten when a unary call fails', () => {
657 | const barrier = new Barrier(2);
658 | const call = client.unary({
659 | error: true,
660 | message: 'existing-metadata'
661 | }, (err, data) => {
662 | Assert(err);
663 | barrier.pass();
664 | });
665 |
666 | call.on('status', (status) => {
667 | Assert.deepStrictEqual(status.metadata.get('existing-present'), ['yes']);
668 | barrier.pass();
669 | });
670 |
671 | return barrier;
672 | });
673 |
674 | describe('Error object should contain the status', () => {
675 | it('for a unary call', () => {
676 | const barrier = new Barrier();
677 |
678 | client.unary({ error: true }, (err, data) => {
679 | Assert(err);
680 | Assert.strictEqual(err.code, Grpc.status.UNKNOWN);
681 | Assert.strictEqual(err.details, 'Requested error');
682 | barrier.pass();
683 | });
684 |
685 | return barrier;
686 | });
687 |
688 | it('for a client stream call', () => {
689 | const barrier = new Barrier();
690 | const call = client.clientStream((err, data) => {
691 | Assert(err);
692 | Assert.strictEqual(err.code, Grpc.status.UNKNOWN);
693 | Assert.strictEqual(err.details, 'Requested error');
694 | barrier.pass();
695 | });
696 |
697 | call.write({ error: false });
698 | call.write({ error: true });
699 | call.end();
700 |
701 | return barrier;
702 | });
703 |
704 | it('for a server stream call', () => {
705 | const barrier = new Barrier();
706 | const call = client.serverStream({ error: true });
707 |
708 | call.on('data', noop);
709 | call.on('error', (error) => {
710 | Assert.strictEqual(error.code, Grpc.status.UNKNOWN);
711 | Assert.strictEqual(error.details, 'Requested error');
712 | barrier.pass();
713 | });
714 |
715 | return barrier;
716 | });
717 |
718 | it('for a bidi stream call', () => {
719 | const barrier = new Barrier();
720 | const call = client.bidiStream();
721 |
722 | call.write({ error: false });
723 | call.write({ error: true });
724 | call.end();
725 | call.on('data', noop);
726 | call.on('error', (error) => {
727 | Assert.strictEqual(error.code, Grpc.status.UNKNOWN);
728 | Assert.strictEqual(error.details, 'Requested error');
729 | barrier.pass();
730 | });
731 |
732 | return barrier;
733 | });
734 |
735 | it('for a UTF-8 error message', () => {
736 | const barrier = new Barrier();
737 |
738 | client.unary({ error: true, message: '測試字符串' }, (err, data) => {
739 | Assert(err);
740 | Assert.strictEqual(err.code, Grpc.status.UNKNOWN);
741 | Assert.strictEqual(err.details, '測試字符串');
742 | barrier.pass();
743 | });
744 |
745 | return barrier;
746 | });
747 |
748 | it('for an error message containing a comma', () => {
749 | const barrier = new Barrier();
750 |
751 | client.unary({ error: true, message: 'foo, bar, and baz' }, (err, data) => {
752 | Assert(err);
753 | Assert.strictEqual(err.code, Grpc.status.UNKNOWN);
754 | Assert.strictEqual(err.details, 'foo, bar, and baz');
755 | barrier.pass();
756 | });
757 |
758 | return barrier;
759 | });
760 | });
761 | });
762 |
763 |
764 | function identity (arg) {
765 | return arg;
766 | }
767 |
768 |
769 | function noop () {}
770 |
--------------------------------------------------------------------------------
/test/fixtures/README:
--------------------------------------------------------------------------------
1 | CONFIRMEDTESTKEY
2 |
--------------------------------------------------------------------------------
/test/fixtures/ca.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV
3 | BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
4 | aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNDExMTEyMjMxMjla
5 | Fw0yNDExMDgyMjMxMjlaMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0
6 | YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMT
7 | BnRlc3RjYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwEDfBV5MYdlHVHJ7
8 | +L4nxrZy7mBfAVXpOc5vMYztssUI7mL2/iYujiIXM+weZYNTEpLdjyJdu7R5gGUu
9 | g1jSVK/EPHfc74O7AyZU34PNIP4Sh33N+/A5YexrNgJlPY+E3GdVYi4ldWJjgkAd
10 | Qah2PH5ACLrIIC6tRka9hcaBlIECAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNV
11 | HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADgYEAHzC7jdYlzAVmddi/gdAeKPau
12 | sPBG/C2HCWqHzpCUHcKuvMzDVkY/MP2o6JIW2DBbY64bO/FceExhjcykgaYtCH/m
13 | oIU63+CFOTtR7otyQAWHqXa7q4SbCDlG7DyRFxqG0txPtGvy12lgldA2+RgcigQG
14 | Dfcog5wrJytaQ6UA0wE=
15 | -----END CERTIFICATE-----
16 |
--------------------------------------------------------------------------------
/test/fixtures/server1.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAOHDFScoLCVJpYDD
3 | M4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1BgzkWF+slf
4 | 3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd9N8YwbBY
5 | AckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAECgYAn7qGnM2vbjJNBm0VZCkOkTIWm
6 | V10okw7EPJrdL2mkre9NasghNXbE1y5zDshx5Nt3KsazKOxTT8d0Jwh/3KbaN+YY
7 | tTCbKGW0pXDRBhwUHRcuRzScjli8Rih5UOCiZkhefUTcRb6xIhZJuQy71tjaSy0p
8 | dHZRmYyBYO2YEQ8xoQJBAPrJPhMBkzmEYFtyIEqAxQ/o/A6E+E4w8i+KM7nQCK7q
9 | K4JXzyXVAjLfyBZWHGM2uro/fjqPggGD6QH1qXCkI4MCQQDmdKeb2TrKRh5BY1LR
10 | 81aJGKcJ2XbcDu6wMZK4oqWbTX2KiYn9GB0woM6nSr/Y6iy1u145YzYxEV/iMwff
11 | DJULAkB8B2MnyzOg0pNFJqBJuH29bKCcHa8gHJzqXhNO5lAlEbMK95p/P2Wi+4Hd
12 | aiEIAF1BF326QJcvYKmwSmrORp85AkAlSNxRJ50OWrfMZnBgzVjDx3xG6KsFQVk2
13 | ol6VhqL6dFgKUORFUWBvnKSyhjJxurlPEahV6oo6+A+mPhFY8eUvAkAZQyTdupP3
14 | XEFQKctGz+9+gKkemDp7LBBMEMBXrGTLPhpEfcjv/7KPdnFHYmhYeBTBnuVmTVWe
15 | F98XJ7tIFfJq
16 | -----END PRIVATE KEY-----
17 |
--------------------------------------------------------------------------------
/test/fixtures/server1.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIICnDCCAgWgAwIBAgIBBzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJBVTET
3 | MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ
4 | dHkgTHRkMQ8wDQYDVQQDEwZ0ZXN0Y2EwHhcNMTUxMTA0MDIyMDI0WhcNMjUxMTAx
5 | MDIyMDI0WjBlMQswCQYDVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNV
6 | BAcTB0NoaWNhZ28xFTATBgNVBAoTDEV4YW1wbGUsIENvLjEaMBgGA1UEAxQRKi50
7 | ZXN0Lmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOHDFSco
8 | LCVJpYDDM4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1Bg
9 | zkWF+slf3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd
10 | 9N8YwbBYAckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAGjazBpMAkGA1UdEwQCMAAw
11 | CwYDVR0PBAQDAgXgME8GA1UdEQRIMEaCECoudGVzdC5nb29nbGUuZnKCGHdhdGVy
12 | em9vaS50ZXN0Lmdvb2dsZS5iZYISKi50ZXN0LnlvdXR1YmUuY29thwTAqAEDMA0G
13 | CSqGSIb3DQEBCwUAA4GBAJFXVifQNub1LUP4JlnX5lXNlo8FxZ2a12AFQs+bzoJ6
14 | hM044EDjqyxUqSbVePK0ni3w1fHQB5rY9yYC5f8G7aqqTY1QOhoUk8ZTSTRpnkTh
15 | y4jjdvTZeLDVBlueZUTDRmy2feY5aZIU18vFDK08dTG0A87pppuv1LNIR3loveU8
16 | -----END CERTIFICATE-----
17 |
--------------------------------------------------------------------------------
/test/logging.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const Assert = require('assert');
3 | const Lab = require('@hapi/lab');
4 | const Grpc = require('../lib');
5 | const Logging = require('../lib/logging');
6 |
7 | // Test shortcuts
8 | const lab = exports.lab = Lab.script();
9 | const { describe, it, afterEach } = lab;
10 |
11 |
12 | describe('Logging', () => {
13 | afterEach(() => {
14 | // Ensure that the logger is restored to its defaults after each test.
15 | Grpc.setLogger(console);
16 | Grpc.setLogVerbosity(Grpc.logVerbosity.ERROR);
17 | });
18 |
19 | it('logger defaults to console', () => {
20 | Assert.strictEqual(Logging.getLogger(), console);
21 | });
22 |
23 | it('sets the logger to a new value', () => {
24 | const logger = {};
25 |
26 | Grpc.setLogger(logger);
27 | Assert.strictEqual(Logging.getLogger(), logger);
28 | });
29 |
30 | it('gates logging based on severity', () => {
31 | const output = [];
32 | const logger = {
33 | error (...args) {
34 | output.push(args);
35 | }
36 | };
37 |
38 | Grpc.setLogger(logger);
39 |
40 | // The default verbosity (ERROR) should not log DEBUG or INFO data.
41 | Logging.log(Grpc.logVerbosity.DEBUG, 4, 5, 6);
42 | Logging.log(Grpc.logVerbosity.INFO, 7, 8);
43 | Logging.log(Grpc.logVerbosity.ERROR, 'j', 'k');
44 |
45 | // The DEBUG verbosity should log everything.
46 | Grpc.setLogVerbosity(Grpc.logVerbosity.DEBUG);
47 | Logging.log(Grpc.logVerbosity.DEBUG, 'a', 'b', 'c');
48 | Logging.log(Grpc.logVerbosity.INFO, 'd', 'e');
49 | Logging.log(Grpc.logVerbosity.ERROR, 'f');
50 |
51 | // The INFO verbosity should not log DEBUG data.
52 | Grpc.setLogVerbosity(Grpc.logVerbosity.INFO);
53 | Logging.log(Grpc.logVerbosity.DEBUG, 1, 2, 3);
54 | Logging.log(Grpc.logVerbosity.INFO, 'g');
55 | Logging.log(Grpc.logVerbosity.ERROR, 'h', 'i');
56 |
57 | Assert.deepStrictEqual(output, [
58 | ['j', 'k'],
59 | ['a', 'b', 'c'],
60 | ['d', 'e'],
61 | ['f'],
62 | ['g'],
63 | ['h', 'i']
64 | ]);
65 | });
66 |
67 | it('handles loggers with no error() function', () => {
68 | const logger = {};
69 |
70 | Grpc.setLogger(logger);
71 | Logging.log(Grpc.logVerbosity.ERROR, 'foo');
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/test/metadata.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const Assert = require('assert');
3 | const Lab = require('@hapi/lab');
4 | const { Metadata } = require('../lib');
5 |
6 | // Test shortcuts
7 | const lab = exports.lab = Lab.script();
8 | const { describe, it, beforeEach } = lab;
9 |
10 |
11 | describe('Metadata', () => {
12 | const validKeyChars = '0123456789abcdefghijklmnopqrstuvwxyz_-.';
13 | const validNonBinValueChars = range(0x20, 0x7f)
14 | .map((code) => { return String.fromCharCode(code); })
15 | .join('');
16 | let metadata;
17 |
18 | beforeEach(() => {
19 | metadata = new Metadata();
20 | });
21 |
22 | describe('set', () => {
23 | it('Only accepts string values for non-binary keys', () => {
24 | Assert.throws(() => {
25 | metadata.set('key', Buffer.from('value'));
26 | });
27 |
28 | Assert.doesNotThrow(() => {
29 | metadata.set('key', 'value');
30 | });
31 | });
32 |
33 | it('Only accepts Buffer values for binary keys', () => {
34 | Assert.throws(() => {
35 | metadata.set('key-bin', 'value');
36 | });
37 |
38 | Assert.doesNotThrow(() => {
39 | metadata.set('key-bin', Buffer.from('value'));
40 | });
41 | });
42 |
43 | it('Rejects invalid keys', () => {
44 | Assert.doesNotThrow(() => {
45 | metadata.set(validKeyChars, 'value');
46 | });
47 |
48 | Assert.throws(() => {
49 | metadata.set('key$', 'value');
50 | }, /Error: Metadata key "key\$" contains illegal characters/);
51 |
52 | Assert.throws(() => {
53 | metadata.set('', 'value');
54 | });
55 | });
56 |
57 | it('Rejects values with non-ASCII characters', () => {
58 | Assert.doesNotThrow(() => {
59 | metadata.set('key', validNonBinValueChars);
60 | });
61 | Assert.throws(() => {
62 | metadata.set('key', 'résumé');
63 | });
64 | });
65 |
66 | it('Saves values that can be retrieved', () => {
67 | metadata.set('key', 'value');
68 | Assert.deepStrictEqual(metadata.get('key'), ['value']);
69 | });
70 |
71 | it('Overwrites previous values', () => {
72 | metadata.set('key', 'value1');
73 | metadata.set('key', 'value2');
74 | Assert.deepStrictEqual(metadata.get('key'), ['value2']);
75 | });
76 |
77 | it('Normalizes keys', () => {
78 | metadata.set('Key', 'value1');
79 | Assert.deepStrictEqual(metadata.get('key'), ['value1']);
80 | metadata.set('KEY', 'value2');
81 | Assert.deepStrictEqual(metadata.get('key'), ['value2']);
82 | });
83 | });
84 |
85 | describe('add', () => {
86 | it('Only accepts string values for non-binary keys', () => {
87 | Assert.throws(() => {
88 | metadata.add('key', Buffer.from('value'));
89 | });
90 |
91 | Assert.doesNotThrow(() => {
92 | metadata.add('key', 'value');
93 | });
94 | });
95 |
96 | it('Only accepts Buffer values for binary keys', () => {
97 | Assert.throws(() => {
98 | metadata.add('key-bin', 'value');
99 | });
100 |
101 | Assert.doesNotThrow(() => {
102 | metadata.add('key-bin', Buffer.from('value'));
103 | });
104 | });
105 |
106 | it('Rejects invalid keys', () => {
107 | Assert.throws(() => {
108 | metadata.add('key$', 'value');
109 | });
110 |
111 | Assert.throws(() => {
112 | metadata.add('', 'value');
113 | });
114 | });
115 |
116 | it('Saves values that can be retrieved', () => {
117 | metadata.add('key', 'value');
118 | Assert.deepStrictEqual(metadata.get('key'), ['value']);
119 | });
120 |
121 | it('Combines with previous values', () => {
122 | metadata.add('key', 'value1');
123 | metadata.add('key', 'value2');
124 | Assert.deepStrictEqual(metadata.get('key'), ['value1', 'value2']);
125 | });
126 |
127 | it('Normalizes keys', () => {
128 | metadata.add('Key', 'value1');
129 | Assert.deepStrictEqual(metadata.get('key'), ['value1']);
130 | metadata.add('KEY', 'value2');
131 | Assert.deepStrictEqual(metadata.get('key'), ['value1', 'value2']);
132 | });
133 | });
134 |
135 | describe('remove', () => {
136 | it('clears values from a key', () => {
137 | metadata.add('key', 'value');
138 | metadata.remove('key');
139 | Assert.deepStrictEqual(metadata.get('key'), []);
140 | });
141 |
142 | it('Normalizes keys', () => {
143 | metadata.add('key', 'value');
144 | metadata.remove('KEY');
145 | Assert.deepStrictEqual(metadata.get('key'), []);
146 | });
147 | });
148 |
149 | describe('get', () => {
150 | beforeEach(() => {
151 | metadata.add('key', 'value1');
152 | metadata.add('key', 'value2');
153 | metadata.add('key-bin', Buffer.from('value'));
154 | });
155 |
156 | it('gets all values associated with a key', () => {
157 | Assert.deepStrictEqual(metadata.get('key'), ['value1', 'value2']);
158 | });
159 |
160 | it('Normalizes keys', () => {
161 | Assert.deepStrictEqual(metadata.get('KEY'), ['value1', 'value2']);
162 | });
163 |
164 | it('returns an empty list for non-existent keys', () => {
165 | Assert.deepStrictEqual(metadata.get('non-existent-key'), []);
166 | });
167 |
168 | it('returns Buffers for binary keys', () => {
169 | Assert.ok(metadata.get('key-bin')[0] instanceof Buffer);
170 | });
171 | });
172 |
173 | describe('getMap', () => {
174 | it('gets a map of keys to values', () => {
175 | metadata.add('key1', 'value1');
176 | metadata.add('Key2', 'value2');
177 | metadata.add('KEY3', 'value3a');
178 | metadata.add('KEY3', 'value3b');
179 | metadata.add('key4-bin', Buffer.from('value4'));
180 | Assert.deepStrictEqual(metadata.getMap(), {
181 | key1: 'value1',
182 | key2: 'value2',
183 | key3: 'value3a',
184 | 'key4-bin': Buffer.from('value4')
185 | });
186 | });
187 | });
188 |
189 | describe('clone', () => {
190 | it('retains values from the original', () => {
191 | metadata.add('key', 'value');
192 | const copy = metadata.clone();
193 | Assert.deepStrictEqual(copy.get('key'), ['value']);
194 | });
195 |
196 | it('Does not see newly added values', () => {
197 | metadata.add('key', 'value1');
198 | const copy = metadata.clone();
199 | metadata.add('key', 'value2');
200 | Assert.deepStrictEqual(copy.get('key'), ['value1']);
201 | });
202 |
203 | it('Does not add new values to the original', () => {
204 | metadata.add('key', 'value1');
205 | const copy = metadata.clone();
206 | copy.add('key', 'value2');
207 | Assert.deepStrictEqual(metadata.get('key'), ['value1']);
208 | });
209 |
210 | it('Copy cannot modify binary values in the original', () => {
211 | const buf = Buffer.from('value-bin');
212 | metadata.add('key-bin', buf);
213 | const copy = metadata.clone();
214 | const copyBuf = copy.get('key-bin')[0];
215 | Assert.deepStrictEqual(copyBuf, buf);
216 | copyBuf.fill(0);
217 | Assert.notDeepStrictEqual(copyBuf, buf);
218 | });
219 | });
220 |
221 | describe('merge', () => {
222 | it('appends values from a given metadata object', () => {
223 | metadata.add('key1', 'value1');
224 | metadata.add('Key2', 'value2a');
225 | metadata.add('KEY3', 'value3a');
226 | metadata.add('key4', 'value4');
227 | const metadata2 = new Metadata();
228 | metadata2.add('KEY1', 'value1');
229 | metadata2.add('key2', 'value2b');
230 | metadata2.add('key3', 'value3b');
231 | metadata2.add('key5', 'value5a');
232 | metadata2.add('key5', 'value5b');
233 | const metadata2IR = metadata2.internalRepr;
234 | metadata.merge(metadata2);
235 | // Ensure metadata2 didn't change
236 | Assert.deepStrictEqual(
237 | metadata2.internalRepr,
238 | metadata2IR
239 | );
240 | Assert.deepStrictEqual(metadata.get('key1'), ['value1', 'value1']);
241 | Assert.deepStrictEqual(metadata.get('key2'), ['value2a', 'value2b']);
242 | Assert.deepStrictEqual(metadata.get('key3'), ['value3a', 'value3b']);
243 | Assert.deepStrictEqual(metadata.get('key4'), ['value4']);
244 | Assert.deepStrictEqual(metadata.get('key5'), ['value5a', 'value5b']);
245 | });
246 | });
247 |
248 | describe('toHttp2Headers', () => {
249 | it('creates an OutgoingHttpHeaders object with expected values', () => {
250 | metadata.add('key1', 'value1');
251 | metadata.add('Key2', 'value2');
252 | metadata.add('KEY3', 'value3a');
253 | metadata.add('key3', 'value3b');
254 | metadata.add('key-bin', Buffer.from(range(0, 16)));
255 | metadata.add('key-bin', Buffer.from(range(16, 32)));
256 | metadata.add('key-bin', Buffer.from(range(0, 32)));
257 | const headers = metadata.toHttp2Headers();
258 | Assert.deepStrictEqual(headers, {
259 | key1: ['value1'],
260 | key2: ['value2'],
261 | key3: ['value3a', 'value3b'],
262 | 'key-bin': [
263 | 'AAECAwQFBgcICQoLDA0ODw==',
264 | 'EBESExQVFhcYGRobHB0eHw==',
265 | 'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8='
266 | ]
267 | });
268 | });
269 |
270 | it('creates an empty header object from empty Metadata', () => {
271 | Assert.deepStrictEqual(metadata.toHttp2Headers(), {});
272 | });
273 | });
274 |
275 | describe('fromHttp2Headers', () => {
276 | it('creates a Metadata object with expected values', () => {
277 | const headers = {
278 | key1: 'value1',
279 | key2: ['value2'],
280 | key3: ['value3a', 'value3b'],
281 | key4: ['key4a, key4b'],
282 | key5: 'value5a, value5b',
283 | 'key-bin': [
284 | 'AAECAwQFBgcICQoLDA0ODw==',
285 | 'EBESExQVFhcYGRobHB0eHw==',
286 | 'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8='
287 | ]
288 | };
289 | const metadataFromHeaders = Metadata.fromHttp2Headers(headers);
290 | const internalRepr = metadataFromHeaders.internalRepr;
291 | const expected = new Map([
292 | ['key1', ['value1']],
293 | ['key2', ['value2']],
294 | ['key3', ['value3a', 'value3b']],
295 | ['key4', ['key4a, key4b']],
296 | ['key5', ['value5a, value5b']],
297 | [
298 | 'key-bin',
299 | [
300 | Buffer.from(range(0, 16)),
301 | Buffer.from(range(16, 32)),
302 | Buffer.from(range(0, 32))
303 | ]
304 | ]
305 | ]);
306 | Assert.deepStrictEqual(internalRepr, expected);
307 | });
308 |
309 | it('creates an empty Metadata object from empty headers', () => {
310 | const metadataFromHeaders = Metadata.fromHttp2Headers({});
311 | const internalRepr = metadataFromHeaders.internalRepr;
312 | Assert.deepStrictEqual(internalRepr, new Map());
313 | });
314 | });
315 |
316 | it('sets and gets metadata options', () => {
317 | const opts1 = { foo: 'bar' };
318 | const opts2 = { baz: 'quux' };
319 |
320 | const m = new Metadata(opts1);
321 | Assert.strictEqual(m.getOptions(), opts1);
322 | m.setOptions(opts2);
323 | Assert.strictEqual(m.getOptions(), opts2);
324 | });
325 | });
326 |
327 |
328 | function range (start, end) {
329 | const result = [];
330 |
331 | for (let i = start; i < end; i++) {
332 | result.push(i);
333 | }
334 |
335 | return result;
336 | }
337 |
--------------------------------------------------------------------------------
/test/options.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const Assert = require('assert');
3 | const Http2 = require('http2');
4 | const Lab = require('@hapi/lab');
5 | const { parseOptions } = require('../lib/options');
6 | const { describe, it } = exports.lab = Lab.script();
7 |
8 |
9 | describe('Options', () => {
10 | describe('parseOptions()', () => {
11 | it('parses default options', () => {
12 | Assert.deepStrictEqual(parseOptions(), {
13 | maxConcurrentStreams: undefined,
14 | maxFrameSize: Http2.getDefaultSettings().maxFrameSize,
15 | keepaliveTimeMs: 7200000,
16 | keepaliveTimeoutMs: 20000,
17 | maxSendMessageLength: Infinity,
18 | maxReceiveMessageLength: 4 * 1024 * 1024
19 | });
20 | });
21 |
22 | it('throws on unexpected options', () => {
23 | Assert.throws(() => {
24 | parseOptions({ foo: 'bar' });
25 | }, /^Error: unknown option: foo$/);
26 | });
27 |
28 | it('grpc.max_{send,receive}_message_length maps -1 to Infinity', () => {
29 | const options = parseOptions({
30 | 'grpc.max_send_message_length': -1,
31 | 'grpc.max_receive_message_length': -1
32 | });
33 |
34 | Assert.strictEqual(options.maxSendMessageLength, Infinity);
35 | Assert.strictEqual(options.maxReceiveMessageLength, Infinity);
36 | });
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/test/proto/echo_service.proto:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright 2018 gRPC authors.
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | *
17 | */
18 |
19 | syntax = "proto3";
20 |
21 | message EchoMessage {
22 | string value = 1;
23 | int32 value2 = 2;
24 | }
25 |
26 | service EchoService {
27 | rpc Echo (EchoMessage) returns (EchoMessage);
28 |
29 | rpc EchoClientStream (stream EchoMessage) returns (EchoMessage);
30 |
31 | rpc EchoServerStream (EchoMessage) returns (stream EchoMessage);
32 |
33 | rpc EchoBidiStream (stream EchoMessage) returns (stream EchoMessage);
34 | }
35 |
--------------------------------------------------------------------------------
/test/proto/math.proto:
--------------------------------------------------------------------------------
1 |
2 | // Copyright 2015 gRPC authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 |
16 | syntax = "proto3";
17 |
18 | package math;
19 |
20 | message DivArgs {
21 | int64 dividend = 1;
22 | int64 divisor = 2;
23 | }
24 |
25 | message DivReply {
26 | int64 quotient = 1;
27 | int64 remainder = 2;
28 | }
29 |
30 | message FibArgs {
31 | int64 limit = 1;
32 | }
33 |
34 | message Num {
35 | int64 num = 1;
36 | }
37 |
38 | message FibReply {
39 | int64 count = 1;
40 | }
41 |
42 | service Math {
43 | // Div divides DivArgs.dividend by DivArgs.divisor and returns the quotient
44 | // and remainder.
45 | rpc Div (DivArgs) returns (DivReply) {
46 | }
47 |
48 | // DivMany accepts an arbitrary number of division args from the client stream
49 | // and sends back the results in the reply stream. The stream continues until
50 | // the client closes its end; the server does the same after sending all the
51 | // replies. The stream ends immediately if either end aborts.
52 | rpc DivMany (stream DivArgs) returns (stream DivReply) {
53 | }
54 |
55 | // Fib generates numbers in the Fibonacci sequence. If FibArgs.limit > 0, Fib
56 | // generates up to limit numbers; otherwise it continues until the call is
57 | // canceled. Unlike Fib above, Fib has no final FibReply.
58 | rpc Fib (FibArgs) returns (stream Num) {
59 | }
60 |
61 | // Sum sums a stream of numbers, returning the final result once the stream
62 | // is closed.
63 | rpc Sum (stream Num) returns (Num) {
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/test/proto/test_messages.proto:
--------------------------------------------------------------------------------
1 | // Copyright 2015 gRPC authors.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | syntax = "proto3";
16 |
17 | message LongValues {
18 | int64 int_64 = 1;
19 | uint64 uint_64 = 2;
20 | sint64 sint_64 = 3;
21 | fixed64 fixed_64 = 4;
22 | sfixed64 sfixed_64 = 5;
23 | }
24 |
25 | message SequenceValues {
26 | bytes bytes_field = 1;
27 | repeated int32 repeated_field = 2;
28 | }
29 |
30 | message OneOfValues {
31 | oneof oneof_choice {
32 | int32 int_choice = 1;
33 | string string_choice = 2;
34 | }
35 | }
36 |
37 | enum TestEnum {
38 | ZERO = 0;
39 | ONE = 1;
40 | TWO = 2;
41 | }
42 |
43 | message EnumValues {
44 | TestEnum enum_value = 1;
45 | }
46 |
--------------------------------------------------------------------------------
/test/proto/test_service.proto:
--------------------------------------------------------------------------------
1 | // Copyright 2015 gRPC authors.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | syntax = "proto3";
16 |
17 | message Request {
18 | bool error = 1;
19 | string message = 2;
20 | }
21 |
22 | message Response {
23 | int32 count = 1;
24 | }
25 |
26 | service TestService {
27 | rpc Unary (Request) returns (Response) {
28 | }
29 |
30 | rpc ClientStream (stream Request) returns (Response) {
31 | }
32 |
33 | rpc ServerStream (Request) returns (stream Response) {
34 | }
35 |
36 | rpc BidiStream (stream Request) returns (stream Response) {
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/test/server-credentials.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const Assert = require('assert');
3 | const Fs = require('fs');
4 | const Path = require('path');
5 | const Barrier = require('cb-barrier');
6 | const Lab = require('@hapi/lab');
7 | const Grpc = require('@grpc/grpc-js');
8 | const { Server, ServerCredentials } = require('../lib');
9 | const { loadProtoFile } = require('./common');
10 |
11 | // Test shortcuts
12 | const lab = exports.lab = Lab.script();
13 | const { describe, it, before, after } = lab;
14 |
15 |
16 | const ca = Fs.readFileSync(Path.join(__dirname, 'fixtures', 'ca.pem'));
17 | const key = Fs.readFileSync(Path.join(__dirname, 'fixtures', 'server1.key'));
18 | const cert = Fs.readFileSync(Path.join(__dirname, 'fixtures', 'server1.pem'));
19 |
20 |
21 | describe('ServerCredentials', () => {
22 | describe('createInsecure', () => {
23 | it('creates an InsecureServerCredentials instance', () => {
24 | const creds = ServerCredentials.createInsecure();
25 |
26 | Assert.strictEqual(creds._isSecure(), false);
27 | Assert.strictEqual(creds._getSettings(), null);
28 | });
29 | });
30 |
31 | describe('createSsl', () => {
32 | it('accepts a buffer and array as the first two arguments', () => {
33 | const creds = ServerCredentials.createSsl(ca, []);
34 |
35 | Assert.strictEqual(creds._isSecure(), true);
36 | Assert.deepStrictEqual(creds._getSettings(), {
37 | ca,
38 | cert: [],
39 | ciphers: undefined,
40 | key: [],
41 | requestCert: false
42 | });
43 | });
44 |
45 | it('accepts a boolean as the third argument', () => {
46 | const creds = ServerCredentials.createSsl(ca, [], true);
47 |
48 | Assert.strictEqual(creds._isSecure(), true);
49 | Assert.deepStrictEqual(creds._getSettings(), {
50 | ca,
51 | cert: [],
52 | ciphers: undefined,
53 | key: [],
54 | requestCert: true
55 | });
56 | });
57 |
58 | it('accepts an object with two buffers in the second argument', () => {
59 | const keyCertPairs = [{ private_key: key, cert_chain: cert }];
60 | const creds = ServerCredentials.createSsl(null, keyCertPairs);
61 |
62 | Assert.strictEqual(creds._isSecure(), true);
63 | Assert.deepStrictEqual(creds._getSettings(), {
64 | ca: undefined,
65 | cert: [cert],
66 | ciphers: undefined,
67 | key: [key],
68 | requestCert: false
69 | });
70 | });
71 |
72 | it('accepts multiple objects in the second argument', () => {
73 | const keyCertPairs = [
74 | { private_key: key, cert_chain: cert },
75 | { private_key: key, cert_chain: cert }
76 | ];
77 | const creds = ServerCredentials.createSsl(null, keyCertPairs, false);
78 |
79 | Assert.strictEqual(creds._isSecure(), true);
80 | Assert.deepStrictEqual(creds._getSettings(), {
81 | ca: undefined,
82 | cert: [cert, cert],
83 | ciphers: undefined,
84 | key: [key, key],
85 | requestCert: false
86 | });
87 | });
88 |
89 | it('fails if the second argument is not an Array', () => {
90 | Assert.throws(() => {
91 | ServerCredentials.createSsl(ca, 'test');
92 | }, /TypeError: keyCertPairs must be an array/);
93 | });
94 |
95 | it('fails if the first argument is a non-Buffer value', () => {
96 | Assert.throws(() => {
97 | ServerCredentials.createSsl('test', []);
98 | }, /TypeError: rootCerts must be null or a Buffer/);
99 | });
100 |
101 | it('fails if the third argument is a non-boolean value', () => {
102 | Assert.throws(() => {
103 | ServerCredentials.createSsl(ca, [], 'test');
104 | }, /TypeError: checkClientCertificate must be a boolean/);
105 | });
106 |
107 | it('fails if the array elements are not objects', () => {
108 | Assert.throws(() => {
109 | ServerCredentials.createSsl(ca, ['test']);
110 | }, /TypeError: keyCertPair\[0\] must be an object/);
111 |
112 | Assert.throws(() => {
113 | ServerCredentials.createSsl(ca, [null]);
114 | }, /TypeError: keyCertPair\[0\] must be an object/);
115 | });
116 |
117 | it('fails if the object does not have a Buffer private_key', () => {
118 | const keyCertPairs = [{ private_key: 'test', cert_chain: cert }];
119 |
120 | Assert.throws(() => {
121 | ServerCredentials.createSsl(null, keyCertPairs);
122 | }, /TypeError: keyCertPair\[0\].private_key must be a Buffer/);
123 | });
124 |
125 | it('fails if the object does not have a Buffer cert_chain', () => {
126 | const keyCertPairs = [{ private_key: key, cert_chain: 'test' }];
127 |
128 | Assert.throws(() => {
129 | ServerCredentials.createSsl(null, keyCertPairs);
130 | }, /TypeError: keyCertPair\[0\].cert_chain must be a Buffer/);
131 | });
132 | });
133 |
134 | it('should bind to an unused port with ssl credentials', async () => {
135 | const keyCertPairs = [{ private_key: key, cert_chain: cert }];
136 | const creds = ServerCredentials.createSsl(ca, keyCertPairs, true);
137 | const server = new Server();
138 |
139 | await server.bind('localhost:0', creds);
140 | server.start();
141 | server.forceShutdown();
142 | });
143 |
144 | it('should bind to an unused port with insecure credentials', async () => {
145 | const server = new Server();
146 |
147 | await server.bind('localhost:0', ServerCredentials.createInsecure());
148 | server.start();
149 | server.forceShutdown();
150 | });
151 | });
152 |
153 | describe('client credentials', () => {
154 | let Client;
155 | let server;
156 | let port;
157 | let clientSslCreds;
158 | const clientOptions = {};
159 | function noop () {}
160 |
161 | before(async () => {
162 | const proto = loadProtoFile(Path.join(__dirname, 'proto', 'test_service.proto'));
163 |
164 | server = new Server();
165 | server.addService(proto.TestService.service, {
166 | unary (call, cb) {
167 | Assert.strictEqual(call.getDeadline(), Infinity);
168 | call.sendMetadata(call.metadata);
169 | cb(null, {});
170 | },
171 |
172 | clientStream (stream, cb) {
173 | stream.on('data', noop);
174 | stream.on('end', () => {
175 | Assert.strictEqual(stream.getDeadline(), Infinity);
176 | stream.sendMetadata(stream.metadata);
177 | cb(null, {});
178 | });
179 | },
180 |
181 | serverStream (stream) {
182 | Assert.strictEqual(stream.getDeadline(), Infinity);
183 | stream.sendMetadata(stream.metadata);
184 | stream.end();
185 | },
186 |
187 | bidiStream (stream) {
188 | Assert.strictEqual(stream.getDeadline(), Infinity);
189 | stream.on('data', noop);
190 | stream.on('end', () => {
191 | stream.sendMetadata(stream.metadata);
192 | stream.end();
193 | });
194 | }
195 | });
196 |
197 | const keyCertPairs = [{ private_key: key, cert_chain: cert }];
198 | const creds = ServerCredentials.createSsl(null, keyCertPairs);
199 | port = await server.bind('localhost:0', creds);
200 | server.start();
201 |
202 | Client = proto.TestService;
203 | clientSslCreds = Grpc.credentials.createSsl(ca);
204 | const hostOverride = 'foo.test.google.fr';
205 | clientOptions['grpc.ssl_target_name_override'] = hostOverride;
206 | clientOptions['grpc.default_authority'] = hostOverride;
207 | });
208 |
209 | after(() => {
210 | server.forceShutdown();
211 | });
212 |
213 | it('Should accept SSL creds for a client', () => {
214 | const barrier = new Barrier();
215 | const client = new Client(`localhost:${port}`, clientSslCreds, clientOptions);
216 |
217 | client.unary({}, (err, data) => {
218 | Assert.ifError(err);
219 | client.close();
220 | barrier.pass();
221 | });
222 |
223 | return barrier;
224 | });
225 |
226 | describe('Per-rpc creds', () => {
227 | let client;
228 | let updaterCreds;
229 |
230 | before(() => {
231 | client = new Client(`localhost:${port}`, clientSslCreds, clientOptions);
232 |
233 | function metadataUpdater (serviceUrl, callback) {
234 | const metadata = new Grpc.Metadata();
235 |
236 | metadata.set('plugin_key', 'plugin_value');
237 | callback(null, metadata);
238 | }
239 |
240 | updaterCreds = Grpc.credentials.createFromMetadataGenerator(metadataUpdater);
241 | });
242 |
243 | after(() => {
244 | client.close();
245 | });
246 |
247 | it('should update metadata on a unary call', () => {
248 | const barrier = new Barrier(2);
249 | const call = client.unary({}, { credentials: updaterCreds }, (err, data) => {
250 | Assert.ifError(err);
251 | barrier.pass();
252 | });
253 |
254 | call.on('metadata', (metadata) => {
255 | Assert.deepStrictEqual(metadata.get('plugin_key'), ['plugin_value']);
256 | barrier.pass();
257 | });
258 |
259 | return barrier;
260 | });
261 |
262 | it('should update metadata on a client streaming call', () => {
263 | const barrier = new Barrier(2);
264 | const call = client.clientStream({ credentials: updaterCreds }, (err, data) => {
265 | Assert.ifError(err);
266 | barrier.pass();
267 | });
268 |
269 | call.on('metadata', (metadata) => {
270 | Assert.deepStrictEqual(metadata.get('plugin_key'), ['plugin_value']);
271 | barrier.pass();
272 | });
273 |
274 | call.end();
275 | return barrier;
276 | });
277 |
278 | it('should update metadata on a server streaming call', () => {
279 | const barrier = new Barrier();
280 | const call = client.serverStream({}, { credentials: updaterCreds });
281 |
282 | call.on('data', noop);
283 | call.on('metadata', (metadata) => {
284 | Assert.deepStrictEqual(metadata.get('plugin_key'), ['plugin_value']);
285 | barrier.pass();
286 | });
287 |
288 | return barrier;
289 | });
290 |
291 | it('should update metadata on a bidi streaming call', () => {
292 | const barrier = new Barrier();
293 | const call = client.bidiStream({ credentials: updaterCreds });
294 |
295 | call.on('data', noop);
296 | call.on('metadata', (metadata) => {
297 | Assert.deepStrictEqual(metadata.get('plugin_key'), ['plugin_value']);
298 | barrier.pass();
299 | });
300 |
301 | call.end();
302 | return barrier;
303 | });
304 |
305 | it('should be able to use multiple plugin credentials', () => {
306 | function altMetadataUpdater (serviceUrl, callback) {
307 | const metadata = new Grpc.Metadata();
308 |
309 | metadata.set('other_plugin_key', 'other_plugin_value');
310 | callback(null, metadata);
311 | }
312 |
313 | const barrier = new Barrier(2);
314 | const altUpdaterCreds = Grpc.credentials.createFromMetadataGenerator(altMetadataUpdater);
315 | const combinedUpdater = Grpc.credentials.combineCallCredentials(updaterCreds, altUpdaterCreds);
316 | const call = client.unary({}, { credentials: combinedUpdater }, (err, data) => {
317 | Assert.ifError(err);
318 | barrier.pass();
319 | });
320 |
321 | call.on('metadata', (metadata) => {
322 | Assert.deepStrictEqual(metadata.get('plugin_key'), ['plugin_value']);
323 | Assert.deepStrictEqual(metadata.get('other_plugin_key'), ['other_plugin_value']);
324 | barrier.pass();
325 | });
326 |
327 | return barrier;
328 | });
329 | });
330 | });
331 |
--------------------------------------------------------------------------------
/test/server-resolver.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const Assert = require('assert');
3 | const Path = require('path');
4 | const Lab = require('@hapi/lab');
5 | const { resolveToListenOptions } = require('../lib/server-resolver');
6 |
7 | // Test shortcuts
8 | const { describe, it } = exports.lab = Lab.script();
9 |
10 |
11 | // Note(cjihrig): As of @grpc/grpc-js@0.6.15, the client claims to support Unix
12 | // domain sockets. However, testing the grpc-js client did not seem to work.
13 | // Testing grpcurl with the flags `-plaintext -unix -authority 'localhost'` did
14 | // work for an insecure server.
15 | describe('Server Resolver', () => {
16 | it('resolveToListenOptions() successfully parses inputs', () => {
17 | [
18 | [
19 | resolveToListenOptions('dns:localhost:81', true),
20 | { host: 'localhost', port: 81 }
21 | ],
22 | [
23 | resolveToListenOptions('dns:127.0.0.1:9999', true),
24 | { host: '127.0.0.1', port: 9999 }
25 | ],
26 | [
27 | resolveToListenOptions('dns:foo.bar.com:9999', false),
28 | { host: 'foo.bar.com', port: 9999 }
29 | ],
30 | [
31 | resolveToListenOptions('localhost:8080', true),
32 | { host: 'localhost', port: 8080 }
33 | ],
34 | [
35 | resolveToListenOptions('localhost:8080', false),
36 | { host: 'localhost', port: 8080 }
37 | ],
38 | [
39 | resolveToListenOptions('[::1]:123', true),
40 | { host: '[::1]', port: 123 }
41 | ],
42 | [
43 | resolveToListenOptions('[::1]', true),
44 | { host: '[::1]', port: 443 }
45 | ],
46 | [
47 | resolveToListenOptions('[::1]:80', true),
48 | { host: '[::1]', port: 80 }
49 | ],
50 | [
51 | resolveToListenOptions('localhost', true),
52 | { host: 'localhost', port: 443 }
53 | ],
54 | [
55 | resolveToListenOptions('localhost', false),
56 | { host: 'localhost', port: 80 }
57 | ],
58 | [
59 | resolveToListenOptions('localhost:80', false),
60 | { host: 'localhost', port: 80 }
61 | ],
62 | [
63 | resolveToListenOptions('localhost:80', true),
64 | { host: 'localhost', port: 80 }
65 | ],
66 | [
67 | resolveToListenOptions('localhost:81', true),
68 | { host: 'localhost', port: 81 }
69 | ],
70 | [
71 | resolveToListenOptions('localhost:443', false),
72 | { host: 'localhost', port: 443 }
73 | ],
74 | [
75 | resolveToListenOptions('dns:///localhost', false),
76 | { host: 'localhost', port: 80 }
77 | ],
78 | [
79 | resolveToListenOptions('unix:/foo/bar1', false),
80 | { path: Path.resolve('/foo/bar1') }
81 | ],
82 | [
83 | resolveToListenOptions('unix:./foo/../baz/bar2', false),
84 | { path: Path.join(process.cwd(), 'baz', 'bar2') }
85 | ],
86 | [
87 | resolveToListenOptions('unix:///foo/bar3', false),
88 | { path: '/foo/bar3' }
89 | ]
90 | ].forEach(([actual, expected]) => {
91 | Assert.deepStrictEqual(actual, expected);
92 | });
93 | });
94 |
95 | it('resolveToListenOptions() throws if unix:// path is not absolute', () => {
96 | Assert.throws(() => {
97 | resolveToListenOptions('unix://./foo', false);
98 | }, /^Error: 'unix:\/\/\.\/foo' must specify an absolute path$/);
99 | });
100 | });
101 |
--------------------------------------------------------------------------------
/test/server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const Assert = require('assert');
3 | const Fs = require('fs');
4 | const Http2 = require('http2');
5 | const Path = require('path');
6 | const Barrier = require('cb-barrier');
7 | const Lab = require('@hapi/lab');
8 | const Grpc = require('@grpc/grpc-js');
9 | const { Server, ServerCredentials } = require('../lib');
10 | const { loadProtoFile } = require('./common');
11 |
12 | // Test shortcuts
13 | const lab = exports.lab = Lab.script();
14 | const { describe, it, before, after, beforeEach, afterEach } = lab;
15 |
16 |
17 | const clientInsecureCreds = Grpc.credentials.createInsecure();
18 | const serverInsecureCreds = ServerCredentials.createInsecure();
19 |
20 |
21 | describe('Server', () => {
22 | describe('constructor', () => {
23 | it('should work with no arguments', () => {
24 | Assert.doesNotThrow(() => {
25 | new Server(); // eslint-disable-line no-new
26 | });
27 | });
28 |
29 | it('should work with an empty object argument', () => {
30 | const options = {};
31 |
32 | Assert.doesNotThrow(() => {
33 | new Server(options); // eslint-disable-line no-new
34 | });
35 |
36 | // The constructor applies default values. Verify that the user's
37 | // options are not overwritten.
38 | Assert.deepStrictEqual(options, {});
39 | });
40 |
41 | it('throws if arguments are the wrong type', () => {
42 | [null, 'foo', 5].forEach((value) => {
43 | Assert.throws(() => {
44 | new Server(value); // eslint-disable-line no-new
45 | }, /TypeError: options must be an object/);
46 | });
47 | });
48 |
49 | it('should be an instance of Server', () => {
50 | const server = new Server();
51 |
52 | Assert(server instanceof Server);
53 | });
54 | });
55 |
56 | describe('bind', () => {
57 | it('uses insecure credentials by default', async () => {
58 | const server = new Server();
59 |
60 | server.bindAsync = function (port, creds, callback) {
61 | Assert.strictEqual(creds._isSecure(), false);
62 | callback(null, 1000);
63 | };
64 |
65 | await server.bind('localhost:0');
66 | await server.bind('localhost:0', null);
67 | });
68 |
69 | it('handles errors during binding', async () => {
70 | const server = new Server();
71 |
72 | server.bindAsync = function (port, creds, callback) {
73 | callback(new Error('test error'), -1);
74 | };
75 |
76 | await Assert.rejects(async () => {
77 | await server.bind('localhost:0');
78 | }, /^Error: test error$/);
79 | });
80 | });
81 |
82 | describe('bindAsync', () => {
83 | it('binds with insecure credentials', () => {
84 | const server = new Server();
85 | const barrier = new Barrier();
86 |
87 | server.bindAsync('localhost:0', serverInsecureCreds, (err, port) => {
88 | Assert.ifError(err);
89 | Assert(typeof port === 'number' && port > 0);
90 | server.tryShutdown(barrier.pass);
91 | });
92 |
93 | return barrier;
94 | });
95 |
96 | it('binds with secure credentials', () => {
97 | const server = new Server();
98 | const barrier = new Barrier();
99 | const ca = Fs.readFileSync(Path.join(__dirname, 'fixtures', 'ca.pem'));
100 | const key = Fs.readFileSync(Path.join(__dirname, 'fixtures', 'server1.key'));
101 | const cert = Fs.readFileSync(Path.join(__dirname, 'fixtures', 'server1.pem'));
102 |
103 | const creds = ServerCredentials.createSsl(ca,
104 | [{ private_key: key, cert_chain: cert }], true);
105 |
106 | server.bindAsync('localhost:0', creds, (err, port) => {
107 | Assert.ifError(err);
108 | Assert(typeof port === 'number' && port > 0);
109 | server.tryShutdown(barrier.pass);
110 | });
111 |
112 | return barrier;
113 | });
114 |
115 | it('throws if bind is called after the server is started', () => {
116 | const server = new Server();
117 | const barrier = new Barrier();
118 |
119 | server.bindAsync('localhost:0', serverInsecureCreds, (err, port) => {
120 | Assert.ifError(err);
121 | server.start();
122 | Assert.throws(() => {
123 | server.bindAsync('localhost:0', serverInsecureCreds, () => {});
124 | }, /server is already started/);
125 | server.tryShutdown(barrier.pass);
126 | });
127 |
128 | return barrier;
129 | });
130 |
131 | it('handles errors while trying to bind', () => {
132 | const server1 = new Server();
133 | const server2 = new Server();
134 | const barrier = new Barrier();
135 |
136 | server1.bindAsync('localhost:0', serverInsecureCreds, (err, port) => {
137 | Assert.ifError(err);
138 | Assert(typeof port === 'number' && port > 0);
139 | server2.bindAsync(`localhost:${port}`, serverInsecureCreds, (err, port) => {
140 | Assert.strictEqual(err.code, 'EADDRINUSE');
141 | Assert.strictEqual(port, -1);
142 | server1.tryShutdown(() => {
143 | server2.tryShutdown(barrier.pass);
144 | });
145 | });
146 | });
147 |
148 | return barrier;
149 | });
150 |
151 | it('throws on invalid inputs', () => {
152 | const server = new Server();
153 |
154 | Assert.throws(() => {
155 | server.bindAsync(null, serverInsecureCreds, () => {});
156 | }, /port must be a string/);
157 |
158 | Assert.throws(() => {
159 | server.bindAsync('localhost:0', null, () => {});
160 | }, /creds must be an object/);
161 |
162 | Assert.throws(() => {
163 | server.bindAsync('localhost:0', 'foo', () => {});
164 | }, /creds must be an object/);
165 |
166 | Assert.throws(() => {
167 | server.bindAsync('localhost:0', serverInsecureCreds, null);
168 | }, /callback must be a function/);
169 | });
170 | });
171 |
172 | describe('start', () => {
173 | let server;
174 |
175 | beforeEach(async () => {
176 | server = new Server();
177 | await server.bind(8000, ServerCredentials.createInsecure());
178 | });
179 |
180 | afterEach(() => {
181 | server.forceShutdown();
182 | });
183 |
184 | it('should start without error', () => {
185 | Assert.doesNotThrow(() => {
186 | server.start();
187 | });
188 | });
189 |
190 | it('should error if started twice', () => {
191 | server.start();
192 | Assert.throws(() => {
193 | server.start();
194 | }, /server is already started/);
195 | });
196 |
197 | it('should error if bind is called after the server starts', () => {
198 | server.start();
199 | Assert.rejects(async () => {
200 | await server.bind('localhost:0', serverInsecureCreds);
201 | }, /server is already started/);
202 | });
203 |
204 | it('throws if the server is not bound', () => {
205 | const server = new Server();
206 |
207 | Assert.throws(() => {
208 | server.start();
209 | }, /server must be bound in order to start/);
210 | });
211 | });
212 |
213 | describe('Server.prototype.unregister', () => {
214 | const mathProtoFile = Path.join(__dirname, 'proto', 'math.proto');
215 | const MathClient = loadProtoFile(mathProtoFile).math.Math;
216 | const mathServiceAttrs = MathClient.service;
217 |
218 | let server;
219 | let client;
220 |
221 | beforeEach(() => {
222 | const barrier = new Barrier();
223 | server = new Server();
224 | server.addService(mathServiceAttrs, {
225 | div (call, callback) {
226 | callback(null, { quotient: '42' });
227 | }
228 | });
229 | server.bindAsync('localhost:0', serverInsecureCreds, (err, port) => {
230 | Assert.ifError(err);
231 | client = new MathClient(`localhost:${port}`, clientInsecureCreds);
232 | server.start();
233 | barrier.pass();
234 | });
235 | return barrier;
236 | });
237 |
238 | afterEach(() => {
239 | client.close();
240 | server.forceShutdown();
241 | });
242 |
243 | it('removes existing handler', () => {
244 | const barrier = new Barrier();
245 |
246 | client.div({ divisor: 4, dividend: 3 }, (err, result) => {
247 | Assert.ifError(err);
248 | Assert.deepStrictEqual(result, { quotient: '42', remainder: '0' });
249 |
250 | const name = mathServiceAttrs['Div'].path;
251 | Assert.strictEqual(server.unregister(name), true);
252 |
253 | client.div({ divisor: 4, dividend: 3 }, (err) => {
254 | Assert(err);
255 | Assert.strictEqual(err.code, Grpc.status.UNIMPLEMENTED);
256 | barrier.pass();
257 | });
258 | });
259 |
260 | return barrier;
261 | });
262 |
263 | it('returns false for unknown handler', () => {
264 | Assert.strictEqual(server.unregister('does-not-exist'), false);
265 | });
266 | });
267 |
268 | describe('Server.prototype.addService', () => {
269 | const mathProtoFile = Path.join(__dirname, 'proto', 'math.proto');
270 | const MathClient = loadProtoFile(mathProtoFile).math.Math;
271 | const mathServiceAttrs = MathClient.service;
272 | const dummyImpls = {
273 | div () {},
274 | divMany () {},
275 | fib () {},
276 | sum () {}
277 | };
278 | const altDummyImpls = {
279 | Div () {},
280 | DivMany () {},
281 | Fib () {},
282 | Sum () {}
283 | };
284 | let server;
285 |
286 | beforeEach(() => {
287 | server = new Server();
288 | });
289 |
290 | afterEach(() => {
291 | server.forceShutdown();
292 | });
293 |
294 | it('Should succeed with a single service', () => {
295 | Assert.doesNotThrow(() => {
296 | server.addService(mathServiceAttrs, dummyImpls);
297 | });
298 | });
299 |
300 | it('Should fail with conflicting method names', () => {
301 | server.addService(mathServiceAttrs, dummyImpls);
302 | Assert.throws(() => {
303 | server.addService(mathServiceAttrs, dummyImpls);
304 | });
305 | });
306 |
307 | it('Should allow method names as originally written', () => {
308 | Assert.doesNotThrow(() => {
309 | server.addService(mathServiceAttrs, altDummyImpls);
310 | });
311 | });
312 |
313 | it('Should succeed even if the server has already been started', async () => {
314 | await server.bind('localhost:0', serverInsecureCreds);
315 | server.start();
316 | server.addService(mathServiceAttrs, dummyImpls);
317 | });
318 |
319 | it('fails trying to add an empty service', () => {
320 | Assert.throws(() => {
321 | server.addService({}, {});
322 | }, /^Error: Cannot add an empty service to a server$/);
323 | });
324 |
325 | it('fails if both inputs are not objects', () => {
326 | [
327 | [null, {}],
328 | ['foo', {}],
329 | [{}, null],
330 | [{}, 'foo']
331 | ].forEach((inputs) => {
332 | Assert.throws(() => {
333 | server.addService(inputs[0], inputs[1]);
334 | });
335 | });
336 | });
337 |
338 | describe('Default handlers', () => {
339 | let client;
340 |
341 | beforeEach(async () => {
342 | server.addService(mathServiceAttrs, {});
343 | const port = await server.bind('localhost:0', serverInsecureCreds);
344 | client = new MathClient(`localhost:${port}`, clientInsecureCreds);
345 | server.start();
346 | });
347 |
348 | afterEach(() => {
349 | client.close();
350 | server.forceShutdown();
351 | });
352 |
353 | it('should respond to a unary call with UNIMPLEMENTED', () => {
354 | const barrier = new Barrier();
355 |
356 | client.div({ divisor: 4, dividend: 3 }, (error, response) => {
357 | Assert(error);
358 | Assert.strictEqual(error.code, Grpc.status.UNIMPLEMENTED);
359 | Assert.strictEqual(error.details, 'The server does not implement the method Div');
360 | barrier.pass();
361 | });
362 |
363 | return barrier;
364 | });
365 |
366 | it('should respond to a client stream with UNIMPLEMENTED', () => {
367 | const barrier = new Barrier();
368 | const call = client.sum((error, respones) => {
369 | Assert(error);
370 | Assert.strictEqual(error.code, Grpc.status.UNIMPLEMENTED);
371 | Assert.strictEqual(error.details, 'The server does not implement the method Sum');
372 | barrier.pass();
373 | });
374 |
375 | call.end();
376 | return barrier;
377 | });
378 |
379 | it('should respond to a server stream with UNIMPLEMENTED', () => {
380 | const barrier = new Barrier();
381 | const call = client.fib({ limit: 5 });
382 |
383 | call.on('data', (value) => {
384 | Assert.fail('No messages expected');
385 | });
386 |
387 | call.on('error', (err) => {
388 | Assert(err);
389 | Assert.strictEqual(err.code, Grpc.status.UNIMPLEMENTED);
390 | Assert.strictEqual(err.details, 'The server does not implement the method Fib');
391 | barrier.pass();
392 | });
393 |
394 | return barrier;
395 | });
396 |
397 | it('should respond to a bidi call with UNIMPLEMENTED', () => {
398 | const barrier = new Barrier();
399 | const call = client.divMany();
400 |
401 | call.on('data', (value) => {
402 | Assert.fail('No messages expected');
403 | });
404 |
405 | call.on('error', (err) => {
406 | Assert.strictEqual(err.code, Grpc.status.UNIMPLEMENTED);
407 | Assert.strictEqual(err.details, 'The server does not implement the method DivMany');
408 | barrier.pass();
409 | });
410 |
411 | call.end();
412 |
413 | return barrier;
414 | });
415 | });
416 | });
417 |
418 | describe('Server.prototype.removeService', () => {
419 | const mathProtoFile = Path.join(__dirname, 'proto', 'math.proto');
420 | const MathClient = loadProtoFile(mathProtoFile).math.Math;
421 | const mathServiceAttrs = MathClient.service;
422 | const dummyImpls = {
423 | div () {},
424 | divMany () {},
425 | fib () {},
426 | sum () {}
427 | };
428 |
429 | let server;
430 | let client;
431 |
432 | beforeEach(() => {
433 | const barrier = new Barrier();
434 | server = new Server();
435 | server.addService(mathServiceAttrs, dummyImpls);
436 | server.bindAsync('localhost:0', serverInsecureCreds, (err, port) => {
437 | Assert.ifError(err);
438 | client = new MathClient(`localhost:${port}`, clientInsecureCreds);
439 | server.start();
440 | barrier.pass();
441 | });
442 | return barrier;
443 | });
444 |
445 | afterEach(() => {
446 | client.close();
447 | server.forceShutdown();
448 | });
449 |
450 | it('removes a service', () => {
451 | const barrier = new Barrier();
452 | server.removeService(mathServiceAttrs);
453 |
454 | let methodsVerifiedCount = 0;
455 | const methodsToVerify = Object.keys(mathServiceAttrs);
456 |
457 | const assertFailsWithUnimplementedError = (err) => {
458 | Assert(err);
459 | Assert.strictEqual(err.code, Grpc.status.UNIMPLEMENTED);
460 | methodsVerifiedCount++;
461 | if (methodsVerifiedCount === methodsToVerify.length) {
462 | barrier.pass();
463 | }
464 | };
465 |
466 | methodsToVerify.forEach((method) => {
467 | const call = client[method]({}, assertFailsWithUnimplementedError);
468 | call.on('error', assertFailsWithUnimplementedError);
469 | });
470 |
471 | return barrier;
472 | });
473 |
474 | it('fails if input is not an object', () => {
475 | [undefined, null, 'foo', 5, true].forEach((input) => {
476 | Assert.throws(() => {
477 | server.removeService(input);
478 | }, /^Error: removeService requires an object argument$/);
479 | });
480 | });
481 | });
482 |
483 | describe('Server.prototype.tryShutdown', () => {
484 | it('calls back without an error if the server is not bound', () => {
485 | const barrier = new Barrier();
486 | const server = new Server();
487 |
488 | server.tryShutdown((err) => {
489 | Assert.ifError(err);
490 | barrier.pass();
491 | });
492 |
493 | return barrier;
494 | });
495 |
496 | it('is idempotent with itself', async () => {
497 | const barrier = new Barrier();
498 | const server = new Server();
499 |
500 | await server.bind('localhost:0', serverInsecureCreds);
501 | server.start();
502 | server.tryShutdown((err) => {
503 | Assert.ifError(err);
504 | server.tryShutdown((err) => {
505 | Assert.ifError(err);
506 | barrier.pass();
507 | });
508 | });
509 |
510 | return barrier;
511 | });
512 |
513 | it('is idempotent with forceShutdown()', async () => {
514 | const barrier = new Barrier();
515 | const server = new Server();
516 |
517 | await server.bind('localhost:0', serverInsecureCreds);
518 | server.start();
519 | server.tryShutdown((err) => {
520 | Assert.ifError(err);
521 | server.forceShutdown();
522 | barrier.pass();
523 | });
524 |
525 | return barrier;
526 | });
527 | });
528 |
529 | describe('Server.prototype.forceShutdown', () => {
530 | it('does not throw if the server is not bound', () => {
531 | const server = new Server();
532 |
533 | server.forceShutdown();
534 | });
535 |
536 | it('is idempotent with itself', async () => {
537 | const server = new Server();
538 |
539 | await server.bind('localhost:0', serverInsecureCreds);
540 | server.start();
541 | server.forceShutdown();
542 | server.forceShutdown();
543 | });
544 |
545 | it('is idempotent with tryShutdown()', async () => {
546 | const barrier = new Barrier();
547 | const server = new Server();
548 |
549 | await server.bind('localhost:0', serverInsecureCreds);
550 | server.start();
551 | server.forceShutdown();
552 | server.tryShutdown((err) => {
553 | Assert.ifError(err);
554 | barrier.pass();
555 | });
556 |
557 | return barrier;
558 | });
559 |
560 | it('forcefully closes connections', async () => {
561 | const barrier = new Barrier();
562 | const server = new Server();
563 | const protoFile = Path.join(__dirname, 'proto', 'echo_service.proto');
564 | const { EchoService } = loadProtoFile(protoFile);
565 | let calledForceShutdown = false;
566 | let client; // eslint-disable-line prefer-const
567 |
568 | server.addService(EchoService.service, {
569 | echoBidiStream (stream) {
570 | // Verify that forceShutdown() triggers tryShutdown().
571 | server.tryShutdown(() => {
572 | Assert.strictEqual(calledForceShutdown, true);
573 | client.close();
574 | barrier.pass();
575 | });
576 |
577 | stream.write({});
578 | }
579 | });
580 |
581 | const port = await server.bind('localhost:0', serverInsecureCreds);
582 | client = new EchoService(`localhost:${port}`, clientInsecureCreds);
583 | server.start();
584 | const stream = client.echoBidiStream();
585 |
586 | stream.on('data', (message) => {
587 | Assert.deepStrictEqual(message, { value: '', value2: 0 });
588 | server.forceShutdown();
589 | calledForceShutdown = true;
590 | });
591 |
592 | stream.on('error', (err) => {
593 | Assert(err);
594 | });
595 |
596 | return barrier;
597 | });
598 | });
599 |
600 | describe('Echo service', () => {
601 | let server;
602 | let client;
603 |
604 | before(async () => {
605 | const protoFile = Path.join(__dirname, 'proto', 'echo_service.proto');
606 | const { EchoService } = loadProtoFile(protoFile);
607 |
608 | server = new Server();
609 | server.addService(EchoService.service, {
610 | echo (call, callback) {
611 | callback(null, call.request);
612 | }
613 | });
614 |
615 | const port = await server.bind('localhost:0', serverInsecureCreds);
616 | client = new EchoService(`localhost:${port}`, clientInsecureCreds);
617 | server.start();
618 | });
619 |
620 | after(() => {
621 | client.close();
622 | server.forceShutdown();
623 | });
624 |
625 | it('should echo the received message directly', () => {
626 | const barrier = new Barrier();
627 |
628 | client.echo({ value: 'test value', value2: 3 }, (error, response) => {
629 | Assert.ifError(error);
630 | Assert.deepStrictEqual(response, { value: 'test value', value2: 3 });
631 | barrier.pass();
632 | });
633 |
634 | return barrier;
635 | });
636 | });
637 |
638 | describe('Generic client and server', () => {
639 | function toString (val) {
640 | return val.toString();
641 | }
642 |
643 | function toBuffer (str) {
644 | return Buffer.from(str);
645 | }
646 |
647 | function capitalize (str) {
648 | return str.charAt(0).toUpperCase() + str.slice(1);
649 | }
650 |
651 | const stringServiceAttrs = {
652 | capitalize: {
653 | path: '/string/capitalize',
654 | requestStream: false,
655 | responseStream: false,
656 | requestSerialize: toBuffer,
657 | requestDeserialize: toString,
658 | responseSerialize: toBuffer,
659 | responseDeserialize: toString
660 | }
661 | };
662 |
663 | describe('String client and server', () => {
664 | let client;
665 | let server;
666 |
667 | before(async () => {
668 | server = new Server();
669 |
670 | server.addService(stringServiceAttrs, {
671 | capitalize (call, callback) {
672 | callback(null, capitalize(call.request));
673 | }
674 | });
675 |
676 | const port = await server.bind('localhost:0', serverInsecureCreds);
677 | server.start();
678 | const Client = Grpc.makeGenericClientConstructor(stringServiceAttrs);
679 | client = new Client(`localhost:${port}`, clientInsecureCreds);
680 | });
681 |
682 | after(() => {
683 | client.close();
684 | server.forceShutdown();
685 | });
686 |
687 | it('Should respond with a capitalized string', () => {
688 | const barrier = new Barrier();
689 |
690 | client.capitalize('abc', (err, response) => {
691 | Assert.ifError(err);
692 | Assert.strictEqual(response, 'Abc');
693 | barrier.pass();
694 | });
695 |
696 | return barrier;
697 | });
698 | });
699 | });
700 |
701 | it('throws when unimplemented methods are called', () => {
702 | const server = new Server();
703 |
704 | Assert.throws(() => {
705 | server.addProtoService();
706 | }, /not implemented. use addService\(\) instead/);
707 |
708 | Assert.throws(() => {
709 | server.addHttp2Port();
710 | }, /not implemented/);
711 | });
712 |
713 | it('responds with HTTP status of 415 on invalid content-type', async () => {
714 | const barrier = new Barrier();
715 | const server = new Server();
716 | const port = await server.bind('localhost:0', serverInsecureCreds);
717 | const client = Http2.connect(`http://localhost:${port}`);
718 | let count = 0;
719 |
720 | server.start();
721 |
722 | function makeRequest (headers) {
723 | const req = client.request(headers);
724 | let statusCode;
725 |
726 | req.on('response', (headers) => {
727 | statusCode = headers[Http2.constants.HTTP2_HEADER_STATUS];
728 | });
729 |
730 | req.on('end', () => {
731 | Assert.strictEqual(statusCode, Http2.constants.HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE);
732 | count++;
733 | if (count === 2) {
734 | client.close();
735 | server.tryShutdown(barrier.pass);
736 | }
737 | });
738 |
739 | req.end();
740 | }
741 |
742 | // Missing Content-Type header.
743 | makeRequest({ ':path': '/' });
744 | // Invalid Content-Type header.
745 | makeRequest({ ':path': '/', 'content-type': 'application/not-grpc' });
746 | return barrier;
747 | });
748 |
749 | it('rejects connections if the server is bound but not started', async () => {
750 | const barrier = new Barrier();
751 | const server = new Server();
752 | const port = await server.bind('localhost:0', serverInsecureCreds);
753 | const protoFile = Path.join(__dirname, 'proto', 'echo_service.proto');
754 | const { EchoService } = loadProtoFile(protoFile);
755 | const client = new EchoService(`localhost:${port}`, clientInsecureCreds);
756 |
757 | client.echo({ value: 'test value', value2: 3 }, (error, response) => {
758 | Assert.strictEqual(error.code, Grpc.status.UNAVAILABLE);
759 | Assert.strictEqual(response, undefined);
760 | client.close();
761 | server.tryShutdown(barrier.pass);
762 | });
763 |
764 | return barrier;
765 | });
766 |
767 | it('returns UNIMPLEMENTED on 404', async () => {
768 | const barrier = new Barrier();
769 | const server = new Server();
770 | const port = await server.bind('localhost:0', serverInsecureCreds);
771 | const protoFile = Path.join(__dirname, 'proto', 'echo_service.proto');
772 | const { EchoService } = loadProtoFile(protoFile);
773 | const client = new EchoService(`localhost:${port}`, clientInsecureCreds);
774 |
775 | server.start();
776 | client.echo({ value: 'test value', value2: 3 }, (error, response) => {
777 | Assert.strictEqual(error.code, Grpc.status.UNIMPLEMENTED);
778 | Assert.strictEqual(error.details, 'The server does not implement the method /EchoService/Echo');
779 | Assert.strictEqual(response, undefined);
780 | client.close();
781 | server.tryShutdown(barrier.pass);
782 | });
783 |
784 | return barrier;
785 | });
786 |
787 | it('sends keepalive pings', async () => {
788 | const barrier = new Barrier();
789 | const server = new Server({
790 | 'grpc.keepalive_time_ms': 10,
791 | 'grpc.keepalive_timeout_ms': 1
792 | });
793 | const protoFile = Path.join(__dirname, 'proto', 'echo_service.proto');
794 | const { EchoService } = loadProtoFile(protoFile);
795 |
796 | server.addService(EchoService.service, {
797 | echoBidiStream (stream) {
798 | stream.on('data', (data) => {
799 | Assert.fail('no data events expected on server');
800 | });
801 | }
802 | });
803 |
804 | const port = await server.bind('localhost:0', serverInsecureCreds);
805 | const client = new EchoService(`localhost:${port}`, clientInsecureCreds);
806 | server.start();
807 | const stream = client.echoBidiStream();
808 |
809 | stream.on('close', () => {
810 | Assert.fail('close event not expected on client');
811 | });
812 |
813 | stream.on('end', () => {
814 | Assert.fail('end event not expected on client');
815 | });
816 |
817 | stream.on('error', (err) => {
818 | Assert(err);
819 | client.close();
820 | server.tryShutdown(barrier.pass);
821 | });
822 |
823 | return barrier;
824 | });
825 |
826 | it('handles multiple messages in a single frame', async () => {
827 | const barrier = new Barrier();
828 | const server = new Server();
829 | const protoFile = Path.join(__dirname, 'proto', 'echo_service.proto');
830 | const { EchoService } = loadProtoFile(protoFile);
831 | let receivedCount = 0;
832 |
833 | server.addService(EchoService.service, {
834 | echoBidiStream (stream) {
835 | stream.pause();
836 |
837 | setImmediate(() => {
838 | stream.resume();
839 | });
840 |
841 | stream.on('data', (data) => {
842 | Assert.deepStrictEqual(data, { value: '', value2: 0 });
843 | receivedCount++;
844 |
845 | // The value 20 is dependent on the number of bytes that each message
846 | // serializes to. If this test ever starts failing, it's likely due to
847 | // a change in protobuf.js, and the expected number may change.
848 | if (receivedCount === 20) {
849 | stream.end();
850 | client.close(); // eslint-disable-line no-use-before-define
851 | server.tryShutdown(barrier.pass);
852 | }
853 | });
854 | }
855 | });
856 |
857 | const port = await server.bind('localhost:0', serverInsecureCreds);
858 | server.start();
859 |
860 | const client = Http2.connect(`http://localhost:${port}`);
861 | const req = client.request({
862 | [Http2.constants.HTTP2_HEADER_PATH]: '/EchoService/EchoBidiStream',
863 | [Http2.constants.HTTP2_HEADER_METHOD]: 'POST',
864 | [Http2.constants.HTTP2_HEADER_CONTENT_TYPE]: 'application/grpc'
865 | });
866 |
867 | req.write(Buffer.alloc(100));
868 | req.end();
869 | return barrier;
870 | });
871 |
872 | it('stream handlers can serialize and deserialize messages', async () => {
873 | const barrier = new Barrier();
874 | const server = new Server();
875 | const protoFile = Path.join(__dirname, 'proto', 'echo_service.proto');
876 | const { EchoService } = loadProtoFile(protoFile);
877 |
878 | server.addService(EchoService.service, {
879 | echoBidiStream (stream) {
880 | stream.on('data', (data) => {
881 | Assert.deepStrictEqual(data, { value: '', value2: 0 });
882 | const bytes = stream.serialize(data);
883 | const message = stream.deserialize(bytes);
884 |
885 | // Verify serialize-deserialize functionality.
886 | Assert(bytes instanceof Buffer);
887 | Assert.deepStrictEqual(message, data);
888 |
889 | // Verify handling of edge cases.
890 | Assert.strictEqual(stream.serialize(null), null);
891 | Assert.strictEqual(stream.serialize(undefined), null);
892 | Assert.strictEqual(stream.deserialize(null), null);
893 | Assert.strictEqual(stream.deserialize(undefined), null);
894 | stream.end();
895 | });
896 | }
897 | });
898 |
899 | const port = await server.bind('localhost:0', serverInsecureCreds);
900 | const client = new EchoService(`localhost:${port}`, clientInsecureCreds);
901 | server.start();
902 | const stream = client.echoBidiStream();
903 |
904 | stream.write({});
905 | stream.on('status', () => {
906 | client.close();
907 | server.forceShutdown();
908 | barrier.pass();
909 | });
910 | stream.end();
911 |
912 | return barrier;
913 | });
914 |
915 | it('can serve traffic on multiple ports', async () => {
916 | const barrier = new Barrier();
917 | const protoFile = Path.join(__dirname, 'proto', 'echo_service.proto');
918 | const { EchoService } = loadProtoFile(protoFile);
919 | const server = new Server();
920 |
921 | server.addService(EchoService.service, {
922 | echo (call, callback) {
923 | callback(null, call.request);
924 | }
925 | });
926 |
927 | const port1 = await server.bind('localhost:0', serverInsecureCreds);
928 | const port2 = await server.bind('localhost:0', serverInsecureCreds);
929 | Assert.notStrictEqual(port1, port2);
930 | server.start();
931 | const client1 = new EchoService(`localhost:${port1}`, clientInsecureCreds);
932 | const client2 = new EchoService(`localhost:${port2}`, clientInsecureCreds);
933 |
934 | client1.echo({ value: 'test value', value2: 3 }, (error, response) => {
935 | Assert.ifError(error);
936 | Assert.deepStrictEqual(response, { value: 'test value', value2: 3 });
937 | client2.echo({ value: 'test two', value2: 99 }, (error, response) => {
938 | Assert.ifError(error);
939 | Assert.deepStrictEqual(response, { value: 'test two', value2: 99 });
940 | client1.close();
941 | client2.close();
942 | server.forceShutdown();
943 | barrier.pass();
944 | });
945 | });
946 |
947 | return barrier;
948 | });
949 |
950 | describe('Unix Domain Socket Support', () => {
951 | const protoFile = Path.join(__dirname, 'proto', 'echo_service.proto');
952 | const { EchoService } = loadProtoFile(protoFile);
953 | const tmpDir = Path.join(__dirname, '.tmpdir');
954 | let counter = 0;
955 |
956 | async function runTest (path) {
957 | const barrier = new Barrier();
958 | const server = new Server();
959 |
960 | server.addService(EchoService.service, {
961 | echo (call, callback) {
962 | Assert.strictEqual(call.getPeer(), 'unknown');
963 | callback(null, call.request);
964 | }
965 | });
966 |
967 | const port = await server.bind(path, serverInsecureCreds);
968 | Assert.strictEqual(port, undefined);
969 | server.start();
970 | const client = new EchoService(path, clientInsecureCreds);
971 |
972 | client.echo({ value: 'test value', value2: 42 }, (error, response) => {
973 | Assert.ifError(error);
974 | Assert.deepStrictEqual(response, { value: 'test value', value2: 42 });
975 | client.close();
976 | server.forceShutdown();
977 | barrier.pass();
978 | });
979 |
980 | return barrier;
981 | }
982 |
983 | function getAbsolutePath () {
984 | const file = Path.join(tmpDir, `test-sock-${counter++}`);
985 |
986 | if (process.platform === 'win32') {
987 | return Path.join('\\\\.\\pipe\\', file);
988 | }
989 |
990 | return file;
991 | }
992 |
993 | function getRelativePath () {
994 | const file = Path.join(Path.relative(process.cwd(), tmpDir),
995 | `test-sock-${counter++}`);
996 |
997 | if (process.platform === 'win32') {
998 | return Path.join('\\\\.\\pipe\\', file);
999 | }
1000 |
1001 | return file;
1002 | }
1003 |
1004 | function cleanup () {
1005 | try {
1006 | Fs.readdirSync(tmpDir).forEach((entry) => {
1007 | try {
1008 | Fs.unlinkSync(entry);
1009 | } catch (ignoreErr) {}
1010 | });
1011 |
1012 | Fs.rmdirSync(tmpDir);
1013 | } catch (ignoreErr) {}
1014 | }
1015 |
1016 | before(() => {
1017 | try {
1018 | cleanup();
1019 | Fs.mkdirSync(tmpDir);
1020 | } catch (ignoreErr) {}
1021 | });
1022 |
1023 | after(() => {
1024 | cleanup();
1025 | });
1026 |
1027 | it('handles unix: followed by an absolute path', async () => {
1028 | const path = `unix:${getAbsolutePath()}`;
1029 | await runTest(path);
1030 | });
1031 |
1032 | it('handles unix: followed by a relative path', async () => {
1033 | const path = `unix:${getRelativePath()}`;
1034 | await runTest(path);
1035 | });
1036 |
1037 | // Skip on Windows. The client no longer seems to connect.
1038 | it('handles unix:// followed by an absolute path', { skip: process.platform === 'win32' }, async () => {
1039 | const path = `unix://${getAbsolutePath()}`;
1040 | await runTest(path);
1041 | });
1042 |
1043 | // Skip on Windows, as the pipe prefix is required, and makes it an absolute path.
1044 | it('throws if unix:// is followed by a relative path', { skip: process.platform === 'win32' }, async () => {
1045 | const path = `unix://${getRelativePath()}`;
1046 | await Assert.rejects(async () => {
1047 | await runTest(path);
1048 | }, /must specify an absolute path/);
1049 | });
1050 | });
1051 |
1052 | describe('Maximum Message Size', () => {
1053 | const protoFile = Path.join(__dirname, 'proto', 'echo_service.proto');
1054 | const { EchoService } = loadProtoFile(protoFile);
1055 |
1056 | async function runTest (settings) {
1057 | const barrier = new Barrier();
1058 | const server = new Server(settings);
1059 |
1060 | server.addService(EchoService.service, {
1061 | echo (call, callback) {
1062 | callback(null, { value: call.request.value });
1063 | },
1064 | echoBidiStream (stream) {
1065 | stream.on('data', (chunk) => {
1066 | stream.write({ value: chunk.value });
1067 | });
1068 | }
1069 | });
1070 |
1071 | const port = await server.bind('localhost:0', serverInsecureCreds);
1072 | server.start();
1073 | const client = new EchoService(`localhost:${port}`, clientInsecureCreds);
1074 |
1075 | // Test a unary send/receive.
1076 | client.echo({ value: 'a' }, (error, response) => {
1077 | Assert.strictEqual(error.code, Grpc.status.RESOURCE_EXHAUSTED);
1078 | if (settings['grpc.max_receive_message_length']) {
1079 | Assert.strictEqual(error.details, 'Received message larger than max (8 vs. 1)');
1080 | } else {
1081 | Assert.strictEqual(error.details, 'Sent message larger than max (8 vs. 1)');
1082 | }
1083 | Assert.strictEqual(response, undefined);
1084 |
1085 | // Test a streaming send/receive.
1086 | const call = client.echoBidiStream();
1087 | call.on('data', () => { throw new Error('should not happen'); });
1088 | call.on('error', (error) => {
1089 | Assert.strictEqual(error.code, Grpc.status.RESOURCE_EXHAUSTED);
1090 | if (settings['grpc.max_receive_message_length']) {
1091 | Assert.strictEqual(error.details, 'Received message larger than max (9 vs. 1)');
1092 | } else {
1093 | Assert.strictEqual(error.details, 'Sent message larger than max (9 vs. 1)');
1094 | }
1095 | client.close();
1096 | server.forceShutdown();
1097 | barrier.pass();
1098 | });
1099 |
1100 | call.write({ value: 'bc' });
1101 | });
1102 |
1103 | return barrier;
1104 | }
1105 |
1106 | it('enforces maximum received message length', async () => {
1107 | await runTest({ 'grpc.max_receive_message_length': 1 });
1108 | });
1109 |
1110 | it('enforces maximum sent message length', async () => {
1111 | await runTest({ 'grpc.max_send_message_length': 1 });
1112 | });
1113 | });
1114 |
1115 |
1116 | describe('No stream end events on error', () => {
1117 | const protoFile = Path.join(__dirname, 'proto', 'echo_service.proto');
1118 | const { EchoService } = loadProtoFile(protoFile);
1119 |
1120 | async function getTestSetup () {
1121 | const barrier = new Barrier();
1122 | const server = new Server();
1123 |
1124 | server.addService(EchoService.service, {
1125 | echoClientStream (stream, callback) {
1126 | stream.on('end', () => {
1127 | throw new Error('should not happen');
1128 | });
1129 |
1130 | stream.on('data', (chunk) => {
1131 | throw new Error('client-stream-error');
1132 | });
1133 | },
1134 | echoBidiStream (stream) {
1135 | stream.on('end', () => {
1136 | throw new Error('should not happen');
1137 | });
1138 |
1139 | stream.on('data', (chunk) => {
1140 | throw new Error('bidi-stream-error');
1141 | });
1142 | }
1143 | });
1144 |
1145 | const port = await server.bind('localhost:0', serverInsecureCreds);
1146 | server.start();
1147 | const client = new EchoService(`localhost:${port}`, clientInsecureCreds);
1148 | return { barrier, client, server };
1149 | }
1150 |
1151 | it('does not emit end event on server for client stream', async () => {
1152 | const { barrier, client, server } = await getTestSetup();
1153 | const stream = client.echoClientStream((err, data) => {
1154 | client.close();
1155 | server.forceShutdown();
1156 | Assert.strictEqual(err.details, 'client-stream-error');
1157 | Assert.strictEqual(data, undefined);
1158 | barrier.pass();
1159 | });
1160 |
1161 | stream.write({});
1162 | return barrier;
1163 | });
1164 |
1165 | it('does not emit end event on server for bidi stream', async () => {
1166 | const { barrier, client, server } = await getTestSetup();
1167 | const stream = client.echoBidiStream();
1168 |
1169 | stream.on('error', (err) => {
1170 | client.close();
1171 | server.forceShutdown();
1172 | Assert.strictEqual(err.details, 'bidi-stream-error');
1173 | barrier.pass();
1174 | });
1175 |
1176 | stream.write({});
1177 | return barrier;
1178 | });
1179 | });
1180 | });
1181 |
--------------------------------------------------------------------------------
/test/stream-decoder.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const Assert = require('assert');
3 | const Lab = require('@hapi/lab');
4 | const { StreamDecoder } = require('../lib/stream-decoder');
5 | const { describe, it } = exports.lab = Lab.script();
6 |
7 |
8 | describe('StreamDecoder', () => {
9 | describe('write()', () => {
10 | it('throws if the decoder is in an unknown state', () => {
11 | const decoder = new StreamDecoder();
12 | const data = Buffer.alloc(1);
13 |
14 | decoder.readState = 'invalid';
15 | Assert.throws(() => {
16 | decoder.write(data);
17 | }, /^Error: Unexpected read state$/);
18 | });
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/test/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const Assert = require('assert');
3 | const Lab = require('@hapi/lab');
4 | const { hasGrpcStatusCode } = require('../lib/utils');
5 | const Status = require('../lib/status');
6 | const { describe, it } = exports.lab = Lab.script();
7 |
8 |
9 | describe('Utils', () => {
10 | describe('hasGrpcStatusCode()', () => {
11 | it('detects valid status codes on objects', () => {
12 | Assert.strictEqual(hasGrpcStatusCode({}), false);
13 | Assert.strictEqual(hasGrpcStatusCode({ code: null }), false);
14 | Assert.strictEqual(hasGrpcStatusCode({ code: -1 }), false);
15 | Assert.strictEqual(hasGrpcStatusCode({ code: 17 }), false);
16 |
17 | Object.keys(Status).forEach((name) => {
18 | const status = Status[name];
19 |
20 | Assert.strictEqual(hasGrpcStatusCode({ code: status }), true);
21 |
22 | // Make sure no new status codes sneak in.
23 | Assert(status >= 0 && status <= 16);
24 | });
25 | });
26 | });
27 | });
28 |
--------------------------------------------------------------------------------