iosignal 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +254 -247
- package/auth_file.json +13 -13
- package/auth_file.mjs +23 -23
- package/dist/iosignal-iife.js +11 -0
- package/dist/iosignal-iife.js.map +1 -0
- package/dist/iosignal.cjs +1 -2
- package/dist/iosignal.js +11 -0
- package/dist/iosignal.js.map +1 -0
- package/index.js +19 -19
- package/indexWebBrowser.js +13 -13
- package/package.json +56 -59
- package/rollup.config.js +72 -69
- package/src/api/RedisAPI.js +65 -65
- package/src/api/api_constant.js +7 -7
- package/src/api/api_reply.js +31 -31
- package/src/api/api_sudo.js +91 -91
- package/src/auth/Auth_Env.js +76 -76
- package/src/auth/Auth_File.js +71 -71
- package/src/auth/Auth_Redis.js +44 -44
- package/src/auth/BohoAuth.js +164 -164
- package/src/client/CongPacket.js +141 -141
- package/src/client/IOCongSocket.js +81 -81
- package/src/client/IOCore.js +822 -822
- package/src/client/IOWS.js +80 -80
- package/src/client/IOWebSocket.js +111 -111
- package/src/common/constants.js +148 -148
- package/src/common/payload.js +81 -81
- package/src/common/quotaTable.js +68 -68
- package/src/common/util.js +61 -61
- package/src/server/FileLogger.js +30 -30
- package/src/server/Manager.js +433 -433
- package/src/server/Metric.js +155 -155
- package/src/server/Remote.js +207 -207
- package/src/server/RemoteCore.js +439 -439
- package/src/server/Server.js +163 -163
- package/src/server/serverOption.js +56 -56
- package/test-browser/favicon.ico +0 -0
- package/test-browser/index.html +96 -96
- package/test-browser/server.js +11 -11
- package/test-client-nodejs/client.cjs +17 -17
- package/test-client-nodejs/client.mjs +17 -17
- package/test-client-nodejs/client_api_reply.mjs +15 -15
- package/test-client-nodejs/stateChange.mjs +19 -19
- package/test-server/server-Auth_Env.js +19 -19
- package/test-server/server-Auth_File.js +12 -12
- package/test-server/server-api_reply.js +6 -6
- package/test-server/server-api_sudo.js +7 -7
- package/test-server/server-commonjs.cjs +8 -8
- package/test-server/server-esm.mjs +20 -20
- package/test-server/simple-server.mjs +11 -11
- package/test-signal-payloadType/signalPack.js +34 -34
- package/test-signal-payloadType/signaling.js +60 -60
- package/dist/iosignal.esm.js +0 -3
- package/dist/iosignal.esm.js.map +0 -1
- package/dist/iosignal.min.js +0 -3
- package/dist/iosignal.min.js.map +0 -1
package/src/server/RemoteCore.js
CHANGED
|
@@ -1,439 +1,439 @@
|
|
|
1
|
-
import { webcrypto } from 'crypto';
|
|
2
|
-
import { MBP } from 'meta-buffer-pack'
|
|
3
|
-
import { Boho, BohoMsg, MetaSize } from 'boho'
|
|
4
|
-
import { serverOption } from './serverOption.js';
|
|
5
|
-
import { IOMsg, ENC_MODE, CLIENT_STATE } from '../common/constants.js'
|
|
6
|
-
import { quotaTable } from '../common/quotaTable.js'
|
|
7
|
-
|
|
8
|
-
const decoder = new TextDecoder()
|
|
9
|
-
|
|
10
|
-
const NB = MBP.NB
|
|
11
|
-
const MB = MBP.MB
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export class RemoteCore {
|
|
15
|
-
constructor(socket, manager) {
|
|
16
|
-
|
|
17
|
-
this.socket = socket;
|
|
18
|
-
socket.isAlive = true;
|
|
19
|
-
socket.txCounter = 0;
|
|
20
|
-
socket.rxCounter = 0;
|
|
21
|
-
socket.openTime = Date.now()
|
|
22
|
-
|
|
23
|
-
this.boho = new Boho()
|
|
24
|
-
this.encMode = ENC_MODE.AUTO;
|
|
25
|
-
this.channels = new Set(); // subscribed tags
|
|
26
|
-
this.retain_signal = new Map();
|
|
27
|
-
|
|
28
|
-
this.ssid = RemoteCore.ssid++; // ordered index number
|
|
29
|
-
this.manager = manager;
|
|
30
|
-
|
|
31
|
-
this.cid; // Communication Id
|
|
32
|
-
this.did; // Device Id
|
|
33
|
-
this.uid; // User Id
|
|
34
|
-
this.nick = ""
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
this.lastEchoMessage = "N"
|
|
38
|
-
this.privateNode = false
|
|
39
|
-
this.HOME_CHANNEL = ""
|
|
40
|
-
this.level = serverOption.defaultQuotaIndex;
|
|
41
|
-
this.quota = quotaTable[this.level];
|
|
42
|
-
this.isAdmin = false;
|
|
43
|
-
|
|
44
|
-
this.state;
|
|
45
|
-
this.stateLog = [];
|
|
46
|
-
this.setState(CLIENT_STATE.INIT)
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
static ssid = 1;
|
|
51
|
-
|
|
52
|
-
setState(state) {
|
|
53
|
-
this.state = state
|
|
54
|
-
if (serverOption.debug.showAuthInfo) {
|
|
55
|
-
this.stateLog.push(state)
|
|
56
|
-
console.log(this.stateLog.join('>'))
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
getState() { // <Number>
|
|
61
|
-
return this.state
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
getStateName() { // <String>
|
|
65
|
-
return (CLIENT_STATE[this.state]).toLowerCase()
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
showMessageLog(message, isBinary) {
|
|
70
|
-
let from = this.boho.isAuthorized ? `did: ${this.did} ${this.cid}@` : `${this.cid}@`
|
|
71
|
-
if (isBinary) {
|
|
72
|
-
let msgTypeName = IOMsg[message[0]]
|
|
73
|
-
if (!msgTypeName) msgTypeName = BohoMsg[message[0]]
|
|
74
|
-
msgTypeName = ' [' + msgTypeName + ']';
|
|
75
|
-
if (message.byteLength > 40) {
|
|
76
|
-
console.log(from + msgTypeName + ' LEN:', message.length);
|
|
77
|
-
} else {
|
|
78
|
-
console.log(from + msgTypeName, message);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
} else {
|
|
82
|
-
console.log(from + ' [TEXT] %s', message);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
rxQuotaChecker(message) {
|
|
87
|
-
let rxBytes = message.byteLength
|
|
88
|
-
this.manager.rxBytes += rxBytes;
|
|
89
|
-
|
|
90
|
-
if (serverOption.useQuota.signalSize && (rxBytes > this.quota.signalSize)) {
|
|
91
|
-
console.log('## quota: size over')
|
|
92
|
-
this.send(Buffer.from([IOMsg.OVER_SIZE]))
|
|
93
|
-
if (serverOption.useQuota.disconnect) this.close();
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// todo:
|
|
98
|
-
// traffic rate :
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
onTimeDelayMessage(message, isBinary = true) {
|
|
103
|
-
setTimeout(() => {
|
|
104
|
-
this.onSocketMessage(message, isBinary)
|
|
105
|
-
}
|
|
106
|
-
, serverOption.debug.delay)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// CongSocket or WebSocket
|
|
110
|
-
onSocketMessage(message, isBinary = true) {
|
|
111
|
-
|
|
112
|
-
this.receiveMonitor() // rx-data, ping/pong and timeout check
|
|
113
|
-
if (!this.rxQuotaChecker(message)) return
|
|
114
|
-
|
|
115
|
-
if (serverOption.showMessage === 'message') this.showMessageLog(message, isBinary)
|
|
116
|
-
|
|
117
|
-
let msgType, decoded
|
|
118
|
-
|
|
119
|
-
if (isBinary) {
|
|
120
|
-
msgType = message[0]
|
|
121
|
-
|
|
122
|
-
if (msgType === BohoMsg.ENC_488) {
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
decoded = this.boho.decrypt_488(message)
|
|
126
|
-
} catch (err) {
|
|
127
|
-
console.log('-- E488 DEC_FAIL', err)
|
|
128
|
-
return
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (decoded) {
|
|
132
|
-
msgType = decoded[0]
|
|
133
|
-
message = decoded
|
|
134
|
-
} else {
|
|
135
|
-
return
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
} else if (msgType === BohoMsg.ENC_E2E) { // symetric E2EE Signal
|
|
139
|
-
try {
|
|
140
|
-
decoded = this.boho.decrypt_488(message)
|
|
141
|
-
} catch (err) {
|
|
142
|
-
// console.log('-- E2E DEC_FAIL', err)
|
|
143
|
-
return
|
|
144
|
-
}
|
|
145
|
-
// console.log('e2e unpack:', decoded )
|
|
146
|
-
if (decoded) {
|
|
147
|
-
msgType = decoded[0]
|
|
148
|
-
message.set(decoded, MetaSize.ENC_488) // set decoded signal_e2e headaer.
|
|
149
|
-
message = message.subarray(MetaSize.ENC_488) // reset offset.
|
|
150
|
-
} else {
|
|
151
|
-
return
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
} else {
|
|
155
|
-
// console.log( `[P] ${ IOMsg[ msgType] || BohoMsg[cmd]} LEN: ${message.byteLength}` )
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
switch (msgType) {
|
|
159
|
-
case IOMsg.PING:
|
|
160
|
-
this.pong();
|
|
161
|
-
break;
|
|
162
|
-
|
|
163
|
-
case IOMsg.PONG:
|
|
164
|
-
break;
|
|
165
|
-
|
|
166
|
-
case IOMsg.CID_REQ:
|
|
167
|
-
if (this.state < CLIENT_STATE.SENT_SERVER_READY) {
|
|
168
|
-
// Protocol violation: CID_REQ was sent before receiving the SERVER_READY signal.
|
|
169
|
-
this.close()
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (!this.cid) {
|
|
173
|
-
this.cid = '?' + webcrypto.getRandomValues(Buffer.alloc(3)).toString('base64url')
|
|
174
|
-
this.manager.cid2remote.set(this.cid, this)
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// console.log('<< SENDING CID_RES:', this.cid)
|
|
178
|
-
this.send_enc_mode(MBP.pack(
|
|
179
|
-
MB('#cid_ack', '8', IOMsg.CID_RES),
|
|
180
|
-
MB('#cid', this.cid)
|
|
181
|
-
))
|
|
182
|
-
|
|
183
|
-
this.setState(CLIENT_STATE.CID_READY)
|
|
184
|
-
break;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
case IOMsg.ECHO: // TEXT only
|
|
188
|
-
try {
|
|
189
|
-
let msg = decoder.decode(message.subarray(1))
|
|
190
|
-
this.lastEchoMessage = msg
|
|
191
|
-
} catch (error) {
|
|
192
|
-
// console.log('ECHO message is not a text')
|
|
193
|
-
}
|
|
194
|
-
this.send(message, isBinary)
|
|
195
|
-
break;
|
|
196
|
-
|
|
197
|
-
case IOMsg.IAM:
|
|
198
|
-
if (message.byteLength > 1) {
|
|
199
|
-
let iamInfo = message.subarray(1)
|
|
200
|
-
this.nick = decoder.decode(iamInfo)
|
|
201
|
-
console.log('iam nick reset', this.nick)
|
|
202
|
-
}
|
|
203
|
-
this.iamResponse()
|
|
204
|
-
break;
|
|
205
|
-
|
|
206
|
-
case IOMsg.SIGNAL_E2E:
|
|
207
|
-
case IOMsg.SIGNAL:
|
|
208
|
-
if (message.byteLength >= 3) {
|
|
209
|
-
let tagLen = message.readUInt8(1)
|
|
210
|
-
if (message.byteLength >= tagLen + 2) {
|
|
211
|
-
let tag = message.subarray(2, 2 + tagLen)
|
|
212
|
-
tag = decoder.decode(tag)
|
|
213
|
-
this.manager.sender(tag, this, message)
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
break;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
case IOMsg.UNSUBSCRIBE:
|
|
221
|
-
if (message.byteLength == 2) {
|
|
222
|
-
this.manager.unsubscribe([""], this)
|
|
223
|
-
} else if (message.byteLength >= 3) {
|
|
224
|
-
let tagLen = message.readUInt8(1)
|
|
225
|
-
|
|
226
|
-
if (message.byteLength == tagLen + 2) {
|
|
227
|
-
let tag = message.subarray(2, 2 + tagLen)
|
|
228
|
-
tag = decoder.decode(tag)
|
|
229
|
-
let tagList = tag.split(',')
|
|
230
|
-
this.manager.unsubscribe(tagList, this)
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
break;
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
case IOMsg.SUBSCRIBE: // 1byte tagLen
|
|
237
|
-
if (message.byteLength >= 3) {
|
|
238
|
-
let tagLen = message.readUInt8(1)
|
|
239
|
-
if (message.byteLength == tagLen + 2) {
|
|
240
|
-
let tag = message.subarray(2, 2 + tagLen)
|
|
241
|
-
tag = decoder.decode(tag)
|
|
242
|
-
let tagList = tag.split(',')
|
|
243
|
-
this.manager.subscribe(tagList, this)
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
break;
|
|
247
|
-
|
|
248
|
-
case IOMsg.SUBSCRIBE_REQ: // 2bytes tagLen
|
|
249
|
-
if (message.byteLength >= 6) {
|
|
250
|
-
let msgID = message.readUInt16BE(1)
|
|
251
|
-
let tagLen = message.readUInt16BE(3)
|
|
252
|
-
if (message.byteLength == tagLen + 5) {
|
|
253
|
-
let tag = message.subarray(5, 5 + tagLen)
|
|
254
|
-
tag = decoder.decode(tag)
|
|
255
|
-
let tagList = tag.split(',')
|
|
256
|
-
// console.log('>> SUBSCRIBE_REQ from:', this.cid, tagList)
|
|
257
|
-
this.manager.subscribe(tagList, this)
|
|
258
|
-
this.response(msgID, 0)
|
|
259
|
-
} else {
|
|
260
|
-
this.response(msgID, 255)
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
break;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
case IOMsg.REQUEST: //api request
|
|
267
|
-
try {
|
|
268
|
-
let req = MBP.unpack(message)
|
|
269
|
-
if (req) {
|
|
270
|
-
if (!req.target || !req.topic) return
|
|
271
|
-
if (this.manager.server.apiNames.has(req.target)) {
|
|
272
|
-
this.manager.server.emit(req.target, this, req)
|
|
273
|
-
} else {
|
|
274
|
-
// console.log('UnKnown API req: ', req.target)
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
} catch (e) {
|
|
278
|
-
console.log('request catch error', e)
|
|
279
|
-
}
|
|
280
|
-
break;
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
case IOMsg.CLOSE:
|
|
284
|
-
if (message.byteLength > 1) {
|
|
285
|
-
let reason = decoder.decode(message.subarray(1))
|
|
286
|
-
console.log('>> CLOSE reason:', reason)
|
|
287
|
-
this.close();
|
|
288
|
-
}
|
|
289
|
-
break;
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
// Auth
|
|
293
|
-
case BohoMsg.AUTH_REQ:
|
|
294
|
-
if (!this.manager.authManager) return
|
|
295
|
-
if (this.state < CLIENT_STATE.SENT_SERVER_READY) {
|
|
296
|
-
// console.log('protocol error. auth_req before server_ready')
|
|
297
|
-
this.close();
|
|
298
|
-
}
|
|
299
|
-
this.setState(CLIENT_STATE.RECV_AUTH_REQ)
|
|
300
|
-
let auth_nonce_pack = this.boho.auth_nonce()
|
|
301
|
-
// console.log('## auth_nonce_pack', auth_nonce_pack )
|
|
302
|
-
this.send(auth_nonce_pack)
|
|
303
|
-
this.setState(CLIENT_STATE.SENT_SERVER_NONCE)
|
|
304
|
-
break;
|
|
305
|
-
|
|
306
|
-
case BohoMsg.AUTH_HMAC:
|
|
307
|
-
if (!this.manager.authManager) return
|
|
308
|
-
if (this.state < CLIENT_STATE.SENT_SERVER_NONCE) {
|
|
309
|
-
// console.log('protocol error. auth_hmac before server_nonce')
|
|
310
|
-
this.close();
|
|
311
|
-
}
|
|
312
|
-
this.setState(CLIENT_STATE.RECV_AUTH_HMAC)
|
|
313
|
-
//async
|
|
314
|
-
this.manager.authManager.verify_auth_hmac(message, this)
|
|
315
|
-
.then(authInfo => {
|
|
316
|
-
if (authInfo) {
|
|
317
|
-
this.manager.deligateSignal(remote, '@$name', authInfo.name)
|
|
318
|
-
// console.log('device login success authInfo', authInfo)
|
|
319
|
-
// this.manager.sender('@$name', authInfo.name )
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
})
|
|
323
|
-
.catch(e => {
|
|
324
|
-
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
return;
|
|
328
|
-
|
|
329
|
-
default:
|
|
330
|
-
// console.log('unknown MsgType:',msgType,' from:', this.ip, this.ssid, this.cid )
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
} else {
|
|
335
|
-
// TEXT FRAME
|
|
336
|
-
try {
|
|
337
|
-
let textMessage = decoder.decode(message)
|
|
338
|
-
// console.log('text buffer:',textMessage )
|
|
339
|
-
// console.log('text buffer:',message.toString('hex') )
|
|
340
|
-
this.manager.server.emit('text_message', textMessage, this)
|
|
341
|
-
} catch (error) {
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
response(msgId, statusCode, body) {
|
|
350
|
-
// console.log('response body', body)
|
|
351
|
-
if (body) {
|
|
352
|
-
|
|
353
|
-
this.send_enc_mode(MBP.pack(
|
|
354
|
-
MB('#type', '8', IOMsg.RESPONSE_MBP),
|
|
355
|
-
MB('status', '8', statusCode),
|
|
356
|
-
MB('mid', '16', msgId),
|
|
357
|
-
MB('body', body)
|
|
358
|
-
))
|
|
359
|
-
|
|
360
|
-
} else {
|
|
361
|
-
this.send_enc_mode(MBP.pack(
|
|
362
|
-
MB('#type', '8', IOMsg.RESPONSE_MBP),
|
|
363
|
-
MB('status', '8', statusCode),
|
|
364
|
-
MB('mid', '16', msgId)
|
|
365
|
-
))
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
send_enc_mode(data, useEncryption = false) {
|
|
373
|
-
|
|
374
|
-
if (this.encMode === ENC_MODE.YES ||
|
|
375
|
-
this.encMode === ENC_MODE.AUTO &&
|
|
376
|
-
!this.TLS && this.boho.isAuthorized
|
|
377
|
-
) useEncryption = true;
|
|
378
|
-
|
|
379
|
-
// console.log('svr useEnc', useEncryption, data )
|
|
380
|
-
|
|
381
|
-
if (useEncryption && data[0] == IOMsg.SIGNAL_E2E) {
|
|
382
|
-
// When the server delivers end-to-end encrypted messages,
|
|
383
|
-
// it should only encrypt the header part and pass the data part as is.
|
|
384
|
-
let tagLen = data[1];
|
|
385
|
-
|
|
386
|
-
// let tagInfo = decoder.decode( data.subarray( 2, 2 + tagLen) )
|
|
387
|
-
// console.log('server bypass E2E signal taginfo:', tagInfo )
|
|
388
|
-
|
|
389
|
-
let encHeader = this.boho.encrypt_488(data.subarray(0, 3 + tagLen))
|
|
390
|
-
encHeader[0] = BohoMsg.ENC_E2E
|
|
391
|
-
let newEncBuffer = Buffer.concat([encHeader, data.subarray(3 + tagLen)])
|
|
392
|
-
this.send(newEncBuffer)
|
|
393
|
-
|
|
394
|
-
} else if (useEncryption) {
|
|
395
|
-
let encPack = this.boho.encrypt_488(data)
|
|
396
|
-
if (encPack) {
|
|
397
|
-
this.send(encPack)
|
|
398
|
-
} else {
|
|
399
|
-
// console.log('encryption FAIL: NO DATA TRANSIT')
|
|
400
|
-
}
|
|
401
|
-
} else {
|
|
402
|
-
// console.log('send -N-')
|
|
403
|
-
this.send(data)
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
iamResponse(info = "") {
|
|
412
|
-
|
|
413
|
-
if (info == "") {
|
|
414
|
-
let channels = []
|
|
415
|
-
for (let tag of this.channels.keys()) {
|
|
416
|
-
channels.push(tag)
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
info = {
|
|
420
|
-
"ssid": this.ssid,
|
|
421
|
-
"uid": this.uid,
|
|
422
|
-
"cid": this.cid,
|
|
423
|
-
"did": this.did,
|
|
424
|
-
"nick": this.nick,
|
|
425
|
-
"ip": this.ip
|
|
426
|
-
, 'tag': channels
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
let pack = MBP.pack(
|
|
431
|
-
MB('#MsgType', '8', IOMsg.IAM_RES),
|
|
432
|
-
MB('#info', info)
|
|
433
|
-
)
|
|
434
|
-
this.send_enc_mode(pack)
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
|
|
1
|
+
import { webcrypto } from 'crypto';
|
|
2
|
+
import { MBP } from 'meta-buffer-pack'
|
|
3
|
+
import { Boho, BohoMsg, MetaSize } from 'boho'
|
|
4
|
+
import { serverOption } from './serverOption.js';
|
|
5
|
+
import { IOMsg, ENC_MODE, CLIENT_STATE } from '../common/constants.js'
|
|
6
|
+
import { quotaTable } from '../common/quotaTable.js'
|
|
7
|
+
|
|
8
|
+
const decoder = new TextDecoder()
|
|
9
|
+
|
|
10
|
+
const NB = MBP.NB
|
|
11
|
+
const MB = MBP.MB
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
export class RemoteCore {
|
|
15
|
+
constructor(socket, manager) {
|
|
16
|
+
|
|
17
|
+
this.socket = socket;
|
|
18
|
+
socket.isAlive = true;
|
|
19
|
+
socket.txCounter = 0;
|
|
20
|
+
socket.rxCounter = 0;
|
|
21
|
+
socket.openTime = Date.now()
|
|
22
|
+
|
|
23
|
+
this.boho = new Boho()
|
|
24
|
+
this.encMode = ENC_MODE.AUTO;
|
|
25
|
+
this.channels = new Set(); // subscribed tags
|
|
26
|
+
this.retain_signal = new Map();
|
|
27
|
+
|
|
28
|
+
this.ssid = RemoteCore.ssid++; // ordered index number
|
|
29
|
+
this.manager = manager;
|
|
30
|
+
|
|
31
|
+
this.cid; // Communication Id
|
|
32
|
+
this.did; // Device Id
|
|
33
|
+
this.uid; // User Id
|
|
34
|
+
this.nick = ""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
this.lastEchoMessage = "N"
|
|
38
|
+
this.privateNode = false
|
|
39
|
+
this.HOME_CHANNEL = ""
|
|
40
|
+
this.level = serverOption.defaultQuotaIndex;
|
|
41
|
+
this.quota = quotaTable[this.level];
|
|
42
|
+
this.isAdmin = false;
|
|
43
|
+
|
|
44
|
+
this.state;
|
|
45
|
+
this.stateLog = [];
|
|
46
|
+
this.setState(CLIENT_STATE.INIT)
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static ssid = 1;
|
|
51
|
+
|
|
52
|
+
setState(state) {
|
|
53
|
+
this.state = state
|
|
54
|
+
if (serverOption.debug.showAuthInfo) {
|
|
55
|
+
this.stateLog.push(state)
|
|
56
|
+
console.log(this.stateLog.join('>'))
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getState() { // <Number>
|
|
61
|
+
return this.state
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getStateName() { // <String>
|
|
65
|
+
return (CLIENT_STATE[this.state]).toLowerCase()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
showMessageLog(message, isBinary) {
|
|
70
|
+
let from = this.boho.isAuthorized ? `did: ${this.did} ${this.cid}@` : `${this.cid}@`
|
|
71
|
+
if (isBinary) {
|
|
72
|
+
let msgTypeName = IOMsg[message[0]]
|
|
73
|
+
if (!msgTypeName) msgTypeName = BohoMsg[message[0]]
|
|
74
|
+
msgTypeName = ' [' + msgTypeName + ']';
|
|
75
|
+
if (message.byteLength > 40) {
|
|
76
|
+
console.log(from + msgTypeName + ' LEN:', message.length);
|
|
77
|
+
} else {
|
|
78
|
+
console.log(from + msgTypeName, message);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
} else {
|
|
82
|
+
console.log(from + ' [TEXT] %s', message);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
rxQuotaChecker(message) {
|
|
87
|
+
let rxBytes = message.byteLength
|
|
88
|
+
this.manager.rxBytes += rxBytes;
|
|
89
|
+
|
|
90
|
+
if (serverOption.useQuota.signalSize && (rxBytes > this.quota.signalSize)) {
|
|
91
|
+
console.log('## quota: size over')
|
|
92
|
+
this.send(Buffer.from([IOMsg.OVER_SIZE]))
|
|
93
|
+
if (serverOption.useQuota.disconnect) this.close();
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// todo:
|
|
98
|
+
// traffic rate :
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
onTimeDelayMessage(message, isBinary = true) {
|
|
103
|
+
setTimeout(() => {
|
|
104
|
+
this.onSocketMessage(message, isBinary)
|
|
105
|
+
}
|
|
106
|
+
, serverOption.debug.delay)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// CongSocket or WebSocket
|
|
110
|
+
onSocketMessage(message, isBinary = true) {
|
|
111
|
+
|
|
112
|
+
this.receiveMonitor() // rx-data, ping/pong and timeout check
|
|
113
|
+
if (!this.rxQuotaChecker(message)) return
|
|
114
|
+
|
|
115
|
+
if (serverOption.showMessage === 'message') this.showMessageLog(message, isBinary)
|
|
116
|
+
|
|
117
|
+
let msgType, decoded
|
|
118
|
+
|
|
119
|
+
if (isBinary) {
|
|
120
|
+
msgType = message[0]
|
|
121
|
+
|
|
122
|
+
if (msgType === BohoMsg.ENC_488) {
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
decoded = this.boho.decrypt_488(message)
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.log('-- E488 DEC_FAIL', err)
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (decoded) {
|
|
132
|
+
msgType = decoded[0]
|
|
133
|
+
message = decoded
|
|
134
|
+
} else {
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
} else if (msgType === BohoMsg.ENC_E2E) { // symetric E2EE Signal
|
|
139
|
+
try {
|
|
140
|
+
decoded = this.boho.decrypt_488(message)
|
|
141
|
+
} catch (err) {
|
|
142
|
+
// console.log('-- E2E DEC_FAIL', err)
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
// console.log('e2e unpack:', decoded )
|
|
146
|
+
if (decoded) {
|
|
147
|
+
msgType = decoded[0]
|
|
148
|
+
message.set(decoded, MetaSize.ENC_488) // set decoded signal_e2e headaer.
|
|
149
|
+
message = message.subarray(MetaSize.ENC_488) // reset offset.
|
|
150
|
+
} else {
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
} else {
|
|
155
|
+
// console.log( `[P] ${ IOMsg[ msgType] || BohoMsg[cmd]} LEN: ${message.byteLength}` )
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
switch (msgType) {
|
|
159
|
+
case IOMsg.PING:
|
|
160
|
+
this.pong();
|
|
161
|
+
break;
|
|
162
|
+
|
|
163
|
+
case IOMsg.PONG:
|
|
164
|
+
break;
|
|
165
|
+
|
|
166
|
+
case IOMsg.CID_REQ:
|
|
167
|
+
if (this.state < CLIENT_STATE.SENT_SERVER_READY) {
|
|
168
|
+
// Protocol violation: CID_REQ was sent before receiving the SERVER_READY signal.
|
|
169
|
+
this.close()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!this.cid) {
|
|
173
|
+
this.cid = '?' + webcrypto.getRandomValues(Buffer.alloc(3)).toString('base64url')
|
|
174
|
+
this.manager.cid2remote.set(this.cid, this)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// console.log('<< SENDING CID_RES:', this.cid)
|
|
178
|
+
this.send_enc_mode(MBP.pack(
|
|
179
|
+
MB('#cid_ack', '8', IOMsg.CID_RES),
|
|
180
|
+
MB('#cid', this.cid)
|
|
181
|
+
))
|
|
182
|
+
|
|
183
|
+
this.setState(CLIENT_STATE.CID_READY)
|
|
184
|
+
break;
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
case IOMsg.ECHO: // TEXT only
|
|
188
|
+
try {
|
|
189
|
+
let msg = decoder.decode(message.subarray(1))
|
|
190
|
+
this.lastEchoMessage = msg
|
|
191
|
+
} catch (error) {
|
|
192
|
+
// console.log('ECHO message is not a text')
|
|
193
|
+
}
|
|
194
|
+
this.send(message, isBinary)
|
|
195
|
+
break;
|
|
196
|
+
|
|
197
|
+
case IOMsg.IAM:
|
|
198
|
+
if (message.byteLength > 1) {
|
|
199
|
+
let iamInfo = message.subarray(1)
|
|
200
|
+
this.nick = decoder.decode(iamInfo)
|
|
201
|
+
console.log('iam nick reset', this.nick)
|
|
202
|
+
}
|
|
203
|
+
this.iamResponse()
|
|
204
|
+
break;
|
|
205
|
+
|
|
206
|
+
case IOMsg.SIGNAL_E2E:
|
|
207
|
+
case IOMsg.SIGNAL:
|
|
208
|
+
if (message.byteLength >= 3) {
|
|
209
|
+
let tagLen = message.readUInt8(1)
|
|
210
|
+
if (message.byteLength >= tagLen + 2) {
|
|
211
|
+
let tag = message.subarray(2, 2 + tagLen)
|
|
212
|
+
tag = decoder.decode(tag)
|
|
213
|
+
this.manager.sender(tag, this, message)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
case IOMsg.UNSUBSCRIBE:
|
|
221
|
+
if (message.byteLength == 2) {
|
|
222
|
+
this.manager.unsubscribe([""], this)
|
|
223
|
+
} else if (message.byteLength >= 3) {
|
|
224
|
+
let tagLen = message.readUInt8(1)
|
|
225
|
+
|
|
226
|
+
if (message.byteLength == tagLen + 2) {
|
|
227
|
+
let tag = message.subarray(2, 2 + tagLen)
|
|
228
|
+
tag = decoder.decode(tag)
|
|
229
|
+
let tagList = tag.split(',')
|
|
230
|
+
this.manager.unsubscribe(tagList, this)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
break;
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
case IOMsg.SUBSCRIBE: // 1byte tagLen
|
|
237
|
+
if (message.byteLength >= 3) {
|
|
238
|
+
let tagLen = message.readUInt8(1)
|
|
239
|
+
if (message.byteLength == tagLen + 2) {
|
|
240
|
+
let tag = message.subarray(2, 2 + tagLen)
|
|
241
|
+
tag = decoder.decode(tag)
|
|
242
|
+
let tagList = tag.split(',')
|
|
243
|
+
this.manager.subscribe(tagList, this)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
|
|
248
|
+
case IOMsg.SUBSCRIBE_REQ: // 2bytes tagLen
|
|
249
|
+
if (message.byteLength >= 6) {
|
|
250
|
+
let msgID = message.readUInt16BE(1)
|
|
251
|
+
let tagLen = message.readUInt16BE(3)
|
|
252
|
+
if (message.byteLength == tagLen + 5) {
|
|
253
|
+
let tag = message.subarray(5, 5 + tagLen)
|
|
254
|
+
tag = decoder.decode(tag)
|
|
255
|
+
let tagList = tag.split(',')
|
|
256
|
+
// console.log('>> SUBSCRIBE_REQ from:', this.cid, tagList)
|
|
257
|
+
this.manager.subscribe(tagList, this)
|
|
258
|
+
this.response(msgID, 0)
|
|
259
|
+
} else {
|
|
260
|
+
this.response(msgID, 255)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
case IOMsg.REQUEST: //api request
|
|
267
|
+
try {
|
|
268
|
+
let req = MBP.unpack(message)
|
|
269
|
+
if (req) {
|
|
270
|
+
if (!req.target || !req.topic) return
|
|
271
|
+
if (this.manager.server.apiNames.has(req.target)) {
|
|
272
|
+
this.manager.server.emit(req.target, this, req)
|
|
273
|
+
} else {
|
|
274
|
+
// console.log('UnKnown API req: ', req.target)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
} catch (e) {
|
|
278
|
+
console.log('request catch error', e)
|
|
279
|
+
}
|
|
280
|
+
break;
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
case IOMsg.CLOSE:
|
|
284
|
+
if (message.byteLength > 1) {
|
|
285
|
+
let reason = decoder.decode(message.subarray(1))
|
|
286
|
+
console.log('>> CLOSE reason:', reason)
|
|
287
|
+
this.close();
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
// Auth
|
|
293
|
+
case BohoMsg.AUTH_REQ:
|
|
294
|
+
if (!this.manager.authManager) return
|
|
295
|
+
if (this.state < CLIENT_STATE.SENT_SERVER_READY) {
|
|
296
|
+
// console.log('protocol error. auth_req before server_ready')
|
|
297
|
+
this.close();
|
|
298
|
+
}
|
|
299
|
+
this.setState(CLIENT_STATE.RECV_AUTH_REQ)
|
|
300
|
+
let auth_nonce_pack = this.boho.auth_nonce()
|
|
301
|
+
// console.log('## auth_nonce_pack', auth_nonce_pack )
|
|
302
|
+
this.send(auth_nonce_pack)
|
|
303
|
+
this.setState(CLIENT_STATE.SENT_SERVER_NONCE)
|
|
304
|
+
break;
|
|
305
|
+
|
|
306
|
+
case BohoMsg.AUTH_HMAC:
|
|
307
|
+
if (!this.manager.authManager) return
|
|
308
|
+
if (this.state < CLIENT_STATE.SENT_SERVER_NONCE) {
|
|
309
|
+
// console.log('protocol error. auth_hmac before server_nonce')
|
|
310
|
+
this.close();
|
|
311
|
+
}
|
|
312
|
+
this.setState(CLIENT_STATE.RECV_AUTH_HMAC)
|
|
313
|
+
//async
|
|
314
|
+
this.manager.authManager.verify_auth_hmac(message, this)
|
|
315
|
+
.then(authInfo => {
|
|
316
|
+
if (authInfo) {
|
|
317
|
+
this.manager.deligateSignal(remote, '@$name', authInfo.name)
|
|
318
|
+
// console.log('device login success authInfo', authInfo)
|
|
319
|
+
// this.manager.sender('@$name', authInfo.name )
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
})
|
|
323
|
+
.catch(e => {
|
|
324
|
+
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
return;
|
|
328
|
+
|
|
329
|
+
default:
|
|
330
|
+
// console.log('unknown MsgType:',msgType,' from:', this.ip, this.ssid, this.cid )
|
|
331
|
+
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
} else {
|
|
335
|
+
// TEXT FRAME
|
|
336
|
+
try {
|
|
337
|
+
let textMessage = decoder.decode(message)
|
|
338
|
+
// console.log('text buffer:',textMessage )
|
|
339
|
+
// console.log('text buffer:',message.toString('hex') )
|
|
340
|
+
this.manager.server.emit('text_message', textMessage, this)
|
|
341
|
+
} catch (error) {
|
|
342
|
+
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
response(msgId, statusCode, body) {
|
|
350
|
+
// console.log('response body', body)
|
|
351
|
+
if (body) {
|
|
352
|
+
|
|
353
|
+
this.send_enc_mode(MBP.pack(
|
|
354
|
+
MB('#type', '8', IOMsg.RESPONSE_MBP),
|
|
355
|
+
MB('status', '8', statusCode),
|
|
356
|
+
MB('mid', '16', msgId),
|
|
357
|
+
MB('body', body)
|
|
358
|
+
))
|
|
359
|
+
|
|
360
|
+
} else {
|
|
361
|
+
this.send_enc_mode(MBP.pack(
|
|
362
|
+
MB('#type', '8', IOMsg.RESPONSE_MBP),
|
|
363
|
+
MB('status', '8', statusCode),
|
|
364
|
+
MB('mid', '16', msgId)
|
|
365
|
+
))
|
|
366
|
+
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
send_enc_mode(data, useEncryption = false) {
|
|
373
|
+
|
|
374
|
+
if (this.encMode === ENC_MODE.YES ||
|
|
375
|
+
this.encMode === ENC_MODE.AUTO &&
|
|
376
|
+
!this.TLS && this.boho.isAuthorized
|
|
377
|
+
) useEncryption = true;
|
|
378
|
+
|
|
379
|
+
// console.log('svr useEnc', useEncryption, data )
|
|
380
|
+
|
|
381
|
+
if (useEncryption && data[0] == IOMsg.SIGNAL_E2E) {
|
|
382
|
+
// When the server delivers end-to-end encrypted messages,
|
|
383
|
+
// it should only encrypt the header part and pass the data part as is.
|
|
384
|
+
let tagLen = data[1];
|
|
385
|
+
|
|
386
|
+
// let tagInfo = decoder.decode( data.subarray( 2, 2 + tagLen) )
|
|
387
|
+
// console.log('server bypass E2E signal taginfo:', tagInfo )
|
|
388
|
+
|
|
389
|
+
let encHeader = this.boho.encrypt_488(data.subarray(0, 3 + tagLen))
|
|
390
|
+
encHeader[0] = BohoMsg.ENC_E2E
|
|
391
|
+
let newEncBuffer = Buffer.concat([encHeader, data.subarray(3 + tagLen)])
|
|
392
|
+
this.send(newEncBuffer)
|
|
393
|
+
|
|
394
|
+
} else if (useEncryption) {
|
|
395
|
+
let encPack = this.boho.encrypt_488(data)
|
|
396
|
+
if (encPack) {
|
|
397
|
+
this.send(encPack)
|
|
398
|
+
} else {
|
|
399
|
+
// console.log('encryption FAIL: NO DATA TRANSIT')
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
// console.log('send -N-')
|
|
403
|
+
this.send(data)
|
|
404
|
+
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
iamResponse(info = "") {
|
|
412
|
+
|
|
413
|
+
if (info == "") {
|
|
414
|
+
let channels = []
|
|
415
|
+
for (let tag of this.channels.keys()) {
|
|
416
|
+
channels.push(tag)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
info = {
|
|
420
|
+
"ssid": this.ssid,
|
|
421
|
+
"uid": this.uid,
|
|
422
|
+
"cid": this.cid,
|
|
423
|
+
"did": this.did,
|
|
424
|
+
"nick": this.nick,
|
|
425
|
+
"ip": this.ip
|
|
426
|
+
, 'tag': channels
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
let pack = MBP.pack(
|
|
431
|
+
MB('#MsgType', '8', IOMsg.IAM_RES),
|
|
432
|
+
MB('#info', info)
|
|
433
|
+
)
|
|
434
|
+
this.send_enc_mode(pack)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
}
|
|
439
|
+
|