iosignal 1.7.0 → 1.7.1

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.
Files changed (49) hide show
  1. package/README.md +254 -247
  2. package/auth_file.json +13 -13
  3. package/auth_file.mjs +23 -23
  4. package/dist/iosignal.esm.js.map +1 -1
  5. package/dist/iosignal.min.js.map +1 -1
  6. package/index.js +19 -19
  7. package/indexWebBrowser.js +13 -13
  8. package/package.json +60 -59
  9. package/rollup.config.js +69 -69
  10. package/src/api/RedisAPI.js +65 -65
  11. package/src/api/api_constant.js +7 -7
  12. package/src/api/api_reply.js +31 -31
  13. package/src/api/api_sudo.js +91 -91
  14. package/src/auth/Auth_Env.js +76 -76
  15. package/src/auth/Auth_File.js +71 -71
  16. package/src/auth/Auth_Redis.js +44 -44
  17. package/src/auth/BohoAuth.js +164 -164
  18. package/src/client/CongPacket.js +141 -141
  19. package/src/client/IOCongSocket.js +81 -81
  20. package/src/client/IOCore.js +822 -822
  21. package/src/client/IOWS.js +80 -80
  22. package/src/client/IOWebSocket.js +111 -111
  23. package/src/common/constants.js +148 -148
  24. package/src/common/payload.js +81 -81
  25. package/src/common/quotaTable.js +68 -68
  26. package/src/common/util.js +61 -61
  27. package/src/server/FileLogger.js +30 -30
  28. package/src/server/Manager.js +433 -433
  29. package/src/server/Metric.js +155 -155
  30. package/src/server/Remote.js +207 -207
  31. package/src/server/RemoteCore.js +439 -439
  32. package/src/server/Server.js +163 -163
  33. package/src/server/serverOption.js +56 -56
  34. package/test-browser/favicon.ico +0 -0
  35. package/test-browser/index.html +96 -96
  36. package/test-browser/server.js +11 -11
  37. package/test-client-nodejs/client.cjs +17 -17
  38. package/test-client-nodejs/client.mjs +17 -17
  39. package/test-client-nodejs/client_api_reply.mjs +15 -15
  40. package/test-client-nodejs/stateChange.mjs +19 -19
  41. package/test-server/server-Auth_Env.js +19 -19
  42. package/test-server/server-Auth_File.js +12 -12
  43. package/test-server/server-api_reply.js +6 -6
  44. package/test-server/server-api_sudo.js +7 -7
  45. package/test-server/server-commonjs.cjs +8 -8
  46. package/test-server/server-esm.mjs +20 -20
  47. package/test-server/simple-server.mjs +11 -11
  48. package/test-signal-payloadType/signalPack.js +34 -34
  49. package/test-signal-payloadType/signaling.js +60 -60
@@ -1,822 +1,822 @@
1
- import { MBP, Buffer } from 'meta-buffer-pack'
2
- import EventEmitter from "eventemitter3";
3
- import { Boho, BohoMsg, MetaSize } from "boho";
4
- import { IOMsg, PAYLOAD_TYPE, SIZE_LIMIT, ENC_MODE, STATES } from '../common/constants.js'
5
- import { quotaTable } from '../common/quotaTable.js'
6
- import { getSignalPack } from '../common/payload.js';
7
-
8
- const encoder = new TextEncoder()
9
- const decoder = new TextDecoder()
10
-
11
- function byteToUrl(buffer) {
12
- //ipv4(4bytes) , port(2bytes)
13
- if (buffer.byteLength != 6) return
14
- let address = buffer[0].toString() + "." + buffer[1].toString()
15
- + "." + buffer[2].toString() + "." + buffer[3].toString();
16
- let port = (buffer[4] << 8) + buffer[5]
17
- return address + ':' + port.toString()
18
- }
19
-
20
- export class IOCore extends EventEmitter {
21
- constructor(url) {
22
- super();
23
- this.cid = "" // get from the server CID_RES
24
- this.ip = "" // get from the server IAM_RES message.
25
- this.socket = null;
26
- this.url = url; // init default server url
27
- this.state = STATES.CLOSED; // Number type
28
- this.stateName = this.getStateName() // String type
29
-
30
- this.txCounter = 0;
31
- this.rxCounter = 0;
32
- this.txBytes = 0;
33
- this.rxBytes = 0;
34
-
35
- this.lastTxRxTime = Date.now();
36
- this.connectionCheckerPeriod = SIZE_LIMIT.CONNECTION_CHECKER_PERIOD;
37
- this.connectionCheckerIntervalID = null;
38
-
39
- this.boho = new Boho()
40
- this.TLS = false // true if protocol is wss(TLS)
41
- this.encMode = ENC_MODE.AUTO;
42
- this.useAuth = false;
43
-
44
- this.nick = "";
45
- this.channels = new Set()
46
- this.promiseMap = new Map()
47
- this.promiseTimeOut = SIZE_LIMIT.PROMISE_TIMEOUT
48
- this.mid = 0 // promise message id
49
-
50
- this.level = 3; // also defaultQuotaLevel
51
- this.quota = quotaTable[this.level];
52
- this.serverSet = {}
53
- this.linkMap = new Map()
54
-
55
- this.on('open', this.onOpen.bind(this))
56
- this.on('close', this.onClose.bind(this))
57
- this.on('socket_data', this.onData.bind(this))
58
- }
59
-
60
-
61
- redirect(url2) {
62
- this.close()
63
- this.stateChange('redirecting')
64
- this.createConnection(url2)
65
- }
66
-
67
- open(url) {
68
- if (!url && !this.url) return;
69
-
70
- if (url) {
71
- if (!this.url) { // default host url
72
- this.url = url
73
- } else if (url !== this.url) { // default host url change
74
- this.url = url;
75
- if (this.socket) {
76
- this.close()
77
- return
78
- }
79
- }
80
- }
81
-
82
- this.createConnection(this.url)
83
-
84
- if (!this.connectionCheckerIntervalID) {
85
- this.connectionCheckerIntervalID = setInterval(this.keepAlive.bind(this), this.connectionCheckerPeriod);
86
- }
87
- }
88
-
89
- onOpen() {
90
- if (this.url.includes("wss://")) {
91
- this.TLS = true;
92
- } else {
93
- this.TLS = false;
94
- }
95
- this.stateChange('open')
96
- }
97
-
98
- onClose() {
99
- this.boho.isAuthorized = false;
100
- this.cid = ""
101
- this.stateChange('closed')
102
- }
103
-
104
- // manual login
105
- login(id, key) {
106
- if (!id && !key) {
107
- console.log('no id and key.')
108
- return
109
- }
110
- console.log('manual login: ', id)
111
-
112
- if (!key && id.includes('.')) {
113
- this.boho.set_id_key(id)
114
- } else if (id && key) {
115
- this.boho.set_id8(id)
116
- this.boho.set_key(key)
117
- } else {
118
- console.log('no id or key.')
119
- return
120
- }
121
- this.useAuth = true
122
- let auth_pack = this.boho.auth_req()
123
- this.send(auth_pack)
124
- }
125
-
126
- // auto login
127
- auth(id, key) {
128
- if (!id && !key) {
129
- console.log('no id and key.')
130
- return
131
- }
132
-
133
- if (!key && id.includes('.')) {
134
- this.boho.set_id_key(id)
135
- } else if (id && key) {
136
- this.boho.set_id8(id)
137
- this.boho.set_key(key)
138
- } else {
139
- console.log('no id or key.')
140
- return
141
- }
142
- this.useAuth = true
143
- }
144
-
145
- onData(buffer) {
146
- let msgType = buffer[0];
147
- let decoded;
148
-
149
- if (msgType === BohoMsg.ENC_488) {
150
- decoded = this.boho.decrypt_488(buffer)
151
- if (decoded) {
152
- msgType = decoded[0]
153
- buffer = decoded
154
- } else {
155
- // console.log('DEC_FAIL', buffer.byteLength)
156
- }
157
- } else if (msgType === BohoMsg.ENC_E2E) {
158
-
159
- try {
160
- decoded = this.boho.decrypt_488(buffer)
161
- if (decoded) {
162
- // console.log( 'ENC_E2E decoded ', decoded )
163
- msgType = decoded[0]
164
- // decoded has msg_header only.
165
- buffer.set(decoded, MetaSize.ENC_488) // set decoded signal_e2e headaer.
166
- buffer = buffer.subarray(MetaSize.ENC_488) // reset offset.
167
- // console.log('DECODED MsgType:', IOMsg[ msgType ] )
168
- } else {
169
- // console.log('488 DEC_FAIL', buffer)
170
- return
171
- }
172
-
173
- } catch (err) {
174
- // console.log('E2E DEC_FAIL decryption error', err)
175
- return
176
- }
177
-
178
- }
179
-
180
- let type = IOMsg[msgType]
181
- if (!type) type = BohoMsg[msgType]
182
-
183
- // console.log( "MsgType: ", type , " LEN ", buffer.byteLength)
184
-
185
- switch (msgType) {
186
- case IOMsg.OVER_SIZE:
187
- console.log('## server sent: over_size event.')
188
- this.emit('over_size', 'over_size')
189
- break;
190
- case IOMsg.PING:
191
- this.pong();
192
- break;
193
-
194
- case IOMsg.PONG:
195
- break;
196
-
197
- case IOMsg.IAM_RES:
198
- try {
199
- let str = decoder.decode(buffer.subarray(1))
200
- let jsonInfo = JSON.parse(str)
201
- if (jsonInfo.ip) {
202
- this.ip = jsonInfo.ip;
203
- }
204
- console.log('<IAM_RES>', JSON.stringify(jsonInfo))
205
- // console.log('<IAM_RES>', JSON.stringify(jsonInfo,null,2))
206
- } catch (error) {
207
- // console.log('<IAM_RES> data error')
208
- }
209
- break;
210
-
211
- case IOMsg.CID_RES:
212
- let cidStr = decoder.decode(buffer.subarray(1))
213
- this.cid = cidStr;
214
-
215
- // **IMPORTANT** change state before subscribe.
216
- this.stateChange('ready', 'cid_ready')
217
- this.subscribe_memory_channels()
218
- break;
219
-
220
- case IOMsg.QUOTA_LEVEL:
221
- let quotaLevel = buffer[1]
222
- this.level = quotaLevel;
223
- this.quota = quotaTable[quotaLevel];
224
- console.log('## QUOTA:', quotaLevel, JSON.stringify(this.quota))
225
- break;
226
-
227
- case IOMsg.SERVER_CLEAR_AUTH:
228
- this.useAuth = false;
229
- this.boho.clearAuth();
230
- this.stop();
231
- break;
232
-
233
- case IOMsg.SERVER_REDIRECT:
234
- let host_port;
235
- let url;
236
- let protocol;
237
- let addressType;
238
- if (buffer.byteLength == 7) { // ipv4 ,port
239
- addressType = 'IPV4:PORT'
240
- host_port = byteToUrl(buffer.subarray(1))
241
- protocol = 'cong://'
242
- } else { // domain url
243
- addressType = 'URL'
244
- host_port = decoder.decode(buffer.subarray(1))
245
- protocol = ''
246
- }
247
-
248
- url = protocol + host_port
249
- this.redirect(url)
250
- break;
251
-
252
- case IOMsg.SERVER_READY:
253
- this.stateChange('server_ready', 'server_ready')
254
- if (this.useAuth) {
255
- this.send(this.boho.auth_req())
256
- // CID_REQ will be called, after auth_ack.
257
- } else {
258
- // CID_REQ here, if not using auth.
259
- this.send(Buffer.from([IOMsg.CID_REQ]))
260
- }
261
- break;
262
-
263
- case IOMsg.SERVER_SIGNAL:
264
- try {
265
- let str = decoder.decode(buffer.subarray(1))
266
- let ss = JSON.parse(str)
267
-
268
- if (ss.event && ss.data) {
269
- this.serverSet = ss.data;
270
- this.emit(ss.event, ss.data)
271
- }
272
-
273
- } catch (error) {
274
- // console.log('<SERVER_SIGNAL> parsing error')
275
- }
276
- break;
277
-
278
- case IOMsg.SET:
279
- try {
280
- let setPack = MBP.unpack(buffer)
281
- if (setPack) {
282
- this.emit(setPack.topic, ...setPack.args)
283
- }
284
- } catch (error) {
285
- // console.log('<SET> parsing error')
286
- }
287
- break;
288
-
289
- case IOMsg.SIGNAL_E2E:
290
- case IOMsg.SIGNAL:
291
- try {
292
- let tagLen = buffer.readUint8(1)
293
- let tagBuf = buffer.subarray(2, 2 + tagLen)
294
- let tag = decoder.decode(tagBuf)
295
-
296
- let payloadType = buffer.readUint8(2 + tagLen)
297
- let payloadBuffer = buffer.subarray(3 + tagLen)
298
-
299
- /* three types of signal message.
300
- > unicast message to me: tag includes @, no cid: '@*'
301
- > cid_sub message: tag includes cid and @ both : 'cid@*'
302
- > ch_sub message: else.
303
- */
304
- switch (payloadType) {
305
-
306
- case PAYLOAD_TYPE.EMPTY:
307
- if (tag.indexOf('@') === 0) this.emit('@', null, tag)
308
- else this.emit(tag, null, tag)
309
- break;
310
-
311
- case PAYLOAD_TYPE.TEXT:
312
- // !! Must remove null char before decode in JS.
313
- // string payload contains null char for the c/cpp devices.
314
- let payloadStringWithoutNull = payloadBuffer.subarray(0, payloadBuffer.byteLength - 1)
315
- let oneString = decoder.decode(payloadStringWithoutNull)
316
- if (tag.indexOf('@') === 0) this.emit('@', oneString, tag)
317
- if (tag !== '@') this.emit(tag, oneString, tag)
318
- break;
319
-
320
- case PAYLOAD_TYPE.BINARY:
321
- if (tag.indexOf('@') === 0) this.emit('@', payloadBuffer, tag)
322
- if (tag !== '@') this.emit(tag, payloadBuffer, tag)
323
- break;
324
-
325
- case PAYLOAD_TYPE.OBJECT:
326
- let oneObjectBuffer = decoder.decode(payloadBuffer)
327
- let oneJSONObject = JSON.parse(oneObjectBuffer)
328
- if (tag.indexOf('@') === 0) this.emit('@', oneJSONObject, tag)
329
- if (tag !== '@') this.emit(tag, oneJSONObject, tag)
330
- break;
331
-
332
- case PAYLOAD_TYPE.MJSON:
333
- let mjsonBuffer = decoder.decode(payloadBuffer)
334
- let mjson = JSON.parse(mjsonBuffer)
335
- if (tag.indexOf('@') === 0) this.emit('@', ...mjson, tag)
336
- if (tag !== '@') this.emit(tag, ...mjson, tag)
337
- break;
338
-
339
- case PAYLOAD_TYPE.MBA:
340
- let mbaObject = MBP.unpack(payloadBuffer)
341
- if (tag.indexOf('@') === 0) this.emit('@', ...mbaObject.args, tag)
342
- if (tag !== '@') this.emit(tag, ...mbaObject.args, tag)
343
- break;
344
-
345
- default:
346
- // console.log('## Unkown payloadtype', payloadType)
347
- }
348
-
349
- } catch (err) {
350
- // console.log('## signal parse err', err)
351
- }
352
- break;
353
-
354
- case IOMsg.RESPONSE_MBP:
355
- this.testPromise(buffer)
356
- break;
357
-
358
- case BohoMsg.AUTH_NONCE:
359
- let auth_hmac = this.boho.auth_hmac(buffer)
360
- if (auth_hmac) {
361
- this.send(auth_hmac)
362
- } else {
363
- this.stateChange('auth_fail', 'Invalid local auth_hmac.')
364
- }
365
- break;
366
-
367
- case BohoMsg.AUTH_FAIL:
368
- this.stateChange('auth_fail', 'server reject auth.')
369
- break;
370
-
371
- case BohoMsg.AUTH_ACK:
372
- if (this.boho.check_auth_ack_hmac(buffer)) {
373
- this.stateChange('auth_ready', 'server sent auth_ack')
374
- this.send(Buffer.from([IOMsg.CID_REQ]))
375
- } else {
376
- this.stateChange('auth_fail', 'invalid server_hmac')
377
- }
378
- break;
379
-
380
- default:
381
- try {
382
- decoded = decoder.decode(buffer)
383
- // console.log('text message:', decoded)
384
- this.emit('text_message', decoded)
385
- } catch (error) {
386
-
387
- }
388
-
389
- break;
390
-
391
- }
392
- }
393
-
394
- iam(title) {
395
- // console.log('iam', title)
396
- if (title) {
397
- this.send_enc_mode(MBP.pack(
398
- MBP.MB('#MsgType', '8', IOMsg.IAM),
399
- MBP.MB('#', title)
400
- ))
401
- } else {
402
- this.send_enc_mode(MBP.pack(
403
- MBP.MB('#MsgType', '8', IOMsg.IAM)
404
- ))
405
- }
406
- }
407
-
408
-
409
- ping() {
410
- this.send(Buffer.from([IOMsg.PING]))
411
- }
412
-
413
- pong() {
414
- this.send(Buffer.from([IOMsg.PONG]))
415
- }
416
-
417
-
418
- // application level ping tool.
419
- // simple message sending and reply.
420
- echo(args) {
421
- if (args) {
422
- console.log('echo args:', args)
423
- this.send_enc_mode(MBP.pack(
424
- MBP.MB('#MsgType', '8', IOMsg.ECHO),
425
- MBP.MB('#msg', args)
426
- ))
427
- } else {
428
- // # do not encrypt blank echo #
429
- this.send(Buffer.from([IOMsg.ECHO]))
430
- }
431
- }
432
-
433
-
434
- bin(...data) {
435
- this.send(MBP.U8pack(...data))
436
- }
437
-
438
- send(data) {
439
- if (data.byteLength > this.quota.signalSize) {
440
- this.emit('over_size')
441
- console.log('## QUOTA LIMIT OVER!! \nsignal message.byteLength: ', data.byteLength)
442
- console.log('## your maximum signalSize(bytes) is:', this.quota.signalSize)
443
- return
444
- }
445
- this.socket_send(data);
446
- }
447
-
448
- /*
449
- Policy. Should message do encrypt?
450
-
451
- if encMode == auto
452
- NO. if connection using TLS line.
453
- // ex. wss://url connection.
454
- YES. if no TLS line.
455
- // ex. ws://url connection.
456
-
457
- if encMode == YES
458
- YES. encrypt the message.
459
-
460
- if encMode == NO
461
- NO. do not ecnrypt message.
462
-
463
- */
464
- getEncryptionMode() {
465
- if (this.encMode === ENC_MODE.YES ||
466
- this.encMode === ENC_MODE.AUTO &&
467
- !this.TLS && this.boho.isAuthorized
468
- ) {
469
- return true;
470
- } else {
471
- return false
472
- }
473
- }
474
-
475
- send_enc_mode(data, useEncryption) {
476
-
477
- // use default policy.
478
- if (useEncryption === undefined) {
479
- useEncryption = this.getEncryptionMode()
480
- }
481
-
482
- if (data[0] == IOMsg.SIGNAL_E2E && useEncryption) {
483
- // input data: signal_header + e2ePayload
484
- // encrypt signal_header area only. payload is encrypted with e2e key already.
485
- let tagLen = data[1]
486
- let encHeader = this.boho.encrypt_488(data.subarray(0, 3 + tagLen))
487
- encHeader[0] = BohoMsg.ENC_E2E
488
- this.send(Buffer.concat([encHeader, data.subarray(3 + tagLen)]))
489
- // console.log('<< send_enc_mode [ ENC_E2E ]')
490
-
491
- } else if (useEncryption) {
492
- // console.log('<< send_enc_mode [ ENC_488 ]')
493
- let encPack = this.boho.encrypt_488(data)
494
- this.send(encPack)
495
- } else {
496
- // console.log('<< send_enc_mode [ PLAIN ]' )
497
- this.send(data)
498
- }
499
-
500
- }
501
-
502
-
503
- setMsgPromise(mid) {
504
- return new Promise((resolve, reject) => {
505
- this.promiseMap.set(mid, [resolve, reject])
506
- setTimeout(e => {
507
- if (this.promiseMap.has(mid)) {
508
- reject('timeout');
509
- this.promiseMap.delete(mid)
510
- }
511
- }, this.promiseTimeOut);
512
- })
513
- }
514
-
515
- testPromise(buffer) {
516
-
517
- let res = MBP.unpack(buffer)
518
- if (!res) return
519
-
520
- if (this.promiseMap.has(res.mid)) {
521
- let [resolve, reject] = this.promiseMap.get(res.mid)
522
- this.promiseMap.delete(res.mid)
523
-
524
- if (res.status < 128) {
525
- res.ok = true;
526
- resolve(res)
527
- } else {
528
- res.ok = false;
529
- reject(res)
530
- }
531
-
532
-
533
- } else {
534
- console.log('no promise id')
535
- }
536
- }
537
-
538
-
539
- publish(...args) {
540
- this.signal(...args)
541
- }
542
-
543
-
544
- signal(tag, ...args) {
545
- if (typeof tag !== 'string') throw TypeError('tag should be string.')
546
-
547
- let signalPack = getSignalPack(tag, ...args)
548
- this.send_enc_mode(signalPack)
549
- }
550
-
551
- decrypt_e2e(data, key) {
552
- return this.boho.decrypt_e2e(data, key)
553
- }
554
-
555
- signal_e2e(tag, data, key) {
556
-
557
- if (typeof tag !== 'string') throw TypeError('tag should be string.')
558
- let tagEncoded = encoder.encode(tag)
559
- let dataPack = MBP.B8(data)
560
-
561
- //encrypt payload area with key
562
- let sercretPack = this.boho.encrypt_e2e(dataPack, key)
563
-
564
- //change signal MsgType header into SIGNAL_E2E
565
- let signalPack = MBP.pack(
566
- MBP.MB('#MsgType', '8', IOMsg.SIGNAL_E2E),
567
- MBP.MB('#tagLen', '8', tagEncoded.byteLength),
568
- MBP.MB('#tag', tagEncoded),
569
- MBP.MB('#payloadType', '8', PAYLOAD_TYPE.BINARY),
570
- MBP.MB('#payload', sercretPack)
571
- )
572
-
573
- this.send_enc_mode(signalPack)
574
- }
575
-
576
-
577
-
578
- set(storeName, ...args) {
579
- if (!storeName || args.length == 0) {
580
- return Promise.reject(new Error('set need storeName and value)'))
581
- }
582
- return this.req('store', 'set', storeName, ...args)
583
- }
584
-
585
- async get(storeName) {
586
- if (!storeName) {
587
- return Promise.reject(new Error('store get need storeName)'))
588
- }
589
- let pack = await this.req('store', 'get', storeName)
590
- let { $ } = MBP.unpack(pack.body)
591
- return $
592
- }
593
-
594
-
595
- req(target, topic, ...args) {
596
- if (!target || !topic)
597
- return Promise.reject(new Error('request need target and topic)'))
598
- let sigPack;
599
- if (args.length > 0) {
600
- sigPack = MBP.pack(
601
- MBP.MB('#MsgType', '8', IOMsg.REQUEST),
602
- MBP.MB('mid', '16', ++this.mid),
603
- MBP.MB('target', target),
604
- MBP.MB('topic', topic),
605
- MBP.MBA(...args)
606
- )
607
- } else {
608
- sigPack = MBP.pack(
609
- MBP.MB('#MsgType', '8', IOMsg.REQUEST),
610
- MBP.MB('mid', '16', ++this.mid),
611
- MBP.MB('target', target),
612
- MBP.MB('topic', topic)
613
- )
614
- }
615
- this.send_enc_mode(sigPack)
616
- return this.setMsgPromise(this.mid)
617
- }
618
-
619
-
620
- subscribe(tag) {
621
- if (typeof tag !== 'string') throw TypeError('tag should be string.')
622
- if (this.state !== STATES.READY) return
623
-
624
- let tagList = tag.split(',')
625
- tagList.forEach(tag => {
626
- this.channels.add(tag)
627
- })
628
-
629
- let tagEncoded = encoder.encode(tag)
630
- if (tagEncoded.byteLength > SIZE_LIMIT.TAG_LEN1) throw TypeError('please use tag string bytelength below:' + SIZE_LIMIT.TAG_LEN1)
631
-
632
- this.send_enc_mode(
633
- Buffer.concat([
634
- MBP.NB('8', IOMsg.SUBSCRIBE),
635
- MBP.NB('8', tagEncoded.byteLength),
636
- tagEncoded]))
637
- }
638
-
639
- subscribe_promise(tag) {
640
- if (typeof tag !== 'string') throw TypeError('tag should be string.')
641
- if (this.state !== STATES.READY) {
642
- return Promise.reject('subscribe_promise:: connection is not ready')
643
- }
644
-
645
- let tagEncoded = encoder.encode(tag)
646
- if (tagEncoded.byteLength > SIZE_LIMIT.TAG_LEN2) throw TypeError('please use tag string bytelength: ' + SIZE_LIMIT.TAG_LEN2)
647
-
648
- this.send_enc_mode(
649
- Buffer.concat([
650
- MBP.NB('8', IOMsg.SUBSCRIBE_REQ),
651
- MBP.NB('16', ++this.mid),
652
- MBP.NB('16', tagEncoded.byteLength),
653
- tagEncoded]))
654
- return this.setMsgPromise(this.mid)
655
- }
656
-
657
- subscribe_memory_channels() { //local cache . auto_resubscribe
658
- if (this.channels.size == 0) return
659
- let chList = Array.from(this.channels).join(',')
660
-
661
- this.subscribe_promise(chList)
662
- .then((res) => {
663
- // console.log('>> SUBSCRIBE_REQ result', res ) // return code == map.size
664
- }).catch((e) => {
665
- console.log('>> SUBSCRIBE FAIL:', e)
666
- })
667
-
668
- }
669
-
670
- unsubscribe(tag = "") {
671
- if (typeof tag !== 'string') throw TypeError('tag should be string.')
672
-
673
- if (tag == "") { // blank tag means unsubscribe all
674
- this.channels.clear();
675
- } else {
676
- let tagList = tag.split(',')
677
- tagList.forEach(tag => {
678
- this.channels.delete(tag)
679
- })
680
- }
681
-
682
- let tagEncoded = encoder.encode(tag)
683
- if (tagEncoded.byteLength > SIZE_LIMIT.TAG_LEN1) throw TypeError('please use tag string bytelength below:' + SIZE_LIMIT.TAG_LEN1)
684
-
685
- this.send_enc_mode(Buffer.concat([
686
- MBP.NB('8', IOMsg.UNSUBSCRIBE),
687
- MBP.NB('8', tagEncoded.byteLength),
688
- tagEncoded]))
689
- }
690
-
691
-
692
- listen(tag, handler) {
693
- if (typeof tag !== 'string') throw TypeError('tag should be string.')
694
- if (tag.length > 255 || tag.length == 0) throw TypeError('tag string length range: 1~255')
695
- if (typeof handler !== 'function') throw TypeError('handler is not a function.')
696
-
697
- if (tag.indexOf('@') !== 0) {
698
- this.channels.add(tag)
699
- }
700
- this.on(tag, handler)
701
- // do not subscribe now.
702
- // will subscribe when receive CID_RES signal from server.
703
-
704
- }
705
-
706
-
707
-
708
- link(to, tag, handler) {
709
- if (typeof to !== 'string') throw TypeError('to(local link target) is not a string.')
710
- if (typeof tag !== 'string') throw TypeError('tag is not a string.')
711
- if (tag.length > 255 || tag.length == 0) throw TypeError('tag string length range: 1~255')
712
- if (typeof handler !== 'function') throw TypeError('handler is not a function.')
713
-
714
- if (tag.indexOf('@') !== 0) {
715
- this.channels.add(tag)
716
- }
717
-
718
- let linkSet;
719
- if (this.linkMap.has(to)) {
720
- linkSet = this.linkMap.get(to)
721
- } else {
722
- linkSet = new Set()
723
- }
724
-
725
- linkSet.add(tag)
726
- this.linkMap.set(to, linkSet)
727
- this.on(tag, handler)
728
- this.subscribe(tag)
729
-
730
- }
731
-
732
-
733
- unlink(to, tag) {
734
- if (typeof to !== 'string') throw TypeError('to(local link target) is not a string.')
735
- if (typeof tag !== 'string') throw TypeError('tag is not a string.')
736
- if (tag.length > 255 || tag.length == 0) throw TypeError('tag string length range: 1~255')
737
-
738
- if (!this.linkMap.has(to)) return;
739
-
740
- let linkSet = this.linkMap.get(to)
741
- let tags = Array.from(linkSet)
742
- for (let i = 0; i < tags.length; i++) {
743
- if (tags[i] == tag) {
744
- this.unsubscribe(tag)
745
- this.removeAllListeners(tag)
746
- linkSet.delete(tag)
747
- this.linkMap.set(to, linkSet)
748
- break;
749
- }
750
- }
751
-
752
- }
753
-
754
- unlinkAll(to) {
755
- if (typeof to !== 'string') throw TypeError('to(local link target) is not a string.')
756
- if (!this.linkMap.has(to)) return;
757
-
758
- let linkSet = this.linkMap.get(to)
759
- let tags = Array.from(linkSet)
760
- for (let i = 0; i < tags.length; i++) {
761
- this.unsubscribe(tags[i])
762
- this.removeAllListeners(tags[i])
763
- linkSet.delete(tags[i])
764
- }
765
- this.linkMap.delete(to)
766
-
767
- }
768
-
769
-
770
-
771
- getMetric() {
772
- return {
773
- tx: this.txCounter,
774
- rx: this.rxCounter,
775
- txb: this.txBytes,
776
- rxb: this.rxBytes,
777
- last: (Date.now() - this.lastTxRxTime) / 1000
778
- }
779
-
780
- }
781
-
782
- getState() {
783
- return this.state
784
- }
785
-
786
- getStateName() {
787
- //state <number>
788
- //value of constant STATES.NAME < number >
789
- //type of constant STATES.NAME name < string uppercase >
790
- //stateName,eventName <string lowercase>
791
- return (STATES[this.state]).toLowerCase()
792
- }
793
-
794
- getSecurity() {
795
- return {
796
- useAuth: this.useAuth,
797
- isTLS: this.TLS,
798
- isAuthorized: this.boho.isAuthorized,
799
- encMode: this.encMode,
800
- usingEncryption: this.getEncryptionMode()
801
- }
802
- }
803
-
804
- stateChange(state, emitEventAndMessage) {
805
- // STATES constant name : string upperCase
806
- // eventName, .stateName : string lowerCase
807
- // .state : number
808
- let eventName = state.toLowerCase()
809
- this.state = STATES[state.toUpperCase()] // state: number
810
- if (emitEventAndMessage) this.emit(eventName, emitEventAndMessage)
811
-
812
- if (this.stateName !== eventName) {
813
- this.stateName = eventName
814
- this.emit('change', eventName)
815
- }
816
- }
817
-
818
- }
819
-
820
-
821
-
822
-
1
+ import { MBP, Buffer } from 'meta-buffer-pack'
2
+ import EventEmitter from "eventemitter3";
3
+ import { Boho, BohoMsg, MetaSize } from "boho";
4
+ import { IOMsg, PAYLOAD_TYPE, SIZE_LIMIT, ENC_MODE, STATES } from '../common/constants.js'
5
+ import { quotaTable } from '../common/quotaTable.js'
6
+ import { getSignalPack } from '../common/payload.js';
7
+
8
+ const encoder = new TextEncoder()
9
+ const decoder = new TextDecoder()
10
+
11
+ function byteToUrl(buffer) {
12
+ //ipv4(4bytes) , port(2bytes)
13
+ if (buffer.byteLength != 6) return
14
+ let address = buffer[0].toString() + "." + buffer[1].toString()
15
+ + "." + buffer[2].toString() + "." + buffer[3].toString();
16
+ let port = (buffer[4] << 8) + buffer[5]
17
+ return address + ':' + port.toString()
18
+ }
19
+
20
+ export class IOCore extends EventEmitter {
21
+ constructor(url) {
22
+ super();
23
+ this.cid = "" // get from the server CID_RES
24
+ this.ip = "" // get from the server IAM_RES message.
25
+ this.socket = null;
26
+ this.url = url; // init default server url
27
+ this.state = STATES.CLOSED; // Number type
28
+ this.stateName = this.getStateName() // String type
29
+
30
+ this.txCounter = 0;
31
+ this.rxCounter = 0;
32
+ this.txBytes = 0;
33
+ this.rxBytes = 0;
34
+
35
+ this.lastTxRxTime = Date.now();
36
+ this.connectionCheckerPeriod = SIZE_LIMIT.CONNECTION_CHECKER_PERIOD;
37
+ this.connectionCheckerIntervalID = null;
38
+
39
+ this.boho = new Boho()
40
+ this.TLS = false // true if protocol is wss(TLS)
41
+ this.encMode = ENC_MODE.AUTO;
42
+ this.useAuth = false;
43
+
44
+ this.nick = "";
45
+ this.channels = new Set()
46
+ this.promiseMap = new Map()
47
+ this.promiseTimeOut = SIZE_LIMIT.PROMISE_TIMEOUT
48
+ this.mid = 0 // promise message id
49
+
50
+ this.level = 3; // also defaultQuotaLevel
51
+ this.quota = quotaTable[this.level];
52
+ this.serverSet = {}
53
+ this.linkMap = new Map()
54
+
55
+ this.on('open', this.onOpen.bind(this))
56
+ this.on('close', this.onClose.bind(this))
57
+ this.on('socket_data', this.onData.bind(this))
58
+ }
59
+
60
+
61
+ redirect(url2) {
62
+ this.close()
63
+ this.stateChange('redirecting')
64
+ this.createConnection(url2)
65
+ }
66
+
67
+ open(url) {
68
+ if (!url && !this.url) return;
69
+
70
+ if (url) {
71
+ if (!this.url) { // default host url
72
+ this.url = url
73
+ } else if (url !== this.url) { // default host url change
74
+ this.url = url;
75
+ if (this.socket) {
76
+ this.close()
77
+ return
78
+ }
79
+ }
80
+ }
81
+
82
+ this.createConnection(this.url)
83
+
84
+ if (!this.connectionCheckerIntervalID) {
85
+ this.connectionCheckerIntervalID = setInterval(this.keepAlive.bind(this), this.connectionCheckerPeriod);
86
+ }
87
+ }
88
+
89
+ onOpen() {
90
+ if (this.url.includes("wss://")) {
91
+ this.TLS = true;
92
+ } else {
93
+ this.TLS = false;
94
+ }
95
+ this.stateChange('open')
96
+ }
97
+
98
+ onClose() {
99
+ this.boho.isAuthorized = false;
100
+ this.cid = ""
101
+ this.stateChange('closed')
102
+ }
103
+
104
+ // manual login
105
+ login(id, key) {
106
+ if (!id && !key) {
107
+ console.log('no id and key.')
108
+ return
109
+ }
110
+ console.log('manual login: ', id)
111
+
112
+ if (!key && id.includes('.')) {
113
+ this.boho.set_id_key(id)
114
+ } else if (id && key) {
115
+ this.boho.set_id8(id)
116
+ this.boho.set_key(key)
117
+ } else {
118
+ console.log('no id or key.')
119
+ return
120
+ }
121
+ this.useAuth = true
122
+ let auth_pack = this.boho.auth_req()
123
+ this.send(auth_pack)
124
+ }
125
+
126
+ // auto login
127
+ auth(id, key) {
128
+ if (!id && !key) {
129
+ console.log('no id and key.')
130
+ return
131
+ }
132
+
133
+ if (!key && id.includes('.')) {
134
+ this.boho.set_id_key(id)
135
+ } else if (id && key) {
136
+ this.boho.set_id8(id)
137
+ this.boho.set_key(key)
138
+ } else {
139
+ console.log('no id or key.')
140
+ return
141
+ }
142
+ this.useAuth = true
143
+ }
144
+
145
+ onData(buffer) {
146
+ let msgType = buffer[0];
147
+ let decoded;
148
+
149
+ if (msgType === BohoMsg.ENC_488) {
150
+ decoded = this.boho.decrypt_488(buffer)
151
+ if (decoded) {
152
+ msgType = decoded[0]
153
+ buffer = decoded
154
+ } else {
155
+ // console.log('DEC_FAIL', buffer.byteLength)
156
+ }
157
+ } else if (msgType === BohoMsg.ENC_E2E) {
158
+
159
+ try {
160
+ decoded = this.boho.decrypt_488(buffer)
161
+ if (decoded) {
162
+ // console.log( 'ENC_E2E decoded ', decoded )
163
+ msgType = decoded[0]
164
+ // decoded has msg_header only.
165
+ buffer.set(decoded, MetaSize.ENC_488) // set decoded signal_e2e headaer.
166
+ buffer = buffer.subarray(MetaSize.ENC_488) // reset offset.
167
+ // console.log('DECODED MsgType:', IOMsg[ msgType ] )
168
+ } else {
169
+ // console.log('488 DEC_FAIL', buffer)
170
+ return
171
+ }
172
+
173
+ } catch (err) {
174
+ // console.log('E2E DEC_FAIL decryption error', err)
175
+ return
176
+ }
177
+
178
+ }
179
+
180
+ let type = IOMsg[msgType]
181
+ if (!type) type = BohoMsg[msgType]
182
+
183
+ // console.log( "MsgType: ", type , " LEN ", buffer.byteLength)
184
+
185
+ switch (msgType) {
186
+ case IOMsg.OVER_SIZE:
187
+ console.log('## server sent: over_size event.')
188
+ this.emit('over_size', 'over_size')
189
+ break;
190
+ case IOMsg.PING:
191
+ this.pong();
192
+ break;
193
+
194
+ case IOMsg.PONG:
195
+ break;
196
+
197
+ case IOMsg.IAM_RES:
198
+ try {
199
+ let str = decoder.decode(buffer.subarray(1))
200
+ let jsonInfo = JSON.parse(str)
201
+ if (jsonInfo.ip) {
202
+ this.ip = jsonInfo.ip;
203
+ }
204
+ console.log('<IAM_RES>', JSON.stringify(jsonInfo))
205
+ // console.log('<IAM_RES>', JSON.stringify(jsonInfo,null,2))
206
+ } catch (error) {
207
+ // console.log('<IAM_RES> data error')
208
+ }
209
+ break;
210
+
211
+ case IOMsg.CID_RES:
212
+ let cidStr = decoder.decode(buffer.subarray(1))
213
+ this.cid = cidStr;
214
+
215
+ // **IMPORTANT** change state before subscribe.
216
+ this.stateChange('ready', 'cid_ready')
217
+ this.subscribe_memory_channels()
218
+ break;
219
+
220
+ case IOMsg.QUOTA_LEVEL:
221
+ let quotaLevel = buffer[1]
222
+ this.level = quotaLevel;
223
+ this.quota = quotaTable[quotaLevel];
224
+ console.log('## QUOTA:', quotaLevel, JSON.stringify(this.quota))
225
+ break;
226
+
227
+ case IOMsg.SERVER_CLEAR_AUTH:
228
+ this.useAuth = false;
229
+ this.boho.clearAuth();
230
+ this.stop();
231
+ break;
232
+
233
+ case IOMsg.SERVER_REDIRECT:
234
+ let host_port;
235
+ let url;
236
+ let protocol;
237
+ let addressType;
238
+ if (buffer.byteLength == 7) { // ipv4 ,port
239
+ addressType = 'IPV4:PORT'
240
+ host_port = byteToUrl(buffer.subarray(1))
241
+ protocol = 'cong://'
242
+ } else { // domain url
243
+ addressType = 'URL'
244
+ host_port = decoder.decode(buffer.subarray(1))
245
+ protocol = ''
246
+ }
247
+
248
+ url = protocol + host_port
249
+ this.redirect(url)
250
+ break;
251
+
252
+ case IOMsg.SERVER_READY:
253
+ this.stateChange('server_ready', 'server_ready')
254
+ if (this.useAuth) {
255
+ this.send(this.boho.auth_req())
256
+ // CID_REQ will be called, after auth_ack.
257
+ } else {
258
+ // CID_REQ here, if not using auth.
259
+ this.send(Buffer.from([IOMsg.CID_REQ]))
260
+ }
261
+ break;
262
+
263
+ case IOMsg.SERVER_SIGNAL:
264
+ try {
265
+ let str = decoder.decode(buffer.subarray(1))
266
+ let ss = JSON.parse(str)
267
+
268
+ if (ss.event && ss.data) {
269
+ this.serverSet = ss.data;
270
+ this.emit(ss.event, ss.data)
271
+ }
272
+
273
+ } catch (error) {
274
+ // console.log('<SERVER_SIGNAL> parsing error')
275
+ }
276
+ break;
277
+
278
+ case IOMsg.SET:
279
+ try {
280
+ let setPack = MBP.unpack(buffer)
281
+ if (setPack) {
282
+ this.emit(setPack.topic, ...setPack.args)
283
+ }
284
+ } catch (error) {
285
+ // console.log('<SET> parsing error')
286
+ }
287
+ break;
288
+
289
+ case IOMsg.SIGNAL_E2E:
290
+ case IOMsg.SIGNAL:
291
+ try {
292
+ let tagLen = buffer.readUint8(1)
293
+ let tagBuf = buffer.subarray(2, 2 + tagLen)
294
+ let tag = decoder.decode(tagBuf)
295
+
296
+ let payloadType = buffer.readUint8(2 + tagLen)
297
+ let payloadBuffer = buffer.subarray(3 + tagLen)
298
+
299
+ /* three types of signal message.
300
+ > unicast message to me: tag includes @, no cid: '@*'
301
+ > cid_sub message: tag includes cid and @ both : 'cid@*'
302
+ > ch_sub message: else.
303
+ */
304
+ switch (payloadType) {
305
+
306
+ case PAYLOAD_TYPE.EMPTY:
307
+ if (tag.indexOf('@') === 0) this.emit('@', null, tag)
308
+ else this.emit(tag, null, tag)
309
+ break;
310
+
311
+ case PAYLOAD_TYPE.TEXT:
312
+ // !! Must remove null char before decode in JS.
313
+ // string payload contains null char for the c/cpp devices.
314
+ let payloadStringWithoutNull = payloadBuffer.subarray(0, payloadBuffer.byteLength - 1)
315
+ let oneString = decoder.decode(payloadStringWithoutNull)
316
+ if (tag.indexOf('@') === 0) this.emit('@', oneString, tag)
317
+ if (tag !== '@') this.emit(tag, oneString, tag)
318
+ break;
319
+
320
+ case PAYLOAD_TYPE.BINARY:
321
+ if (tag.indexOf('@') === 0) this.emit('@', payloadBuffer, tag)
322
+ if (tag !== '@') this.emit(tag, payloadBuffer, tag)
323
+ break;
324
+
325
+ case PAYLOAD_TYPE.OBJECT:
326
+ let oneObjectBuffer = decoder.decode(payloadBuffer)
327
+ let oneJSONObject = JSON.parse(oneObjectBuffer)
328
+ if (tag.indexOf('@') === 0) this.emit('@', oneJSONObject, tag)
329
+ if (tag !== '@') this.emit(tag, oneJSONObject, tag)
330
+ break;
331
+
332
+ case PAYLOAD_TYPE.MJSON:
333
+ let mjsonBuffer = decoder.decode(payloadBuffer)
334
+ let mjson = JSON.parse(mjsonBuffer)
335
+ if (tag.indexOf('@') === 0) this.emit('@', ...mjson, tag)
336
+ if (tag !== '@') this.emit(tag, ...mjson, tag)
337
+ break;
338
+
339
+ case PAYLOAD_TYPE.MBA:
340
+ let mbaObject = MBP.unpack(payloadBuffer)
341
+ if (tag.indexOf('@') === 0) this.emit('@', ...mbaObject.args, tag)
342
+ if (tag !== '@') this.emit(tag, ...mbaObject.args, tag)
343
+ break;
344
+
345
+ default:
346
+ // console.log('## Unkown payloadtype', payloadType)
347
+ }
348
+
349
+ } catch (err) {
350
+ // console.log('## signal parse err', err)
351
+ }
352
+ break;
353
+
354
+ case IOMsg.RESPONSE_MBP:
355
+ this.testPromise(buffer)
356
+ break;
357
+
358
+ case BohoMsg.AUTH_NONCE:
359
+ let auth_hmac = this.boho.auth_hmac(buffer)
360
+ if (auth_hmac) {
361
+ this.send(auth_hmac)
362
+ } else {
363
+ this.stateChange('auth_fail', 'Invalid local auth_hmac.')
364
+ }
365
+ break;
366
+
367
+ case BohoMsg.AUTH_FAIL:
368
+ this.stateChange('auth_fail', 'server reject auth.')
369
+ break;
370
+
371
+ case BohoMsg.AUTH_ACK:
372
+ if (this.boho.check_auth_ack_hmac(buffer)) {
373
+ this.stateChange('auth_ready', 'server sent auth_ack')
374
+ this.send(Buffer.from([IOMsg.CID_REQ]))
375
+ } else {
376
+ this.stateChange('auth_fail', 'invalid server_hmac')
377
+ }
378
+ break;
379
+
380
+ default:
381
+ try {
382
+ decoded = decoder.decode(buffer)
383
+ // console.log('text message:', decoded)
384
+ this.emit('text_message', decoded)
385
+ } catch (error) {
386
+
387
+ }
388
+
389
+ break;
390
+
391
+ }
392
+ }
393
+
394
+ iam(title) {
395
+ // console.log('iam', title)
396
+ if (title) {
397
+ this.send_enc_mode(MBP.pack(
398
+ MBP.MB('#MsgType', '8', IOMsg.IAM),
399
+ MBP.MB('#', title)
400
+ ))
401
+ } else {
402
+ this.send_enc_mode(MBP.pack(
403
+ MBP.MB('#MsgType', '8', IOMsg.IAM)
404
+ ))
405
+ }
406
+ }
407
+
408
+
409
+ ping() {
410
+ this.send(Buffer.from([IOMsg.PING]))
411
+ }
412
+
413
+ pong() {
414
+ this.send(Buffer.from([IOMsg.PONG]))
415
+ }
416
+
417
+
418
+ // application level ping tool.
419
+ // simple message sending and reply.
420
+ echo(args) {
421
+ if (args) {
422
+ console.log('echo args:', args)
423
+ this.send_enc_mode(MBP.pack(
424
+ MBP.MB('#MsgType', '8', IOMsg.ECHO),
425
+ MBP.MB('#msg', args)
426
+ ))
427
+ } else {
428
+ // # do not encrypt blank echo #
429
+ this.send(Buffer.from([IOMsg.ECHO]))
430
+ }
431
+ }
432
+
433
+
434
+ bin(...data) {
435
+ this.send(MBP.U8pack(...data))
436
+ }
437
+
438
+ send(data) {
439
+ if (data.byteLength > this.quota.signalSize) {
440
+ this.emit('over_size')
441
+ console.log('## QUOTA LIMIT OVER!! \nsignal message.byteLength: ', data.byteLength)
442
+ console.log('## your maximum signalSize(bytes) is:', this.quota.signalSize)
443
+ return
444
+ }
445
+ this.socket_send(data);
446
+ }
447
+
448
+ /*
449
+ Policy. Should message do encrypt?
450
+
451
+ if encMode == auto
452
+ NO. if connection using TLS line.
453
+ // ex. wss://url connection.
454
+ YES. if no TLS line.
455
+ // ex. ws://url connection.
456
+
457
+ if encMode == YES
458
+ YES. encrypt the message.
459
+
460
+ if encMode == NO
461
+ NO. do not ecnrypt message.
462
+
463
+ */
464
+ getEncryptionMode() {
465
+ if (this.encMode === ENC_MODE.YES ||
466
+ this.encMode === ENC_MODE.AUTO &&
467
+ !this.TLS && this.boho.isAuthorized
468
+ ) {
469
+ return true;
470
+ } else {
471
+ return false
472
+ }
473
+ }
474
+
475
+ send_enc_mode(data, useEncryption) {
476
+
477
+ // use default policy.
478
+ if (useEncryption === undefined) {
479
+ useEncryption = this.getEncryptionMode()
480
+ }
481
+
482
+ if (data[0] == IOMsg.SIGNAL_E2E && useEncryption) {
483
+ // input data: signal_header + e2ePayload
484
+ // encrypt signal_header area only. payload is encrypted with e2e key already.
485
+ let tagLen = data[1]
486
+ let encHeader = this.boho.encrypt_488(data.subarray(0, 3 + tagLen))
487
+ encHeader[0] = BohoMsg.ENC_E2E
488
+ this.send(Buffer.concat([encHeader, data.subarray(3 + tagLen)]))
489
+ // console.log('<< send_enc_mode [ ENC_E2E ]')
490
+
491
+ } else if (useEncryption) {
492
+ // console.log('<< send_enc_mode [ ENC_488 ]')
493
+ let encPack = this.boho.encrypt_488(data)
494
+ this.send(encPack)
495
+ } else {
496
+ // console.log('<< send_enc_mode [ PLAIN ]' )
497
+ this.send(data)
498
+ }
499
+
500
+ }
501
+
502
+
503
+ setMsgPromise(mid) {
504
+ return new Promise((resolve, reject) => {
505
+ this.promiseMap.set(mid, [resolve, reject])
506
+ setTimeout(e => {
507
+ if (this.promiseMap.has(mid)) {
508
+ reject('timeout');
509
+ this.promiseMap.delete(mid)
510
+ }
511
+ }, this.promiseTimeOut);
512
+ })
513
+ }
514
+
515
+ testPromise(buffer) {
516
+
517
+ let res = MBP.unpack(buffer)
518
+ if (!res) return
519
+
520
+ if (this.promiseMap.has(res.mid)) {
521
+ let [resolve, reject] = this.promiseMap.get(res.mid)
522
+ this.promiseMap.delete(res.mid)
523
+
524
+ if (res.status < 128) {
525
+ res.ok = true;
526
+ resolve(res)
527
+ } else {
528
+ res.ok = false;
529
+ reject(res)
530
+ }
531
+
532
+
533
+ } else {
534
+ console.log('no promise id')
535
+ }
536
+ }
537
+
538
+
539
+ publish(...args) {
540
+ this.signal(...args)
541
+ }
542
+
543
+
544
+ signal(tag, ...args) {
545
+ if (typeof tag !== 'string') throw TypeError('tag should be string.')
546
+
547
+ let signalPack = getSignalPack(tag, ...args)
548
+ this.send_enc_mode(signalPack)
549
+ }
550
+
551
+ decrypt_e2e(data, key) {
552
+ return this.boho.decrypt_e2e(data, key)
553
+ }
554
+
555
+ signal_e2e(tag, data, key) {
556
+
557
+ if (typeof tag !== 'string') throw TypeError('tag should be string.')
558
+ let tagEncoded = encoder.encode(tag)
559
+ let dataPack = MBP.B8(data)
560
+
561
+ //encrypt payload area with key
562
+ let sercretPack = this.boho.encrypt_e2e(dataPack, key)
563
+
564
+ //change signal MsgType header into SIGNAL_E2E
565
+ let signalPack = MBP.pack(
566
+ MBP.MB('#MsgType', '8', IOMsg.SIGNAL_E2E),
567
+ MBP.MB('#tagLen', '8', tagEncoded.byteLength),
568
+ MBP.MB('#tag', tagEncoded),
569
+ MBP.MB('#payloadType', '8', PAYLOAD_TYPE.BINARY),
570
+ MBP.MB('#payload', sercretPack)
571
+ )
572
+
573
+ this.send_enc_mode(signalPack)
574
+ }
575
+
576
+
577
+
578
+ set(storeName, ...args) {
579
+ if (!storeName || args.length == 0) {
580
+ return Promise.reject(new Error('set need storeName and value)'))
581
+ }
582
+ return this.req('store', 'set', storeName, ...args)
583
+ }
584
+
585
+ async get(storeName) {
586
+ if (!storeName) {
587
+ return Promise.reject(new Error('store get need storeName)'))
588
+ }
589
+ let pack = await this.req('store', 'get', storeName)
590
+ let { $ } = MBP.unpack(pack.body)
591
+ return $
592
+ }
593
+
594
+
595
+ req(target, topic, ...args) {
596
+ if (!target || !topic)
597
+ return Promise.reject(new Error('request need target and topic)'))
598
+ let sigPack;
599
+ if (args.length > 0) {
600
+ sigPack = MBP.pack(
601
+ MBP.MB('#MsgType', '8', IOMsg.REQUEST),
602
+ MBP.MB('mid', '16', ++this.mid),
603
+ MBP.MB('target', target),
604
+ MBP.MB('topic', topic),
605
+ MBP.MBA(...args)
606
+ )
607
+ } else {
608
+ sigPack = MBP.pack(
609
+ MBP.MB('#MsgType', '8', IOMsg.REQUEST),
610
+ MBP.MB('mid', '16', ++this.mid),
611
+ MBP.MB('target', target),
612
+ MBP.MB('topic', topic)
613
+ )
614
+ }
615
+ this.send_enc_mode(sigPack)
616
+ return this.setMsgPromise(this.mid)
617
+ }
618
+
619
+
620
+ subscribe(tag) {
621
+ if (typeof tag !== 'string') throw TypeError('tag should be string.')
622
+ if (this.state !== STATES.READY) return
623
+
624
+ let tagList = tag.split(',')
625
+ tagList.forEach(tag => {
626
+ this.channels.add(tag)
627
+ })
628
+
629
+ let tagEncoded = encoder.encode(tag)
630
+ if (tagEncoded.byteLength > SIZE_LIMIT.TAG_LEN1) throw TypeError('please use tag string bytelength below:' + SIZE_LIMIT.TAG_LEN1)
631
+
632
+ this.send_enc_mode(
633
+ Buffer.concat([
634
+ MBP.NB('8', IOMsg.SUBSCRIBE),
635
+ MBP.NB('8', tagEncoded.byteLength),
636
+ tagEncoded]))
637
+ }
638
+
639
+ subscribe_promise(tag) {
640
+ if (typeof tag !== 'string') throw TypeError('tag should be string.')
641
+ if (this.state !== STATES.READY) {
642
+ return Promise.reject('subscribe_promise:: connection is not ready')
643
+ }
644
+
645
+ let tagEncoded = encoder.encode(tag)
646
+ if (tagEncoded.byteLength > SIZE_LIMIT.TAG_LEN2) throw TypeError('please use tag string bytelength: ' + SIZE_LIMIT.TAG_LEN2)
647
+
648
+ this.send_enc_mode(
649
+ Buffer.concat([
650
+ MBP.NB('8', IOMsg.SUBSCRIBE_REQ),
651
+ MBP.NB('16', ++this.mid),
652
+ MBP.NB('16', tagEncoded.byteLength),
653
+ tagEncoded]))
654
+ return this.setMsgPromise(this.mid)
655
+ }
656
+
657
+ subscribe_memory_channels() { //local cache . auto_resubscribe
658
+ if (this.channels.size == 0) return
659
+ let chList = Array.from(this.channels).join(',')
660
+
661
+ this.subscribe_promise(chList)
662
+ .then((res) => {
663
+ // console.log('>> SUBSCRIBE_REQ result', res ) // return code == map.size
664
+ }).catch((e) => {
665
+ console.log('>> SUBSCRIBE FAIL:', e)
666
+ })
667
+
668
+ }
669
+
670
+ unsubscribe(tag = "") {
671
+ if (typeof tag !== 'string') throw TypeError('tag should be string.')
672
+
673
+ if (tag == "") { // blank tag means unsubscribe all
674
+ this.channels.clear();
675
+ } else {
676
+ let tagList = tag.split(',')
677
+ tagList.forEach(tag => {
678
+ this.channels.delete(tag)
679
+ })
680
+ }
681
+
682
+ let tagEncoded = encoder.encode(tag)
683
+ if (tagEncoded.byteLength > SIZE_LIMIT.TAG_LEN1) throw TypeError('please use tag string bytelength below:' + SIZE_LIMIT.TAG_LEN1)
684
+
685
+ this.send_enc_mode(Buffer.concat([
686
+ MBP.NB('8', IOMsg.UNSUBSCRIBE),
687
+ MBP.NB('8', tagEncoded.byteLength),
688
+ tagEncoded]))
689
+ }
690
+
691
+
692
+ listen(tag, handler) {
693
+ if (typeof tag !== 'string') throw TypeError('tag should be string.')
694
+ if (tag.length > 255 || tag.length == 0) throw TypeError('tag string length range: 1~255')
695
+ if (typeof handler !== 'function') throw TypeError('handler is not a function.')
696
+
697
+ if (tag.indexOf('@') !== 0) {
698
+ this.channels.add(tag)
699
+ }
700
+ this.on(tag, handler)
701
+ // do not subscribe now.
702
+ // will subscribe when receive CID_RES signal from server.
703
+
704
+ }
705
+
706
+
707
+
708
+ link(to, tag, handler) {
709
+ if (typeof to !== 'string') throw TypeError('to(local link target) is not a string.')
710
+ if (typeof tag !== 'string') throw TypeError('tag is not a string.')
711
+ if (tag.length > 255 || tag.length == 0) throw TypeError('tag string length range: 1~255')
712
+ if (typeof handler !== 'function') throw TypeError('handler is not a function.')
713
+
714
+ if (tag.indexOf('@') !== 0) {
715
+ this.channels.add(tag)
716
+ }
717
+
718
+ let linkSet;
719
+ if (this.linkMap.has(to)) {
720
+ linkSet = this.linkMap.get(to)
721
+ } else {
722
+ linkSet = new Set()
723
+ }
724
+
725
+ linkSet.add(tag)
726
+ this.linkMap.set(to, linkSet)
727
+ this.on(tag, handler)
728
+ this.subscribe(tag)
729
+
730
+ }
731
+
732
+
733
+ unlink(to, tag) {
734
+ if (typeof to !== 'string') throw TypeError('to(local link target) is not a string.')
735
+ if (typeof tag !== 'string') throw TypeError('tag is not a string.')
736
+ if (tag.length > 255 || tag.length == 0) throw TypeError('tag string length range: 1~255')
737
+
738
+ if (!this.linkMap.has(to)) return;
739
+
740
+ let linkSet = this.linkMap.get(to)
741
+ let tags = Array.from(linkSet)
742
+ for (let i = 0; i < tags.length; i++) {
743
+ if (tags[i] == tag) {
744
+ this.unsubscribe(tag)
745
+ this.removeAllListeners(tag)
746
+ linkSet.delete(tag)
747
+ this.linkMap.set(to, linkSet)
748
+ break;
749
+ }
750
+ }
751
+
752
+ }
753
+
754
+ unlinkAll(to) {
755
+ if (typeof to !== 'string') throw TypeError('to(local link target) is not a string.')
756
+ if (!this.linkMap.has(to)) return;
757
+
758
+ let linkSet = this.linkMap.get(to)
759
+ let tags = Array.from(linkSet)
760
+ for (let i = 0; i < tags.length; i++) {
761
+ this.unsubscribe(tags[i])
762
+ this.removeAllListeners(tags[i])
763
+ linkSet.delete(tags[i])
764
+ }
765
+ this.linkMap.delete(to)
766
+
767
+ }
768
+
769
+
770
+
771
+ getMetric() {
772
+ return {
773
+ tx: this.txCounter,
774
+ rx: this.rxCounter,
775
+ txb: this.txBytes,
776
+ rxb: this.rxBytes,
777
+ last: (Date.now() - this.lastTxRxTime) / 1000
778
+ }
779
+
780
+ }
781
+
782
+ getState() {
783
+ return this.state
784
+ }
785
+
786
+ getStateName() {
787
+ //state <number>
788
+ //value of constant STATES.NAME < number >
789
+ //type of constant STATES.NAME name < string uppercase >
790
+ //stateName,eventName <string lowercase>
791
+ return (STATES[this.state]).toLowerCase()
792
+ }
793
+
794
+ getSecurity() {
795
+ return {
796
+ useAuth: this.useAuth,
797
+ isTLS: this.TLS,
798
+ isAuthorized: this.boho.isAuthorized,
799
+ encMode: this.encMode,
800
+ usingEncryption: this.getEncryptionMode()
801
+ }
802
+ }
803
+
804
+ stateChange(state, emitEventAndMessage) {
805
+ // STATES constant name : string upperCase
806
+ // eventName, .stateName : string lowerCase
807
+ // .state : number
808
+ let eventName = state.toLowerCase()
809
+ this.state = STATES[state.toUpperCase()] // state: number
810
+ if (emitEventAndMessage) this.emit(eventName, emitEventAndMessage)
811
+
812
+ if (this.stateName !== eventName) {
813
+ this.stateName = eventName
814
+ this.emit('change', eventName)
815
+ }
816
+ }
817
+
818
+ }
819
+
820
+
821
+
822
+