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