packet-events-js 1.0.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 (40) hide show
  1. package/README.md +398 -0
  2. package/package.json +31 -0
  3. package/src/auth/AuthHandler.js +138 -0
  4. package/src/auth/MojangAPI.js +186 -0
  5. package/src/client/MinecraftClient.js +336 -0
  6. package/src/crypto/Encryption.js +125 -0
  7. package/src/events/EventEmitter.js +267 -0
  8. package/src/events/PacketEvent.js +78 -0
  9. package/src/index.js +18 -0
  10. package/src/manager/PacketManager.js +258 -0
  11. package/src/protocol/ConnectionState.js +37 -0
  12. package/src/protocol/PacketDirection.js +8 -0
  13. package/src/protocol/ProtocolVersion.js +141 -0
  14. package/src/protocol/packets/Packet.js +119 -0
  15. package/src/protocol/packets/PacketRegistry.js +145 -0
  16. package/src/protocol/packets/handshake/HandshakePacket.js +44 -0
  17. package/src/protocol/packets/index.js +265 -0
  18. package/src/protocol/packets/login/DisconnectPacket.js +71 -0
  19. package/src/protocol/packets/login/EncryptionRequestPacket.js +47 -0
  20. package/src/protocol/packets/login/EncryptionResponsePacket.js +34 -0
  21. package/src/protocol/packets/login/LoginStartPacket.js +35 -0
  22. package/src/protocol/packets/login/LoginSuccessPacket.js +61 -0
  23. package/src/protocol/packets/login/SetCompressionPacket.js +29 -0
  24. package/src/protocol/packets/play/ChatPacket.js +238 -0
  25. package/src/protocol/packets/play/ChunkPacket.js +122 -0
  26. package/src/protocol/packets/play/EntityPacket.js +302 -0
  27. package/src/protocol/packets/play/KeepAlivePacket.js +55 -0
  28. package/src/protocol/packets/play/PlayerPositionPacket.js +266 -0
  29. package/src/protocol/packets/status/PingPacket.js +29 -0
  30. package/src/protocol/packets/status/PongPacket.js +29 -0
  31. package/src/protocol/packets/status/StatusRequestPacket.js +20 -0
  32. package/src/protocol/packets/status/StatusResponsePacket.js +58 -0
  33. package/src/protocol/types/NBT.js +594 -0
  34. package/src/protocol/types/Position.js +125 -0
  35. package/src/protocol/types/TextComponent.js +355 -0
  36. package/src/protocol/types/UUID.js +105 -0
  37. package/src/protocol/types/VarInt.js +144 -0
  38. package/src/protocol/types/index.js +5 -0
  39. package/src/utils/Logger.js +207 -0
  40. package/src/utils/PacketBuffer.js +389 -0
@@ -0,0 +1,186 @@
1
+ import https from 'https';
2
+
3
+ export class MojangAPI {
4
+ static SESSION_SERVER = 'sessionserver.mojang.com';
5
+ static API_SERVER = 'api.mojang.com';
6
+
7
+ static async hasJoinedServer(username, serverHash, ip = null) {
8
+ return new Promise((resolve, reject) => {
9
+ let url = `/session/minecraft/hasJoined?username=${encodeURIComponent(username)}&serverId=${encodeURIComponent(serverHash)}`;
10
+
11
+ if (ip) {
12
+ url += `&ip=${encodeURIComponent(ip)}`;
13
+ }
14
+
15
+ const options = {
16
+ hostname: MojangAPI.SESSION_SERVER,
17
+ path: url,
18
+ method: 'GET',
19
+ headers: {
20
+ 'Accept': 'application/json'
21
+ }
22
+ };
23
+
24
+ const req = https.request(options, (res) => {
25
+ let data = '';
26
+
27
+ res.on('data', chunk => data += chunk);
28
+ res.on('end', () => {
29
+ if (res.statusCode === 200 && data) {
30
+ try {
31
+ const profile = JSON.parse(data);
32
+ resolve({
33
+ success: true,
34
+ uuid: profile.id,
35
+ name: profile.name,
36
+ properties: profile.properties || []
37
+ });
38
+ } catch (e) {
39
+ reject(new Error('Invalid JSON response'));
40
+ }
41
+ } else if (res.statusCode === 204) {
42
+ resolve({ success: false, reason: 'NOT_AUTHENTICATED' });
43
+ } else {
44
+ resolve({ success: false, reason: 'SERVER_ERROR', statusCode: res.statusCode });
45
+ }
46
+ });
47
+ });
48
+
49
+ req.on('error', reject);
50
+ req.setTimeout(10000, () => {
51
+ req.destroy();
52
+ reject(new Error('Request timeout'));
53
+ });
54
+ req.end();
55
+ });
56
+ }
57
+
58
+ static async joinServer(accessToken, selectedProfile, serverHash) {
59
+ return new Promise((resolve, reject) => {
60
+ const payload = JSON.stringify({
61
+ accessToken,
62
+ selectedProfile,
63
+ serverId: serverHash
64
+ });
65
+
66
+ const options = {
67
+ hostname: MojangAPI.SESSION_SERVER,
68
+ path: '/session/minecraft/join',
69
+ method: 'POST',
70
+ headers: {
71
+ 'Content-Type': 'application/json',
72
+ 'Content-Length': Buffer.byteLength(payload)
73
+ }
74
+ };
75
+
76
+ const req = https.request(options, (res) => {
77
+ let data = '';
78
+
79
+ res.on('data', chunk => data += chunk);
80
+ res.on('end', () => {
81
+ if (res.statusCode === 204) {
82
+ resolve({ success: true });
83
+ } else {
84
+ try {
85
+ const error = JSON.parse(data);
86
+ resolve({ success: false, error: error.error, message: error.errorMessage });
87
+ } catch (e) {
88
+ resolve({ success: false, reason: 'SERVER_ERROR' });
89
+ }
90
+ }
91
+ });
92
+ });
93
+
94
+ req.on('error', reject);
95
+ req.setTimeout(10000, () => {
96
+ req.destroy();
97
+ reject(new Error('Request timeout'));
98
+ });
99
+ req.write(payload);
100
+ req.end();
101
+ });
102
+ }
103
+
104
+ static async getProfile(uuid) {
105
+ return new Promise((resolve, reject) => {
106
+ const cleanUUID = uuid.replace(/-/g, '');
107
+
108
+ const options = {
109
+ hostname: MojangAPI.SESSION_SERVER,
110
+ path: `/session/minecraft/profile/${cleanUUID}`,
111
+ method: 'GET',
112
+ headers: {
113
+ 'Accept': 'application/json'
114
+ }
115
+ };
116
+
117
+ const req = https.request(options, (res) => {
118
+ let data = '';
119
+
120
+ res.on('data', chunk => data += chunk);
121
+ res.on('end', () => {
122
+ if (res.statusCode === 200) {
123
+ try {
124
+ resolve(JSON.parse(data));
125
+ } catch (e) {
126
+ reject(new Error('Invalid JSON'));
127
+ }
128
+ } else {
129
+ resolve(null);
130
+ }
131
+ });
132
+ });
133
+
134
+ req.on('error', reject);
135
+ req.end();
136
+ });
137
+ }
138
+
139
+ static async getUUID(username) {
140
+ return new Promise((resolve, reject) => {
141
+ const options = {
142
+ hostname: MojangAPI.API_SERVER,
143
+ path: `/users/profiles/minecraft/${encodeURIComponent(username)}`,
144
+ method: 'GET',
145
+ headers: {
146
+ 'Accept': 'application/json'
147
+ }
148
+ };
149
+
150
+ const req = https.request(options, (res) => {
151
+ let data = '';
152
+
153
+ res.on('data', chunk => data += chunk);
154
+ res.on('end', () => {
155
+ if (res.statusCode === 200) {
156
+ try {
157
+ const profile = JSON.parse(data);
158
+ resolve({
159
+ uuid: profile.id,
160
+ name: profile.name
161
+ });
162
+ } catch (e) {
163
+ reject(new Error('Invalid JSON'));
164
+ }
165
+ } else {
166
+ resolve(null);
167
+ }
168
+ });
169
+ });
170
+
171
+ req.on('error', reject);
172
+ req.end();
173
+ });
174
+ }
175
+
176
+ static decodeTextures(base64Properties) {
177
+ try {
178
+ const decoded = Buffer.from(base64Properties, 'base64').toString('utf8');
179
+ return JSON.parse(decoded);
180
+ } catch (e) {
181
+ return null;
182
+ }
183
+ }
184
+ }
185
+
186
+ export default MojangAPI;
@@ -0,0 +1,336 @@
1
+ import { createConnection, Socket } from 'net';
2
+ import { createCipheriv, createDecipheriv, createHash, randomBytes, publicEncrypt } from 'crypto';
3
+ import { EventEmitter } from '../events/EventEmitter.js';
4
+ import { PacketManager } from '../manager/PacketManager.js';
5
+ import { PacketRegistry, defaultRegistry } from '../protocol/packets/PacketRegistry.js';
6
+ import { ConnectionState } from '../protocol/ConnectionState.js';
7
+ import { PacketDirection } from '../protocol/PacketDirection.js';
8
+ import { ProtocolVersion } from '../protocol/ProtocolVersion.js';
9
+ import { Logger } from '../utils/Logger.js';
10
+ import { UUID } from '../protocol/types/UUID.js';
11
+
12
+ import {
13
+ HandshakePacket,
14
+ StatusRequestPacket,
15
+ StatusResponsePacket,
16
+ PingPacket,
17
+ PongPacket,
18
+ LoginStartPacket,
19
+ EncryptionRequestPacket,
20
+ EncryptionResponsePacket,
21
+ LoginSuccessPacket,
22
+ SetCompressionPacket,
23
+ DisconnectPacket
24
+ } from '../protocol/packets/index.js';
25
+
26
+ const logger = Logger.getLogger('MinecraftClient');
27
+
28
+ export const ClientState = Object.freeze({
29
+ DISCONNECTED: 'DISCONNECTED',
30
+ CONNECTING: 'CONNECTING',
31
+ CONNECTED: 'CONNECTED',
32
+ AUTHENTICATING: 'AUTHENTICATING',
33
+ READY: 'READY',
34
+ DISCONNECTING: 'DISCONNECTING'
35
+ });
36
+
37
+ export class MinecraftClient extends EventEmitter {
38
+
39
+ constructor(options) {
40
+ super();
41
+
42
+ this.host = options.host;
43
+ this.port = options.port || 25565;
44
+ this.version = options.version || ProtocolVersion.getLatest();
45
+ this.username = options.username || 'Player';
46
+ this.offline = options.offline !== false;
47
+
48
+ this.state = ClientState.DISCONNECTED;
49
+ this.connectionState = ConnectionState.HANDSHAKING;
50
+
51
+ this.socket = null;
52
+
53
+ this.packetManager = new PacketManager({
54
+ registry: options.registry || defaultRegistry
55
+ });
56
+
57
+ this.uuid = null;
58
+ this.playerName = null;
59
+
60
+ this._setupPacketHandlers();
61
+ }
62
+
63
+ _setupPacketHandlers() {
64
+
65
+ this.packetManager.on('packet', (event) => {
66
+ this.emit('packet', event);
67
+ });
68
+
69
+ this.packetManager.on('packetSend', (event) => {
70
+ this.emit('packetSend', event);
71
+ });
72
+
73
+ this.packetManager.on('unknownPacket', (data) => {
74
+ this.emit('unknownPacket', data);
75
+ });
76
+
77
+ this.packetManager.on('stateChange', (data) => {
78
+ this.connectionState = data.newState;
79
+ this.emit('stateChange', data);
80
+ });
81
+ }
82
+
83
+ connect() {
84
+ return new Promise((resolve, reject) => {
85
+ if (this.state !== ClientState.DISCONNECTED) {
86
+ reject(new Error('Client is already connected or connecting'));
87
+ return;
88
+ }
89
+
90
+ this.state = ClientState.CONNECTING;
91
+ logger.info(`Connecting to ${this.host}:${this.port}...`);
92
+
93
+ this.socket = createConnection({
94
+ host: this.host,
95
+ port: this.port
96
+ });
97
+
98
+ this.socket.on('connect', () => {
99
+ this.state = ClientState.CONNECTED;
100
+ logger.info('Connected!');
101
+ this.emit('connect');
102
+ resolve();
103
+ });
104
+
105
+ this.socket.on('data', async (data) => {
106
+ try {
107
+ this.packetManager.addReceivedData(data);
108
+ await this.packetManager.readPackets(
109
+ PacketDirection.CLIENTBOUND,
110
+ { connection: this }
111
+ );
112
+ } catch (error) {
113
+ logger.error('Error processing data:', error);
114
+ this.emit('error', error);
115
+ }
116
+ });
117
+
118
+ this.socket.on('close', (hadError) => {
119
+ const wasConnected = this.state !== ClientState.DISCONNECTED;
120
+ this.state = ClientState.DISCONNECTED;
121
+
122
+ if (wasConnected) {
123
+ logger.info('Disconnected from server');
124
+ this.emit('disconnect', { hadError });
125
+ }
126
+ });
127
+
128
+ this.socket.on('error', (error) => {
129
+ logger.error('Socket error:', error);
130
+
131
+ if (this.state === ClientState.CONNECTING) {
132
+ reject(error);
133
+ } else {
134
+ this.emit('error', error);
135
+ }
136
+ });
137
+
138
+ this.socket.on('timeout', () => {
139
+ logger.warn('Connection timeout');
140
+ this.emit('timeout');
141
+ });
142
+ });
143
+ }
144
+
145
+ disconnect(reason) {
146
+ return new Promise((resolve) => {
147
+ if (this.state === ClientState.DISCONNECTED) {
148
+ resolve();
149
+ return;
150
+ }
151
+
152
+ this.state = ClientState.DISCONNECTING;
153
+ logger.info(`Disconnecting${reason ? `: ${reason}` : ''}...`);
154
+
155
+ if (this.socket) {
156
+ this.socket.end();
157
+ this.socket.destroy();
158
+ this.socket = null;
159
+ }
160
+
161
+ this.state = ClientState.DISCONNECTED;
162
+ this.packetManager.clearBuffer();
163
+ resolve();
164
+ });
165
+ }
166
+
167
+ async sendPacket(packet) {
168
+ if (!this.socket || this.state === ClientState.DISCONNECTED) {
169
+ throw new Error('Not connected to server');
170
+ }
171
+
172
+ return this.packetManager.sendPacket(
173
+ packet,
174
+ (data) => {
175
+ return new Promise((resolve, reject) => {
176
+ this.socket.write(data, (err) => {
177
+ if (err) reject(err);
178
+ else resolve();
179
+ });
180
+ });
181
+ },
182
+ { connection: this }
183
+ );
184
+ }
185
+
186
+ async ping() {
187
+ await this.connect();
188
+
189
+ try {
190
+
191
+ const handshake = new HandshakePacket();
192
+ handshake.protocolVersion = this.version.protocolId;
193
+ handshake.serverAddress = this.host;
194
+ handshake.serverPort = this.port;
195
+ handshake.nextState = ConnectionState.STATUS.id;
196
+
197
+ await this.sendPacket(handshake);
198
+ this.packetManager.setState(ConnectionState.STATUS);
199
+
200
+ await this.sendPacket(new StatusRequestPacket());
201
+
202
+ const response = await this.waitForPacket(StatusResponsePacket, 10000);
203
+
204
+ const pingPacket = new PingPacket();
205
+ pingPacket.payload = BigInt(Date.now());
206
+ await this.sendPacket(pingPacket);
207
+
208
+ const pong = await this.waitForPacket(PongPacket, 5000);
209
+ const latency = Date.now() - Number(pingPacket.payload);
210
+
211
+ await this.disconnect();
212
+
213
+ return {
214
+ ...response.status,
215
+ latency
216
+ };
217
+ } catch (error) {
218
+ await this.disconnect();
219
+ throw error;
220
+ }
221
+ }
222
+
223
+ async login() {
224
+ await this.connect();
225
+
226
+ try {
227
+
228
+ const handshake = new HandshakePacket();
229
+ handshake.protocolVersion = this.version.protocolId;
230
+ handshake.serverAddress = this.host;
231
+ handshake.serverPort = this.port;
232
+ handshake.nextState = ConnectionState.LOGIN.id;
233
+
234
+ await this.sendPacket(handshake);
235
+ this.packetManager.setState(ConnectionState.LOGIN);
236
+
237
+ const loginStart = new LoginStartPacket();
238
+ loginStart.username = this.username;
239
+ loginStart.uuid = this.offline ?
240
+ UUID.offlinePlayerUUID(this.username) :
241
+ UUID.randomUUID();
242
+
243
+ await this.sendPacket(loginStart);
244
+ this.state = ClientState.AUTHENTICATING;
245
+
246
+ return new Promise((resolve, reject) => {
247
+ const timeout = setTimeout(() => {
248
+ reject(new Error('Login timeout'));
249
+ }, 30000);
250
+
251
+ this.packetManager.onPacket(SetCompressionPacket, (event) => {
252
+ const threshold = event.packet.threshold;
253
+ this.packetManager.setCompression(threshold);
254
+ logger.debug(`Compression set to ${threshold}`);
255
+ });
256
+
257
+ this.packetManager.onPacket(EncryptionRequestPacket, async (event) => {
258
+ if (this.offline) {
259
+ clearTimeout(timeout);
260
+ reject(new Error('Server requires online mode authentication'));
261
+ return;
262
+ }
263
+
264
+ logger.warn('Online mode encryption not fully implemented');
265
+ });
266
+
267
+ this.packetManager.onPacket(LoginSuccessPacket, (event) => {
268
+ clearTimeout(timeout);
269
+
270
+ this.uuid = event.packet.uuid;
271
+ this.playerName = event.packet.username;
272
+ this.state = ClientState.READY;
273
+
274
+ this.packetManager.setState(ConnectionState.PLAY);
275
+
276
+ this.emit('login', {
277
+ uuid: this.uuid,
278
+ username: this.playerName
279
+ });
280
+
281
+ resolve({
282
+ uuid: this.uuid,
283
+ username: this.playerName
284
+ });
285
+ });
286
+
287
+ this.packetManager.onPacket(DisconnectPacket, (event) => {
288
+ clearTimeout(timeout);
289
+ const reason = event.packet.reason?.toPlainText?.() ||
290
+ JSON.stringify(event.packet.reason);
291
+ reject(new Error(`Disconnected: ${reason}`));
292
+ });
293
+ });
294
+ } catch (error) {
295
+ await this.disconnect();
296
+ throw error;
297
+ }
298
+ }
299
+
300
+ waitForPacket(packetClass, timeout = 5000) {
301
+ return new Promise((resolve, reject) => {
302
+ const timeoutId = setTimeout(() => {
303
+ this.packetManager.off(`packet:${packetClass.packetName}`, handler);
304
+ reject(new Error(`Timeout waiting for ${packetClass.packetName}`));
305
+ }, timeout);
306
+
307
+ const handler = (event) => {
308
+ clearTimeout(timeoutId);
309
+ resolve(event.packet);
310
+ };
311
+
312
+ this.packetManager.once(`packet:${packetClass.packetName}`, handler);
313
+ });
314
+ }
315
+
316
+ onPacket(packetClass, callback, options) {
317
+ this.packetManager.onPacket(packetClass, callback, options);
318
+ return this;
319
+ }
320
+
321
+ get isConnected() {
322
+ return this.state === ClientState.CONNECTED ||
323
+ this.state === ClientState.AUTHENTICATING ||
324
+ this.state === ClientState.READY;
325
+ }
326
+
327
+ get isReady() {
328
+ return this.state === ClientState.READY;
329
+ }
330
+
331
+ get protocolId() {
332
+ return this.version.protocolId;
333
+ }
334
+ }
335
+
336
+ export default MinecraftClient;
@@ -0,0 +1,125 @@
1
+ import * as crypto from 'crypto';
2
+
3
+ export class MinecraftEncryption {
4
+
5
+ static generateKeyPair() {
6
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
7
+ modulusLength: 1024,
8
+ publicKeyEncoding: { type: 'spki', format: 'der' },
9
+ privateKeyEncoding: { type: 'pkcs8', format: 'der' }
10
+ });
11
+ return { publicKey, privateKey };
12
+ }
13
+
14
+ static generateVerifyToken() {
15
+ return crypto.randomBytes(4);
16
+ }
17
+
18
+ static generateSharedSecret() {
19
+ return crypto.randomBytes(16);
20
+ }
21
+
22
+ static generateServerId() {
23
+ return '';
24
+ }
25
+
26
+ static encryptRSA(publicKeyDER, data) {
27
+ const publicKey = crypto.createPublicKey({
28
+ key: publicKeyDER,
29
+ format: 'der',
30
+ type: 'spki'
31
+ });
32
+
33
+ return crypto.publicEncrypt({
34
+ key: publicKey,
35
+ padding: crypto.constants.RSA_PKCS1_PADDING
36
+ }, data);
37
+ }
38
+
39
+ static decryptRSA(privateKeyDER, data) {
40
+ const privateKey = crypto.createPrivateKey({
41
+ key: privateKeyDER,
42
+ format: 'der',
43
+ type: 'pkcs8'
44
+ });
45
+
46
+ return crypto.privateDecrypt({
47
+ key: privateKey,
48
+ padding: crypto.constants.RSA_PKCS1_PADDING
49
+ }, data);
50
+ }
51
+
52
+ static computeServerIdHash(serverId, sharedSecret, publicKeyDER) {
53
+ const hash = crypto.createHash('sha1');
54
+ hash.update(serverId, 'utf8');
55
+ hash.update(sharedSecret);
56
+ hash.update(publicKeyDER);
57
+
58
+ return MinecraftEncryption.minecraftHexDigest(hash.digest());
59
+ }
60
+
61
+ static minecraftHexDigest(buffer) {
62
+ const isNegative = (buffer[0] & 0x80) !== 0;
63
+
64
+ if (isNegative) {
65
+ let carry = true;
66
+ for (let i = buffer.length - 1; i >= 0; i--) {
67
+ buffer[i] = ~buffer[i] & 0xff;
68
+ if (carry) {
69
+ carry = buffer[i] === 0xff;
70
+ buffer[i] = (buffer[i] + 1) & 0xff;
71
+ }
72
+ }
73
+ }
74
+
75
+ let hex = buffer.toString('hex').replace(/^0+/, '');
76
+ if (hex === '') hex = '0';
77
+
78
+ return isNegative ? '-' + hex : hex;
79
+ }
80
+
81
+ static createCipherStream(sharedSecret) {
82
+ const cipher = crypto.createCipheriv('aes-128-cfb8', sharedSecret, sharedSecret);
83
+ return cipher;
84
+ }
85
+
86
+ static createDecipherStream(sharedSecret) {
87
+ const decipher = crypto.createDecipheriv('aes-128-cfb8', sharedSecret, sharedSecret);
88
+ return decipher;
89
+ }
90
+
91
+ static encryptAES(sharedSecret, data) {
92
+ const cipher = crypto.createCipheriv('aes-128-cfb8', sharedSecret, sharedSecret);
93
+ return Buffer.concat([cipher.update(data), cipher.final()]);
94
+ }
95
+
96
+ static decryptAES(sharedSecret, data) {
97
+ const decipher = crypto.createDecipheriv('aes-128-cfb8', sharedSecret, sharedSecret);
98
+ return Buffer.concat([decipher.update(data), decipher.final()]);
99
+ }
100
+ }
101
+
102
+ export class EncryptedConnection {
103
+ constructor(sharedSecret) {
104
+ this.sharedSecret = sharedSecret;
105
+ this.cipher = MinecraftEncryption.createCipherStream(sharedSecret);
106
+ this.decipher = MinecraftEncryption.createDecipherStream(sharedSecret);
107
+ this.enabled = false;
108
+ }
109
+
110
+ enable() {
111
+ this.enabled = true;
112
+ }
113
+
114
+ encrypt(data) {
115
+ if (!this.enabled) return data;
116
+ return this.cipher.update(data);
117
+ }
118
+
119
+ decrypt(data) {
120
+ if (!this.enabled) return data;
121
+ return this.decipher.update(data);
122
+ }
123
+ }
124
+
125
+ export default MinecraftEncryption;