diff --git a/src/client.js b/src/client.js index b6bb0ac..af3666b 100644 --- a/src/client.js +++ b/src/client.js @@ -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 () { diff --git a/src/connection.js b/src/connection.js index bc1d56b..2fcbd62 100644 --- a/src/connection.js +++ b/src/connection.js @@ -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]) } } } diff --git a/src/server.js b/src/server.js index 5476455..355e79a 100644 --- a/src/server.js +++ b/src/server.js @@ -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': diff --git a/src/serverPlayer.js b/src/serverPlayer.js index ffb1961..89e100c 100644 --- a/src/serverPlayer.js +++ b/src/serverPlayer.js @@ -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 diff --git a/src/transforms/encryption.js b/src/transforms/encryption.js index b02351b..ff067cc 100644 --- a/src/transforms/encryption.js +++ b/src/transforms/encryption.js @@ -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) } diff --git a/src/transforms/framer.js b/src/transforms/framer.js index 2452f01..c59dd4e 100644 --- a/src/transforms/framer.js +++ b/src/transforms/framer.js @@ -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) {