Skip to content

Commit 446e5e8

Browse files
committed
Support 1.19 offline mode servers, fix a crash.
1 parent 4d87389 commit 446e5e8

File tree

6 files changed

+56
-33
lines changed

6 files changed

+56
-33
lines changed

src/minecraft/chatToJsx.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -231,15 +231,15 @@ const parseChatToJsx = (
231231

232232
// React Component-ised.
233233
const ChatToJsxNonMemo = (props: {
234-
chat: MinecraftChat
234+
chat?: MinecraftChat
235235
component: React.ComponentType<TextProps>
236236
colorMap: ColorMap
237237
componentProps?: {}
238238
clickEventHandler?: (clickEvent: ClickEvent) => void
239239
trim?: boolean
240240
}) =>
241241
parseChatToJsx(
242-
props.chat,
242+
props.chat ?? { text: '' },
243243
props.component,
244244
props.colorMap,
245245
props.clickEventHandler,

src/minecraft/connection.ts

+27-20
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ import {
2323
readVarInt,
2424
writeVarInt,
2525
resolveHostname,
26-
generateSharedSecret,
2726
mcHexDigest,
28-
protocolMap
27+
protocolMap,
28+
getRandomBytes
2929
} from './utils'
3030
import { Certificate, joinMinecraftSession } from './api/mojang'
3131

@@ -59,6 +59,7 @@ export class ServerConnection extends events.EventEmitter {
5959
disconnectReason?: string
6060
aesDecipher?: Decipher
6161
aesCipher?: Cipher
62+
msgSalt?: Buffer
6263

6364
constructor(socket: net.Socket, options: ConnectionOptions) {
6465
super()
@@ -180,39 +181,44 @@ const initiateConnection = async (opts: ConnectionOptions) => {
180181
? Buffer.alloc(0) // Avoid errors shortening.
181182
: conn.bufferedData.slice(packet.packetLength)
182183
// Internally handle login packets.
183-
if (packet.id === 0x03 && !conn.loggedIn) {
184+
const is119 = conn.options.protocolVersion >= protocolMap[1.19]
185+
if (packet.id === 0x03 && !conn.loggedIn /* Set Compression */) {
184186
const [threshold] = readVarInt(packet.data)
185187
conn.compressionThreshold = threshold
186188
conn.compressionEnabled = threshold >= 0
187189
} else if (packet.id === 0x02 && !conn.loggedIn) {
188-
conn.loggedIn = true
189-
} else if (packet.id === 0x21) {
190+
conn.loggedIn = true // Login Success
191+
} else if (
192+
// Keep Alive (clientbound)
193+
(packet.id === 0x21 && !is119) ||
194+
(packet.id === 0x1e && is119)
195+
) {
190196
conn
191-
.writePacket(0x0f, packet.data)
197+
.writePacket(is119 ? 0x11 : 0x0f, packet.data)
192198
.catch(err => conn.emit('error', err))
193199
} else if (
200+
// Disconnect (login) or Disconnect (play)
194201
(packet.id === 0x00 && !conn.loggedIn) ||
195-
(packet.id === 0x1a &&
196-
conn.loggedIn &&
197-
opts.protocolVersion < protocolMap[1.19]) ||
198-
(packet.id === 0x17 &&
199-
conn.loggedIn &&
200-
opts.protocolVersion >= protocolMap[1.19])
202+
(packet.id === 0x1a && conn.loggedIn && !is119) ||
203+
(packet.id === 0x17 && conn.loggedIn && is119)
201204
) {
202205
const [chatLength, chatVarIntLength] = readVarInt(packet.data)
203206
conn.disconnectReason = packet.data
204207
.slice(chatVarIntLength, chatVarIntLength + chatLength)
205208
.toString('utf8')
206-
} else if (packet.id === 0x01 && !conn.loggedIn && !accessToken) {
207-
conn.disconnectReason =
208-
'{"text":"This server requires a premium account to be logged in!"}'
209-
conn.close()
210209
} else if (packet.id === 0x01 && !conn.loggedIn) {
210+
/* Encryption Request */
211+
if (!accessToken || !selectedProfile) {
212+
conn.disconnectReason =
213+
'{"text":"This server requires a premium account to be logged in!"}'
214+
conn.close()
215+
continue
216+
}
211217
// https://wiki.vg/Protocol_Encryption
212218
const [serverId, publicKey, verifyToken] =
213219
parseEncryptionRequestPacket(packet)
214220
;(async () => {
215-
const secret = await generateSharedSecret() // Generate random 16-byte shared secret.
221+
const secret = await getRandomBytes(16) // Generate random 16-byte shared secret.
216222
// Generate hash.
217223
const sha1 = createHash('sha1')
218224
sha1.update(serverId) // ASCII encoding of the server id string from Encryption Request
@@ -221,8 +227,8 @@ const initiateConnection = async (opts: ConnectionOptions) => {
221227
const hash = mcHexDigest(sha1.digest())
222228
// Send hash to Mojang servers.
223229
const req = await joinMinecraftSession(
224-
accessToken as string,
225-
selectedProfile as string,
230+
accessToken,
231+
selectedProfile,
226232
hash
227233
)
228234
if (!req.ok) {
@@ -244,7 +250,8 @@ const initiateConnection = async (opts: ConnectionOptions) => {
244250
writeVarInt(encryptedVerifyToken.byteLength),
245251
encryptedVerifyToken
246252
]
247-
if (opts.protocolVersion >= protocolMap[1.19]) {
253+
if (is119) {
254+
conn.msgSalt = await getRandomBytes(8)
248255
response.splice(2, 0, true)
249256
}
250257
const AES_ALG = 'aes-128-cfb8'

src/minecraft/utils.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export const protocolMap = {
1111
'1.18.1': 757,
1212
'1.18.2': 758,
1313
1.19: 759,
14+
'1.19.1': 760,
15+
'1.19.2': 760,
1416
auto: -1
1517
}
1618

@@ -72,9 +74,9 @@ export const resolveHostname = async (
7274
} else return [hostname, port]
7375
}
7476

75-
export const generateSharedSecret = async () =>
77+
export const getRandomBytes = async (size: number) =>
7678
await new Promise<Buffer>((resolve, reject) => {
77-
randomBytes(16, (err, buf) => {
79+
randomBytes(size, (err, buf) => {
7880
if (err) reject(err)
7981
else resolve(buf)
8082
})

src/screens/ServerScreen.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ const ServerScreen = (props: Props) => {
218218
dropdownIconColor={darkMode ? '#ffffff' : '#000000'}
219219
>
220220
<Picker.Item label='Auto' value='auto' />
221+
<Picker.Item label='1.19' value='1.19' />
221222
<Picker.Item label='1.18.2' value='1.18.2' />
222223
<Picker.Item label='1.18/1.18.1' value='1.18' />
223224
<Picker.Item label='1.17.1' value='1.17.1' />

src/screens/chat/ChatScreen.tsx

+12-5
Original file line numberDiff line numberDiff line change
@@ -162,14 +162,21 @@ const ChatScreen = ({ navigation, route }: Props) => {
162162
.writePacket(0x03, concatPacketData([msg]))
163163
.catch(handleError(addMessage, sendMessageError))
164164
} else {
165+
const id = msg.startsWith('/') ? 0x03 : 0x04
165166
const timestamp = Buffer.alloc(8)
167+
timestamp.writeIntBE(Date.now(), 2, 6) // writeBigInt64BE(BigInt(Date.now()))
168+
const salt = connection.connection.msgSalt ?? Buffer.alloc(8)
169+
// TODO-1.19: Send signature(s) and preview chat if possible.
170+
const data = concatPacketData([
171+
id === 0x03 ? msg.substring(1) : msg,
172+
timestamp,
173+
salt,
174+
writeVarInt(0),
175+
false
176+
])
166177
connection.connection
167-
.writePacket(
168-
0x04,
169-
concatPacketData([msg, timestamp, writeVarInt(0), false])
170-
)
178+
.writePacket(id, data)
171179
.catch(handleError(addMessage, sendMessageError))
172-
// TODO-1.19: Support sending Chat Command/Chat Message/Chat Preview.
173180
}
174181
}
175182

src/screens/chat/packetHandler.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export const packetHandler =
5858
.slice(chatVarIntLength, chatVarIntLength + chatLength)
5959
.toString('utf8')
6060
const position = packet.data.readInt8(chatVarIntLength + chatLength)
61-
// TODO: Support position 2 (also in 0x5f packet) and sender for disableChat/blocked players.
61+
// TODO: Support position 2 (also in 0x5f 1.19 packet) and sender for disableChat/blocked players.
6262
if (position === 0 || position === 1) {
6363
addMessage(parseValidJson(chatJson))
6464
}
@@ -87,15 +87,21 @@ export const packetHandler =
8787
} catch (e) {
8888
handleError(addMessage, parseMessageError)(e)
8989
}
90-
} else if (packet.id === 0x2e /* Open Window */) {
90+
} else if (
91+
(packet.id === 0x2e && !is119) ||
92+
(packet.id === 0x2b && is119) /* Open Window */
93+
) {
9194
// Just close the window.
9295
const [windowId] = readVarInt(packet.data)
9396
const buf = Buffer.alloc(1)
9497
buf.writeUInt8(windowId)
9598
connection // Close Window (serverbound)
9699
.writePacket(0x09, buf)
97100
.catch(handleError(addMessage, inventoryCloseError))
98-
} else if (packet.id === 0x35 /* Death Combat Event */) {
101+
} else if (
102+
(packet.id === 0x35 && !is119) ||
103+
(packet.id === 0x33 && is119) /* Death Combat Event */
104+
) {
99105
const [, playerIdLen] = readVarInt(packet.data)
100106
const offset = playerIdLen + 4 // Entity ID
101107
const [chatLen, chatVarIntLength] = readVarInt(packet.data, offset)
@@ -108,7 +114,7 @@ export const packetHandler =
108114
// Automatically respawn.
109115
// LOW-TODO: Should this be manual, or a dialog, like MC?
110116
connection // Client Status
111-
.writePacket(0x04, writeVarInt(0))
117+
.writePacket(is119 ? 0x06 : 0x04, writeVarInt(0))
112118
.catch(handleError(addMessage, respawnError))
113119
} else if (packet.id === 0x52 /* Update Health */) {
114120
const newHealth = packet.data.readFloatBE(0)

0 commit comments

Comments
 (0)