bare-rpc 1.3.9 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/index.d.ts +25 -4
- package/index.js +214 -168
- package/lib/command-router.js +6 -1
- package/lib/errors.d.ts +3 -0
- package/lib/errors.js +12 -0
- package/lib/incoming-stream.js +2 -1
- package/lib/messages.js +5 -0
- package/lib/outgoing-request.js +4 -0
- package/lib/outgoing-stream.js +18 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,6 +31,18 @@ console.log(replyBuffer.toString()) // pong
|
|
|
31
31
|
|
|
32
32
|
See the [`bare-rpc` reference](https://docs.pears.com/reference/bare/modules/bare-rpc).
|
|
33
33
|
|
|
34
|
+
## Untrusted peers
|
|
35
|
+
|
|
36
|
+
`bare-rpc` does not trust the remote end to send valid frames. If a frame is larger than `maxFrameSize`, cannot be decoded, or breaks the protocol, the connection fails: `stream` is destroyed with an `RPCError`, and every request and stream in flight is rejected or destroyed with that error. The error code tells you why:
|
|
37
|
+
|
|
38
|
+
- `FRAME_TOO_LARGE`: The length prefix is larger than `maxFrameSize`. This is checked before the rest of the frame is read.
|
|
39
|
+
- `UNKNOWN_MESSAGE`: The frame has a message type that `bare-rpc` does not know.
|
|
40
|
+
- `INVALID_MESSAGE`: The frame cannot be decoded, the stream ends partway through a frame, or the frame is one that a valid peer never sends. Examples are a response or stream message with an id of 0, a stream message with an invalid set of flags, and a new request that reuses the id of a request that is still in flight.
|
|
41
|
+
|
|
42
|
+
A frame that refers to a request or stream that has already finished, or that was never known, is ignored. The same goes for frames that arrive for a stream that has already ended or been destroyed.
|
|
43
|
+
|
|
44
|
+
If you read the response to a request as a stream but the remote end replies without one, the reply arrives on the response stream as a single chunk, unless it is empty, and the stream then ends. An error reply destroys the response stream with that error.
|
|
45
|
+
|
|
34
46
|
## License
|
|
35
47
|
|
|
36
48
|
Apache-2.0
|
package/index.d.ts
CHANGED
|
@@ -139,7 +139,8 @@ interface RPCOutgoingRequest {
|
|
|
139
139
|
createRequestStream(opts?: WritableOptions): RPCOutgoingStream
|
|
140
140
|
/**
|
|
141
141
|
* Create a [`Readable`](https://github.com/mafintosh/streamx#readable-stream) stream for
|
|
142
|
-
* receiving data in reply to the request.
|
|
142
|
+
* receiving data in reply to the request. If the remote end replies without a stream, the reply
|
|
143
|
+
* arrives as a single chunk, unless it is empty, and the stream then ends.
|
|
143
144
|
* @param opts - Options for the returned
|
|
144
145
|
* [`Readable`](https://github.com/mafintosh/streamx#readable-stream) stream.
|
|
145
146
|
* @throws {ALREADY_RECEIVED} the response has already been received.
|
|
@@ -221,7 +222,9 @@ type RPCRequestHandler = (req: RPCIncomingRequest, data: any) => unknown
|
|
|
221
222
|
|
|
222
223
|
interface RPCCommandRouter {
|
|
223
224
|
/**
|
|
224
|
-
* Answer requests for `command` with `onrequest`, in place of any handler registered before.
|
|
225
|
+
* Answer requests for `command` with `onrequest`, in place of any handler registered before. A
|
|
226
|
+
* request for a command with no handler is answered with an error whose code is
|
|
227
|
+
* `UNKNOWN_COMMAND`, and an event for such a command is dropped.
|
|
225
228
|
* @param command - The command number to answer.
|
|
226
229
|
* @param opts - The encodings of the command.
|
|
227
230
|
* @param onrequest - The handler of the requests.
|
|
@@ -237,6 +240,15 @@ declare class RPCCommandRouter {
|
|
|
237
240
|
constructor(opts?: RPCCommandRouterOptions)
|
|
238
241
|
}
|
|
239
242
|
|
|
243
|
+
interface RPCOptions {
|
|
244
|
+
/**
|
|
245
|
+
* The largest frame to accept from the remote end, in bytes, counting the 4-byte length prefix. A
|
|
246
|
+
* larger frame fails the connection with `FRAME_TOO_LARGE` as soon as its length prefix arrives.
|
|
247
|
+
* Defaults to 16 MiB.
|
|
248
|
+
*/
|
|
249
|
+
maxFrameSize?: number
|
|
250
|
+
}
|
|
251
|
+
|
|
240
252
|
interface RPC {
|
|
241
253
|
/**
|
|
242
254
|
* Whether there are no requests or responses currently in flight. Useful for determining when it
|
|
@@ -266,10 +278,18 @@ declare class RPC {
|
|
|
266
278
|
* `RPCIncomingRequest` (or `RPCIncomingEvent`) to inspect and `reply()` to, or an
|
|
267
279
|
* `RPCCommandRouter` that answers each request with the handler of its command. Defaults to a
|
|
268
280
|
* no-op.
|
|
281
|
+
* @param opts - Options for the connection.
|
|
282
|
+
*
|
|
283
|
+
* The remote end is not trusted to send valid frames. A frame that is too large, cannot be
|
|
284
|
+
* decoded, or breaks the protocol fails the connection: `stream` is destroyed with an `RPCError`
|
|
285
|
+
* whose code is `FRAME_TOO_LARGE`, `UNKNOWN_MESSAGE` or `INVALID_MESSAGE`, and every request and
|
|
286
|
+
* stream in flight is rejected or destroyed with it. A frame that refers to a request or stream
|
|
287
|
+
* that has already finished, or that was never known, is ignored.
|
|
269
288
|
*/
|
|
270
289
|
constructor(
|
|
271
290
|
stream: Duplex,
|
|
272
|
-
onrequest?: ((req: RPCIncomingRequest) => void | Promise<void>) | RPCCommandRouter
|
|
291
|
+
onrequest?: ((req: RPCIncomingRequest) => void | Promise<void>) | RPCCommandRouter,
|
|
292
|
+
opts?: RPCOptions
|
|
273
293
|
)
|
|
274
294
|
}
|
|
275
295
|
|
|
@@ -290,7 +310,8 @@ declare namespace RPC {
|
|
|
290
310
|
type RPCCommandRouterOptions as CommandRouterOptions,
|
|
291
311
|
type RPCRespondOptions as RespondOptions,
|
|
292
312
|
type RPCRequestHandler as RequestHandler,
|
|
293
|
-
type RPCValueEncoding as ValueEncoding
|
|
313
|
+
type RPCValueEncoding as ValueEncoding,
|
|
314
|
+
type RPCOptions as Options
|
|
294
315
|
}
|
|
295
316
|
}
|
|
296
317
|
|
package/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const safetyCatch = require('safety-catch')
|
|
2
2
|
const b4a = require('b4a')
|
|
3
3
|
const c = require('compact-encoding')
|
|
4
|
+
const { isEnding } = require('bare-stream')
|
|
4
5
|
const m = require('./lib/messages')
|
|
5
6
|
const errors = require('./lib/errors')
|
|
6
7
|
const { type: t, stream: s } = require('./lib/constants')
|
|
@@ -12,17 +13,42 @@ const OutgoingRequest = require('./lib/outgoing-request')
|
|
|
12
13
|
const OutgoingStream = require('./lib/outgoing-stream')
|
|
13
14
|
const CommandRouter = require('./lib/command-router')
|
|
14
15
|
|
|
16
|
+
const DEFAULT_MAX_FRAME_SIZE = 16 * 1024 * 1024
|
|
17
|
+
const MAX_PENDING_RESPONSES = 1024
|
|
18
|
+
|
|
19
|
+
const streamFlags = new Set()
|
|
20
|
+
|
|
21
|
+
for (const direction of [s.REQUEST, s.RESPONSE]) {
|
|
22
|
+
for (const action of [
|
|
23
|
+
s.OPEN,
|
|
24
|
+
s.CLOSE,
|
|
25
|
+
s.CLOSE | s.ERROR,
|
|
26
|
+
s.PAUSE,
|
|
27
|
+
s.RESUME,
|
|
28
|
+
s.DATA,
|
|
29
|
+
s.END,
|
|
30
|
+
s.DESTROY,
|
|
31
|
+
s.DESTROY | s.ERROR
|
|
32
|
+
]) {
|
|
33
|
+
streamFlags.add(direction | action)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
15
37
|
module.exports = exports = class RPC {
|
|
16
|
-
constructor(stream, onrequest = noop) {
|
|
38
|
+
constructor(stream, onrequest = noop, opts = {}) {
|
|
39
|
+
const { maxFrameSize = DEFAULT_MAX_FRAME_SIZE } = opts
|
|
40
|
+
|
|
17
41
|
this._stream = stream
|
|
42
|
+
this._maxFrameSize = maxFrameSize
|
|
18
43
|
this._id = 0
|
|
19
44
|
|
|
20
45
|
this._outgoingRequests = new Map()
|
|
21
46
|
this._outgoingResponses = new Map()
|
|
22
47
|
this._incomingRequests = new Map()
|
|
23
48
|
this._incomingResponses = new Map()
|
|
24
|
-
this._pendingRequests = new Set()
|
|
25
49
|
this._pendingResponses = new Set()
|
|
50
|
+
this._unansweredRequests = new Set()
|
|
51
|
+
this._highestRequestId = 0
|
|
26
52
|
|
|
27
53
|
this._buffer = []
|
|
28
54
|
this._buffered = 0
|
|
@@ -42,12 +68,14 @@ module.exports = exports = class RPC {
|
|
|
42
68
|
this._onerror = this._onerror.bind(this)
|
|
43
69
|
this._onclose = this._onclose.bind(this)
|
|
44
70
|
this._ondata = this._ondata.bind(this)
|
|
71
|
+
this._onend = this._onend.bind(this)
|
|
45
72
|
this._ondrain = this._ondrain.bind(this)
|
|
46
73
|
|
|
47
74
|
this._stream
|
|
48
75
|
.on('error', this._onerror)
|
|
49
76
|
.on('close', this._onclose)
|
|
50
77
|
.on('data', this._ondata)
|
|
78
|
+
.on('end', this._onend)
|
|
51
79
|
.on('drain', this._ondrain)
|
|
52
80
|
}
|
|
53
81
|
|
|
@@ -59,7 +87,6 @@ module.exports = exports = class RPC {
|
|
|
59
87
|
this._outgoingResponses.size === 0 &&
|
|
60
88
|
this._incomingRequests.size === 0 &&
|
|
61
89
|
this._incomingResponses.size === 0 &&
|
|
62
|
-
this._pendingRequests.size === 0 &&
|
|
63
90
|
this._pendingResponses.size === 0
|
|
64
91
|
)
|
|
65
92
|
}
|
|
@@ -149,6 +176,8 @@ module.exports = exports = class RPC {
|
|
|
149
176
|
// settle.
|
|
150
177
|
if (this._closed) return
|
|
151
178
|
|
|
179
|
+
this._onanswered(request)
|
|
180
|
+
|
|
152
181
|
this._sendMessage({
|
|
153
182
|
type: t.RESPONSE,
|
|
154
183
|
id: request.id,
|
|
@@ -168,6 +197,8 @@ module.exports = exports = class RPC {
|
|
|
168
197
|
|
|
169
198
|
this._outgoingResponses.set(request.id, request)
|
|
170
199
|
|
|
200
|
+
if (this._onanswered(request)) request._responseStream._continueOpen()
|
|
201
|
+
|
|
171
202
|
request._responseStream.on('close', () => this._outgoingResponses.delete(request.id))
|
|
172
203
|
} else {
|
|
173
204
|
request._responseStream = new IncomingStream(this, request, t.RESPONSE, opts)
|
|
@@ -186,15 +217,23 @@ module.exports = exports = class RPC {
|
|
|
186
217
|
}
|
|
187
218
|
|
|
188
219
|
_sendError(request, err) {
|
|
220
|
+
this._onanswered(request)
|
|
221
|
+
|
|
189
222
|
this._sendMessage({
|
|
190
223
|
type: t.RESPONSE,
|
|
191
224
|
id: request.id,
|
|
192
225
|
stream: 0,
|
|
193
|
-
error: err,
|
|
226
|
+
error: m.toError(err),
|
|
194
227
|
data: null
|
|
195
228
|
})
|
|
196
229
|
}
|
|
197
230
|
|
|
231
|
+
// Returns whether the remote end had already opened its response stream.
|
|
232
|
+
_onanswered(request) {
|
|
233
|
+
this._unansweredRequests.delete(request.id)
|
|
234
|
+
return this._pendingResponses.delete(request.id)
|
|
235
|
+
}
|
|
236
|
+
|
|
198
237
|
_gcOutgoingRequest(request) {
|
|
199
238
|
// The entry must stay reachable for as long as an incoming message might
|
|
200
239
|
// still target it; until the response has settled and, if the request was
|
|
@@ -255,13 +294,19 @@ module.exports = exports = class RPC {
|
|
|
255
294
|
this._outgoingResponses.clear()
|
|
256
295
|
this._incomingRequests.clear()
|
|
257
296
|
this._incomingResponses.clear()
|
|
258
|
-
this._pendingRequests.clear()
|
|
259
297
|
this._pendingResponses.clear()
|
|
298
|
+
this._unansweredRequests.clear()
|
|
260
299
|
|
|
261
300
|
for (const stream of streams) stream.destroy(err)
|
|
262
301
|
}
|
|
263
302
|
|
|
303
|
+
_fail(err) {
|
|
304
|
+
this._stream.destroy(err)
|
|
305
|
+
}
|
|
306
|
+
|
|
264
307
|
_ondata(data) {
|
|
308
|
+
if (this._stream.destroying) return
|
|
309
|
+
|
|
265
310
|
this._buffer.push(data)
|
|
266
311
|
this._buffered += data.byteLength
|
|
267
312
|
|
|
@@ -272,13 +317,25 @@ module.exports = exports = class RPC {
|
|
|
272
317
|
}
|
|
273
318
|
}
|
|
274
319
|
|
|
320
|
+
_onend() {
|
|
321
|
+
if (this._buffered > 0) this._fail(errors.INVALID_MESSAGE('Truncated frame'))
|
|
322
|
+
}
|
|
323
|
+
|
|
275
324
|
_onbeforeframe() {
|
|
276
|
-
while (this._buffered >= 4) {
|
|
325
|
+
while (this._buffered >= 4 && !this._stream.destroying) {
|
|
277
326
|
const buffer = this._buffer.length === 1 ? this._buffer[0] : b4a.concat(this._buffer)
|
|
278
327
|
|
|
279
328
|
this._buffer = [buffer]
|
|
280
329
|
this._frame = 4 + c.uint32.decode(c.state(0, 4, buffer))
|
|
281
330
|
|
|
331
|
+
if (this._frame > this._maxFrameSize) {
|
|
332
|
+
return this._fail(
|
|
333
|
+
errors.FRAME_TOO_LARGE(
|
|
334
|
+
`Frame of ${this._frame} bytes exceeds the limit of ${this._maxFrameSize} bytes`
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
|
|
282
339
|
if (this._buffered < this._frame) return
|
|
283
340
|
|
|
284
341
|
this._onframe()
|
|
@@ -304,52 +361,72 @@ module.exports = exports = class RPC {
|
|
|
304
361
|
this._onmessage(buffer.subarray(0, frame))
|
|
305
362
|
}
|
|
306
363
|
|
|
307
|
-
|
|
364
|
+
_onmessage(buffer) {
|
|
308
365
|
let message
|
|
309
366
|
try {
|
|
310
367
|
message = m.message.decode(c.state(0, buffer.length, buffer))
|
|
311
368
|
} catch (err) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
369
|
+
return this._fail(
|
|
370
|
+
err instanceof errors ? err : errors.INVALID_MESSAGE(`Malformed message: ${err.message}`)
|
|
371
|
+
)
|
|
315
372
|
}
|
|
316
373
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
else this._stream.destroy(err)
|
|
331
|
-
}
|
|
332
|
-
break
|
|
333
|
-
}
|
|
334
|
-
case t.RESPONSE:
|
|
335
|
-
try {
|
|
374
|
+
const violation = protocolViolation(message)
|
|
375
|
+
|
|
376
|
+
if (violation !== null) return this._fail(errors.INVALID_MESSAGE(violation))
|
|
377
|
+
|
|
378
|
+
// Handling a message can run user code, such as stream listeners, so route
|
|
379
|
+
// anything it throws through the connection failure path rather than let it
|
|
380
|
+
// escape into the underlying stream.
|
|
381
|
+
try {
|
|
382
|
+
switch (message.type) {
|
|
383
|
+
case t.REQUEST:
|
|
384
|
+
this._onrequestmessage(message)
|
|
385
|
+
break
|
|
386
|
+
case t.RESPONSE:
|
|
336
387
|
this._onresponse(message)
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
break
|
|
341
|
-
case t.STREAM:
|
|
342
|
-
try {
|
|
388
|
+
break
|
|
389
|
+
case t.STREAM:
|
|
343
390
|
this._onstream(message)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
391
|
+
}
|
|
392
|
+
} catch (err) {
|
|
393
|
+
this._fail(err)
|
|
347
394
|
}
|
|
348
395
|
}
|
|
349
396
|
|
|
350
|
-
|
|
351
|
-
|
|
397
|
+
async _onrequestmessage(message) {
|
|
398
|
+
// A repeated id can only be caught while its request is tracked here, which
|
|
399
|
+
// is also the only time it could corrupt any state.
|
|
400
|
+
if (
|
|
401
|
+
this._unansweredRequests.has(message.id) ||
|
|
402
|
+
this._incomingRequests.has(message.id) ||
|
|
403
|
+
this._outgoingResponses.has(message.id)
|
|
404
|
+
) {
|
|
405
|
+
return this._fail(errors.INVALID_MESSAGE(`Request '${message.id}' is already in flight`))
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
let request
|
|
409
|
+
|
|
410
|
+
if (message.id === 0) {
|
|
411
|
+
request = new IncomingEvent(this, message.command, message.data)
|
|
412
|
+
} else {
|
|
413
|
+
request = new IncomingRequest(this, message.id, message.command, message.data)
|
|
352
414
|
|
|
415
|
+
this._unansweredRequests.add(message.id)
|
|
416
|
+
this._highestRequestId = Math.max(this._highestRequestId, message.id)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
await this._onrequest(request)
|
|
421
|
+
} catch (err) {
|
|
422
|
+
safetyCatch(err)
|
|
423
|
+
|
|
424
|
+
if (message.id) this._sendError(request, err)
|
|
425
|
+
else this._stream.destroy(err)
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
_onresponse(message) {
|
|
353
430
|
const request = this._outgoingRequests.get(message.id)
|
|
354
431
|
if (request === undefined) return
|
|
355
432
|
|
|
@@ -358,17 +435,36 @@ module.exports = exports = class RPC {
|
|
|
358
435
|
this._gcOutgoingRequest(request)
|
|
359
436
|
|
|
360
437
|
request._reject(message.error)
|
|
438
|
+
|
|
439
|
+
if (request._responseStream !== null) request._responseStream.destroy(message.error)
|
|
440
|
+
|
|
441
|
+
this._releaseRequestStream(request, message.error)
|
|
361
442
|
} else if (message.stream === 0) {
|
|
362
443
|
request._responded = true
|
|
363
444
|
this._gcOutgoingRequest(request)
|
|
364
445
|
|
|
365
446
|
request._resolve(message.data)
|
|
447
|
+
|
|
448
|
+
const stream = request._responseStream
|
|
449
|
+
|
|
450
|
+
if (isPushable(stream)) {
|
|
451
|
+
if (message.data.byteLength > 0) stream.push(message.data)
|
|
452
|
+
stream.push(null)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
this._releaseRequestStream(request, null)
|
|
366
456
|
}
|
|
367
457
|
}
|
|
368
458
|
|
|
369
|
-
|
|
370
|
-
|
|
459
|
+
// Once the request is answered, a request stream that the remote end never
|
|
460
|
+
// opened will never be read, and would otherwise wait for its open forever.
|
|
461
|
+
_releaseRequestStream(request, err) {
|
|
462
|
+
const stream = request._requestStream
|
|
463
|
+
|
|
464
|
+
if (stream !== null && !stream._acknowledged) stream.destroy(err)
|
|
465
|
+
}
|
|
371
466
|
|
|
467
|
+
_onstream(message) {
|
|
372
468
|
if (message.stream & s.OPEN) this._onstreamopen(message)
|
|
373
469
|
else if (message.stream & s.CLOSE) this._onstreamclose(message)
|
|
374
470
|
else if (message.stream & s.PAUSE) this._onstreampause(message)
|
|
@@ -379,123 +475,56 @@ module.exports = exports = class RPC {
|
|
|
379
475
|
}
|
|
380
476
|
|
|
381
477
|
_onstreamopen(message) {
|
|
382
|
-
let stream
|
|
383
|
-
|
|
384
478
|
if (message.stream & s.REQUEST) {
|
|
385
|
-
const
|
|
386
|
-
if (
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
479
|
+
const stream = this._outgoingStream(message)
|
|
480
|
+
if (stream !== null) stream._continueOpen()
|
|
481
|
+
} else {
|
|
482
|
+
const request = this._outgoingResponses.get(message.id)
|
|
390
483
|
|
|
391
|
-
|
|
484
|
+
if (request !== undefined) return request._responseStream._continueOpen()
|
|
392
485
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (
|
|
400
|
-
|
|
486
|
+
// The remote end opens its response stream as soon as it is created,
|
|
487
|
+
// which may be before its request arrives or before the handler here
|
|
488
|
+
// creates the response stream. Remember the open until the response
|
|
489
|
+
// stream is created, the request is answered, or the remote end destroys
|
|
490
|
+
// its stream. Any other open is for a request that has already been
|
|
491
|
+
// answered.
|
|
492
|
+
if (this._pendingResponses.has(message.id)) return
|
|
493
|
+
|
|
494
|
+
if (!this._unansweredRequests.has(message.id) && message.id <= this._highestRequestId) {
|
|
401
495
|
return
|
|
402
496
|
}
|
|
403
497
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if (stream === null || stream._pendingOpen === null) {
|
|
407
|
-
this._pendingResponses.add(message.id)
|
|
408
|
-
return
|
|
498
|
+
if (this._pendingResponses.size >= MAX_PENDING_RESPONSES) {
|
|
499
|
+
return this._fail(errors.INVALID_MESSAGE('Too many response streams opened early'))
|
|
409
500
|
}
|
|
410
|
-
} else {
|
|
411
|
-
return
|
|
412
|
-
}
|
|
413
501
|
|
|
414
|
-
|
|
502
|
+
this._pendingResponses.add(message.id)
|
|
503
|
+
}
|
|
415
504
|
}
|
|
416
505
|
|
|
417
506
|
_onstreamclose(message) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if (message.stream & s.REQUEST) {
|
|
421
|
-
const request = this._incomingRequests.get(message.id)
|
|
422
|
-
if (request === undefined) return
|
|
423
|
-
|
|
424
|
-
stream = request._requestStream
|
|
425
|
-
} else if (message.stream & s.RESPONSE) {
|
|
426
|
-
const request = this._incomingResponses.get(message.id)
|
|
427
|
-
if (request === undefined) return
|
|
428
|
-
|
|
429
|
-
stream = request._responseStream
|
|
430
|
-
} else {
|
|
431
|
-
return
|
|
432
|
-
}
|
|
433
|
-
|
|
507
|
+
const stream = this._incomingStream(message)
|
|
434
508
|
if (stream === null) return
|
|
509
|
+
|
|
435
510
|
if (message.error) stream.destroy(message.error)
|
|
436
511
|
else stream.push(null)
|
|
437
512
|
}
|
|
438
513
|
|
|
439
514
|
_onstreampause(message) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
if (message.stream & s.REQUEST) {
|
|
443
|
-
const request = this._outgoingRequests.get(message.id)
|
|
444
|
-
if (request === undefined) return
|
|
445
|
-
|
|
446
|
-
stream = request._requestStream
|
|
447
|
-
} else if (message.stream & s.RESPONSE) {
|
|
448
|
-
const request = this._outgoingResponses.get(message.id)
|
|
449
|
-
if (request === undefined) return
|
|
450
|
-
|
|
451
|
-
stream = request._responseStream
|
|
452
|
-
} else {
|
|
453
|
-
return
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (stream === null) return
|
|
457
|
-
stream.cork()
|
|
515
|
+
const stream = this._outgoingStream(message)
|
|
516
|
+
if (stream !== null) stream.cork()
|
|
458
517
|
}
|
|
459
518
|
|
|
460
519
|
_onstreamresume(message) {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if (message.stream & s.REQUEST) {
|
|
464
|
-
const request = this._outgoingRequests.get(message.id)
|
|
465
|
-
if (request === undefined) return
|
|
466
|
-
|
|
467
|
-
stream = request._requestStream
|
|
468
|
-
} else if (message.stream & s.RESPONSE) {
|
|
469
|
-
const request = this._outgoingResponses.get(message.id)
|
|
470
|
-
if (request === undefined) return
|
|
471
|
-
|
|
472
|
-
stream = request._responseStream
|
|
473
|
-
} else {
|
|
474
|
-
return
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
if (stream === null) return
|
|
478
|
-
stream.uncork()
|
|
520
|
+
const stream = this._outgoingStream(message)
|
|
521
|
+
if (stream !== null) stream.uncork()
|
|
479
522
|
}
|
|
480
523
|
|
|
481
524
|
_onstreamdata(message) {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
if (message.stream & s.REQUEST) {
|
|
485
|
-
const request = this._incomingRequests.get(message.id)
|
|
486
|
-
if (request === undefined) return
|
|
487
|
-
|
|
488
|
-
stream = request._requestStream
|
|
489
|
-
} else if (message.stream & s.RESPONSE) {
|
|
490
|
-
const request = this._incomingResponses.get(message.id)
|
|
491
|
-
if (request === undefined) return
|
|
525
|
+
const stream = this._incomingStream(message)
|
|
526
|
+
if (!isPushable(stream)) return
|
|
492
527
|
|
|
493
|
-
stream = request._responseStream
|
|
494
|
-
} else {
|
|
495
|
-
return
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
if (stream === null) return
|
|
499
528
|
if (stream.push(message.data) === false) {
|
|
500
529
|
this._sendMessage({
|
|
501
530
|
type: t.STREAM,
|
|
@@ -508,45 +537,23 @@ module.exports = exports = class RPC {
|
|
|
508
537
|
}
|
|
509
538
|
|
|
510
539
|
_onstreamend(message) {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
if (message.stream & s.REQUEST) {
|
|
514
|
-
const request = this._incomingRequests.get(message.id)
|
|
515
|
-
if (request === undefined) return
|
|
516
|
-
|
|
517
|
-
stream = request._requestStream
|
|
518
|
-
} else if (message.stream & s.RESPONSE) {
|
|
519
|
-
const request = this._incomingResponses.get(message.id)
|
|
520
|
-
if (request === undefined) return
|
|
521
|
-
|
|
522
|
-
stream = request._responseStream
|
|
523
|
-
} else {
|
|
524
|
-
return
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
if (stream === null) return
|
|
528
|
-
stream.push(null)
|
|
540
|
+
const stream = this._incomingStream(message)
|
|
541
|
+
if (stream !== null) stream.push(null)
|
|
529
542
|
}
|
|
530
543
|
|
|
531
544
|
_onstreamdestroy(message) {
|
|
532
|
-
|
|
545
|
+
if (message.stream & s.RESPONSE) this._pendingResponses.delete(message.id)
|
|
533
546
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
stream = request._requestStream
|
|
539
|
-
} else if (message.stream & s.RESPONSE) {
|
|
540
|
-
const request = this._outgoingResponses.get(message.id)
|
|
541
|
-
if (request === undefined) return
|
|
547
|
+
const stream = this._outgoingStream(message)
|
|
548
|
+
if (stream !== null) stream.destroy(message.error)
|
|
549
|
+
}
|
|
542
550
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
551
|
+
_incomingStream(message) {
|
|
552
|
+
return findStream(message, this._incomingRequests, this._incomingResponses)
|
|
553
|
+
}
|
|
547
554
|
|
|
548
|
-
|
|
549
|
-
|
|
555
|
+
_outgoingStream(message) {
|
|
556
|
+
return findStream(message, this._outgoingRequests, this._outgoingResponses)
|
|
550
557
|
}
|
|
551
558
|
|
|
552
559
|
_ondrain(err = null) {
|
|
@@ -560,4 +567,43 @@ module.exports = exports = class RPC {
|
|
|
560
567
|
|
|
561
568
|
exports.CommandRouter = CommandRouter
|
|
562
569
|
|
|
570
|
+
// Messages that decode cleanly but that no valid peer ever sends, as derived
|
|
571
|
+
// from what the outgoing side encodes.
|
|
572
|
+
function protocolViolation(message) {
|
|
573
|
+
switch (message.type) {
|
|
574
|
+
case t.REQUEST:
|
|
575
|
+
if (message.stream === 0) return null
|
|
576
|
+
if (message.stream !== s.OPEN) return `Invalid request stream flags '${message.stream}'`
|
|
577
|
+
if (message.id === 0) return 'Stream request without an id'
|
|
578
|
+
return null
|
|
579
|
+
|
|
580
|
+
case t.RESPONSE:
|
|
581
|
+
if (message.id === 0) return 'Response without an id'
|
|
582
|
+
if (message.stream === 0) return null
|
|
583
|
+
if (message.stream !== s.OPEN || message.error) {
|
|
584
|
+
return `Invalid response stream flags '${message.stream}'`
|
|
585
|
+
}
|
|
586
|
+
return null
|
|
587
|
+
|
|
588
|
+
case t.STREAM:
|
|
589
|
+
if (message.id === 0) return 'Stream message without an id'
|
|
590
|
+
if (!streamFlags.has(message.stream)) return `Invalid stream flags '${message.stream}'`
|
|
591
|
+
return null
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function findStream(message, requests, responses) {
|
|
596
|
+
if (message.stream & s.REQUEST) {
|
|
597
|
+
const request = requests.get(message.id)
|
|
598
|
+
return request === undefined ? null : request._requestStream
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const request = responses.get(message.id)
|
|
602
|
+
return request === undefined ? null : request._responseStream
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function isPushable(stream) {
|
|
606
|
+
return stream !== null && !stream.destroying && !isEnding(stream)
|
|
607
|
+
}
|
|
608
|
+
|
|
563
609
|
function noop() {}
|
package/lib/command-router.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const c = require('compact-encoding')
|
|
2
|
+
const errors = require('./errors')
|
|
2
3
|
|
|
3
4
|
module.exports = class RPCCommandRouter {
|
|
4
5
|
constructor(opts = {}) {
|
|
@@ -30,7 +31,11 @@ module.exports = class RPCCommandRouter {
|
|
|
30
31
|
async _onrequest(req) {
|
|
31
32
|
const responder = this._responders.get(req.command)
|
|
32
33
|
|
|
33
|
-
if (responder === undefined)
|
|
34
|
+
if (responder === undefined) {
|
|
35
|
+
if (req.id === undefined) return
|
|
36
|
+
|
|
37
|
+
throw errors.UNKNOWN_COMMAND(`Unknown command ${req.command}`)
|
|
38
|
+
}
|
|
34
39
|
|
|
35
40
|
const { onrequest, requestEncoding, responseEncoding } = responder
|
|
36
41
|
|
package/lib/errors.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
declare class RPCError extends Error {
|
|
2
2
|
static UNKNOWN_MESSAGE(msg: string): RPCError
|
|
3
|
+
static UNKNOWN_COMMAND(msg: string): RPCError
|
|
4
|
+
static INVALID_MESSAGE(msg: string): RPCError
|
|
5
|
+
static FRAME_TOO_LARGE(msg: string): RPCError
|
|
3
6
|
static ALREADY_SENT(msg: string): RPCError
|
|
4
7
|
static ALREADY_RECEIVED(msg: string): RPCError
|
|
5
8
|
static CHANNEL_CLOSED(msg: string): RPCError
|
package/lib/errors.js
CHANGED
|
@@ -16,6 +16,18 @@ module.exports = class RPCError extends Error {
|
|
|
16
16
|
return new RPCError(msg, 'UNKNOWN_MESSAGE', RPCError.UNKNOWN_MESSAGE)
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
static UNKNOWN_COMMAND(msg) {
|
|
20
|
+
return new RPCError(msg, 'UNKNOWN_COMMAND', RPCError.UNKNOWN_COMMAND)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static INVALID_MESSAGE(msg) {
|
|
24
|
+
return new RPCError(msg, 'INVALID_MESSAGE', RPCError.INVALID_MESSAGE)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static FRAME_TOO_LARGE(msg) {
|
|
28
|
+
return new RPCError(msg, 'FRAME_TOO_LARGE', RPCError.FRAME_TOO_LARGE)
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
static ALREADY_SENT(msg) {
|
|
20
32
|
return new RPCError(msg, 'ALREADY_SENT', RPCError.ALREADY_SENT)
|
|
21
33
|
}
|
package/lib/incoming-stream.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { Readable } = require('bare-stream')
|
|
2
2
|
const { type: t, stream: s } = require('./constants')
|
|
3
|
+
const { toError } = require('./messages')
|
|
3
4
|
|
|
4
5
|
module.exports = class RPCIncomingStream extends Readable {
|
|
5
6
|
constructor(rpc, request, type, opts) {
|
|
@@ -41,7 +42,7 @@ module.exports = class RPCIncomingStream extends Readable {
|
|
|
41
42
|
type: t.STREAM,
|
|
42
43
|
id: this._request.id,
|
|
43
44
|
stream: this._mask | s.DESTROY | s.ERROR,
|
|
44
|
-
error: err,
|
|
45
|
+
error: toError(err),
|
|
45
46
|
data: null
|
|
46
47
|
},
|
|
47
48
|
cb
|
package/lib/messages.js
CHANGED
|
@@ -21,6 +21,11 @@ const error = {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
exports.toError = function toError(err) {
|
|
25
|
+
if (typeof err === 'object' && err !== null && typeof err.message === 'string') return err
|
|
26
|
+
return new Error(String(err))
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
function hasData(m) {
|
|
25
30
|
switch (m.type) {
|
|
26
31
|
case t.REQUEST:
|
package/lib/outgoing-request.js
CHANGED
|
@@ -16,6 +16,8 @@ module.exports = class RPCOutgoingRequest {
|
|
|
16
16
|
this._reject = reject
|
|
17
17
|
})
|
|
18
18
|
|
|
19
|
+
this._promise.catch(noop)
|
|
20
|
+
|
|
19
21
|
this._requestStream = null
|
|
20
22
|
this._responseStream = null
|
|
21
23
|
}
|
|
@@ -79,3 +81,5 @@ module.exports = class RPCOutgoingRequest {
|
|
|
79
81
|
return this._responseStream
|
|
80
82
|
}
|
|
81
83
|
}
|
|
84
|
+
|
|
85
|
+
function noop() {}
|
package/lib/outgoing-stream.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { Writable } = require('bare-stream')
|
|
2
2
|
const { type: t, stream: s } = require('./constants')
|
|
3
|
+
const { toError } = require('./messages')
|
|
3
4
|
|
|
4
5
|
module.exports = class RPCOutgoingStream extends Writable {
|
|
5
6
|
constructor(rpc, request, type, opts) {
|
|
@@ -11,25 +12,18 @@ module.exports = class RPCOutgoingStream extends Writable {
|
|
|
11
12
|
this._mask = type === t.REQUEST ? s.REQUEST : s.RESPONSE
|
|
12
13
|
|
|
13
14
|
this._pendingOpen = null
|
|
15
|
+
this._acknowledged = false
|
|
16
|
+
this._ended = false
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
_open(cb) {
|
|
17
|
-
let pending
|
|
18
|
-
|
|
19
20
|
const onflushed = () => {
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
cb(null)
|
|
24
|
-
} else {
|
|
25
|
-
this._pendingOpen = cb
|
|
26
|
-
}
|
|
21
|
+
if (this._acknowledged) cb(null)
|
|
22
|
+
else this._pendingOpen = cb
|
|
27
23
|
}
|
|
28
24
|
|
|
29
25
|
switch (this._type) {
|
|
30
26
|
case t.REQUEST:
|
|
31
|
-
pending = this._rpc._pendingRequests
|
|
32
|
-
|
|
33
27
|
this._rpc._sendMessage(
|
|
34
28
|
{
|
|
35
29
|
type: t.REQUEST,
|
|
@@ -43,8 +37,6 @@ module.exports = class RPCOutgoingStream extends Writable {
|
|
|
43
37
|
break
|
|
44
38
|
|
|
45
39
|
case t.RESPONSE:
|
|
46
|
-
pending = this._rpc._pendingResponses
|
|
47
|
-
|
|
48
40
|
this._rpc._sendMessage(
|
|
49
41
|
{
|
|
50
42
|
type: t.RESPONSE,
|
|
@@ -60,12 +52,19 @@ module.exports = class RPCOutgoingStream extends Writable {
|
|
|
60
52
|
}
|
|
61
53
|
|
|
62
54
|
_continueOpen() {
|
|
55
|
+
this._acknowledged = true
|
|
56
|
+
|
|
63
57
|
if (this._pendingOpen === null) return
|
|
58
|
+
|
|
64
59
|
const cb = this._pendingOpen
|
|
65
60
|
this._pendingOpen = null
|
|
66
61
|
cb()
|
|
67
62
|
}
|
|
68
63
|
|
|
64
|
+
_predestroy() {
|
|
65
|
+
this._continueOpen()
|
|
66
|
+
}
|
|
67
|
+
|
|
69
68
|
_write(data, encoding, cb) {
|
|
70
69
|
this._rpc._sendMessage(
|
|
71
70
|
{
|
|
@@ -80,6 +79,8 @@ module.exports = class RPCOutgoingStream extends Writable {
|
|
|
80
79
|
}
|
|
81
80
|
|
|
82
81
|
_final(cb) {
|
|
82
|
+
this._ended = true
|
|
83
|
+
|
|
83
84
|
this._rpc._sendMessage(
|
|
84
85
|
{
|
|
85
86
|
type: t.STREAM,
|
|
@@ -93,13 +94,15 @@ module.exports = class RPCOutgoingStream extends Writable {
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
_destroy(err, cb) {
|
|
96
|
-
if (
|
|
97
|
+
if (this._ended) {
|
|
98
|
+
cb(null)
|
|
99
|
+
} else if (err) {
|
|
97
100
|
this._rpc._sendMessage(
|
|
98
101
|
{
|
|
99
102
|
type: t.STREAM,
|
|
100
103
|
id: this._request.id,
|
|
101
104
|
stream: this._mask | s.CLOSE | s.ERROR,
|
|
102
|
-
error: err,
|
|
105
|
+
error: toError(err),
|
|
103
106
|
data: null
|
|
104
107
|
},
|
|
105
108
|
cb
|