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.
- package/README.md +398 -0
- package/package.json +31 -0
- package/src/auth/AuthHandler.js +138 -0
- package/src/auth/MojangAPI.js +186 -0
- package/src/client/MinecraftClient.js +336 -0
- package/src/crypto/Encryption.js +125 -0
- package/src/events/EventEmitter.js +267 -0
- package/src/events/PacketEvent.js +78 -0
- package/src/index.js +18 -0
- package/src/manager/PacketManager.js +258 -0
- package/src/protocol/ConnectionState.js +37 -0
- package/src/protocol/PacketDirection.js +8 -0
- package/src/protocol/ProtocolVersion.js +141 -0
- package/src/protocol/packets/Packet.js +119 -0
- package/src/protocol/packets/PacketRegistry.js +145 -0
- package/src/protocol/packets/handshake/HandshakePacket.js +44 -0
- package/src/protocol/packets/index.js +265 -0
- package/src/protocol/packets/login/DisconnectPacket.js +71 -0
- package/src/protocol/packets/login/EncryptionRequestPacket.js +47 -0
- package/src/protocol/packets/login/EncryptionResponsePacket.js +34 -0
- package/src/protocol/packets/login/LoginStartPacket.js +35 -0
- package/src/protocol/packets/login/LoginSuccessPacket.js +61 -0
- package/src/protocol/packets/login/SetCompressionPacket.js +29 -0
- package/src/protocol/packets/play/ChatPacket.js +238 -0
- package/src/protocol/packets/play/ChunkPacket.js +122 -0
- package/src/protocol/packets/play/EntityPacket.js +302 -0
- package/src/protocol/packets/play/KeepAlivePacket.js +55 -0
- package/src/protocol/packets/play/PlayerPositionPacket.js +266 -0
- package/src/protocol/packets/status/PingPacket.js +29 -0
- package/src/protocol/packets/status/PongPacket.js +29 -0
- package/src/protocol/packets/status/StatusRequestPacket.js +20 -0
- package/src/protocol/packets/status/StatusResponsePacket.js +58 -0
- package/src/protocol/types/NBT.js +594 -0
- package/src/protocol/types/Position.js +125 -0
- package/src/protocol/types/TextComponent.js +355 -0
- package/src/protocol/types/UUID.js +105 -0
- package/src/protocol/types/VarInt.js +144 -0
- package/src/protocol/types/index.js +5 -0
- package/src/utils/Logger.js +207 -0
- 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;
|