iosignal 5.1.4 → 5.3.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 (43) hide show
  1. package/README.md +163 -0
  2. package/dist/{io.d.ts → browser/esm/io.d.ts} +10 -12
  3. package/dist/browser/esm/io.js +23 -0
  4. package/dist/browser/esm/io.js.map +1 -0
  5. package/dist/browser/iife/io.js +23 -0
  6. package/dist/browser/iife/io.js.map +1 -0
  7. package/dist/{iosignal.cjs → node/iosignal.cjs} +124 -119
  8. package/dist/{iosignal.js → node/iosignal.js} +123 -118
  9. package/dist/types/client/IOCore.d.ts +3 -4
  10. package/dist/types/client/browser/IOWebSocket.d.ts +5 -5
  11. package/dist/types/common/payload.d.ts +2 -2
  12. package/package.json +32 -15
  13. package/dist/io.js +0 -23
  14. package/dist/io.js.map +0 -1
  15. package/dist/io.min.js +0 -23
  16. package/dist/io.min.js.map +0 -1
  17. package/index.js +0 -26
  18. package/rollup.config.js +0 -59
  19. package/src/auth/BohoAuth.js +0 -196
  20. package/src/auth/key_providers/FileKeyProvider.js +0 -59
  21. package/src/auth/key_providers/RedisKeyProvider.js +0 -46
  22. package/src/auth/key_providers/StringKeyProvider.js +0 -54
  23. package/src/client/CongPacket.js +0 -143
  24. package/src/client/IOCongSocket.js +0 -92
  25. package/src/client/IOCore.js +0 -1202
  26. package/src/client/IOWS.js +0 -78
  27. package/src/client/browser/IOWebSocket.js +0 -202
  28. package/src/common/constants.js +0 -210
  29. package/src/common/payload.js +0 -82
  30. package/src/common/quotaTable.js +0 -68
  31. package/src/common/util.js +0 -64
  32. package/src/server/FileLogger.js +0 -30
  33. package/src/server/Manager.js +0 -450
  34. package/src/server/Metrics.js +0 -155
  35. package/src/server/Remote.js +0 -216
  36. package/src/server/RemoteCore.js +0 -427
  37. package/src/server/Server.js +0 -236
  38. package/src/server/serverOption.js +0 -56
  39. package/src/services/RedisService.js +0 -53
  40. package/src/services/constant.js +0 -7
  41. package/src/services/replyService.js +0 -39
  42. package/src/services/sudoService.js +0 -85
  43. package/tsconfig.json +0 -24
@@ -1,1202 +0,0 @@
1
- import MBP from 'meta-buffer-pack'
2
- import EventEmitter from "eventemitter3";
3
- import { IOMsg, PAYLOAD_TYPE, SIZE_LIMIT, ENC_MODE, STATE } from '../common/constants.js'
4
- import { quotaTable } from '../common/quotaTable.js'
5
- import { getSignalPack } from '../common/payload.js';
6
- import Boho from "boho";
7
-
8
- /**
9
- * @typedef {import('meta-buffer-pack').MBP} MBP
10
- * @typedef {import('../common/constants.js').IOMsg} IOMsg
11
- * @typedef {import('../common/constants.js').PAYLOAD_TYPE} PAYLOAD_TYPE
12
- * @typedef {import('../common/constants.js').SIZE_LIMIT} SIZE_LIMIT
13
- * @typedef {import('../common/constants.js').ENC_MODE} ENC_MODE
14
- * @typedef {import('../common/constants.js').STATE} STATE
15
- * @typedef {import('../common/quotaTable.js').quotaTable} quotaTable
16
-
17
- * @typedef {import('boho').Boho} Boho
18
- * @typedef {import('boho').Buffer} Buffer
19
- */
20
-
21
- const Buffer = MBP.Buffer;
22
- const encoder = new TextEncoder()
23
- const decoder = new TextDecoder()
24
-
25
- function byteToUrl(buffer) {
26
- //ipv4(4bytes) , port(2bytes)
27
- if (buffer.byteLength != 6) return
28
- let address = buffer[0].toString() + "." + buffer[1].toString()
29
- + "." + buffer[2].toString() + "." + buffer[3].toString();
30
- let port = (buffer[4] << 8) + buffer[5]
31
- return address + ':' + port.toString()
32
- }
33
-
34
- /**
35
- * Core class for handling WebSocket communication.
36
- * @augments {EventEmitter}
37
- */
38
- export class IOCore extends EventEmitter {
39
- /**
40
- * @param {string} url - The WebSocket URL to connect to.
41
- */
42
- constructor(url) {
43
- super();
44
- /**
45
- * Client ID received from the server.
46
- * @type {string}
47
- */
48
- this.cid = "" // get from the server CID_RES
49
- /**
50
- * IP address received from the server.
51
- * @type {string}
52
- */
53
- this.ip = "" // get from the server IAM_RES message.
54
- /**
55
- * The WebSocket instance.
56
- * @type {WebSocket | null}
57
- */
58
- this.socket = null;
59
- /**
60
- * The default server URL.
61
- * @type {string}
62
- */
63
- this.url = url; // init default server url
64
- /**
65
- * Current connection state (number).
66
- * @type {number}
67
- */
68
- this.state = STATE.CLOSED; // Number type
69
- /**
70
- * Current connection state (string).
71
- * @type {string}
72
- */
73
- this.stateName = this.getStateName() // String type
74
-
75
- /**
76
- * Transmitted message counter.
77
- * @type {number}
78
- */
79
- this.txCounter = 0;
80
- /**
81
- * Received message counter.
82
- * @type {number}
83
- */
84
- this.rxCounter = 0;
85
- /**
86
- * Transmitted bytes counter.
87
- * @type {number}
88
- */
89
- this.txBytes = 0;
90
- /**
91
- * Received bytes counter.
92
- * @type {number}
93
- */
94
- this.rxBytes = 0;
95
-
96
- /**
97
- * Last transmit/receive time.
98
- * @type {number}
99
- */
100
- this.lastTxRxTime = Date.now();
101
- /**
102
- * Period for connection checker.
103
- * @type {number}
104
- */
105
- this.connectionCheckerPeriod = SIZE_LIMIT.CONNECTION_CHECKER_PERIOD;
106
- /**
107
- * Interval ID for connection checker.
108
- * @type {NodeJS.Timeout | null}
109
- */
110
- this.connectionCheckerIntervalID = null;
111
-
112
- /**
113
- * Boho instance for encryption/decryption.
114
- * @type {Boho}
115
- */
116
- this.boho = new Boho()
117
-
118
- this.serverTimeNonce = Buffer.alloc( Boho.MetaSize.SERVER_TIME_NONCE );
119
- /**
120
- * Indicates if the connection is TLS (wss).
121
- * @type {boolean}
122
- */
123
- this.TLS = false // true if protocol is wss(TLS)
124
- /**
125
- * Encryption mode.
126
- * @type {number}
127
- */
128
- this.encMode = ENC_MODE.AUTO;
129
- /**
130
- * Indicates if authentication is used.
131
- * @type {boolean}
132
- */
133
- this.useAuth = false;
134
-
135
- /**
136
- * Nickname.
137
- * @type {string}
138
- */
139
- this.nick = "";
140
- /**
141
- * Set of subscribed channels.
142
- * @type {Set<string>}
143
- */
144
- this.channels = new Set()
145
- /**
146
- * Map of promises for message responses.
147
- * @type {Map<number, Array<Function>>}
148
- */
149
- this.promiseMap = new Map()
150
- /**
151
- * Timeout for message promises.
152
- * @type {number}
153
- */
154
- this.promiseTimeOut = SIZE_LIMIT.PROMISE_TIMEOUT
155
- /**
156
- * Message ID for promises.
157
- * @type {number}
158
- */
159
- this.mid = 0 // promise message id
160
-
161
- /**
162
- * Quota level.
163
- * @type {number}
164
- */
165
- this.level = 3; // also defaultQuotaLevel
166
- /**
167
- * Quota table for current level.
168
- * @type {object}
169
- */
170
- this.quota = quotaTable[this.level];
171
- /**
172
- * Server settings.
173
- * @type {object}
174
- */
175
- this.serverSet = {}
176
- /**
177
- * Map of linked channels.
178
- * @type {Map<string, Set<string>>}
179
- */
180
- this.linkMap = new Map()
181
-
182
- /**
183
- * Indicates if auto-reconnect is enabled.
184
- * @type {boolean}
185
- * @default true
186
- * */
187
- this.autoReconnect = true; // default true
188
-
189
- /**
190
- * A flag to prevent duplicate close operations.
191
- * @type {boolean}
192
- * @private
193
- */
194
- this._closed = false; // 중복 close 방지
195
-
196
- this.on('open', this.onOpen.bind(this))
197
- this.on('close', this.onClose.bind(this))
198
- this.on('socket_data', this.onData.bind(this))
199
- }
200
-
201
-
202
-
203
- /**
204
- * Performs common cleanup for the connection. It clears pending promises,
205
- * resets the socket reference, and sets the state to closed.
206
- * This method is guarded to only run once.
207
- * If autoReconnect is false, it also clears the keep-alive timer.
208
- */
209
- close() {
210
- if (this._closed) return;
211
- this._closed = true;
212
- // console.log('####### IOCOre.js close() called')
213
- // If auto-reconnect is disabled, we must stop the keep-alive timer.
214
- if (this.autoReconnect === false) {
215
- clearInterval(this.connectionCheckerIntervalID);
216
- this.connectionCheckerIntervalID = null;
217
- }
218
-
219
- // socket clean
220
- if (this.socket) {
221
- // For WebSockets, readyState are: 0-CONNECTING, 1-OPEN, 2-CLOSING, 3-CLOSED
222
- // Calling close() on a CONNECTING socket will cause a browser error.
223
- if (this.socket.readyState === 0) { // 0 is WebSocket.CONNECTING
224
- // To avoid the error, we wait for the connection to open, then immediately close it.
225
- // We also clear other handlers to prevent any other logic from running.
226
- const socket = this.socket;
227
- socket.onopen = () => { if (socket) socket.close(); };
228
- socket.onmessage = null;
229
- socket.onerror = null;
230
- socket.onclose = null;
231
- } else {
232
- try {
233
- // For other sockets (like TCP) or other WebSocket states, close directly.
234
- this.socket.close?.();
235
- } catch { }
236
- }
237
- this.socket = null;
238
- }
239
- this.promiseMap.clear();
240
- this.emit('closed');
241
- this.stateChange('closed');
242
- }
243
-
244
- /**
245
- * Disables auto-reconnect and closes the current connection.
246
- * The instance can be re-opened manually later. For complete cleanup, use destroy().
247
- */
248
- stop() {
249
- this.autoReconnect = false;
250
- this.close();
251
- this.cid = ''
252
- this.stateChange('stop','stop');
253
- }
254
-
255
- /**
256
- * Permanently destroys the instance, cleaning up all resources.
257
- * The instance will not be usable after this.
258
- */
259
- destroy() {
260
- this.stop();
261
- this.removeAllListeners();
262
-
263
- this.channels.clear();
264
- this.linkMap.clear();
265
-
266
- // Help GC
267
- this.boho = null;
268
- }
269
-
270
- /**
271
- * The core keep-alive logic.
272
- * The specific logic for checking the socket's state and reconnecting
273
- * is implemented keepConnection() in the child classes (IOWS, IOCongSocket, etc.).
274
- */
275
- keepAlive() {
276
- this.keepConnection();
277
- if( Date.now() - this.lastTxRxTime > SIZE_LIMIT.CLIENT_PING_PERIOD ){
278
- this.ping();
279
- }
280
- }
281
-
282
- /**
283
- * Redirects the connection to a new URL.
284
- * @param {string} url2 - The new URL to redirect to.
285
- */
286
- redirect(url2) {
287
- this.stateChange('redirecting','redirecting')
288
- this.close()
289
- this.createConnection(url2)
290
- }
291
-
292
- /**
293
- * Opens the WebSocket connection.
294
- * @param {string} [url] - Optional URL to connect to. If not provided, uses the instance's URL.
295
- */
296
- open(url) {
297
- // If a connection is already active or in progress, calling open() implies a reconnect.
298
- // Close the existing socket first to ensure a clean state.
299
- if (this.socket) {
300
- this.close();
301
- }
302
-
303
- if (url) {
304
- this.url = url;
305
- }
306
-
307
- if (!this.url) {
308
- this.emit('error', new Error('URL is not set.'));
309
- return;
310
- }
311
-
312
- // The actual connection is created here.
313
- this.createConnection(this.url);
314
-
315
- // Ensure the keep-alive timer is running.
316
- if (!this.connectionCheckerIntervalID) {
317
- this.connectionCheckerIntervalID = setInterval(this.keepAlive.bind(this), this.connectionCheckerPeriod);
318
- }
319
- }
320
-
321
- /**
322
- * Handles the 'open' event of the WebSocket. Resets the closed flag and sets the state to open.
323
- */
324
- onOpen() {
325
- this._closed = false;
326
- if (this.url.includes("wss://")) {
327
- this.TLS = true;
328
- } else {
329
- this.TLS = false;
330
- }
331
- this.stateChange('open')
332
- }
333
-
334
- /**
335
- * Handles the 'close' event of the WebSocket.
336
- */
337
- onClose() {
338
- this.boho.isAuthorized = false;
339
- this.cid = ""
340
- this.stateChange('closed')
341
- }
342
-
343
- /**
344
- * Manually logs in with provided ID and key.
345
- * @param {string} id - The user ID. or 'id.key'
346
- * @param {string} key - The user key.
347
- * @returns {this}
348
- */
349
- login(id, key) {
350
- if( this.serverTimeNonce ){
351
- console.log('iosignal.login serverTimeNonce', this.serverTimeNonce)
352
- this.auth(id, key)
353
- this.useAuth = true
354
- let auth_pack = this.boho.auth_req(this.serverTimeNonce )
355
- this.send(auth_pack)
356
- }
357
- return this
358
- }
359
-
360
- /**
361
- * Sets up authentication for auto-login.
362
- * @param {string} id - The user ID. or 'id.Key'
363
- * @param {string} key - The user key.
364
- * @returns {this}
365
- */
366
- auth(id, key) {
367
- if (!id && !key) {
368
- this.emit('error', new Error('auth failed. no id and key.'))
369
- }
370
-
371
- if (!key && id.includes('.')) {
372
- this.boho.set_id_key(id)
373
- } else if (id && key) {
374
- this.boho.set_id8(id)
375
- this.boho.set_key(key)
376
- } else {
377
- this.emit('error', new Error('auth failed. no id or key.'))
378
- }
379
- this.useAuth = true
380
- return this
381
- }
382
-
383
- /**
384
- * Handles incoming data from the WebSocket.
385
- * @param {Buffer} buffer - The incoming data buffer.
386
- */
387
- onData(buffer) {
388
- let msgType = buffer[0];
389
- let decoded;
390
-
391
- if (msgType === Boho.BohoMsg.ENC_488) {
392
- decoded = this.boho.decrypt_488(buffer)
393
- if (decoded) {
394
- msgType = decoded[0]
395
- buffer = decoded
396
- } else {
397
- // console.log('DEC_FAIL', buffer.byteLength)
398
- }
399
- } else if (msgType === Boho.BohoMsg.ENC_E2E) {
400
-
401
- try {
402
- decoded = this.boho.decrypt_488(buffer)
403
- if (decoded) {
404
- // console.log( 'ENC_E2E decoded ', decoded )
405
- msgType = decoded[0]
406
- // decoded has msg_header only.
407
- buffer.set(decoded, Boho.MetaSize.ENC_488) // set decoded signal_e2e headaer.
408
- buffer = buffer.subarray(Boho.MetaSize.ENC_488) // reset offset.
409
- // console.log('DECODED MsgType:', IOMsg[ msgType ] )
410
- } else {
411
- // console.log('488 DEC_FAIL', buffer)
412
- return
413
- }
414
-
415
- } catch (err) {
416
- // console.log('E2E DEC_FAIL decryption error', err)
417
- return
418
- }
419
-
420
- }
421
-
422
- let type = IOMsg[msgType]
423
- if (!type) type = Boho.BohoMsg[msgType]
424
-
425
- // console.log( "MsgType: ", type , " LEN ", buffer.byteLength)
426
-
427
- switch (msgType) {
428
- case IOMsg.OVER_SIZE:
429
- console.log('## server sent: over_size event.')
430
- this.emit('over_size', 'over_size')
431
- break;
432
- case IOMsg.PING:
433
- this.pong();
434
- break;
435
-
436
- case IOMsg.PONG:
437
- break;
438
-
439
- case IOMsg.ECHO:
440
- try {
441
- let str = decoder.decode(buffer.subarray(1))
442
-
443
- this.emit('echo', str)
444
- } catch (error) {
445
- this.emit('error', new Error('ECHO data error'))
446
- }
447
- break;
448
-
449
- case IOMsg.IAM_RES:
450
- try {
451
- let str = decoder.decode(buffer.subarray(1))
452
- let jsonInfo = JSON.parse(str)
453
- if (jsonInfo.ip) { this.ip = jsonInfo.ip; }
454
- if (jsonInfo.nick) { this.nick = jsonInfo.nick; }
455
- if (jsonInfo.did) { this.did = jsonInfo.did; }
456
- if (jsonInfo.uid) { this.uid = jsonInfo.uid; }
457
- this.emit('iam_res', str)
458
- } catch (error) {
459
- this.emit('error', new Error('IAM_RES data error'))
460
- }
461
- break;
462
-
463
- case IOMsg.CID_RES:
464
- let cidStr = decoder.decode(buffer.subarray(1))
465
- this.cid = cidStr;
466
-
467
- // **IMPORTANT** change state before subscribe.
468
- this.stateChange('ready', 'cid_ready')
469
- this.subscribe_channels()
470
- break;
471
-
472
- case IOMsg.QUOTA_LEVEL:
473
- let quotaLevel = buffer[1]
474
- this.level = quotaLevel;
475
- this.quota = quotaTable[quotaLevel];
476
- // console.log('[QUOTA_LEVEL]', JSON.stringify(this.quota))
477
- break;
478
-
479
- case IOMsg.AUTH_CLEAR:
480
- this.useAuth = false;
481
- this.boho.clearAuth();
482
- this.stateChange('auth_clear', 'server request auth_clear.')
483
- this.stop();
484
- break;
485
-
486
- case IOMsg.SERVER_REDIRECT:
487
- let host_port;
488
- let url;
489
- let protocol;
490
- let addressType;
491
- if (buffer.byteLength == 7) { // ipv4 ,port
492
- addressType = 'IPV4:PORT'
493
- host_port = byteToUrl(buffer.subarray(1))
494
- protocol = 'cong://'
495
- } else { // domain url
496
- addressType = 'URL'
497
- host_port = decoder.decode(buffer.subarray(1))
498
- protocol = ''
499
- }
500
-
501
- url = protocol + host_port
502
- this.redirect(url)
503
- break;
504
-
505
- case Boho.BohoMsg.SERVER_TIME_NONCE: // SERVER_READY
506
- this.stateChange('server_ready', 'server_ready')
507
- if (this.useAuth) {
508
- this.send(this.boho.auth_req(buffer))
509
- this.stateChange('auth_req','auth_req')
510
- // CID_REQ will be called, after auth_res.
511
- } else {
512
- // keep server_time_nonce for manual login()
513
- buffer.copy( this.serverTimeNonce);
514
- // CID_REQ here, if not using auth.
515
- this.send(Buffer.from([IOMsg.CID_REQ]))
516
- }
517
- break;
518
-
519
- case IOMsg.SERVER_SIGNAL:
520
- try {
521
- let str = decoder.decode(buffer.subarray(1))
522
- let ss = JSON.parse(str)
523
-
524
- if (ss.event && ss.data) {
525
- this.serverSet = ss.data;
526
- this.emit(ss.event, ss.data)
527
- }
528
-
529
- } catch (error) {
530
- this.emit('error', new Error('SERVER_SIGNAL parsing error'))
531
- }
532
- break;
533
-
534
- case IOMsg.SET:
535
- try {
536
- let setPack = MBP.unpack(buffer)
537
- if (setPack) {
538
- this.emit(setPack.topic, ...setPack.args)
539
- }
540
- } catch (error) {
541
- this.emit('error', new Error('SET parsing error'))
542
- }
543
- break;
544
-
545
- case IOMsg.SIGNAL_E2E:
546
- case IOMsg.SIGNAL:
547
- try {
548
- let tagLen = buffer.readUint8(1)
549
- let tagBuf = buffer.subarray(2, 2 + tagLen)
550
- let tag = decoder.decode(tagBuf)
551
-
552
- let payloadType = buffer.readUint8(2 + tagLen)
553
- let payloadBuffer = buffer.subarray(3 + tagLen)
554
-
555
- /* three types of signal message.
556
- > unicast message to me: tag includes @, no cid: '@*'
557
- > cid_sub message: tag includes cid and @ both : 'cid@*'
558
- > ch_sub message: else.
559
- */
560
- // console.log('payloadType', payloadType )
561
- switch (payloadType) {
562
-
563
- case PAYLOAD_TYPE.EMPTY:
564
- if (tag.indexOf('@') === 0) this.emit('@', tag, null)
565
- else {
566
- this.emit(tag, tag, null)
567
- this.emit('message', tag, null)
568
- }
569
- break;
570
-
571
- case PAYLOAD_TYPE.TEXT:
572
- // !! Must remove null char before decode in JS.
573
- // string payload contains null char for the c/cpp devices.
574
- let payloadStringWithoutNull = payloadBuffer
575
- if (payloadBuffer[payloadBuffer.byteLength - 1] === 0) {
576
- payloadStringWithoutNull = payloadBuffer.subarray(0, payloadBuffer.byteLength - 1)
577
- }
578
- let oneString = decoder.decode(payloadStringWithoutNull)
579
- if (tag.indexOf('@') === 0) this.emit('@', tag, oneString)
580
- else {
581
- this.emit(tag, tag, oneString)
582
- this.emit('message', tag, oneString)
583
- }
584
- break;
585
-
586
- case PAYLOAD_TYPE.BINARY:
587
- if (tag.indexOf('@') === 0) this.emit('@', tag, payloadBuffer)
588
- else {
589
- this.emit(tag, tag, payloadBuffer)
590
- this.emit('message', tag, payloadBuffer)
591
- }
592
- break;
593
-
594
- case PAYLOAD_TYPE.OBJECT:
595
- let oneObjectBuffer = decoder.decode(payloadBuffer)
596
- let oneJSONObject = JSON.parse(oneObjectBuffer)
597
- if (tag.indexOf('@') === 0) this.emit('@', tag, oneJSONObject)
598
- else {
599
- this.emit(tag, tag, oneJSONObject)
600
- this.emit('message', tag, oneJSONObject)
601
- }
602
- break;
603
-
604
- case PAYLOAD_TYPE.MJSON:
605
- let mjsonBuffer = decoder.decode(payloadBuffer)
606
- let mjson = JSON.parse(mjsonBuffer)
607
- if (tag.indexOf('@') === 0) this.emit('@', tag, ...mjson)
608
- else {
609
- this.emit(tag, tag, ...mjson)
610
- this.emit('message', tag, ...mjson)
611
- }
612
- break;
613
-
614
- case PAYLOAD_TYPE.MBA:
615
- let mbaObject = MBP.unpack(payloadBuffer)
616
- if (tag.indexOf('@') === 0) this.emit('@', tag, ...mbaObject.args)
617
- else {
618
- this.emit(tag, tag, ...mbaObject.args)
619
- this.emit('message', tag, ...mbaObject.args)
620
- }
621
- break;
622
-
623
- default:
624
- // console.log('## Unkown payloadtype', payloadType)
625
- }
626
-
627
- } catch (err) {
628
- // this.emit('error', new Error('IOCore IOMsg.SIGNAL parser err', err))
629
- console.log('IOCore IOMsg.SIGNAL parser err', err)
630
- }
631
- break;
632
-
633
- case IOMsg.RESPONSE_MBP:
634
- this.testPromise(buffer)
635
- break;
636
-
637
- case Boho.BohoMsg.AUTH_FAIL:
638
- this.stateChange('auth_fail', 'auth_fail from server.')
639
- break;
640
-
641
- case Boho.BohoMsg.AUTH_RES:
642
- if (this.boho.verify_auth_res(buffer)) {
643
- this.stateChange('auth_res', 'server sent auth_res')
644
- this.send(Buffer.from([IOMsg.CID_REQ]))
645
- } else {
646
- this.stateChange('auth_fail', 'verify_auth_res() invalid server_hmac')
647
- }
648
- break;
649
-
650
- default:
651
- try {
652
- decoded = decoder.decode(buffer)
653
- // console.log('text message:', decoded)
654
- this.emit('text_message', decoded)
655
- } catch (error) {
656
-
657
- }
658
-
659
- break;
660
-
661
- }
662
- }
663
-
664
- /**
665
- * Sends an IAM (I Am) message to the server.
666
- * @param {string} [title] - Optional title for the IAM message.
667
- */
668
- iam(title) {
669
- // console.log('iam', title)
670
- if (title) {
671
- this.send_enc_mode(MBP.pack(
672
- MBP.MB('#MsgType', '8', IOMsg.IAM),
673
- MBP.MB('#', title)
674
- ))
675
- } else {
676
- this.send_enc_mode(MBP.pack(
677
- MBP.MB('#MsgType', '8', IOMsg.IAM)
678
- ))
679
- }
680
- }
681
-
682
-
683
- /**
684
- * Sends a PING message to the server.
685
- */
686
- ping() {
687
- this.send(Buffer.from([IOMsg.PING]))
688
- }
689
-
690
- /**
691
- * Sends a PONG message to the server.
692
- */
693
- pong() {
694
- this.send(Buffer.from([IOMsg.PONG]))
695
- }
696
-
697
-
698
- /**
699
- * Sends an ECHO message to the server.
700
- * @param {*} [args] - Optional arguments to echo.
701
- */
702
- echo(args) {
703
- if (args) {
704
- // console.log('send echo args:', args)
705
- this.send_enc_mode(MBP.pack(
706
- MBP.MB('#MsgType', '8', IOMsg.ECHO),
707
- MBP.MB('#msg', args)
708
- ))
709
- } else {
710
- // # do not encrypt blank echo #
711
- this.send(Buffer.from([IOMsg.ECHO]))
712
- }
713
- }
714
-
715
-
716
- /**
717
- * Sends binary data.
718
- * @param {...any} data - Data to send.
719
- */
720
- bin(...data) {
721
- this.send(MBP.U8pack(...data))
722
- }
723
-
724
- /**
725
- * Sends data over the WebSocket.
726
- * @param {Buffer} data - The data buffer to send.
727
- */
728
- send(data) {
729
- if (data.byteLength > this.quota.signalSize) {
730
- this.emit('over_size')
731
- console.log('## QUOTA LIMIT OVER!! \nsignal message.byteLength: ', data.byteLength)
732
- console.log('## your maximum signalSize(bytes) is:', this.quota.signalSize)
733
- return
734
- }
735
- // console.log(`C->[${IOMsg[ data[0]]}]`)
736
- this.socket_send(data);
737
- }
738
-
739
- /**
740
- * Determines if encryption should be used based on current mode and TLS status.
741
- * @returns {boolean}
742
- */
743
- getEncryptionMode() {
744
- if (this.encMode === ENC_MODE.YES ||
745
- this.encMode === ENC_MODE.AUTO &&
746
- !this.TLS && this.boho.isAuthorized
747
- ) {
748
- return true;
749
- } else {
750
- return false
751
- }
752
- }
753
-
754
- /**
755
- * Sends data with encryption based on the encryption mode.
756
- * @param {Buffer} data - The data buffer to send.
757
- * @param {boolean} [useEncryption] - Optional. Force encryption or not. If undefined, uses default policy.
758
- */
759
- send_enc_mode(data, useEncryption) {
760
-
761
- // use default policy.
762
- if (useEncryption === undefined) {
763
- useEncryption = this.getEncryptionMode()
764
- }
765
-
766
- if (data[0] == IOMsg.SIGNAL_E2E && useEncryption) {
767
- // input data: signal_header + e2ePayload
768
- // encrypt signal_header area only. payload is encrypted with e2e key already.
769
- let tagLen = data[1]
770
- let encHeader = this.boho.encrypt_488(data.subarray(0, 3 + tagLen))
771
- encHeader[0] = Boho.BohoMsg.ENC_E2E
772
- this.send(Buffer.concat([encHeader, data.subarray(3 + tagLen)]))
773
- // console.log('<< send_enc_mode [ ENC_E2E ]')
774
-
775
- } else if (useEncryption) {
776
- // console.log('<< send_enc_mode [ ENC_488 ]')
777
- let encPack = this.boho.encrypt_488(data)
778
- this.send(encPack)
779
- } else {
780
- // console.log('<< send_enc_mode [ PLAIN ]' )
781
- this.send(data)
782
- }
783
-
784
- }
785
-
786
-
787
- /**
788
- * Sets a message promise for a given message ID.
789
- * @param {number} mid - The message ID.
790
- * @returns {Promise<any>}
791
- */
792
- setMsgPromise(mid) {
793
- return new Promise((resolve, reject) => {
794
- const timeoutId = setTimeout(e => {
795
- if (this.promiseMap.has(mid)) {
796
- reject('timeout');
797
- this.promiseMap.delete(mid)
798
- }
799
- }, this.promiseTimeOut);
800
- this.promiseMap.set(mid, [resolve, reject, timeoutId]);
801
- })
802
- }
803
-
804
- /**
805
- * Tests and resolves/rejects a promise based on the incoming buffer.
806
- * @param {Buffer} buffer - The incoming data buffer.
807
- */
808
- testPromise(buffer) {
809
-
810
- let res = MBP.unpack(buffer)
811
- if (!res) return
812
-
813
- if (this.promiseMap.has(res.mid)) {
814
- let [resolve, reject, timeoutId] = this.promiseMap.get(res.mid)
815
- clearTimeout(timeoutId)
816
- this.promiseMap.delete(res.mid)
817
-
818
- if (res.status < 128) {
819
- res.ok = true;
820
- resolve(res)
821
- } else {
822
- res.ok = false;
823
- reject(res)
824
- }
825
-
826
-
827
- } else {
828
- console.log('no promise id')
829
- }
830
- }
831
-
832
-
833
- /**
834
- * alias of signal()
835
- * Sends a signal with a tag and arguments.
836
- * @param {string} tag - The signal tag.
837
- * @param {...any} args - Arguments for the signal.
838
- */
839
- publish(tag, ...args) {
840
- this.signal(tag, ...args)
841
- }
842
-
843
- /**
844
- * Sends a signal with a tag and arguments.
845
- * @param {string} tag - The signal tag.
846
- * @param {...any} args - Arguments for the signal.
847
- * @throws {TypeError} If tag is not a string.
848
- */
849
- signal(tag, ...args) {
850
- if (typeof tag !== 'string') throw TypeError('tag should be string.')
851
- let signalPack = getSignalPack(tag, ...args)
852
- this.send_enc_mode(signalPack)
853
- }
854
-
855
- /**
856
- * Decrypts E2E data.
857
- * @param {Buffer} data - The encrypted data.
858
- * @param {string} key - The decryption key.
859
- * @returns {Buffer}
860
- */
861
- decrypt_e2e(data, key) {
862
- return this.boho.decrypt_e2e(data, key)
863
- }
864
-
865
- /**
866
- * Sends an E2E (End-to-End) encrypted signal.
867
- * @param {string} tag - The signal tag.
868
- * @param {Buffer} data - The data to encrypt and send.
869
- * @param {string} key - The encryption key.
870
- * @throws {TypeError} If tag is not a string.
871
- */
872
- signal_e2e(tag, data, key) {
873
-
874
- if( !this.boho.isAuthorized ) return;
875
- if (typeof tag !== 'string') throw TypeError('tag should be string.')
876
- let tagEncoded = encoder.encode(tag)
877
- let dataPack = MBP.B8(data)
878
-
879
- //encrypt payload area with key
880
- let sercretPack = this.boho.encrypt_e2e(dataPack, key)
881
-
882
- //change signal MsgType header into SIGNAL_E2E
883
- let signalPack = MBP.pack(
884
- MBP.MB('#MsgType', '8', IOMsg.SIGNAL_E2E),
885
- MBP.MB('#tagLen', '8', tagEncoded.byteLength),
886
- MBP.MB('#tag', tagEncoded),
887
- MBP.MB('#payloadType', '8', PAYLOAD_TYPE.BINARY),
888
- MBP.MB('#payload', sercretPack)
889
- )
890
-
891
- this.send_enc_mode(signalPack)
892
- }
893
-
894
-
895
- /**
896
- * Sets a value in the store.
897
- * @param {string} storeName - The name of the store.
898
- * @param {...any} args - Arguments to set.
899
- * @returns {Promise<any>}
900
- */
901
- set(storeName, ...args) {
902
- if (!storeName || args.length == 0) {
903
- return Promise.reject(new Error('set need storeName and value)'))
904
- }
905
- return this.call('store', 'set', storeName, ...args)
906
- }
907
-
908
- /**
909
- * Gets a value from the store.
910
- * @param {string} storeName - The name of the store.
911
- * @returns {Promise<any>}
912
- */
913
- async get(storeName) {
914
- if (!storeName) {
915
- return Promise.reject(new Error('store get need storeName)'))
916
- }
917
- let pack = await this.call('store', 'get', storeName)
918
- let { $ } = MBP.unpack(pack.body)
919
- return $
920
- }
921
-
922
-
923
- /**
924
- * Sends a request to a target and topic.(remote service call)
925
- * @param {string} target - The target(service name) of the request.
926
- * @param {string} topic - The topic(service function name) of the request.
927
- * @param {...any} args - Optional arguments for the request.
928
- * @returns {Promise<any>}
929
- */
930
- call(target, topic, ...args) {
931
- if (!target || !topic)
932
- return Promise.reject(new Error('request need target and topic)'))
933
- let sigPack;
934
- if (args.length > 0) {
935
- sigPack = MBP.pack(
936
- MBP.MB('#MsgType', '8', IOMsg.CALL),
937
- MBP.MB('mid', '16', ++this.mid),
938
- MBP.MB('target', target),
939
- MBP.MB('topic', topic),
940
- MBP.MBA(...args)
941
- )
942
- } else {
943
- sigPack = MBP.pack(
944
- MBP.MB('#MsgType', '8', IOMsg.CALL),
945
- MBP.MB('mid', '16', ++this.mid),
946
- MBP.MB('target', target),
947
- MBP.MB('topic', topic)
948
- )
949
- }
950
- this.send_enc_mode(sigPack)
951
- return this.setMsgPromise(this.mid)
952
- }
953
-
954
-
955
- /**
956
- * Subscribes to a channel or channels.
957
- * @param {string} tag - The tag(s) of the channel(s) to subscribe to (comma-separated).
958
- * @throws {TypeError} If tag is not a string or exceeds length limit.
959
- */
960
- subscribe(tag) {
961
- if (typeof tag !== 'string') throw TypeError('tag should be string.')
962
- if (tag.length > SIZE_LIMIT.TAG_LEN1) throw TypeError('please check tag string length limit:' + SIZE_LIMIT.TAG_LEN1)
963
-
964
- try {
965
- let tagEncoded = encoder.encode(tag)
966
- this.send_enc_mode(
967
- Buffer.concat([
968
- MBP.NB('8', IOMsg.SUBSCRIBE),
969
- MBP.NB('8', tagEncoded.byteLength),
970
- tagEncoded]))
971
- } catch (error) { }
972
-
973
- }
974
-
975
-
976
- /**
977
- * Subscribes stored channels.
978
- * called client state become 'ready'
979
- */
980
- subscribe_channels() {
981
- if (this.state !== STATE.READY) return
982
- if (this.channels.size == 0) return
983
- let tag = Array.from(this.channels).join(',')
984
-
985
- try {
986
- let tagEncoded = encoder.encode(tag)
987
- this.send_enc_mode(
988
- Buffer.concat([
989
- MBP.NB('8', IOMsg.SUBSCRIBE),
990
- MBP.NB('8', tagEncoded.byteLength),
991
- tagEncoded]))
992
- } catch (error) { }
993
-
994
- }
995
-
996
- /**
997
- * Unsubscribes from a channel or channels.
998
- * @param {string} [tag=""] - The tag(s) of the channel(s) to unsubscribe from (comma-separated). If empty, unsubscribes from all.
999
- * @throws {TypeError} If tag is not a string or exceeds length limit.
1000
- */
1001
- unsubscribe(tag = "") {
1002
- if (typeof tag !== 'string') throw TypeError('tag should be string.')
1003
-
1004
- if (tag == "") { // blank tag means unsubscribe all
1005
- this.channels.clear();
1006
- } else {
1007
- let tagList = tag.split(',')
1008
- tagList.forEach(tag => {
1009
- this.channels.delete(tag)
1010
- })
1011
- }
1012
-
1013
- let tagEncoded = encoder.encode(tag)
1014
- if (tagEncoded.byteLength > SIZE_LIMIT.TAG_LEN1) throw TypeError('please use tag string bytelength below:' + SIZE_LIMIT.TAG_LEN1)
1015
-
1016
- this.send_enc_mode(Buffer.concat([
1017
- MBP.NB('8', IOMsg.UNSUBSCRIBE),
1018
- MBP.NB('8', tagEncoded.byteLength),
1019
- tagEncoded]))
1020
- }
1021
-
1022
-
1023
- /**
1024
- * Listens for signals on a specific tag.
1025
- * @param {string} tag - The tag to listen on.
1026
- * @param {Function} handler - The callback function to handle the signal.
1027
- * @throws {TypeError} If tag is not a string, handler is not a function, or tag length is invalid.
1028
- */
1029
- listen(tag, handler) {
1030
- if (typeof tag !== 'string') throw TypeError('tag should be string.')
1031
- if (tag.length > 255 || tag.length == 0) throw TypeError('tag string length range: 1~255')
1032
- if (typeof handler !== 'function') throw TypeError('handler is not a function.')
1033
-
1034
- if (tag.indexOf('@') !== 0) {
1035
- this.channels.add(tag)
1036
- }
1037
- this.on(tag, handler)
1038
- // do not subscribe now.
1039
- // will subscribe when io state is 'ready'. (receive CID_RES from server)
1040
-
1041
- }
1042
-
1043
-
1044
-
1045
- /**
1046
- * Links a local target to a remote tag and sets up a handler.
1047
- * @param {string} to - The local link target.
1048
- * @param {string} tag - The remote tag.
1049
- * @param {Function} handler - The callback function to handle the signal.
1050
- * @throws {TypeError} If 'to' or 'tag' are not strings, handler is not a function, or tag length is invalid.
1051
- */
1052
- link(to, tag, handler) {
1053
- if (typeof to !== 'string') throw TypeError('to(local link target) is not a string.')
1054
- if (typeof tag !== 'string') throw TypeError('tag is not a string.')
1055
- if (tag.length > 255 || tag.length == 0) throw TypeError('tag string length range: 1~255')
1056
- if (typeof handler !== 'function') throw TypeError('handler is not a function.')
1057
-
1058
- if (tag.indexOf('@') !== 0) {
1059
- this.channels.add(tag)
1060
- }
1061
-
1062
- let linkSet;
1063
- if (this.linkMap.has(to)) {
1064
- linkSet = this.linkMap.get(to)
1065
- } else {
1066
- linkSet = new Set()
1067
- }
1068
-
1069
- linkSet.add(tag)
1070
- this.linkMap.set(to, linkSet)
1071
- this.on(tag, handler)
1072
- this.subscribe(tag)
1073
-
1074
- }
1075
-
1076
-
1077
- /**
1078
- * Unlinks a specific tag from a local target.
1079
- * @param {string} to - The local link target.
1080
- * @param {string} tag - The tag to unlink.
1081
- * @throws {TypeError} If 'to' or 'tag' are not strings or tag length is invalid.
1082
- */
1083
- unlink(to, tag) {
1084
- if (typeof to !== 'string') throw TypeError('to(local link target) is not a string.')
1085
- if (typeof tag !== 'string') throw TypeError('tag is not a string.')
1086
- if (tag.length > 255 || tag.length == 0) throw TypeError('tag string length range: 1~255')
1087
-
1088
- const linkSet = this.linkMap.get(to);
1089
- if (!linkSet || !linkSet.has(tag)) return;
1090
-
1091
- this.unsubscribe(tag);
1092
- this.removeAllListeners(tag);
1093
- linkSet.delete(tag);
1094
-
1095
- if (linkSet.size === 0) {
1096
- this.linkMap.delete(to);
1097
- }
1098
- }
1099
-
1100
- /**
1101
- * Unlinks all tags from a local target.
1102
- * @param {string} to - The local link target.
1103
- * @throws {TypeError} If 'to' is not a string.
1104
- */
1105
- unlinkAll(to) {
1106
- if (typeof to !== 'string') throw TypeError('to(local link target) is not a string.')
1107
-
1108
- const linkSet = this.linkMap.get(to);
1109
- if (!linkSet) return;
1110
-
1111
- for (const tag of linkSet) {
1112
- this.unsubscribe(tag);
1113
- this.removeAllListeners(tag);
1114
- }
1115
-
1116
- this.linkMap.delete(to);
1117
- }
1118
-
1119
-
1120
-
1121
- /**
1122
- * Gets connection metrics.
1123
- * @returns {{tx: number, rx: number, txb: number, rxb: number, last: number}}
1124
- */
1125
- getMetric() {
1126
- return {
1127
- tx: this.txCounter,
1128
- rx: this.rxCounter,
1129
- txb: this.txBytes,
1130
- rxb: this.rxBytes,
1131
- last: (Date.now() - this.lastTxRxTime) / 1000
1132
- }
1133
-
1134
- }
1135
-
1136
- /**
1137
- * Gets the current connection state.
1138
- * @returns {number}
1139
- */
1140
- getState() {
1141
- return this.state
1142
- }
1143
-
1144
- /**
1145
- * Gets the current connection state name.
1146
- * @returns {string}
1147
- */
1148
- getStateName() {
1149
- //state <number>
1150
- //value of constant STATE.NAME < number >
1151
- //type of constant STATE.NAME name < string uppercase >
1152
- //stateName,eventName <string lowercase>
1153
- return (STATE[this.state]).toLowerCase()
1154
- }
1155
-
1156
- /**
1157
- * Gets security-related information.
1158
- * @returns {{useAuth: boolean, isTLS: boolean, isAuthorized: boolean, encMode: number, usingEncryption: boolean}}
1159
- */
1160
- getSecurity() {
1161
- return {
1162
- useAuth: this.useAuth,
1163
- isTLS: this.TLS,
1164
- isAuthorized: this.boho.isAuthorized,
1165
- encMode: this.encMode,
1166
- usingEncryption: this.getEncryptionMode()
1167
- }
1168
- }
1169
-
1170
- /**
1171
- * Changes the connection state and emits events.
1172
- * @param {string} state - The new state name (e.g., 'ready', 'closed').
1173
- * @param {string} [emitEventAndMessage] - Optional message to emit with the state change event.
1174
- *
1175
- * 주의.
1176
- * 1. 상태가 변경 될 때만 'change' 이벤트 호출된다.
1177
- * 2. emitEventAndMessage 옵션 값이 지정되야 해당 이벤트 이름이 호출된다.
1178
- * 보통 이벤트 이름과 동일하게 적거나 이벤트 상황 안내문을 넣는다.
1179
- */
1180
- stateChange(state, emitEventAndMessage) {
1181
- // STATE constant name <string> upperCase
1182
- // eventName and .stateName <string> lowerCase
1183
- // .state <number>
1184
- // console.log('### stateChange reason:', emitEventAndMessage )
1185
- let eventName = state.toLowerCase()
1186
- this.state = STATE[state.toUpperCase()] // state: number
1187
-
1188
- if (emitEventAndMessage) {
1189
- this.emit(eventName, emitEventAndMessage)
1190
- }
1191
-
1192
- if (this.stateName !== eventName) {
1193
- this.stateName = eventName
1194
- this.emit('change', eventName)
1195
- }
1196
- }
1197
-
1198
- }
1199
-
1200
-
1201
-
1202
-