Skip to content

Commit 14dfd67

Browse files
committed
Improve error handling, fix packet parsing crash.
1 parent 15b134d commit 14dfd67

File tree

2 files changed

+121
-119
lines changed

2 files changed

+121
-119
lines changed

src/minecraft/connection.ts

+103-106
Original file line numberDiff line numberDiff line change
@@ -118,126 +118,123 @@ const initiateConnection = async (opts: ConnectionOptions) => {
118118
)
119119
)
120120
})
121+
socket.on('close', () => {
122+
conn.closed = true
123+
conn.emit('close')
124+
})
125+
socket.on('error', err => {
126+
if (!resolved) reject(err)
127+
else conn.emit('error', err)
128+
})
121129
socket.on('data', newData => {
122130
// Handle timeout after 20 seconds of no data.
123131
if (conn.disconnectTimer) clearTimeout(conn.disconnectTimer)
124132
conn.disconnectTimer = setTimeout(() => conn.close(), 20000)
125133
// Run after interactions to improve user experience.
126134
InteractionManager.runAfterInteractions(() => {
127-
// Note: the entire packet is encrypted, including the length fields and the packet's data.
128-
// https://github.com/PrismarineJS/node-minecraft-protocol/blob/master/src/transforms/encryption.js
129-
let finalData = newData
130-
if (conn.aesDecipher) finalData = conn.aesDecipher.update(newData)
131-
// Buffer data for read.
132-
conn.bufferedData = Buffer.concat([conn.bufferedData, finalData])
133-
// ;(async () => { This would need a mutex.
134-
while (true) {
135-
const packet = conn.compressionEnabled
136-
? parseCompressedPacket(conn.bufferedData)
137-
: parsePacket(conn.bufferedData)
138-
if (packet) {
139-
if (packet.id === 0x03 && !conn.loggedIn) {
140-
const [threshold] = readVarInt(packet.data)
141-
conn.compressionThreshold = threshold
142-
conn.compressionEnabled = threshold >= 0
143-
} else if (packet.id === 0x02 && !conn.loggedIn) {
144-
conn.loggedIn = true
145-
} else if (packet.id === 0x21) {
146-
conn
147-
.writePacket(0x0f, packet.data)
148-
.catch(err => conn.emit('error', err))
149-
} else if (
150-
(packet.id === 0x00 && !conn.loggedIn) ||
151-
(packet.id === 0x1a && conn.loggedIn)
152-
) {
153-
const [chatLength, chatVarIntLength] = readVarInt(packet.data)
154-
conn.disconnectReason = packet.data
155-
.slice(chatVarIntLength, chatVarIntLength + chatLength)
156-
.toString('utf8')
157-
} else if (packet.id === 0x01 && !conn.loggedIn && !accessToken) {
158-
conn.disconnectReason =
159-
'{"text":"This server requires a premium account to be logged in!"}'
160-
conn.close()
161-
} else if (packet.id === 0x01 && !conn.loggedIn) {
162-
// https://wiki.vg/Protocol_Encryption
163-
const [serverId, publicKey, verifyToken] =
164-
parseEncryptionRequestPacket(packet)
165-
;(async () => {
166-
const sharedSecret = await generateSharedSecret() // Generate random 16-byte shared secret.
167-
// Generate hash.
168-
const sha1 = createHash('sha1')
169-
sha1.update(serverId) // ASCII encoding of the server id string from Encryption Request
170-
sha1.update(sharedSecret)
171-
sha1.update(publicKey) // Server's encoded public key from Encryption Request
172-
const hash = mcHexDigest(sha1.digest())
173-
// Send hash to Mojang servers.
174-
const body = JSON.stringify({
175-
accessToken,
176-
selectedProfile,
177-
serverId: hash
178-
})
179-
const req = await fetch(authUrl, {
180-
headers: { 'content-type': 'application/json' },
181-
method: 'POST',
182-
body
183-
})
184-
if (!req.ok) {
185-
throw new Error('Mojang online mode network request failed')
186-
}
187-
// Encrypt shared secret and verify token with public key.
188-
const pk =
189-
'-----BEGIN PUBLIC KEY-----\n' +
190-
publicKey.toString('base64') +
191-
'\n-----END PUBLIC KEY-----'
192-
const ePrms = { key: pk, padding: 1 } // RSA_PKCS1_PADDING
193-
const encryptedSharedSecret = publicEncrypt(ePrms, sharedSecret)
194-
const encryptedVerifyToken = publicEncrypt(ePrms, verifyToken)
195-
// Send encryption response packet.
196-
// From this point forward, everything is encrypted, including the Login Success packet.
197-
conn.aesDecipher = createDecipheriv(
198-
'aes-128-cfb8',
199-
sharedSecret,
200-
sharedSecret
201-
)
202-
await conn.writePacket(
203-
0x01,
204-
concatPacketData([
135+
try {
136+
// Note: the entire packet is encrypted, including the length fields and the packet's data.
137+
// https://github.com/PrismarineJS/node-minecraft-protocol/blob/master/src/transforms/encryption.js
138+
let finalData = newData
139+
if (conn.aesDecipher) finalData = conn.aesDecipher.update(newData)
140+
// Buffer data for read.
141+
conn.bufferedData = Buffer.concat([conn.bufferedData, finalData])
142+
// ;(async () => { This would need a mutex.
143+
while (true) {
144+
const packet = conn.compressionEnabled
145+
? parseCompressedPacket(conn.bufferedData)
146+
: parsePacket(conn.bufferedData)
147+
if (packet) {
148+
// Remove packet from buffered data.
149+
conn.bufferedData =
150+
conn.bufferedData.length <= packet.packetLength
151+
? Buffer.alloc(0) // Avoid errors shortening.
152+
: conn.bufferedData.slice(packet.packetLength)
153+
// Internally handle login packets.
154+
if (packet.id === 0x03 && !conn.loggedIn) {
155+
const [threshold] = readVarInt(packet.data)
156+
conn.compressionThreshold = threshold
157+
conn.compressionEnabled = threshold >= 0
158+
} else if (packet.id === 0x02 && !conn.loggedIn) {
159+
conn.loggedIn = true
160+
} else if (packet.id === 0x21) {
161+
conn
162+
.writePacket(0x0f, packet.data)
163+
.catch(err => conn.emit('error', err))
164+
} else if (
165+
(packet.id === 0x00 && !conn.loggedIn) ||
166+
(packet.id === 0x1a && conn.loggedIn)
167+
) {
168+
const [chatLength, chatVarIntLength] = readVarInt(packet.data)
169+
conn.disconnectReason = packet.data
170+
.slice(chatVarIntLength, chatVarIntLength + chatLength)
171+
.toString('utf8')
172+
} else if (packet.id === 0x01 && !conn.loggedIn && !accessToken) {
173+
conn.disconnectReason =
174+
'{"text":"This server requires a premium account to be logged in!"}'
175+
conn.close()
176+
} else if (packet.id === 0x01 && !conn.loggedIn) {
177+
// https://wiki.vg/Protocol_Encryption
178+
const [serverId, publicKey, verifyToken] =
179+
parseEncryptionRequestPacket(packet)
180+
;(async () => {
181+
const secret = await generateSharedSecret() // Generate random 16-byte shared secret.
182+
// Generate hash.
183+
const sha1 = createHash('sha1')
184+
sha1.update(serverId) // ASCII encoding of the server id string from Encryption Request
185+
sha1.update(secret)
186+
sha1.update(publicKey) // Server's encoded public key from Encryption Request
187+
const hash = mcHexDigest(sha1.digest())
188+
// Send hash to Mojang servers.
189+
const body = JSON.stringify({
190+
accessToken,
191+
selectedProfile,
192+
serverId: hash
193+
})
194+
const req = await fetch(authUrl, {
195+
headers: { 'content-type': 'application/json' },
196+
method: 'POST',
197+
body
198+
})
199+
if (!req.ok) {
200+
throw new Error('Mojang online mode network request failed')
201+
}
202+
// Encrypt shared secret and verify token with public key.
203+
const pk =
204+
'-----BEGIN PUBLIC KEY-----\n' +
205+
publicKey.toString('base64') +
206+
'\n-----END PUBLIC KEY-----'
207+
const ePrms = { key: pk, padding: 1 } // RSA_PKCS1_PADDING
208+
const encryptedSharedSecret = publicEncrypt(ePrms, secret)
209+
const encryptedVerifyToken = publicEncrypt(ePrms, verifyToken)
210+
// Send encryption response packet.
211+
// From this point forward, everything is encrypted, including the Login Success packet.
212+
const encryptionResponse = concatPacketData([
205213
writeVarInt(encryptedSharedSecret.byteLength),
206214
encryptedSharedSecret,
207215
writeVarInt(encryptedVerifyToken.byteLength),
208216
encryptedVerifyToken
209217
])
210-
)
211-
conn.aesCipher = createCipheriv(
212-
'aes-128-cfb8',
213-
sharedSecret,
214-
sharedSecret
215-
)
216-
})().catch(e => {
217-
console.error(e)
218-
conn.disconnectReason =
219-
'{"text":"Failed to authenticate with Mojang servers!"}'
220-
conn.close()
221-
})
222-
}
223-
conn.bufferedData =
224-
conn.bufferedData.length <= packet.packetLength
225-
? Buffer.alloc(0) // Avoid errors shortening.
226-
: conn.bufferedData.slice(packet.packetLength)
227-
conn.emit('packet', packet)
228-
} else break
218+
const AES_ALG = 'aes-128-cfb8'
219+
conn.aesDecipher = createDecipheriv(AES_ALG, secret, secret)
220+
await conn.writePacket(0x01, encryptionResponse)
221+
conn.aesCipher = createCipheriv(AES_ALG, secret, secret)
222+
})().catch(e => {
223+
console.error(e)
224+
conn.disconnectReason =
225+
'{"text":"Failed to authenticate with Mojang servers!"}'
226+
conn.close()
227+
})
228+
}
229+
conn.emit('packet', packet)
230+
} else break
231+
}
232+
conn.emit('data', newData)
233+
} catch (err) {
234+
conn.emit('error', err)
229235
}
230-
conn.emit('data', newData)
231236
}).then(() => {}, console.error)
232237
})
233-
socket.on('close', () => {
234-
conn.closed = true
235-
conn.emit('close')
236-
})
237-
socket.on('error', err => {
238-
if (!resolved) reject(err)
239-
else conn.emit('error', err)
240-
})
241238
})
242239
}
243240

src/minecraft/packet.ts

+18-13
Original file line numberDiff line numberDiff line change
@@ -53,19 +53,24 @@ export interface Packet {
5353

5454
export const parsePacket = (packet: Buffer): Packet | undefined => {
5555
if (packet.byteLength === 0) return
56-
const [packetBodyLength, varIntLength] = readVarInt(packet)
57-
if (packet.byteLength < packetBodyLength + varIntLength) return
58-
const packetBody = packet.slice(varIntLength, varIntLength + packetBodyLength)
59-
const [packetId, packetIdLength] = readVarInt(packetBody)
60-
const packetData = packetBody.slice(packetIdLength)
61-
return {
62-
id: packetId,
63-
data: packetData,
64-
idLength: packetIdLength,
65-
dataLength: packetBodyLength - packetIdLength,
66-
packetLength: packetBodyLength + varIntLength,
67-
lengthLength: varIntLength
68-
}
56+
try {
57+
const [packetBodyLength, varIntLength] = readVarInt(packet)
58+
if (packet.byteLength < packetBodyLength + varIntLength) return
59+
const packetBody = packet.slice(
60+
varIntLength,
61+
varIntLength + packetBodyLength
62+
)
63+
const [packetId, packetIdLength] = readVarInt(packetBody)
64+
const packetData = packetBody.slice(packetIdLength)
65+
return {
66+
id: packetId,
67+
data: packetData,
68+
idLength: packetIdLength,
69+
dataLength: packetBodyLength - packetIdLength,
70+
packetLength: packetBodyLength + varIntLength,
71+
lengthLength: varIntLength
72+
}
73+
} catch (e) {} // If the packet is incomplete, readVarInt could error, so no packet parsed.
6974
}
7075

7176
export const parseCompressedPacket = (packet: Buffer): Packet | undefined => {

0 commit comments

Comments
 (0)