vrpc 3.8.3 → 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.3",
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)
@@ -971,4 +1011,11 @@ VrpcAdapter._blackList = new Set([
971
1011
  '__lookupSetter__'
972
1012
  ])
973
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
+
974
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)`
@@ -1454,8 +1473,8 @@ class VrpcClient extends EventEmitter {
1454
1473
  return (
1455
1474
  variable &&
1456
1475
  typeof variable === 'object' &&
1457
- variable.hasOwnProperty('emitter') &&
1458
- variable.hasOwnProperty('event') &&
1476
+ Object.prototype.hasOwnProperty.call(variable, 'emitter') &&
1477
+ Object.prototype.hasOwnProperty.call(variable, 'event') &&
1459
1478
  typeof variable.emitter === 'object' &&
1460
1479
  typeof variable.emitter.emit === 'function'
1461
1480
  )