@omindu/yaksha 1.0.0 → 1.0.2
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/package.json +1 -1
- package/src/client/index.js +6 -6
- package/src/core/protocol.js +189 -116
- package/src/core/protocol.js.backup +268 -0
- package/src/server/index.js +7 -7
package/package.json
CHANGED
package/src/client/index.js
CHANGED
|
@@ -251,10 +251,10 @@ class YakshaClient extends EventEmitter {
|
|
|
251
251
|
|
|
252
252
|
const handler = (data) => {
|
|
253
253
|
try {
|
|
254
|
-
const
|
|
254
|
+
const packet = this.protocol.deserialize(data);
|
|
255
255
|
|
|
256
|
-
if (
|
|
257
|
-
const response = JSON.parse(payload.toString('utf8'));
|
|
256
|
+
if (packet.type === Protocol.PACKET_TYPES.HANDSHAKE) {
|
|
257
|
+
const response = JSON.parse(packet.payload.toString('utf8'));
|
|
258
258
|
|
|
259
259
|
// Store session ID
|
|
260
260
|
this.sessionId = response.sessionId;
|
|
@@ -296,15 +296,15 @@ class YakshaClient extends EventEmitter {
|
|
|
296
296
|
this.emit('_rawData', data);
|
|
297
297
|
|
|
298
298
|
try {
|
|
299
|
-
const
|
|
299
|
+
const packet = this.protocol.deserialize(data);
|
|
300
300
|
|
|
301
301
|
this.stats.packetsReceived++;
|
|
302
302
|
this.stats.bytesReceived += data.length;
|
|
303
303
|
|
|
304
304
|
// Handle different packet types
|
|
305
|
-
switch (
|
|
305
|
+
switch (packet.type) {
|
|
306
306
|
case Protocol.PACKET_TYPES.DATA:
|
|
307
|
-
this._handleDataPacket(payload);
|
|
307
|
+
this._handleDataPacket(packet.payload);
|
|
308
308
|
break;
|
|
309
309
|
|
|
310
310
|
case Protocol.PACKET_TYPES.KEEPALIVE:
|
package/src/core/protocol.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Yaksha Protocol
|
|
5
|
-
*
|
|
4
|
+
* Yaksha Protocol Implementation (v1.1.0 - Cross-Platform Fixed)
|
|
5
|
+
* Fixed for Linux server <-> Windows client compatibility
|
|
6
|
+
* - Explicit unsigned integer operations
|
|
7
|
+
* - Platform-independent CRC32 checksum
|
|
8
|
+
* - Robust buffer handling with proper byte ordering
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
const crypto = require('crypto');
|
|
9
|
-
|
|
12
|
+
|
|
13
|
+
// Protocol constants
|
|
14
|
+
const PROTOCOL_VERSION = 0x01;
|
|
15
|
+
const HEADER_SIZE = 16; // bytes
|
|
10
16
|
|
|
11
17
|
// Packet types
|
|
12
18
|
const PACKET_TYPES = {
|
|
@@ -17,12 +23,6 @@ const PACKET_TYPES = {
|
|
|
17
23
|
KEEPALIVE: 0x05
|
|
18
24
|
};
|
|
19
25
|
|
|
20
|
-
// Protocol version
|
|
21
|
-
const PROTOCOL_VERSION = 0x01;
|
|
22
|
-
|
|
23
|
-
// Header size (fixed 16 bytes)
|
|
24
|
-
const HEADER_SIZE = 16;
|
|
25
|
-
|
|
26
26
|
class Protocol {
|
|
27
27
|
constructor(options = {}) {
|
|
28
28
|
this.version = PROTOCOL_VERSION;
|
|
@@ -31,56 +31,59 @@ class Protocol {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* Create
|
|
35
|
-
*
|
|
36
|
-
* - Version (1 byte)
|
|
37
|
-
* - Type (1 byte)
|
|
38
|
-
* - Length (2 bytes) - payload length
|
|
39
|
-
* - Session ID (4 bytes)
|
|
40
|
-
* - Sequence (4 bytes)
|
|
41
|
-
* - Checksum (4 bytes)
|
|
34
|
+
* Create protocol header with explicit byte ordering
|
|
35
|
+
* CRITICAL: Uses big-endian for all multi-byte values for cross-platform consistency
|
|
42
36
|
*/
|
|
43
|
-
createHeader(type,
|
|
44
|
-
const header =
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
header.
|
|
54
|
-
|
|
55
|
-
// Session ID (
|
|
56
|
-
header.writeUInt32BE(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
header.writeUInt32BE(sequence, 8);
|
|
60
|
-
|
|
61
|
-
// Checksum placeholder (will be filled later)
|
|
62
|
-
header.writeUInt32BE(0, 12);
|
|
63
|
-
|
|
37
|
+
createHeader(type, length, sessionId, sequence) {
|
|
38
|
+
const header = Buffer.alloc(HEADER_SIZE); // Zero-filled for consistency
|
|
39
|
+
|
|
40
|
+
// Force unsigned integers to prevent sign issues across platforms
|
|
41
|
+
const safeType = (type & 0xFF) >>> 0;
|
|
42
|
+
const safeLength = (length & 0xFFFF) >>> 0;
|
|
43
|
+
const safeSessionId = (sessionId >>> 0);
|
|
44
|
+
const safeSequence = (sequence >>> 0);
|
|
45
|
+
|
|
46
|
+
header.writeUInt8(this.version, 0); // Version (1 byte)
|
|
47
|
+
header.writeUInt8(safeType, 1); // Type (1 byte)
|
|
48
|
+
header.writeUInt16BE(safeLength, 2); // Length (2 bytes, big-endian)
|
|
49
|
+
header.writeUInt32BE(safeSessionId, 4); // Session ID (4 bytes, big-endian)
|
|
50
|
+
header.writeUInt32BE(safeSequence, 8); // Sequence (4 bytes, big-endian)
|
|
51
|
+
header.writeUInt32BE(0, 12); // Checksum placeholder (4 bytes)
|
|
52
|
+
|
|
64
53
|
return header;
|
|
65
54
|
}
|
|
66
55
|
|
|
67
56
|
/**
|
|
68
|
-
* Calculate CRC32 checksum
|
|
57
|
+
* Calculate CRC32 checksum with explicit unsigned operations
|
|
58
|
+
* CRITICAL FIX: Ensures same result on Linux and Windows
|
|
69
59
|
*/
|
|
70
60
|
calculateChecksum(data) {
|
|
71
|
-
//
|
|
72
|
-
|
|
61
|
+
// Ensure we're working with a Buffer
|
|
62
|
+
if (!Buffer.isBuffer(data)) {
|
|
63
|
+
data = Buffer.from(data);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let crc = 0xFFFFFFFF >>> 0; // Force unsigned
|
|
67
|
+
|
|
73
68
|
for (let i = 0; i < data.length; i++) {
|
|
74
|
-
|
|
69
|
+
const byte = data[i] & 0xFF; // Ensure byte is 0-255
|
|
70
|
+
crc = (crc ^ byte) >>> 0;
|
|
71
|
+
|
|
75
72
|
for (let j = 0; j < 8; j++) {
|
|
76
|
-
|
|
73
|
+
// Explicit unsigned right shift and bitwise operations
|
|
74
|
+
if ((crc & 1) !== 0) {
|
|
75
|
+
crc = ((crc >>> 1) ^ 0xEDB88320) >>> 0;
|
|
76
|
+
} else {
|
|
77
|
+
crc = (crc >>> 1) >>> 0;
|
|
78
|
+
}
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
|
-
|
|
81
|
+
|
|
82
|
+
return ((crc ^ 0xFFFFFFFF) >>> 0);
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
/**
|
|
83
|
-
* Add random padding for
|
|
86
|
+
* Add random padding for obfuscation
|
|
84
87
|
*/
|
|
85
88
|
addPadding(data, maxPaddingSize = 255) {
|
|
86
89
|
if (!this.enablePadding) {
|
|
@@ -102,27 +105,48 @@ class Protocol {
|
|
|
102
105
|
* Remove padding from data
|
|
103
106
|
*/
|
|
104
107
|
removePadding(data, paddingSize) {
|
|
105
|
-
if (paddingSize === 0) {
|
|
108
|
+
if (paddingSize === 0 || !paddingSize) {
|
|
106
109
|
return data;
|
|
107
110
|
}
|
|
108
111
|
return data.slice(0, -paddingSize);
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
/**
|
|
112
|
-
* Serialize packet
|
|
115
|
+
* Serialize packet (Linux/Windows compatible)
|
|
116
|
+
* CRITICAL FIX: Proper buffer construction and checksum calculation
|
|
113
117
|
*/
|
|
114
118
|
serialize(type, payload, sessionId, sequence) {
|
|
119
|
+
// Ensure payload is a proper Buffer
|
|
120
|
+
if (!Buffer.isBuffer(payload)) {
|
|
121
|
+
if (typeof payload === 'string') {
|
|
122
|
+
payload = Buffer.from(payload, 'utf8');
|
|
123
|
+
} else {
|
|
124
|
+
payload = Buffer.from(payload);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
115
128
|
// Add padding to payload
|
|
116
129
|
const { data: paddedPayload, paddingSize } = this.addPadding(payload);
|
|
117
130
|
|
|
118
|
-
//
|
|
119
|
-
const
|
|
131
|
+
// Force safe unsigned integers
|
|
132
|
+
const safeSessionId = (sessionId >>> 0);
|
|
133
|
+
const safeSequence = (sequence >>> 0);
|
|
134
|
+
|
|
135
|
+
// Create header with zero checksum
|
|
136
|
+
const header = this.createHeader(type, paddedPayload.length, safeSessionId, safeSequence);
|
|
120
137
|
|
|
121
|
-
//
|
|
122
|
-
const
|
|
138
|
+
// Create complete packet buffer (avoid Buffer.concat for consistency)
|
|
139
|
+
const totalSize = header.length + paddedPayload.length;
|
|
140
|
+
const packet = Buffer.allocUnsafe(totalSize);
|
|
141
|
+
|
|
142
|
+
// Copy header and payload explicitly
|
|
143
|
+
header.copy(packet, 0, 0, header.length);
|
|
144
|
+
paddedPayload.copy(packet, header.length, 0, paddedPayload.length);
|
|
123
145
|
|
|
124
|
-
// Calculate
|
|
125
|
-
const checksum = this.calculateChecksum(packet);
|
|
146
|
+
// Calculate checksum over entire packet (with checksum field zeroed)
|
|
147
|
+
const checksum = this.calculateChecksum(packet) >>> 0;
|
|
148
|
+
|
|
149
|
+
// Write checksum to header
|
|
126
150
|
packet.writeUInt32BE(checksum, 12);
|
|
127
151
|
|
|
128
152
|
return { packet, paddingSize };
|
|
@@ -132,20 +156,25 @@ class Protocol {
|
|
|
132
156
|
* Parse packet header
|
|
133
157
|
*/
|
|
134
158
|
parseHeader(buffer) {
|
|
159
|
+
if (!Buffer.isBuffer(buffer)) {
|
|
160
|
+
throw new Error('Invalid buffer: not a Buffer object');
|
|
161
|
+
}
|
|
162
|
+
|
|
135
163
|
if (buffer.length < HEADER_SIZE) {
|
|
136
|
-
throw new Error(
|
|
164
|
+
throw new Error(`Buffer too small for header: ${buffer.length} bytes (need ${HEADER_SIZE})`);
|
|
137
165
|
}
|
|
138
166
|
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
const
|
|
167
|
+
// Read with explicit unsigned operations
|
|
168
|
+
const version = buffer.readUInt8(0) & 0xFF;
|
|
169
|
+
const type = buffer.readUInt8(1) & 0xFF;
|
|
170
|
+
const length = buffer.readUInt16BE(2) >>> 0;
|
|
171
|
+
const sessionId = buffer.readUInt32BE(4) >>> 0;
|
|
172
|
+
const sequence = buffer.readUInt32BE(8) >>> 0;
|
|
173
|
+
const checksum = buffer.readUInt32BE(12) >>> 0;
|
|
145
174
|
|
|
146
175
|
// Validate version
|
|
147
176
|
if (version !== this.version) {
|
|
148
|
-
throw new Error(`Unsupported protocol version: ${version}`);
|
|
177
|
+
throw new Error(`Unsupported protocol version: ${version} (expected ${this.version})`);
|
|
149
178
|
}
|
|
150
179
|
|
|
151
180
|
// Validate type
|
|
@@ -155,7 +184,7 @@ class Protocol {
|
|
|
155
184
|
|
|
156
185
|
// Validate length
|
|
157
186
|
if (length > this.maxPayloadSize) {
|
|
158
|
-
throw new Error(`Payload too large: ${length}`);
|
|
187
|
+
throw new Error(`Payload too large: ${length} (max ${this.maxPayloadSize})`);
|
|
159
188
|
}
|
|
160
189
|
|
|
161
190
|
return {
|
|
@@ -169,99 +198,143 @@ class Protocol {
|
|
|
169
198
|
}
|
|
170
199
|
|
|
171
200
|
/**
|
|
172
|
-
* Deserialize packet
|
|
201
|
+
* Deserialize packet (Linux/Windows compatible)
|
|
202
|
+
* CRITICAL FIX: Robust validation and error reporting
|
|
173
203
|
*/
|
|
174
204
|
deserialize(buffer, paddingSize = 0) {
|
|
175
|
-
|
|
176
|
-
|
|
205
|
+
if (!Buffer.isBuffer(buffer)) {
|
|
206
|
+
throw new Error('Invalid buffer: not a Buffer object');
|
|
207
|
+
}
|
|
177
208
|
|
|
178
|
-
//
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
209
|
+
// Parse header first
|
|
210
|
+
const header = this.parseHeader(buffer);
|
|
211
|
+
|
|
212
|
+
const totalExpectedSize = HEADER_SIZE + header.length;
|
|
213
|
+
if (buffer.length < totalExpectedSize) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Incomplete packet: got ${buffer.length} bytes, expected ${totalExpectedSize} ` +
|
|
216
|
+
`(header=${HEADER_SIZE}, payload=${header.length})`
|
|
217
|
+
);
|
|
182
218
|
}
|
|
183
219
|
|
|
184
|
-
//
|
|
185
|
-
const
|
|
220
|
+
// Extract payload (create new buffer to avoid reference issues)
|
|
221
|
+
const payloadStart = HEADER_SIZE;
|
|
222
|
+
const payloadEnd = payloadStart + header.length;
|
|
223
|
+
const payload = Buffer.allocUnsafe(header.length);
|
|
224
|
+
buffer.copy(payload, 0, payloadStart, payloadEnd);
|
|
225
|
+
|
|
226
|
+
// Verify checksum - recalculate with checksum field zeroed
|
|
227
|
+
const verifyBuffer = Buffer.allocUnsafe(buffer.length);
|
|
228
|
+
buffer.copy(verifyBuffer, 0, 0, buffer.length);
|
|
229
|
+
verifyBuffer.writeUInt32BE(0, 12); // Zero out checksum field
|
|
186
230
|
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
packetCopy.writeUInt32BE(0, 12);
|
|
231
|
+
const calculatedChecksum = this.calculateChecksum(verifyBuffer) >>> 0;
|
|
232
|
+
const receivedChecksum = header.checksum >>> 0;
|
|
190
233
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
234
|
+
if (receivedChecksum !== calculatedChecksum) {
|
|
235
|
+
// Detailed error for debugging cross-platform issues
|
|
236
|
+
const debugInfo = {
|
|
237
|
+
received: '0x' + receivedChecksum.toString(16).padStart(8, '0').toUpperCase(),
|
|
238
|
+
calculated: '0x' + calculatedChecksum.toString(16).padStart(8, '0').toUpperCase(),
|
|
239
|
+
packetSize: buffer.length,
|
|
240
|
+
headerSize: HEADER_SIZE,
|
|
241
|
+
payloadSize: header.length,
|
|
242
|
+
type: header.type,
|
|
243
|
+
sequence: header.sequence,
|
|
244
|
+
platform: process.platform,
|
|
245
|
+
nodeVersion: process.version
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
throw new Error(
|
|
249
|
+
`Checksum verification failed!\n` +
|
|
250
|
+
` Received: ${debugInfo.received}\n` +
|
|
251
|
+
` Calculated: ${debugInfo.calculated}\n` +
|
|
252
|
+
` Packet: ${debugInfo.packetSize} bytes (header=${debugInfo.headerSize}, payload=${debugInfo.payloadSize})\n` +
|
|
253
|
+
` Type: ${debugInfo.type}, Sequence: ${debugInfo.sequence}\n` +
|
|
254
|
+
` Platform: ${debugInfo.platform}, Node: ${debugInfo.nodeVersion}`
|
|
255
|
+
);
|
|
195
256
|
}
|
|
196
257
|
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
// Remove padding if present
|
|
201
|
-
payload = this.removePadding(payload, paddingSize);
|
|
258
|
+
// Remove padding from payload
|
|
259
|
+
const unpaddedPayload = this.removePadding(payload, paddingSize);
|
|
202
260
|
|
|
203
261
|
return {
|
|
204
|
-
header,
|
|
205
|
-
payload,
|
|
206
|
-
|
|
262
|
+
type: header.type,
|
|
263
|
+
payload: unpaddedPayload,
|
|
264
|
+
sessionId: header.sessionId,
|
|
265
|
+
sequence: header.sequence,
|
|
266
|
+
checksum: header.checksum
|
|
207
267
|
};
|
|
208
268
|
}
|
|
209
269
|
|
|
210
270
|
/**
|
|
211
|
-
*
|
|
271
|
+
* Get packet type name
|
|
212
272
|
*/
|
|
213
|
-
|
|
214
|
-
|
|
273
|
+
getPacketTypeName(type) {
|
|
274
|
+
for (const [name, value] of Object.entries(PACKET_TYPES)) {
|
|
275
|
+
if (value === type) {
|
|
276
|
+
return name;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return `UNKNOWN(${type})`;
|
|
215
280
|
}
|
|
216
281
|
|
|
217
282
|
/**
|
|
218
|
-
* Create
|
|
283
|
+
* Create handshake packet
|
|
284
|
+
* @param {number} sessionId - Session identifier
|
|
285
|
+
* @param {number} sequence - Packet sequence number
|
|
286
|
+
* @param {Buffer} payload - Handshake data (usually JSON)
|
|
287
|
+
* @returns {Object} { packet: Buffer, paddingSize: number }
|
|
219
288
|
*/
|
|
220
|
-
|
|
221
|
-
return this.serialize(PACKET_TYPES.
|
|
289
|
+
createHandshake(sessionId, sequence, payload) {
|
|
290
|
+
return this.serialize(PACKET_TYPES.HANDSHAKE, payload, sessionId, sequence);
|
|
222
291
|
}
|
|
223
292
|
|
|
224
293
|
/**
|
|
225
|
-
* Create
|
|
294
|
+
* Create data packet
|
|
295
|
+
* @param {number} sessionId - Session identifier
|
|
296
|
+
* @param {number} sequence - Packet sequence number
|
|
297
|
+
* @param {Buffer} payload - Data payload
|
|
298
|
+
* @returns {Object} { packet: Buffer, paddingSize: number }
|
|
226
299
|
*/
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
ackBuffer.writeUInt32BE(ackSequence, 0);
|
|
230
|
-
return this.serialize(PACKET_TYPES.ACK, ackBuffer, sessionId, sequence);
|
|
300
|
+
createData(sessionId, sequence, payload) {
|
|
301
|
+
return this.serialize(PACKET_TYPES.DATA, payload, sessionId, sequence);
|
|
231
302
|
}
|
|
232
303
|
|
|
233
304
|
/**
|
|
234
|
-
* Create
|
|
305
|
+
* Create ACK packet
|
|
306
|
+
* @param {number} sessionId - Session identifier
|
|
307
|
+
* @param {number} sequence - Packet sequence number
|
|
308
|
+
* @param {Buffer} payload - ACK data (optional)
|
|
309
|
+
* @returns {Object} { packet: Buffer, paddingSize: number }
|
|
235
310
|
*/
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return this.serialize(PACKET_TYPES.CLOSE, reasonBuffer, sessionId, sequence);
|
|
311
|
+
createAck(sessionId, sequence, payload = Buffer.alloc(0)) {
|
|
312
|
+
return this.serialize(PACKET_TYPES.ACK, payload, sessionId, sequence);
|
|
239
313
|
}
|
|
240
314
|
|
|
241
315
|
/**
|
|
242
|
-
* Create
|
|
316
|
+
* Create close packet
|
|
317
|
+
* @param {number} sessionId - Session identifier
|
|
318
|
+
* @param {number} sequence - Packet sequence number
|
|
319
|
+
* @param {Buffer} payload - Close reason (optional)
|
|
320
|
+
* @returns {Object} { packet: Buffer, paddingSize: number }
|
|
243
321
|
*/
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
timestamp.writeBigUInt64BE(BigInt(Date.now()), 0);
|
|
247
|
-
return this.serialize(PACKET_TYPES.KEEPALIVE, timestamp, sessionId, sequence);
|
|
322
|
+
createClose(sessionId, sequence, payload = Buffer.alloc(0)) {
|
|
323
|
+
return this.serialize(PACKET_TYPES.CLOSE, payload, sessionId, sequence);
|
|
248
324
|
}
|
|
249
325
|
|
|
250
326
|
/**
|
|
251
|
-
*
|
|
327
|
+
* Create keepalive packet
|
|
328
|
+
* @param {number} sessionId - Session identifier
|
|
329
|
+
* @param {number} sequence - Packet sequence number
|
|
330
|
+
* @returns {Object} { packet: Buffer, paddingSize: number }
|
|
252
331
|
*/
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
[PACKET_TYPES.HANDSHAKE]: 'HANDSHAKE',
|
|
256
|
-
[PACKET_TYPES.DATA]: 'DATA',
|
|
257
|
-
[PACKET_TYPES.ACK]: 'ACK',
|
|
258
|
-
[PACKET_TYPES.CLOSE]: 'CLOSE',
|
|
259
|
-
[PACKET_TYPES.KEEPALIVE]: 'KEEPALIVE'
|
|
260
|
-
};
|
|
261
|
-
return typeNames[type] || 'UNKNOWN';
|
|
332
|
+
createKeepalive(sessionId, sequence) {
|
|
333
|
+
return this.serialize(PACKET_TYPES.KEEPALIVE, Buffer.alloc(0), sessionId, sequence);
|
|
262
334
|
}
|
|
263
335
|
}
|
|
264
336
|
|
|
337
|
+
// Export
|
|
265
338
|
module.exports = Protocol;
|
|
266
339
|
module.exports.PACKET_TYPES = PACKET_TYPES;
|
|
267
340
|
module.exports.HEADER_SIZE = HEADER_SIZE;
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Yaksha Protocol Handler
|
|
5
|
+
* Custom protocol for packet parsing and handling
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
const { globalPool } = require('../utils/buffer-pool');
|
|
10
|
+
|
|
11
|
+
// Packet types
|
|
12
|
+
const PACKET_TYPES = {
|
|
13
|
+
HANDSHAKE: 0x01,
|
|
14
|
+
DATA: 0x02,
|
|
15
|
+
ACK: 0x03,
|
|
16
|
+
CLOSE: 0x04,
|
|
17
|
+
KEEPALIVE: 0x05
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Protocol version
|
|
21
|
+
const PROTOCOL_VERSION = 0x01;
|
|
22
|
+
|
|
23
|
+
// Header size (fixed 16 bytes)
|
|
24
|
+
const HEADER_SIZE = 16;
|
|
25
|
+
|
|
26
|
+
class Protocol {
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
this.version = PROTOCOL_VERSION;
|
|
29
|
+
this.maxPayloadSize = options.maxPayloadSize || 65535;
|
|
30
|
+
this.enablePadding = options.enablePadding !== false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create a packet header
|
|
35
|
+
* Header structure (16 bytes):
|
|
36
|
+
* - Version (1 byte)
|
|
37
|
+
* - Type (1 byte)
|
|
38
|
+
* - Length (2 bytes) - payload length
|
|
39
|
+
* - Session ID (4 bytes)
|
|
40
|
+
* - Sequence (4 bytes)
|
|
41
|
+
* - Checksum (4 bytes)
|
|
42
|
+
*/
|
|
43
|
+
createHeader(type, payloadLength, sessionId, sequence) {
|
|
44
|
+
const header = globalPool.acquire(HEADER_SIZE, true);
|
|
45
|
+
|
|
46
|
+
// Version
|
|
47
|
+
header.writeUInt8(this.version, 0);
|
|
48
|
+
|
|
49
|
+
// Type
|
|
50
|
+
header.writeUInt8(type, 1);
|
|
51
|
+
|
|
52
|
+
// Length (uint16, max 65535)
|
|
53
|
+
header.writeUInt16BE(payloadLength, 2);
|
|
54
|
+
|
|
55
|
+
// Session ID (uint32)
|
|
56
|
+
header.writeUInt32BE(sessionId, 4);
|
|
57
|
+
|
|
58
|
+
// Sequence (uint32)
|
|
59
|
+
header.writeUInt32BE(sequence, 8);
|
|
60
|
+
|
|
61
|
+
// Checksum placeholder (will be filled later)
|
|
62
|
+
header.writeUInt32BE(0, 12);
|
|
63
|
+
|
|
64
|
+
return header;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Calculate CRC32 checksum
|
|
69
|
+
*/
|
|
70
|
+
calculateChecksum(data) {
|
|
71
|
+
// Simple CRC32 implementation
|
|
72
|
+
let crc = 0xFFFFFFFF;
|
|
73
|
+
for (let i = 0; i < data.length; i++) {
|
|
74
|
+
crc ^= data[i];
|
|
75
|
+
for (let j = 0; j < 8; j++) {
|
|
76
|
+
crc = (crc >>> 1) ^ (0xEDB88320 & -(crc & 1));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return (crc ^ 0xFFFFFFFF) >>> 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Add random padding for traffic obfuscation
|
|
84
|
+
*/
|
|
85
|
+
addPadding(data, maxPaddingSize = 255) {
|
|
86
|
+
if (!this.enablePadding) {
|
|
87
|
+
return { data, paddingSize: 0 };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const paddingSize = crypto.randomInt(0, Math.min(maxPaddingSize + 1, 256));
|
|
91
|
+
if (paddingSize === 0) {
|
|
92
|
+
return { data, paddingSize: 0 };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const padding = crypto.randomBytes(paddingSize);
|
|
96
|
+
const paddedData = Buffer.concat([data, padding]);
|
|
97
|
+
|
|
98
|
+
return { data: paddedData, paddingSize };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Remove padding from data
|
|
103
|
+
*/
|
|
104
|
+
removePadding(data, paddingSize) {
|
|
105
|
+
if (paddingSize === 0) {
|
|
106
|
+
return data;
|
|
107
|
+
}
|
|
108
|
+
return data.slice(0, -paddingSize);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Serialize packet
|
|
113
|
+
*/
|
|
114
|
+
serialize(type, payload, sessionId, sequence) {
|
|
115
|
+
// Add padding to payload
|
|
116
|
+
const { data: paddedPayload, paddingSize } = this.addPadding(payload);
|
|
117
|
+
|
|
118
|
+
// Create header
|
|
119
|
+
const header = this.createHeader(type, paddedPayload.length, sessionId, sequence);
|
|
120
|
+
|
|
121
|
+
// Combine header and payload
|
|
122
|
+
const packet = Buffer.concat([header, paddedPayload]);
|
|
123
|
+
|
|
124
|
+
// Calculate and update checksum (checksum covers header + payload)
|
|
125
|
+
const checksum = this.calculateChecksum(packet);
|
|
126
|
+
packet.writeUInt32BE(checksum, 12);
|
|
127
|
+
|
|
128
|
+
return { packet, paddingSize };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Parse packet header
|
|
133
|
+
*/
|
|
134
|
+
parseHeader(buffer) {
|
|
135
|
+
if (buffer.length < HEADER_SIZE) {
|
|
136
|
+
throw new Error('Buffer too small for header');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const version = buffer.readUInt8(0);
|
|
140
|
+
const type = buffer.readUInt8(1);
|
|
141
|
+
const length = buffer.readUInt16BE(2);
|
|
142
|
+
const sessionId = buffer.readUInt32BE(4);
|
|
143
|
+
const sequence = buffer.readUInt32BE(8);
|
|
144
|
+
const checksum = buffer.readUInt32BE(12);
|
|
145
|
+
|
|
146
|
+
// Validate version
|
|
147
|
+
if (version !== this.version) {
|
|
148
|
+
throw new Error(`Unsupported protocol version: ${version}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Validate type
|
|
152
|
+
if (!Object.values(PACKET_TYPES).includes(type)) {
|
|
153
|
+
throw new Error(`Invalid packet type: ${type}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Validate length
|
|
157
|
+
if (length > this.maxPayloadSize) {
|
|
158
|
+
throw new Error(`Payload too large: ${length}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
version,
|
|
163
|
+
type,
|
|
164
|
+
length,
|
|
165
|
+
sessionId,
|
|
166
|
+
sequence,
|
|
167
|
+
checksum
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Deserialize packet
|
|
173
|
+
*/
|
|
174
|
+
deserialize(buffer, paddingSize = 0) {
|
|
175
|
+
// Parse header
|
|
176
|
+
const header = this.parseHeader(buffer);
|
|
177
|
+
|
|
178
|
+
// Check if we have complete packet
|
|
179
|
+
const expectedSize = HEADER_SIZE + header.length;
|
|
180
|
+
if (buffer.length < expectedSize) {
|
|
181
|
+
throw new Error(`Incomplete packet: expected ${expectedSize}, got ${buffer.length}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Verify checksum
|
|
185
|
+
const storedChecksum = header.checksum;
|
|
186
|
+
|
|
187
|
+
// Create a copy of packet and zero out checksum field for verification
|
|
188
|
+
const packetCopy = Buffer.from(buffer.slice(0, expectedSize));
|
|
189
|
+
packetCopy.writeUInt32BE(0, 12);
|
|
190
|
+
|
|
191
|
+
const calculatedChecksum = this.calculateChecksum(packetCopy);
|
|
192
|
+
|
|
193
|
+
if (storedChecksum !== calculatedChecksum) {
|
|
194
|
+
throw new Error('Checksum verification failed');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Extract payload
|
|
198
|
+
let payload = buffer.slice(HEADER_SIZE, expectedSize);
|
|
199
|
+
|
|
200
|
+
// Remove padding if present
|
|
201
|
+
payload = this.removePadding(payload, paddingSize);
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
header,
|
|
205
|
+
payload,
|
|
206
|
+
totalSize: expectedSize
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Create HANDSHAKE packet
|
|
212
|
+
*/
|
|
213
|
+
createHandshake(sessionId, sequence, data) {
|
|
214
|
+
return this.serialize(PACKET_TYPES.HANDSHAKE, data, sessionId, sequence);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Create DATA packet
|
|
219
|
+
*/
|
|
220
|
+
createData(sessionId, sequence, data) {
|
|
221
|
+
return this.serialize(PACKET_TYPES.DATA, data, sessionId, sequence);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Create ACK packet
|
|
226
|
+
*/
|
|
227
|
+
createAck(sessionId, sequence, ackSequence) {
|
|
228
|
+
const ackBuffer = Buffer.allocUnsafe(4);
|
|
229
|
+
ackBuffer.writeUInt32BE(ackSequence, 0);
|
|
230
|
+
return this.serialize(PACKET_TYPES.ACK, ackBuffer, sessionId, sequence);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Create CLOSE packet
|
|
235
|
+
*/
|
|
236
|
+
createClose(sessionId, sequence, reason = '') {
|
|
237
|
+
const reasonBuffer = Buffer.from(reason, 'utf8');
|
|
238
|
+
return this.serialize(PACKET_TYPES.CLOSE, reasonBuffer, sessionId, sequence);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Create KEEPALIVE packet
|
|
243
|
+
*/
|
|
244
|
+
createKeepalive(sessionId, sequence) {
|
|
245
|
+
const timestamp = Buffer.allocUnsafe(8);
|
|
246
|
+
timestamp.writeBigUInt64BE(BigInt(Date.now()), 0);
|
|
247
|
+
return this.serialize(PACKET_TYPES.KEEPALIVE, timestamp, sessionId, sequence);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get packet type name
|
|
252
|
+
*/
|
|
253
|
+
getTypeName(type) {
|
|
254
|
+
const typeNames = {
|
|
255
|
+
[PACKET_TYPES.HANDSHAKE]: 'HANDSHAKE',
|
|
256
|
+
[PACKET_TYPES.DATA]: 'DATA',
|
|
257
|
+
[PACKET_TYPES.ACK]: 'ACK',
|
|
258
|
+
[PACKET_TYPES.CLOSE]: 'CLOSE',
|
|
259
|
+
[PACKET_TYPES.KEEPALIVE]: 'KEEPALIVE'
|
|
260
|
+
};
|
|
261
|
+
return typeNames[type] || 'UNKNOWN';
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module.exports = Protocol;
|
|
266
|
+
module.exports.PACKET_TYPES = PACKET_TYPES;
|
|
267
|
+
module.exports.HEADER_SIZE = HEADER_SIZE;
|
|
268
|
+
module.exports.PROTOCOL_VERSION = PROTOCOL_VERSION;
|
package/src/server/index.js
CHANGED
|
@@ -242,20 +242,20 @@ class YakshaServer extends EventEmitter {
|
|
|
242
242
|
_handleTCPData(buffer, client) {
|
|
243
243
|
try {
|
|
244
244
|
// Parse packet
|
|
245
|
-
const
|
|
245
|
+
const packet = this.protocol.deserialize(buffer);
|
|
246
246
|
|
|
247
247
|
this.stats.packetsReceived++;
|
|
248
|
-
this.stats.bytesReceived +=
|
|
248
|
+
this.stats.bytesReceived += buffer.length;
|
|
249
249
|
client.lastActivity = Date.now();
|
|
250
250
|
|
|
251
251
|
// Handle different packet types
|
|
252
|
-
switch (
|
|
252
|
+
switch (packet.type) {
|
|
253
253
|
case Protocol.PACKET_TYPES.HANDSHAKE:
|
|
254
|
-
this._handleHandshake(payload, client);
|
|
254
|
+
this._handleHandshake(packet.payload, client);
|
|
255
255
|
break;
|
|
256
256
|
|
|
257
257
|
case Protocol.PACKET_TYPES.DATA:
|
|
258
|
-
this._handleData(payload, client);
|
|
258
|
+
this._handleData(packet.payload, client);
|
|
259
259
|
break;
|
|
260
260
|
|
|
261
261
|
case Protocol.PACKET_TYPES.KEEPALIVE:
|
|
@@ -292,14 +292,14 @@ class YakshaServer extends EventEmitter {
|
|
|
292
292
|
}
|
|
293
293
|
|
|
294
294
|
// Process packet
|
|
295
|
-
const
|
|
295
|
+
const packet = this.protocol.deserialize(data);
|
|
296
296
|
|
|
297
297
|
this.stats.packetsReceived++;
|
|
298
298
|
this.stats.bytesReceived += data.length;
|
|
299
299
|
client.lastActivity = Date.now();
|
|
300
300
|
|
|
301
301
|
// Handle data packet
|
|
302
|
-
this._handleData(payload, client, 'udp');
|
|
302
|
+
this._handleData(packet.payload, client, 'udp');
|
|
303
303
|
|
|
304
304
|
} catch (error) {
|
|
305
305
|
this.logger.error('Error processing UDP data:', error.message);
|