Compressor handling update for 1.20.60 (#479)

* support compressor in header

* use mcdata features

* cleanup
This commit is contained in:
extremeheat 2024-02-07 20:03:26 -05:00 committed by GitHub
commit d3161badc6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 88 additions and 27 deletions

View file

@ -42,6 +42,7 @@ class Client extends Connection {
this.validateOptions()
this.serializer = createSerializer(this.options.version)
this.deserializer = createDeserializer(this.options.version)
this._loadFeatures()
KeyExchange(this, null, this.options)
Login(this, null, this.options)
@ -55,6 +56,17 @@ class Client extends Connection {
this.emit('connect_allowed')
}
_loadFeatures () {
try {
const mcData = require('minecraft-data')('bedrock_' + this.options.version)
this.features = {
compressorInHeader: mcData.supportFeature('compressorInPacketHeader')
}
} catch (e) {
throw new Error(`Unsupported version: '${this.options.version}', no data available`)
}
}
connect () {
if (!this.connection) throw new Error('Connect not currently allowed') // must wait for `connect_allowed`, or use `createClient`
this.on('session', this._connect)
@ -120,6 +132,7 @@ class Client extends Connection {
updateCompressorSettings (packet) {
this.compressionAlgorithm = packet.compression_algorithm || 'deflate'
this.compressionThreshold = packet.compression_threshold
this.compressionReady = true
}
sendLogin () {

View file

@ -65,7 +65,7 @@ class Connection extends EventEmitter {
write (name, params) {
this.outLog?.(name, params)
if (name === 'start_game') this.updateItemPalette(params.itemstates)
const batch = new Framer(this.compressionAlgorithm, this.compressionLevel, this.compressionThreshold)
const batch = new Framer(this)
const packet = this.serializer.createPacketBuffer({ name, params })
batch.addEncodedPacket(packet)
@ -91,7 +91,7 @@ class Connection extends EventEmitter {
_tick () {
if (this.sendQ.length) {
const batch = new Framer(this.compressionAlgorithm, this.compressionLevel, this.compressionThreshold)
const batch = new Framer(this)
batch.addEncodedPackets(this.sendQ)
this.sendQ = []
this.sendIds = []
@ -115,7 +115,7 @@ class Connection extends EventEmitter {
*/
sendBuffer (buffer, immediate = false) {
if (immediate) {
const batch = new Framer(this.compressionAlgorithm, this.compressionLevel, this.compressionThreshold)
const batch = new Framer(this)
batch.addEncodedPacket(buffer)
if (this.encryptionEnabled) {
this.sendEncryptedBatch(batch)
@ -150,13 +150,11 @@ class Connection extends EventEmitter {
// These are callbacks called from encryption.js
onEncryptedPacket = (buf) => {
const packet = Buffer.concat([Buffer.from([0xfe]), buf]) // add header
this.sendMCPE(packet)
}
onDecryptedPacket = (buf) => {
const packets = Framer.getPackets(buf)
for (const packet of packets) {
this.readPacket(packet)
}
@ -167,11 +165,13 @@ class Connection extends EventEmitter {
if (this.encryptionEnabled) {
this.decrypt(buffer.slice(1))
} else {
const packets = Framer.decode(this.compressionAlgorithm, buffer)
const packets = Framer.decode(this, buffer)
for (const packet of packets) {
this.readPacket(packet)
}
}
} else {
throw Error('Bad packet header ' + buffer[0])
}
}
}

View file

@ -15,6 +15,7 @@ class Server extends EventEmitter {
this.RakServer = require('./rak')(this.options.raknetBackend).RakServer
this._loadFeatures(this.options.version)
this.serializer = createSerializer(this.options.version)
this.deserializer = createDeserializer(this.options.version)
this.advertisement = new ServerAdvertisement(this.options.motd, this.options.port, this.options.version)
@ -27,6 +28,17 @@ class Server extends EventEmitter {
this.setCompressor(this.options.compressionAlgorithm, this.options.compressionLevel, this.options.compressionThreshold)
}
_loadFeatures (version) {
try {
const mcData = require('minecraft-data')('bedrock_' + version)
this.features = {
compressorInHeader: mcData.supportFeature('compressorInPacketHeader')
}
} catch (e) {
throw new Error(`Unsupported version: '${version}', no data available`)
}
}
setCompressor (algorithm, level = 1, threshold = 256) {
switch (algorithm) {
case 'none':

View file

@ -10,6 +10,7 @@ class Player extends Connection {
constructor (server, connection) {
super()
this.server = server
this.features = server.features
this.serializer = server.serializer
this.deserializer = server.deserializer
this.connection = connection
@ -23,8 +24,8 @@ class Player extends Connection {
this.status = ClientStatus.Authenticating
if (isDebug) {
this.inLog = (...args) => debug('S ->', ...args)
this.outLog = (...args) => debug('S <-', ...args)
this.inLog = (...args) => debug('-> S', ...args)
this.outLog = (...args) => debug('<- S', ...args)
}
// Compression is server-wide
@ -48,6 +49,7 @@ class Player extends Connection {
client_throttle_scalar: 0
})
this._sentNetworkSettings = true
this.compressionReady = true
}
handleClientProtocolVersion (clientVersion) {
@ -152,7 +154,7 @@ class Player extends Connection {
return
}
this.inLog?.(des.data.name, serialize(des.data.params).slice(0, 200))
this.inLog?.(des.data.name, serialize(des.data.params))
switch (des.data.name) {
// This is the first packet on 1.19.30 & above

View file

@ -36,7 +36,10 @@ function createEncryptor (client, iv) {
// The send counter is represented as a little-endian 64-bit long and incremented after each packet.
function process (chunk) {
const buffer = Zlib.deflateRawSync(chunk, { level: client.compressionLevel })
const compressed = Zlib.deflateRawSync(chunk, { level: client.compressionLevel })
const buffer = client.features.compressorInHeader
? Buffer.concat([Buffer.from([0]), compressed])
: compressed
const packet = Buffer.concat([buffer, computeCheckSum(buffer, client.sendCounter, client.secretKeyBytes)])
client.sendCounter++
client.cipher.write(packet)
@ -70,7 +73,22 @@ function createDecryptor (client, iv) {
return
}
const buffer = Zlib.inflateRawSync(chunk, { chunkSize: 512000 })
let buffer
if (client.features.compressorInHeader) {
switch (packet[0]) {
case 0:
buffer = Zlib.inflateRawSync(packet.slice(1), { chunkSize: 512000 })
break
case 255:
buffer = packet.slice(1)
break
default:
client.emit('error', Error(`Unsupported compressor: ${packet[0]}`))
}
} else {
buffer = Zlib.inflateRawSync(packet, { chunkSize: 512000 })
}
client.onDecryptedPacket(buffer)
}

View file

@ -3,12 +3,13 @@ const zlib = require('zlib')
// Concatenates packets into one batch packet, and adds length prefixs.
class Framer {
constructor (compressor, compressionLevel, compressionThreshold) {
constructor (client) {
// Encoding
this.packets = []
this.compressor = compressor || 'none'
this.compressionLevel = compressionLevel
this.compressionThreshold = compressionThreshold
this.compressor = client.compressionAlgorithm || 'none'
this.compressionLevel = client.compressionLevel
this.compressionThreshold = client.compressionThreshold
this.writeCompressor = client.features.compressorInHeader && client.compressionReady
}
// No compression in base class
@ -21,30 +22,45 @@ class Framer {
}
static decompress (algorithm, buffer) {
try {
switch (algorithm) {
case 'deflate': return zlib.inflateRawSync(buffer, { chunkSize: 512000 })
case 'snappy': throw Error('Snappy compression not implemented')
case 'none': return buffer
default: throw Error('Unknown compression type ' + this.compressor)
}
} catch {
return buffer
switch (algorithm) {
case 0:
case 'deflate':
return zlib.inflateRawSync(buffer, { chunkSize: 512000 })
case 1:
case 'snappy':
throw Error('Snappy compression not implemented')
case 'none':
case 255:
return buffer
default: throw Error('Unknown compression type ' + algorithm)
}
}
static decode (compressor, buf) {
static decode (client, buf) {
// Read header
if (buf[0] !== 0xfe) throw Error('bad batch packet header ' + buf[0])
const buffer = buf.slice(1)
const decompressed = this.decompress(compressor, buffer)
// Decompress
let decompressed
if (client.features.compressorInHeader && client.compressionReady) {
decompressed = this.decompress(buffer[0], buffer.slice(1))
} else {
// On old versions, compressor is session-wide ; failing to decompress
// a packet will assume it's not compressed
try {
decompressed = this.decompress(client.compressionAlgorithm, buffer)
} catch (e) {
decompressed = buffer
}
}
return Framer.getPackets(decompressed)
}
encode () {
const buf = Buffer.concat(this.packets)
const compressed = (buf.length > this.compressionThreshold) ? this.compress(buf) : buf
return Buffer.concat([Buffer.from([0xfe]), compressed])
const header = this.writeCompressor ? [0xfe, 0] : [0xfe]
return Buffer.concat([Buffer.from(header), compressed])
}
addEncodedPacket (chunk) {