vrpc 3.8.2 → 3.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vrpc",
3
- "version": "3.8.2",
3
+ "version": "3.9.0",
4
4
  "description": "Variadic Remote Procedure Calls",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -9,12 +9,10 @@ __\/\\\_______\/\\\__/\\\///////\\\___\/\\\/////////\\\____/\\\////////__
9
9
  _______\//\\\_______\/\\\______\//\\\_\/\\\_______________\////\\\\\\\\\_
10
10
  ________\///________\///________\///__\///___________________\/////////__
11
11
 
12
-
13
12
  Non-intrusively adapts code and provides access in form of asynchronous remote
14
13
  procedure calls (RPC).
15
14
  Author: Dr. Burkhard C. Heisen (https://github.com/heisenware/vrpc)
16
15
 
17
-
18
16
  Licensed under the MIT License <http://opensource.org/licenses/MIT>.
19
17
  Copyright (c) 2018 - 2022 Dr. Burkhard C. Heisen <burkhard.heisen@heisenware.com>.
20
18
 
@@ -419,16 +417,45 @@ class VrpcAdapter {
419
417
  return this._mustTrackClient
420
418
  }
421
419
 
420
+ /**
421
+ * Refuses a request on an isolated instance that belongs to another
422
+ * connection. An isolated instance created over the wire remembers the
423
+ * connection that created it (the request's sender) and answers that
424
+ * connection alone; one the agent created itself has no owner and is
425
+ * open. The agent's own housekeeping passes as VrpcAdapter.LOCAL_SENDER,
426
+ * which no wire message can carry.
427
+ */
428
+ static _assertOwner (entry, json, instanceId) {
429
+ if (!entry || !entry.isIsolated || entry.owner === undefined) return
430
+ if (json.s === VrpcAdapter.LOCAL_SENDER || json.s === entry.owner) return
431
+ throw new Error(
432
+ `Instance '${instanceId}' is isolated to another connection`
433
+ )
434
+ }
435
+
422
436
  static _handleCreateIsolated (json) {
423
437
  let instance
424
438
  try {
425
439
  const className = json.c
426
440
  const [instanceId, ...args] = json.a
441
+ const existing = VrpcAdapter._instances.get(instanceId)
442
+ if (existing) {
443
+ // an id is never re-homed: a shared instance stays shared, an
444
+ // isolated one keeps its owner
445
+ if (!existing.isIsolated) {
446
+ throw new Error(`Instance '${instanceId}' exists as a shared instance`)
447
+ }
448
+ VrpcAdapter._assertOwner(existing, json, instanceId)
449
+ json.r = instanceId
450
+ return existing.instance
451
+ }
427
452
  instance = VrpcAdapter._create(className, instanceId, ...args)
428
453
  VrpcAdapter._instances.set(instanceId, {
429
454
  instance,
430
455
  className,
431
- isIsolated: true
456
+ isIsolated: true,
457
+ // undefined for instances the agent creates itself (open)
458
+ owner: typeof json.s === 'string' ? json.s : undefined
432
459
  })
433
460
  VrpcAdapter._emitter.emit('create', {
434
461
  args,
@@ -449,6 +476,10 @@ class VrpcAdapter {
449
476
  try {
450
477
  const className = json.c
451
478
  const [instanceId, ...args] = json.a
479
+ const existing = VrpcAdapter._instances.get(instanceId)
480
+ if (existing && existing.isIsolated) {
481
+ throw new Error(`Instance '${instanceId}' exists as an isolated instance`)
482
+ }
452
483
  instance = VrpcAdapter._create(className, instanceId, ...args)
453
484
  VrpcAdapter._instances.set(instanceId, {
454
485
  instance,
@@ -524,6 +555,7 @@ class VrpcAdapter {
524
555
  static _handleDelete (json) {
525
556
  try {
526
557
  const instance = json.a[0] // first arg is instance to be deleted
558
+ VrpcAdapter._assertOwner(VrpcAdapter._instances.get(instance), json, instance)
527
559
  VrpcAdapter._emitter.emit('beforeDelete', {
528
560
  instance,
529
561
  className: json.c
@@ -540,6 +572,14 @@ class VrpcAdapter {
540
572
  }
541
573
 
542
574
  static _handleCall (json) {
575
+ // an isolated instance answers its creating connection only
576
+ try {
577
+ VrpcAdapter._assertOwner(VrpcAdapter._instances.get(json.c), json, json.c)
578
+ } catch (err) {
579
+ const { message, cause } = err
580
+ json.e = { message, cause }
581
+ return
582
+ }
543
583
  // special case: removeAllListeners
544
584
  if (json.f === 'removeAllListeners') {
545
585
  VrpcAdapter._removeAllListeners(json.a[0], json.s, json.c)
@@ -547,6 +587,17 @@ class VrpcAdapter {
547
587
  return
548
588
  }
549
589
  const unwrapped = VrpcAdapter._unwrapArguments(json)
590
+ // special case: removing a listener the adapter does not hold (the
591
+ // client's subscription outlived a restart of this agent, or it sent
592
+ // no listener at all) - nothing to remove, and the instance's
593
+ // EventEmitter would throw on a listener that is not a function
594
+ if (
595
+ (json.f === 'off' || json.f === 'removeListener') &&
596
+ typeof unwrapped[1] !== 'function'
597
+ ) {
598
+ json.r = true
599
+ return
600
+ }
550
601
  // Check whether context is a registered class
551
602
  const entry = VrpcAdapter._functionRegistry.get(json.c)
552
603
  if (entry !== undefined) {
@@ -960,4 +1011,11 @@ VrpcAdapter._blackList = new Set([
960
1011
  '__lookupSetter__'
961
1012
  ])
962
1013
 
1014
+ /**
1015
+ * The sender the agent uses for its own housekeeping calls (deleting the
1016
+ * isolated instances of a departed connection). A Symbol: no message from
1017
+ * the wire can carry it, so it can never be claimed remotely.
1018
+ */
1019
+ VrpcAdapter.LOCAL_SENDER = Symbol('vrpc.local-sender')
1020
+
963
1021
  module.exports = VrpcAdapter
package/vrpc/VrpcAgent.js CHANGED
@@ -663,7 +663,12 @@ class VrpcAgent extends EventEmitter {
663
663
  const entry = this._isolatedInstances.get(connectionId)
664
664
  if (entry) {
665
665
  entry.forEach(instanceId => {
666
- const json = { f: '__delete__', a: [instanceId], r: null }
666
+ const json = {
667
+ f: '__delete__',
668
+ a: [instanceId],
669
+ r: null,
670
+ s: VrpcAdapter.LOCAL_SENDER
671
+ }
667
672
  VrpcAdapter._call(json)
668
673
  if (json.r) {
669
674
  this._log.debug(`Auto-deleted isolated instance: ${instanceId}`)
@@ -718,7 +723,7 @@ class VrpcAgent extends EventEmitter {
718
723
  const entryIsolated = this._isolatedInstances.get(clientId)
719
724
  if (entryIsolated && entryIsolated.has(instanceId)) {
720
725
  entryIsolated.delete(instanceId)
721
- if (entryIsolated.length === 0) {
726
+ if (entryIsolated.size === 0) {
722
727
  this._isolatedInstances.delete(clientId)
723
728
  this._mqttUnsubscribe(`${clientId}/__clientInfo__`)
724
729
  this._log.debug(`Stopped tracking lifetime of client: ${clientId}`)
@@ -730,7 +735,7 @@ class VrpcAgent extends EventEmitter {
730
735
  if (v.has(instanceId)) {
731
736
  found = true
732
737
  v.delete(instanceId)
733
- if (v.length === 0) {
738
+ if (v.size === 0) {
734
739
  this._sharedInstances.delete(clientId)
735
740
  this._mqttUnsubscribe(`${clientId}/__clientInfo__`)
736
741
  this._log.debug(`Stopped tracking lifetime of client: ${clientId}`)
@@ -9,12 +9,10 @@ __\/\\\_______\/\\\__/\\\///////\\\___\/\\\/////////\\\____/\\\////////__
9
9
  _______\//\\\_______\/\\\______\//\\\_\/\\\_______________\////\\\\\\\\\_
10
10
  ________\///________\///________\///__\///___________________\/////////__
11
11
 
12
-
13
12
  Non-intrusively adapts code and provides access in form of asynchronous remote
14
13
  procedure calls (RPC).
15
14
  Author: Dr. Burkhard C. Heisen (https://github.com/heisenware/vrpc)
16
15
 
17
-
18
16
  Licensed under the MIT License <http://opensource.org/licenses/MIT>.
19
17
  Copyright (c) 2018 - 2022 Dr. Burkhard C. Heisen <burkhard.heisen@heisenware.com>.
20
18
 
@@ -319,16 +317,45 @@ class VrpcAdapter {
319
317
  return VrpcAdapter._mustTrackClient
320
318
  }
321
319
 
320
+ /**
321
+ * Refuses a request on an isolated instance that belongs to another
322
+ * connection. An isolated instance created over the wire remembers the
323
+ * connection that created it (the request's sender) and answers that
324
+ * connection alone; one the agent created itself has no owner and is
325
+ * open. The agent's own housekeeping passes as VrpcAdapter.LOCAL_SENDER,
326
+ * which no wire message can carry.
327
+ */
328
+ static _assertOwner (entry, json, instanceId) {
329
+ if (!entry || !entry.isIsolated || entry.owner === undefined) return
330
+ if (json.s === VrpcAdapter.LOCAL_SENDER || json.s === entry.owner) return
331
+ throw new Error(
332
+ `Instance '${instanceId}' is isolated to another connection`
333
+ )
334
+ }
335
+
322
336
  static _handleCreateIsolated (json) {
323
337
  let instance
324
338
  try {
325
339
  const className = json.c
326
340
  const [instanceId, ...args] = json.a
341
+ const existing = VrpcAdapter._instances.get(instanceId)
342
+ if (existing) {
343
+ // an id is never re-homed: a shared instance stays shared, an
344
+ // isolated one keeps its owner
345
+ if (!existing.isIsolated) {
346
+ throw new Error(`Instance '${instanceId}' exists as a shared instance`)
347
+ }
348
+ VrpcAdapter._assertOwner(existing, json, instanceId)
349
+ json.r = instanceId
350
+ return existing.instance
351
+ }
327
352
  instance = VrpcAdapter._create(className, instanceId, ...args)
328
353
  VrpcAdapter._instances.set(instanceId, {
329
354
  instance,
330
355
  className,
331
- isIsolated: true
356
+ isIsolated: true,
357
+ // undefined for instances the agent creates itself (open)
358
+ owner: typeof json.s === 'string' ? json.s : undefined
332
359
  })
333
360
  VrpcAdapter._emitter.emit('create', {
334
361
  args,
@@ -348,6 +375,10 @@ class VrpcAdapter {
348
375
  try {
349
376
  const className = json.c
350
377
  const [instanceId, ...args] = json.a
378
+ const existing = VrpcAdapter._instances.get(instanceId)
379
+ if (existing && existing.isIsolated) {
380
+ throw new Error(`Instance '${instanceId}' exists as an isolated instance`)
381
+ }
351
382
  instance = VrpcAdapter._create(className, instanceId, ...args)
352
383
  VrpcAdapter._instances.set(instanceId, {
353
384
  instance,
@@ -421,6 +452,7 @@ class VrpcAdapter {
421
452
  static _handleDelete (json) {
422
453
  try {
423
454
  const instance = json.a[0] // first arg is instance to be deleted
455
+ VrpcAdapter._assertOwner(VrpcAdapter._instances.get(instance), json, instance)
424
456
  VrpcAdapter._emitter.emit('beforeDelete', {
425
457
  instance,
426
458
  className: json.c
@@ -436,6 +468,14 @@ class VrpcAdapter {
436
468
  }
437
469
 
438
470
  static _handleCall (json) {
471
+ // an isolated instance answers its creating connection only
472
+ try {
473
+ VrpcAdapter._assertOwner(VrpcAdapter._instances.get(json.c), json, json.c)
474
+ } catch (err) {
475
+ const { message, cause } = err
476
+ json.e = { message, cause }
477
+ return
478
+ }
439
479
  // special case: removeAllListeners
440
480
  if (json.f === 'removeAllListeners') {
441
481
  VrpcAdapter._removeAllListeners(json.a[0], json.s, json.c)
@@ -877,4 +917,11 @@ VrpcAdapter._blackList = new Set([
877
917
  '__lookupSetter__'
878
918
  ])
879
919
 
920
+ /**
921
+ * The sender the agent uses for its own housekeeping calls (deleting the
922
+ * isolated instances of a departed connection). A Symbol: no message from
923
+ * the wire can carry it, so it can never be claimed remotely.
924
+ */
925
+ VrpcAdapter.LOCAL_SENDER = Symbol('vrpc.local-sender')
926
+
880
927
  module.exports = VrpcAdapter
@@ -504,7 +504,12 @@ class VrpcAgent extends EventEmitter {
504
504
  const entry = this._isolatedInstances.get(connectionId)
505
505
  if (entry) {
506
506
  entry.forEach(instanceId => {
507
- const json = { f: '__delete__', a: [instanceId], r: null }
507
+ const json = {
508
+ f: '__delete__',
509
+ a: [instanceId],
510
+ r: null,
511
+ s: VrpcAdapter.LOCAL_SENDER
512
+ }
508
513
  VrpcAdapter._call(json)
509
514
  if (json.r) {
510
515
  this._log.debug(`Auto-deleted isolated instance: ${instanceId}`)
@@ -559,7 +564,7 @@ class VrpcAgent extends EventEmitter {
559
564
  const entryIsolated = this._isolatedInstances.get(clientId)
560
565
  if (entryIsolated && entryIsolated.has(instanceId)) {
561
566
  entryIsolated.delete(instanceId)
562
- if (entryIsolated.length === 0) {
567
+ if (entryIsolated.size === 0) {
563
568
  this._isolatedInstances.delete(clientId)
564
569
  this._mqttUnsubscribe(`${clientId}/__clientInfo__`)
565
570
  this._log.debug(`Stopped tracking lifetime of client: ${clientId}`)
@@ -571,7 +576,7 @@ class VrpcAgent extends EventEmitter {
571
576
  if (v.has(instanceId)) {
572
577
  found = true
573
578
  v.delete(instanceId)
574
- if (v.length === 0) {
579
+ if (v.size === 0) {
575
580
  this._sharedInstances.delete(clientId)
576
581
  this._mqttUnsubscribe(`${clientId}/__clientInfo__`)
577
582
  this._log.debug(`Stopped tracking lifetime of client: ${clientId}`)
@@ -9,12 +9,10 @@ __\/\\\_______\/\\\__/\\\///////\\\___\/\\\/////////\\\____/\\\////////__
9
9
  _______\//\\\_______\/\\\______\//\\\_\/\\\_______________\////\\\\\\\\\_
10
10
  ________\///________\///________\///__\///___________________\/////////__
11
11
 
12
-
13
12
  Non-intrusively adapts code and provides access in form of asynchronous remote
14
13
  procedure calls (RPC).
15
14
  Author: Dr. Burkhard C. Heisen (https://github.com/heisenware/vrpc)
16
15
 
17
-
18
16
  Licensed under the MIT License <http://opensource.org/licenses/MIT>.
19
17
  Copyright (c) 2018 - 2022 Dr. Burkhard C. Heisen <burkhard.heisen@heisenware.com>.
20
18
 
@@ -113,6 +111,18 @@ class VrpcClient extends EventEmitter {
113
111
  'The identity must NOT contain any of those characters: "+", "/", "#", "$"'
114
112
  )
115
113
  }
114
+ // mqtt client id sanity check: it becomes the second segment of every
115
+ // topic this client receives on, so it must be one plain segment
116
+ if (mqttClientId !== null && mqttClientId !== undefined) {
117
+ if (typeof mqttClientId !== 'string' || mqttClientId.length === 0) {
118
+ throw new Error('The mqttClientId must be a non-empty string')
119
+ }
120
+ if (mqttClientId.match(/[+/#]/)) {
121
+ throw new Error(
122
+ 'The mqttClientId must NOT contain any of those characters: "+", "/", "#"'
123
+ )
124
+ }
125
+ }
116
126
  this._token = token
117
127
  this._username = username
118
128
  this._password = password
@@ -128,6 +138,11 @@ class VrpcClient extends EventEmitter {
128
138
  this._instance = nanoid(8)
129
139
  this._mqttClientId = mqttClientId || this._createMqttClientId()
130
140
  this._vrpcClientId = this._createVrpcClientId()
141
+ // the part of the connection id nobody else can learn: a broker that
142
+ // confines a client to topics under its own mqtt client id keeps
143
+ // every response, event and presence message of this connection
144
+ // private, and a sender field nobody can read cannot be forged
145
+ this._connectionSecret = nanoid(16)
131
146
  this._vrpcConnectionId = this._createVrpcConnectionId()
132
147
  this._agents = {}
133
148
  this._eventEmitter = new EventEmitter()
@@ -175,7 +190,12 @@ class VrpcClient extends EventEmitter {
175
190
  * have distinct connection ids. Agents key all their bookkeeping by it
176
191
  * (response routing, event listeners, isolated instances, presence), so
177
192
  * one connection ending never disturbs the others of the same identity.
178
- * Without an identity the connection id equals the client id.
193
+ *
194
+ * It reads `<domain>/<mqttClientId>/<secret>`: the mqtt client id is
195
+ * what the broker knows this connection by, so a broker can confine the
196
+ * client to its own topics, and the secret is random per connection, so
197
+ * nobody who cannot read those topics can impersonate the connection
198
+ * towards an agent. Treat it as a credential: do not log or share it.
179
199
  *
180
200
  * @returns {String} connectionId
181
201
  */
@@ -866,11 +886,10 @@ class VrpcClient extends EventEmitter {
866
886
  }
867
887
 
868
888
  _createVrpcConnectionId () {
869
- // Without an identity the client id already is unique per instance.
870
- // With one, the instance token makes every connection of that identity
871
- // distinct while keeping the three-segment layout agents parse.
872
- if (!this._identity) return this._vrpcClientId
873
- return `${this._vrpcClientId}:${this._instance}`
889
+ // Three segments, always: agents (this one, the C++ and the Python
890
+ // ports) recognise a presence message by `__clientInfo__` arriving as
891
+ // the fourth topic token, so the layout is part of the wire format.
892
+ return `${this._domain}/${this._mqttClientId}/${this._connectionSecret}`
874
893
  }
875
894
 
876
895
  _mqttPublish (topic, message, options) {
@@ -1225,15 +1244,15 @@ class VrpcClient extends EventEmitter {
1225
1244
  return new Promise((resolve, reject) => {
1226
1245
  const handler =
1227
1246
  timer =>
1228
- (instances, { agent, className }) => {
1229
- if (instances.includes(instance)) {
1230
- if (options.agent && agent !== options.agent) return
1231
- if (options.className && className !== options.className) return
1232
- clearTimeout(timer)
1233
- this.removeListener('instanceNew', handler)
1234
- resolve({ agent, className, instance })
1247
+ (instances, { agent, className }) => {
1248
+ if (instances.includes(instance)) {
1249
+ if (options.agent && agent !== options.agent) return
1250
+ if (options.className && className !== options.className) return
1251
+ clearTimeout(timer)
1252
+ this.removeListener('instanceNew', handler)
1253
+ resolve({ agent, className, instance })
1254
+ }
1235
1255
  }
1236
- }
1237
1256
  const timer = setTimeout(() => {
1238
1257
  this.removeListener('instanceNew', handler(timer))
1239
1258
  const msg = `Could not find instance: ${instance} (> ${this._timeout} ms)`
@@ -1284,6 +1303,17 @@ class VrpcClient extends EventEmitter {
1284
1303
  isContinuousRemover
1285
1304
  ) {
1286
1305
  const id = await this._offRemoteEvent({ callback })
1306
+ if (
1307
+ !id &&
1308
+ (functionName === 'off' || functionName === 'removeListener')
1309
+ ) {
1310
+ // Nothing of ours to remove - the subscription was already
1311
+ // dropped (the instance or its agent went away and the cache
1312
+ // was cleared, or the listener was never registered). Sending
1313
+ // the call anyway would hand the agent a null listener, which
1314
+ // its EventEmitter rejects. Handled locally: nothing to do.
1315
+ return null
1316
+ }
1287
1317
  wrapped.push(id || null)
1288
1318
  continue
1289
1319
  } else {
@@ -1443,8 +1473,8 @@ class VrpcClient extends EventEmitter {
1443
1473
  return (
1444
1474
  variable &&
1445
1475
  typeof variable === 'object' &&
1446
- variable.hasOwnProperty('emitter') &&
1447
- variable.hasOwnProperty('event') &&
1476
+ Object.prototype.hasOwnProperty.call(variable, 'emitter') &&
1477
+ Object.prototype.hasOwnProperty.call(variable, 'event') &&
1448
1478
  typeof variable.emitter === 'object' &&
1449
1479
  typeof variable.emitter.emit === 'function'
1450
1480
  )