267 lines
8.8 KiB
JavaScript
267 lines
8.8 KiB
JavaScript
'use strict'
|
|
const EventEmitter = require('events').EventEmitter
|
|
const debug = require('debug')('minecraft-protocol')
|
|
const compression = require('./transforms/compression')
|
|
const framing = require('./transforms/framing')
|
|
const states = require('./states')
|
|
|
|
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()
|
|
} else { parts = [] }
|
|
const deserializerDirection = this.isServer ? 'toServer' : 'toClient'
|
|
e.field = [this.protocolState, deserializerDirection].concat(parts).join('.')
|
|
e.message = `Deserialization 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
|
|
debug('read packet ' + state + '.' + parsed.metadata.name)
|
|
if (debug.enabled) {
|
|
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 }
|
|
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
|