UNPKG

minecraft-protocol

Version:

Parse and serialize minecraft packets, plus authentication and encryption.

271 lines (238 loc) 9.29 kB
'use strict' const EventEmitter = require('events').EventEmitter const compression = require('./transforms/compression') const framing = require('./transforms/framing') const states = require('./states') const debug = require('debug')('minecraft-protocol') const debugSkip = process.env.DEBUG_SKIP?.split(',') ?? [] const createSerializer = require('./transforms/serializer').createSerializer const createDeserializer = require('./transforms/serializer').createDeserializer const createCipher = require('./transforms/encryption').createCipher const createDecipher = require('./transforms/encryption').createDecipher const closeTimeout = 30 * 1000 class Client extends EventEmitter { constructor (isServer, version, customPackets, hideErrors = false) { super() this.customPackets = customPackets this.version = version this.isServer = !!isServer this.splitter = framing.createSplitter() this.packetsToParse = {} this.compressor = null this.framer = framing.createFramer() this.cipher = null this.decipher = null this.decompressor = null this.ended = true this.latency = 0 this.hideErrors = hideErrors this.closeTimer = null const mcData = require('minecraft-data')(version) this._supportFeature = mcData.supportFeature this.state = states.HANDSHAKING this._hasBundlePacket = mcData.supportFeature('hasBundlePacket') } get state () { return this.protocolState } setSerializer (state) { this.serializer = createSerializer({ isServer: this.isServer, version: this.version, state, customPackets: this.customPackets }) this.deserializer = createDeserializer({ isServer: this.isServer, version: this.version, state, packetsToParse: this.packetsToParse, customPackets: this.customPackets, noErrorLogging: this.hideErrors }) this.splitter.recognizeLegacyPing = state === states.HANDSHAKING this.serializer.on('error', (e) => { let parts if (e.field) { parts = e.field.split('.') parts.shift() } else { parts = [] } const serializerDirection = !this.isServer ? 'toServer' : 'toClient' e.field = [this.protocolState, serializerDirection].concat(parts).join('.') e.message = `Serialization error for ${e.field} : ${e.message}` if (!this.compressor) { this.serializer.pipe(this.framer) } else { this.serializer.pipe(this.compressor) } this.emit('error', e) }) this.deserializer.on('error', (e) => { let parts = [] if (e.field) { parts = e.field.split('.') parts.shift() } const deserializerDirection = this.isServer ? 'toServer' : 'toClient' e.field = [this.protocolState, deserializerDirection].concat(parts).join('.') e.message = e.buffer ? `Parse error for ${e.field} (${e.buffer?.length} bytes, ${e.buffer?.toString('hex').slice(0, 6)}...) : ${e.message}` : `Parse error for ${e.field}: ${e.message}` if (!this.compressor) { this.splitter.pipe(this.deserializer) } else { this.decompressor.pipe(this.deserializer) } this.emit('error', e) }) this._mcBundle = [] const emitPacket = (parsed) => { this.emit('packet', parsed.data, parsed.metadata, parsed.buffer, parsed.fullBuffer) this.emit(parsed.metadata.name, parsed.data, parsed.metadata) this.emit('raw.' + parsed.metadata.name, parsed.buffer, parsed.metadata) this.emit('raw', parsed.buffer, parsed.metadata) } this.deserializer.on('data', (parsed) => { parsed.metadata.name = parsed.data.name parsed.data = parsed.data.params parsed.metadata.state = state if (debug.enabled && !debugSkip.includes(parsed.metadata.name)) { debug('read packet ' + state + '.' + parsed.metadata.name) const s = JSON.stringify(parsed.data, null, 2) debug(s && s.length > 10000 ? parsed.data : s) } if (this._hasBundlePacket && parsed.metadata.name === 'bundle_delimiter') { if (this._mcBundle.length) { // End bundle this._mcBundle.forEach(emitPacket) emitPacket(parsed) this._mcBundle = [] } else { // Start bundle this._mcBundle.push(parsed) } } else if (this._mcBundle.length) { this._mcBundle.push(parsed) if (this._mcBundle.length > 32) { this._mcBundle.forEach(emitPacket) this._mcBundle = [] this._hasBundlePacket = false } } else { emitPacket(parsed) } }) } set state (newProperty) { const oldProperty = this.protocolState this.protocolState = newProperty if (this.serializer) { if (!this.compressor) { this.serializer.unpipe() this.splitter.unpipe(this.deserializer) } else { this.serializer.unpipe(this.compressor) this.decompressor.unpipe(this.deserializer) } this.serializer.removeAllListeners() this.deserializer.removeAllListeners() } this.setSerializer(this.protocolState) if (!this.compressor) { this.serializer.pipe(this.framer) this.splitter.pipe(this.deserializer) } else { this.serializer.pipe(this.compressor) if (globalThis.debugNMP) this.decompressor.on('data', (data) => { console.log('DES>', data.toString('hex')) }) this.decompressor.pipe(this.deserializer) } this.emit('state', newProperty, oldProperty) } get compressionThreshold () { return this.compressor == null ? -2 : this.compressor.compressionThreshold } set compressionThreshold (threshold) { this.setCompressionThreshold(threshold) } setSocket (socket) { this.ended = false // TODO : A lot of other things needs to be done. const endSocket = () => { if (this.ended) return this.ended = true clearTimeout(this.closeTimer) this.socket.removeListener('close', endSocket) this.socket.removeListener('end', endSocket) this.socket.removeListener('timeout', endSocket) this.emit('end', this._endReason || 'socketClosed') } const onFatalError = (err) => { this.emit('error', err) endSocket() } const onError = (err) => this.emit('error', err) this.socket = socket if (this.socket.setNoDelay) { this.socket.setNoDelay(true) } this.socket.on('connect', () => this.emit('connect')) this.socket.on('error', onFatalError) this.socket.on('close', endSocket) this.socket.on('end', endSocket) this.socket.on('timeout', endSocket) this.framer.on('error', onError) this.splitter.on('error', onError) this.socket.pipe(this.splitter) this.framer.pipe(this.socket) } end (reason) { this._endReason = reason /* ending the serializer will end the whole chain serializer -> framer -> socket -> splitter -> deserializer */ if (this.serializer) { this.serializer.end() } else { if (this.socket) this.socket.end() } if (this.socket) { this.closeTimer = setTimeout( this.socket.destroy.bind(this.socket), closeTimeout ) } } setEncryption (sharedSecret) { if (this.cipher != null) { this.emit('error', new Error('Set encryption twice!')) } this.cipher = createCipher(sharedSecret) this.cipher.on('error', (err) => this.emit('error', err)) this.framer.unpipe(this.socket) this.framer.pipe(this.cipher).pipe(this.socket) this.decipher = createDecipher(sharedSecret) this.decipher.on('error', (err) => this.emit('error', err)) this.socket.unpipe(this.splitter) this.socket.pipe(this.decipher).pipe(this.splitter) } setCompressionThreshold (threshold) { if (this.compressor == null) { this.compressor = compression.createCompressor(threshold) this.compressor.on('error', (err) => this.emit('error', err)) this.serializer.unpipe(this.framer) this.serializer.pipe(this.compressor).pipe(this.framer) this.decompressor = compression.createDecompressor(threshold, this.hideErrors) this.decompressor.on('error', (err) => this.emit('error', err)) this.splitter.unpipe(this.deserializer) this.splitter.pipe(this.decompressor).pipe(this.deserializer) } else { this.decompressor.threshold = threshold this.compressor.threshold = threshold } } write (name, params) { if (!this.serializer.writable) { return } if (debug.enabled && !debugSkip.includes(name)) { debug('writing packet ' + this.state + '.' + name) debug(params) } this.serializer.write({ name, params }) } writeBundle (packets) { if (this._hasBundlePacket) this.write('bundle_delimiter', {}) for (const [name, params] of packets) this.write(name, params) if (this._hasBundlePacket) this.write('bundle_delimiter', {}) } writeRaw (buffer) { const stream = this.compressor === null ? this.framer : this.compressor if (!stream.writable) { return } stream.write(buffer) } // TCP/IP-specific (not generic Stream) method for backwards-compatibility connect (port, host) { const options = { port, host } if (!this.options) this.options = options require('./client/tcp_dns')(this, options) options.connect(this) } } module.exports = Client