gamedigz 0.1.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 (73) hide show
  1. package/GAMES_LIST.md +453 -0
  2. package/LICENSE +21 -0
  3. package/README.md +144 -0
  4. package/bin/gamedig.js +79 -0
  5. package/lib/DnsResolver.js +76 -0
  6. package/lib/GlobalUdpSocket.js +69 -0
  7. package/lib/HexUtil.js +20 -0
  8. package/lib/Logger.js +45 -0
  9. package/lib/Promises.js +18 -0
  10. package/lib/ProtocolResolver.js +7 -0
  11. package/lib/QueryRunner.js +95 -0
  12. package/lib/Results.js +32 -0
  13. package/lib/game-resolver.js +17 -0
  14. package/lib/gamedig.js +23 -0
  15. package/lib/games.js +2747 -0
  16. package/lib/index.js +5 -0
  17. package/lib/reader.js +172 -0
  18. package/package.json +74 -0
  19. package/protocols/armagetron.js +65 -0
  20. package/protocols/asa.js +12 -0
  21. package/protocols/ase.js +45 -0
  22. package/protocols/assettocorsa.js +40 -0
  23. package/protocols/battlefield.js +162 -0
  24. package/protocols/beammp.js +32 -0
  25. package/protocols/beammpmaster.js +17 -0
  26. package/protocols/buildandshoot.js +55 -0
  27. package/protocols/core.js +349 -0
  28. package/protocols/cs2d.js +65 -0
  29. package/protocols/dayz.js +196 -0
  30. package/protocols/discord.js +29 -0
  31. package/protocols/doom3.js +148 -0
  32. package/protocols/eco.js +20 -0
  33. package/protocols/eldewrito.js +21 -0
  34. package/protocols/epic.js +95 -0
  35. package/protocols/ffow.js +38 -0
  36. package/protocols/fivem.js +33 -0
  37. package/protocols/gamespy1.js +181 -0
  38. package/protocols/gamespy2.js +144 -0
  39. package/protocols/gamespy3.js +197 -0
  40. package/protocols/geneshift.js +46 -0
  41. package/protocols/goldsrc.js +8 -0
  42. package/protocols/hexen2.js +14 -0
  43. package/protocols/index.js +61 -0
  44. package/protocols/jc2mp.js +16 -0
  45. package/protocols/kspdmp.js +28 -0
  46. package/protocols/mafia2mp.js +41 -0
  47. package/protocols/mafia2online.js +9 -0
  48. package/protocols/minecraft.js +102 -0
  49. package/protocols/minecraftbedrock.js +72 -0
  50. package/protocols/minecraftvanilla.js +87 -0
  51. package/protocols/mumble.js +39 -0
  52. package/protocols/mumbleping.js +24 -0
  53. package/protocols/nadeo.js +86 -0
  54. package/protocols/openttd.js +127 -0
  55. package/protocols/quake1.js +9 -0
  56. package/protocols/quake2.js +88 -0
  57. package/protocols/quake3.js +24 -0
  58. package/protocols/rfactor.js +69 -0
  59. package/protocols/samp.js +102 -0
  60. package/protocols/savage2.js +25 -0
  61. package/protocols/starmade.js +67 -0
  62. package/protocols/starsiege.js +10 -0
  63. package/protocols/teamspeak2.js +71 -0
  64. package/protocols/teamspeak3.js +69 -0
  65. package/protocols/terraria.js +24 -0
  66. package/protocols/tribes1.js +153 -0
  67. package/protocols/tribes1master.js +80 -0
  68. package/protocols/unreal2.js +150 -0
  69. package/protocols/ut3.js +45 -0
  70. package/protocols/valve.js +455 -0
  71. package/protocols/vcmp.js +10 -0
  72. package/protocols/ventrilo.js +237 -0
  73. package/protocols/warsow.js +13 -0
@@ -0,0 +1,455 @@
1
+ import Bzip2 from 'seek-bzip'
2
+ import Core from './core.js'
3
+ import { Buffer } from 'node:buffer'
4
+
5
+ const AppId = {
6
+ Squad: 393380,
7
+ Bat1944: 489940,
8
+ Ship: 2400,
9
+ Rust: 252490,
10
+ CSGO: 730,
11
+ CS_Source: 240,
12
+ EternalSilence: 17550,
13
+ Insurgency_MIC: 17700,
14
+ Source_SDK_Base_2006: 215
15
+ }
16
+
17
+ export default class valve extends Core {
18
+ constructor () {
19
+ super()
20
+
21
+ // legacy goldsrc info response -- basically not used by ANYTHING now,
22
+ // as most (all?) goldsrc servers respond with the source info reponse
23
+ // delete in a few years if nothing ends up using it anymore
24
+ this.goldsrcInfo = false
25
+
26
+ // unfortunately, the split format from goldsrc is still around, but we
27
+ // can detect that during the query
28
+ this.goldsrcSplits = false
29
+
30
+ // some mods require a challenge, but don't provide them in the new format
31
+ // at all, use the old dedicated challenge query if needed
32
+ this.legacyChallenge = false
33
+
34
+ // 2006 engines don't pass packet switching size in split packet header
35
+ // while all others do, this need is detected automatically
36
+ this._skipSizeInSplitHeader = false
37
+
38
+ this._challenge = ''
39
+ }
40
+
41
+ async run (state) {
42
+ if (!this.options.port) this.options.port = 27015
43
+ await this.queryInfo(state)
44
+ await this.queryChallenge()
45
+ await this.queryPlayers(state)
46
+ await this.queryRules(state)
47
+ await this.cleanup(state)
48
+ }
49
+
50
+ async queryInfo (/** Results */ state) {
51
+ this.logger.debug('Requesting info ...')
52
+ const b = await this.sendPacket(
53
+ this.goldsrcInfo ? undefined : 0x54,
54
+ this.goldsrcInfo ? 'details' : 'Source Engine Query\0',
55
+ this.goldsrcInfo ? 0x6D : 0x49,
56
+ false
57
+ )
58
+
59
+ const reader = this.reader(b)
60
+
61
+ if (this.goldsrcInfo) state.raw.address = reader.string()
62
+ else state.raw.protocol = reader.uint(1)
63
+
64
+ state.name = reader.string()
65
+ state.map = reader.string()
66
+ state.raw.folder = reader.string()
67
+ state.raw.game = reader.string()
68
+ if (!this.goldsrcInfo) state.raw.appId = reader.uint(2)
69
+ state.numplayers = reader.uint(1)
70
+ state.maxplayers = reader.uint(1)
71
+
72
+ if (this.goldsrcInfo) state.raw.protocol = reader.uint(1)
73
+ else state.raw.numbots = reader.uint(1)
74
+
75
+ state.raw.listentype = String.fromCharCode(reader.uint(1))
76
+ state.raw.environment = String.fromCharCode(reader.uint(1))
77
+
78
+ state.password = !!reader.uint(1)
79
+ if (this.goldsrcInfo) {
80
+ state.raw.ismod = reader.uint(1)
81
+ if (state.raw.ismod) {
82
+ state.raw.modlink = reader.string()
83
+ state.raw.moddownload = reader.string()
84
+ reader.skip(1)
85
+ state.raw.modversion = reader.uint(4)
86
+ state.raw.modsize = reader.uint(4)
87
+ state.raw.modtype = reader.uint(1)
88
+ state.raw.moddll = reader.uint(1)
89
+ }
90
+ } else {
91
+ state.raw.secure = reader.uint(1)
92
+ if (state.raw.appId === AppId.Ship) {
93
+ state.raw.shipmode = reader.uint(1)
94
+ state.raw.shipwitnesses = reader.uint(1)
95
+ state.raw.shipduration = reader.uint(1)
96
+ }
97
+ state.raw.version = reader.string()
98
+ const extraFlag = reader.uint(1)
99
+ if (extraFlag & 0x80) state.gamePort = reader.uint(2)
100
+ if (extraFlag & 0x10) state.raw.steamid = reader.uint(8).toString()
101
+ if (extraFlag & 0x40) {
102
+ state.raw.sourcetvport = reader.uint(2)
103
+ state.raw.sourcetvname = reader.string()
104
+ }
105
+ if (extraFlag & 0x20) state.raw.tags = reader.string().split(',')
106
+ if (extraFlag & 0x01) {
107
+ const gameId = reader.uint(8)
108
+ const betterAppId = gameId.getLowBitsUnsigned() & 0xffffff
109
+ if (betterAppId) {
110
+ state.raw.appId = betterAppId
111
+ }
112
+ }
113
+ }
114
+
115
+ const appId = state.raw.appId
116
+
117
+ // from https://developer.valvesoftware.com/wiki/Server_queries
118
+ if (
119
+ state.raw.protocol === 7 && (
120
+ state.raw.appId === AppId.Source_SDK_Base_2006 ||
121
+ state.raw.appId === AppId.EternalSilence ||
122
+ state.raw.appId === AppId.Insurgency_MIC ||
123
+ state.raw.appId === AppId.CS_Source
124
+ )
125
+ ) {
126
+ this._skipSizeInSplitHeader = true
127
+ }
128
+ this.logger.debug('INFO: ', state.raw)
129
+ if (state.raw.protocol === 48) {
130
+ this.logger.debug('GOLDSRC DETECTED - USING MODIFIED SPLIT FORMAT')
131
+ this.goldsrcSplits = true
132
+ }
133
+
134
+ if (appId === AppId.Rust) {
135
+ if (state.raw.tags) {
136
+ for (const tag of state.raw.tags) {
137
+ if (tag.startsWith('mp')) {
138
+ const value = parseInt(tag.replace('mp', ''))
139
+ if (!isNaN(value)) {
140
+ state.maxplayers = value
141
+ }
142
+ }
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ async queryChallenge () {
149
+ if (this.legacyChallenge) {
150
+ // sendPacket will catch the response packet and
151
+ // save the challenge for us
152
+ this.logger.debug('Requesting legacy challenge key ...')
153
+ await this.sendPacket(
154
+ 0x57,
155
+ null,
156
+ 0x41,
157
+ false
158
+ )
159
+ }
160
+ }
161
+
162
+ async queryPlayers (/** Results */ state) {
163
+ state.raw.players = []
164
+
165
+ this.logger.debug('Requesting player list ...')
166
+ const b = await this.sendPacket(
167
+ this.goldsrcInfo ? undefined : 0x55,
168
+ this.goldsrcInfo ? 'players' : null,
169
+ 0x44,
170
+ true
171
+ )
172
+
173
+ if (b === null && !this.options.requestPlayersRequired) {
174
+ // Player query timed out
175
+ // CSGO doesn't respond to player query if host_players_show is not 2
176
+ // Conan Exiles never responds to player query
177
+ // Just skip it, and we'll fill with dummy objects in cleanup()
178
+ return
179
+ }
180
+
181
+ const reader = this.reader(b)
182
+ const num = reader.uint(1)
183
+ for (let i = 0; i < num; i++) {
184
+ reader.skip(1)
185
+ const name = reader.string()
186
+ const score = reader.int(4)
187
+ const time = reader.float()
188
+
189
+ this.logger.debug('Found player: ' + name + ' ' + score + ' ' + time)
190
+
191
+ // CSGO sometimes adds a bot named 'Max Players' if host_players_show is not 2
192
+ if (state.raw.appId === AppId.CSGO && name === 'Max Players') continue
193
+
194
+ state.raw.players.push({
195
+ name, score, time
196
+ })
197
+ }
198
+ }
199
+
200
+ async queryRules (/** Results */ state) {
201
+ const appId = state.raw.appId
202
+ if (appId === AppId.Squad ||
203
+ appId === AppId.Bat1944 ||
204
+ this.options.requestRules) {
205
+ // let's get 'em
206
+ } else {
207
+ return
208
+ }
209
+
210
+ const rules = {}
211
+ state.raw.rules = rules
212
+
213
+ this.logger.debug('Requesting rules ...')
214
+
215
+ if (this.goldsrcInfo) {
216
+ const b = await this.udpSend('\xff\xff\xff\xffrules', b => b, () => null)
217
+ if (b === null && !this.options.requestRulesRequired) return // timed out - the server probably has rules disabled
218
+ const reader = this.reader(b)
219
+ while (!reader.done()) {
220
+ const key = reader.string()
221
+ rules[key] = reader.string()
222
+ }
223
+ } else {
224
+ const b = await this.sendPacket(0x56, null, 0x45, true)
225
+ if (b === null && !this.options.requestRulesRequired) return // timed out - the server probably has rules disabled
226
+
227
+ const reader = this.reader(b)
228
+ const num = reader.uint(2)
229
+ for (let i = 0; i < num; i++) {
230
+ const key = reader.string()
231
+ rules[key] = reader.string()
232
+ }
233
+ }
234
+
235
+ // Battalion 1944 puts its info into rules fields for some reason
236
+ if (appId === AppId.Bat1944) {
237
+ if ('bat_name_s' in rules) {
238
+ state.name = rules.bat_name_s
239
+ delete rules.bat_name_s
240
+ if ('bat_player_count_s' in rules) {
241
+ state.numplayers = parseInt(rules.bat_player_count_s)
242
+ delete rules.bat_player_count_s
243
+ }
244
+ if ('bat_max_players_i' in rules) {
245
+ state.maxplayers = parseInt(rules.bat_max_players_i)
246
+ delete rules.bat_max_players_i
247
+ }
248
+ if ('bat_has_password_s' in rules) {
249
+ state.password = rules.bat_has_password_s === 'Y'
250
+ delete rules.bat_has_password_s
251
+ }
252
+ // apparently map is already right, and this var is often wrong
253
+ delete rules.bat_map_s
254
+ }
255
+ }
256
+
257
+ // Squad keeps its password in a separate field
258
+ if (appId === AppId.Squad) {
259
+ if (rules.Password_b === 'true') {
260
+ state.password = true
261
+ }
262
+ }
263
+ }
264
+
265
+ async cleanup (/** Results */ state) {
266
+ // Organize players / hidden players into player / bot arrays
267
+ const botProbability = (p) => {
268
+ if (p.time === -1) return Number.MAX_VALUE
269
+ return p.time
270
+ }
271
+ const sortedPlayers = state.raw.players.sort((a, b) => {
272
+ return botProbability(a) - botProbability(b)
273
+ })
274
+
275
+ const numBots = state.raw.numbots || 0
276
+
277
+ while (state.bots.length < numBots && sortedPlayers.length) {
278
+ state.bots.push(sortedPlayers.pop())
279
+ }
280
+ while ((state.players.length < state.numplayers - numBots || sortedPlayers.length) && sortedPlayers.length) {
281
+ state.players.push(sortedPlayers.pop())
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Sends a request packet and returns only the response type expected
287
+ * @param {number} type
288
+ * @param {boolean} sendChallenge
289
+ * @param {?string|Buffer} payload
290
+ * @param {number} expect
291
+ * @param {boolean=} allowTimeout
292
+ * @returns Buffer|null
293
+ **/
294
+ async sendPacket (
295
+ type,
296
+ payload,
297
+ expect,
298
+ allowTimeout
299
+ ) {
300
+ for (let keyRetry = 0; keyRetry < 3; keyRetry++) {
301
+ let receivedNewChallengeKey = false
302
+ const response = await this.sendPacketRaw(
303
+ type, payload,
304
+ (payload) => {
305
+ const reader = this.reader(payload)
306
+ const type = reader.uint(1)
307
+ this.logger.debug(() => 'Received 0x' + type.toString(16) + ' expected 0x' + expect.toString(16))
308
+ if (type === 0x41) {
309
+ const key = reader.uint(4)
310
+ if (this._challenge !== key) {
311
+ this.logger.debug('Received new challenge key: 0x' + key.toString(16))
312
+ this._challenge = key
313
+ receivedNewChallengeKey = true
314
+ }
315
+ }
316
+ if (type === expect) {
317
+ return reader.rest()
318
+ } else if (receivedNewChallengeKey) {
319
+ return null
320
+ }
321
+ },
322
+ () => {
323
+ if (allowTimeout) return null
324
+ }
325
+ )
326
+ if (!receivedNewChallengeKey) {
327
+ return response
328
+ }
329
+ }
330
+ throw new Error('Received too many challenge key responses')
331
+ }
332
+
333
+ /**
334
+ * Sends a request packet and assembles partial responses
335
+ * @param {number} type
336
+ * @param {boolean} sendChallenge
337
+ * @param {?string|Buffer} payload
338
+ * @param {function(Buffer)} onResponse
339
+ * @param {function()} onTimeout
340
+ **/
341
+ async sendPacketRaw (
342
+ type,
343
+ payload,
344
+ onResponse,
345
+ onTimeout
346
+ ) {
347
+ const challengeAtBeginning = type === 0x55 || type === 0x56
348
+ const challengeAtEnd = type === 0x54 && !!this._challenge
349
+
350
+ if (typeof payload === 'string') payload = Buffer.from(payload, 'binary')
351
+
352
+ const b = Buffer.alloc(4 +
353
+ (type !== undefined ? 1 : 0) +
354
+ (challengeAtBeginning ? 4 : 0) +
355
+ (challengeAtEnd ? 4 : 0) +
356
+ (payload ? payload.length : 0)
357
+ )
358
+ let offset = 0
359
+
360
+ let challenge = this._challenge
361
+ if (!challenge) challenge = 0xffffffff
362
+
363
+ b.writeInt32LE(-1, offset)
364
+ offset += 4
365
+
366
+ if (type !== undefined) {
367
+ b.writeUInt8(type, offset)
368
+ offset += 1
369
+ }
370
+
371
+ if (challengeAtBeginning) {
372
+ if (this.byteorder === 'le') b.writeUInt32LE(challenge, offset)
373
+ else b.writeUInt32BE(challenge, offset)
374
+ offset += 4
375
+ }
376
+
377
+ if (payload) {
378
+ payload.copy(b, offset)
379
+ offset += payload.length
380
+ }
381
+
382
+ if (challengeAtEnd) {
383
+ if (this.byteorder === 'le') b.writeUInt32LE(challenge, offset)
384
+ else b.writeUInt32BE(challenge, offset)
385
+ offset += 4
386
+ }
387
+
388
+ const packetStorage = {}
389
+ return await this.udpSend(
390
+ b,
391
+ (buffer) => {
392
+ const reader = this.reader(buffer)
393
+ const header = reader.int(4)
394
+ if (header === -1) {
395
+ // full package
396
+ this.logger.debug('Received full packet')
397
+ return onResponse(reader.rest())
398
+ }
399
+ if (header === -2) {
400
+ // partial package
401
+ const uid = reader.uint(4)
402
+ if (!(uid in packetStorage)) packetStorage[uid] = {}
403
+ const packets = packetStorage[uid]
404
+
405
+ let bzip = false
406
+ if (!this.goldsrcSplits && uid & 0x80000000) bzip = true
407
+
408
+ let packetNum, payload, numPackets
409
+ if (this.goldsrcSplits) {
410
+ packetNum = reader.uint(1)
411
+ numPackets = packetNum & 0x0f
412
+ packetNum = (packetNum & 0xf0) >> 4
413
+ payload = reader.rest()
414
+ } else {
415
+ numPackets = reader.uint(1)
416
+ packetNum = reader.uint(1)
417
+ if (!this._skipSizeInSplitHeader) reader.skip(2)
418
+ if (packetNum === 0 && bzip) reader.skip(8)
419
+ payload = reader.rest()
420
+ }
421
+
422
+ packets[packetNum] = payload
423
+
424
+ this.logger.debug(() => 'Received partial packet uid: 0x' + uid.toString(16) + ' num: ' + packetNum)
425
+ this.logger.debug(() => 'Received ' + Object.keys(packets).length + '/' + numPackets + ' packets for this UID')
426
+
427
+ if (Object.keys(packets).length !== numPackets) return
428
+
429
+ // assemble the parts
430
+ const list = []
431
+ for (let i = 0; i < numPackets; i++) {
432
+ if (!(i in packets)) {
433
+ throw new Error('Missing packet #' + i)
434
+ }
435
+ list.push(packets[i])
436
+ }
437
+
438
+ let assembled = Buffer.concat(list)
439
+ if (bzip) {
440
+ this.logger.debug('BZIP DETECTED - Extracing packet...')
441
+ try {
442
+ assembled = Bzip2.decode(assembled)
443
+ } catch (e) {
444
+ throw new Error('Invalid bzip packet')
445
+ }
446
+ }
447
+ const assembledReader = this.reader(assembled)
448
+ assembledReader.skip(4) // header
449
+ return onResponse(assembledReader.rest())
450
+ }
451
+ },
452
+ onTimeout
453
+ )
454
+ }
455
+ }
@@ -0,0 +1,10 @@
1
+ import samp from './samp.js'
2
+
3
+ export default class vcmp extends samp {
4
+ constructor () {
5
+ super()
6
+ this.magicHeader = 'VCMP'
7
+ this.responseMagicHeader = 'MP04'
8
+ this.isVcmp = true
9
+ }
10
+ }
@@ -0,0 +1,237 @@
1
+ import Core from './core.js'
2
+
3
+ export default class ventrilo extends Core {
4
+ constructor () {
5
+ super()
6
+ this.byteorder = 'be'
7
+ }
8
+
9
+ async run (state) {
10
+ const data = await this.sendCommand(2, '')
11
+ state.raw = splitFields(data.toString())
12
+ for (const client of state.raw.CLIENTS) {
13
+ client.name = client.NAME
14
+ delete client.NAME
15
+ client.ping = parseInt(client.PING)
16
+ delete client.PING
17
+ state.players.push(client)
18
+ }
19
+ delete state.raw.CLIENTS
20
+ state.numplayers = state.players.length
21
+
22
+ if ('NAME' in state.raw) state.name = state.raw.NAME
23
+ if ('MAXCLIENTS' in state.raw) state.maxplayers = state.raw.MAXCLIENTS
24
+ if (this.trueTest(state.raw.AUTH)) state.password = true
25
+ }
26
+
27
+ async sendCommand (cmd, password) {
28
+ const body = Buffer.alloc(16)
29
+ body.write(password, 0, 15, 'utf8')
30
+ const encrypted = encrypt(cmd, body)
31
+
32
+ const packets = {}
33
+ return await this.udpSend(encrypted, (buffer) => {
34
+ if (buffer.length < 20) return
35
+ const data = decrypt(buffer)
36
+
37
+ if (data.zero !== 0) return
38
+ packets[data.packetNum] = data.body
39
+ if (Object.keys(packets).length !== data.packetTotal) return
40
+
41
+ const out = []
42
+ for (let i = 0; i < data.packetTotal; i++) {
43
+ if (!(i in packets)) throw new Error('Missing packet #' + i)
44
+ out.push(packets[i])
45
+ }
46
+ return Buffer.concat(out)
47
+ })
48
+ }
49
+ }
50
+
51
+ function splitFields (str, subMode) {
52
+ let splitter, delim
53
+ if (subMode) {
54
+ splitter = '='
55
+ delim = ','
56
+ } else {
57
+ splitter = ': '
58
+ delim = '\n'
59
+ }
60
+
61
+ const split = str.split(delim)
62
+ const out = {}
63
+ if (!subMode) {
64
+ out.CHANNELS = []
65
+ out.CLIENTS = []
66
+ }
67
+ for (const one of split) {
68
+ const equal = one.indexOf(splitter)
69
+ const key = equal === -1 ? one : one.substring(0, equal)
70
+ if (!key || key === '\0') continue
71
+ const value = equal === -1 ? '' : one.substring(equal + splitter.length)
72
+ if (!subMode && key === 'CHANNEL') out.CHANNELS.push(splitFields(value, true))
73
+ else if (!subMode && key === 'CLIENT') out.CLIENTS.push(splitFields(value, true))
74
+ else out[key] = value
75
+ }
76
+ return out
77
+ }
78
+
79
+ function randInt (min, max) {
80
+ return Math.floor(Math.random() * (max - min + 1) + min)
81
+ }
82
+
83
+ function crc (body) {
84
+ let crc = 0
85
+ for (let i = 0; i < body.length; i++) {
86
+ crc = crcTable[crc >> 8] ^ body.readUInt8(i) ^ (crc << 8)
87
+ crc &= 0xffff
88
+ }
89
+ return crc
90
+ }
91
+
92
+ function encrypt (cmd, body) {
93
+ const headerKeyStart = randInt(0, 0xff)
94
+ const headerKeyAdd = randInt(1, 0xff)
95
+ const bodyKeyStart = randInt(0, 0xff)
96
+ const bodyKeyAdd = randInt(1, 0xff)
97
+
98
+ const header = Buffer.alloc(20)
99
+ header.writeUInt8(headerKeyStart, 0)
100
+ header.writeUInt8(headerKeyAdd, 1)
101
+ header.writeUInt16BE(cmd, 4)
102
+ header.writeUInt16BE(body.length, 8)
103
+ header.writeUInt16BE(body.length, 10)
104
+ header.writeUInt16BE(1, 12)
105
+ header.writeUInt16BE(0, 14)
106
+ header.writeUInt8(bodyKeyStart, 16)
107
+ header.writeUInt8(bodyKeyAdd, 17)
108
+ header.writeUInt16BE(crc(body), 18)
109
+
110
+ let offset = headerKeyStart
111
+ for (let i = 2; i < header.length; i++) {
112
+ let val = header.readUInt8(i)
113
+ val += codeHead.charCodeAt(offset) + ((i - 2) % 5)
114
+ val = val & 0xff
115
+ header.writeUInt8(val, i)
116
+ offset = (offset + headerKeyAdd) & 0xff
117
+ }
118
+
119
+ offset = bodyKeyStart
120
+ for (let i = 0; i < body.length; i++) {
121
+ let val = body.readUInt8(i)
122
+ val += codeBody.charCodeAt(offset) + (i % 72)
123
+ val = val & 0xff
124
+ body.writeUInt8(val, i)
125
+ offset = (offset + bodyKeyAdd) & 0xff
126
+ }
127
+
128
+ return Buffer.concat([header, body])
129
+ }
130
+ function decrypt (data) {
131
+ const header = data.slice(0, 20)
132
+ const body = data.slice(20)
133
+ const headerKeyStart = header.readUInt8(0)
134
+ const headerKeyAdd = header.readUInt8(1)
135
+
136
+ let offset = headerKeyStart
137
+ for (let i = 2; i < header.length; i++) {
138
+ let val = header.readUInt8(i)
139
+ val -= codeHead.charCodeAt(offset) + ((i - 2) % 5)
140
+ val = val & 0xff
141
+ header.writeUInt8(val, i)
142
+ offset = (offset + headerKeyAdd) & 0xff
143
+ }
144
+
145
+ const bodyKeyStart = header.readUInt8(16)
146
+ const bodyKeyAdd = header.readUInt8(17)
147
+ offset = bodyKeyStart
148
+ for (let i = 0; i < body.length; i++) {
149
+ let val = body.readUInt8(i)
150
+ val -= codeBody.charCodeAt(offset) + (i % 72)
151
+ val = val & 0xff
152
+ body.writeUInt8(val, i)
153
+ offset = (offset + bodyKeyAdd) & 0xff
154
+ }
155
+
156
+ // header format:
157
+ // key, zero, cmd, echo, totallength, thislength
158
+ // totalpacket, packetnum, body key, crc
159
+ return {
160
+ zero: header.readUInt16BE(2),
161
+ cmd: header.readUInt16BE(4),
162
+ packetTotal: header.readUInt16BE(12),
163
+ packetNum: header.readUInt16BE(14),
164
+ body
165
+ }
166
+ }
167
+
168
+ const codeHead =
169
+ '\x80\xe5\x0e\x38\xba\x63\x4c\x99\x88\x63\x4c\xd6\x54\xb8\x65\x7e' +
170
+ '\xbf\x8a\xf0\x17\x8a\xaa\x4d\x0f\xb7\x23\x27\xf6\xeb\x12\xf8\xea' +
171
+ '\x17\xb7\xcf\x52\x57\xcb\x51\xcf\x1b\x14\xfd\x6f\x84\x38\xb5\x24' +
172
+ '\x11\xcf\x7a\x75\x7a\xbb\x78\x74\xdc\xbc\x42\xf0\x17\x3f\x5e\xeb' +
173
+ '\x74\x77\x04\x4e\x8c\xaf\x23\xdc\x65\xdf\xa5\x65\xdd\x7d\xf4\x3c' +
174
+ '\x4c\x95\xbd\xeb\x65\x1c\xf4\x24\x5d\x82\x18\xfb\x50\x86\xb8\x53' +
175
+ '\xe0\x4e\x36\x96\x1f\xb7\xcb\xaa\xaf\xea\xcb\x20\x27\x30\x2a\xae' +
176
+ '\xb9\x07\x40\xdf\x12\x75\xc9\x09\x82\x9c\x30\x80\x5d\x8f\x0d\x09' +
177
+ '\xa1\x64\xec\x91\xd8\x8a\x50\x1f\x40\x5d\xf7\x08\x2a\xf8\x60\x62' +
178
+ '\xa0\x4a\x8b\xba\x4a\x6d\x00\x0a\x93\x32\x12\xe5\x07\x01\x65\xf5' +
179
+ '\xff\xe0\xae\xa7\x81\xd1\xba\x25\x62\x61\xb2\x85\xad\x7e\x9d\x3f' +
180
+ '\x49\x89\x26\xe5\xd5\xac\x9f\x0e\xd7\x6e\x47\x94\x16\x84\xc8\xff' +
181
+ '\x44\xea\x04\x40\xe0\x33\x11\xa3\x5b\x1e\x82\xff\x7a\x69\xe9\x2f' +
182
+ '\xfb\xea\x9a\xc6\x7b\xdb\xb1\xff\x97\x76\x56\xf3\x52\xc2\x3f\x0f' +
183
+ '\xb6\xac\x77\xc4\xbf\x59\x5e\x80\x74\xbb\xf2\xde\x57\x62\x4c\x1a' +
184
+ '\xff\x95\x6d\xc7\x04\xa2\x3b\xc4\x1b\x72\xc7\x6c\x82\x60\xd1\x0d'
185
+
186
+ const codeBody =
187
+ '\x82\x8b\x7f\x68\x90\xe0\x44\x09\x19\x3b\x8e\x5f\xc2\x82\x38\x23' +
188
+ '\x6d\xdb\x62\x49\x52\x6e\x21\xdf\x51\x6c\x76\x37\x86\x50\x7d\x48' +
189
+ '\x1f\x65\xe7\x52\x6a\x88\xaa\xc1\x32\x2f\xf7\x54\x4c\xaa\x6d\x7e' +
190
+ '\x6d\xa9\x8c\x0d\x3f\xff\x6c\x09\xb3\xa5\xaf\xdf\x98\x02\xb4\xbe' +
191
+ '\x6d\x69\x0d\x42\x73\xe4\x34\x50\x07\x30\x79\x41\x2f\x08\x3f\x42' +
192
+ '\x73\xa7\x68\xfa\xee\x88\x0e\x6e\xa4\x70\x74\x22\x16\xae\x3c\x81' +
193
+ '\x14\xa1\xda\x7f\xd3\x7c\x48\x7d\x3f\x46\xfb\x6d\x92\x25\x17\x36' +
194
+ '\x26\xdb\xdf\x5a\x87\x91\x6f\xd6\xcd\xd4\xad\x4a\x29\xdd\x7d\x59' +
195
+ '\xbd\x15\x34\x53\xb1\xd8\x50\x11\x83\x79\x66\x21\x9e\x87\x5b\x24' +
196
+ '\x2f\x4f\xd7\x73\x34\xa2\xf7\x09\xd5\xd9\x42\x9d\xf8\x15\xdf\x0e' +
197
+ '\x10\xcc\x05\x04\x35\x81\xb2\xd5\x7a\xd2\xa0\xa5\x7b\xb8\x75\xd2' +
198
+ '\x35\x0b\x39\x8f\x1b\x44\x0e\xce\x66\x87\x1b\x64\xac\xe1\xca\x67' +
199
+ '\xb4\xce\x33\xdb\x89\xfe\xd8\x8e\xcd\x58\x92\x41\x50\x40\xcb\x08' +
200
+ '\xe1\x15\xee\xf4\x64\xfe\x1c\xee\x25\xe7\x21\xe6\x6c\xc6\xa6\x2e' +
201
+ '\x52\x23\xa7\x20\xd2\xd7\x28\x07\x23\x14\x24\x3d\x45\xa5\xc7\x90' +
202
+ '\xdb\x77\xdd\xea\x38\x59\x89\x32\xbc\x00\x3a\x6d\x61\x4e\xdb\x29'
203
+
204
+ const crcTable = [
205
+ 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
206
+ 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
207
+ 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
208
+ 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
209
+ 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
210
+ 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
211
+ 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
212
+ 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
213
+ 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
214
+ 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
215
+ 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
216
+ 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
217
+ 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
218
+ 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
219
+ 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
220
+ 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
221
+ 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
222
+ 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
223
+ 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
224
+ 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
225
+ 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
226
+ 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
227
+ 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
228
+ 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
229
+ 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
230
+ 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
231
+ 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
232
+ 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
233
+ 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
234
+ 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
235
+ 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
236
+ 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
237
+ ]