247 lines
8.5 KiB
JavaScript
247 lines
8.5 KiB
JavaScript
const { Client } = require('./client')
|
|
const { Server } = require('./server')
|
|
const { Player } = require('./serverPlayer')
|
|
const debug = globalThis.isElectron ? console.debug : require('debug')('minecraft-protocol')
|
|
|
|
const debugging = false // Do re-encoding tests
|
|
|
|
class RelayPlayer extends Player {
|
|
constructor (server, conn) {
|
|
super(server, conn)
|
|
|
|
this.startRelaying = false
|
|
this.once('join', () => { // The client has joined our proxy
|
|
this.flushDownQueue() // Send queued packets from the upstream backend
|
|
this.startRelaying = true
|
|
})
|
|
this.downQ = []
|
|
this.upQ = []
|
|
this.upInLog = (...msg) => console.debug('* Backend -> Proxy', ...msg)
|
|
this.upOutLog = (...msg) => console.debug('* Proxy -> Backend', ...msg)
|
|
this.downInLog = (...msg) => console.debug('* Client -> Proxy', ...msg)
|
|
this.downOutLog = (...msg) => console.debug('* Proxy -> Client', ...msg)
|
|
|
|
if (!server.options.logging) {
|
|
this.upInLog = () => { }
|
|
this.upOutLog = () => { }
|
|
this.downInLog = () => { }
|
|
this.downOutLog = () => { }
|
|
}
|
|
|
|
this.outLog = this.downOutLog
|
|
this.inLog = this.downInLog
|
|
this.chunkSendCache = []
|
|
this.sentStartGame = false
|
|
this.respawnPacket = []
|
|
}
|
|
|
|
// Called when we get a packet from backend server (Backend -> PROXY -> Client)
|
|
readUpstream (packet) {
|
|
if (!this.startRelaying) {
|
|
this.upInLog('Client not ready, queueing packet until join')
|
|
this.downQ.push(packet)
|
|
return
|
|
}
|
|
const des = this.server.deserializer.parsePacketBuffer(packet)
|
|
const name = des.data.name
|
|
const params = des.data.params
|
|
this.upInLog('->', name, params)
|
|
|
|
if (name === 'play_status' && params.status === 'login_success') return // Already sent this, this needs to be sent ASAP or client will disconnect
|
|
|
|
if (debugging) { // some packet encode/decode testing stuff
|
|
this.server.deserializer.verify(des, this.server.serializer)
|
|
}
|
|
|
|
this.emit('clientbound', des.data, des)
|
|
|
|
if (!des.canceled) {
|
|
if (name === 'start_game') {
|
|
setTimeout(() => {
|
|
this.sentStartGame = true
|
|
}, 500)
|
|
} else if (name === 'level_chunk' && !this.sentStartGame) {
|
|
this.chunkSendCache.push(params)
|
|
return
|
|
}
|
|
|
|
this.queue(name, params)
|
|
}
|
|
|
|
if (this.chunkSendCache.length > 0 && this.sentStartGame) {
|
|
for (const entry of this.chunkSendCache) {
|
|
this.queue('level_chunk', entry)
|
|
}
|
|
this.chunkSendCache = []
|
|
}
|
|
}
|
|
|
|
// Send queued packets to the connected client
|
|
flushDownQueue () {
|
|
this.downOutLog('Flushing downstream queue')
|
|
for (const packet of this.downQ) {
|
|
const des = this.server.deserializer.parsePacketBuffer(packet)
|
|
this.write(des.data.name, des.data.params)
|
|
}
|
|
this.downQ = []
|
|
}
|
|
|
|
// Send queued packets to the backend upstream server from the client
|
|
flushUpQueue () {
|
|
this.upOutLog('Flushing upstream queue')
|
|
for (const e of this.upQ) { // Send the queue
|
|
const des = this.server.deserializer.parsePacketBuffer(e)
|
|
if (des.data.name === 'client_cache_status') {
|
|
// Currently not working, force off the chunk cache
|
|
} else {
|
|
this.upstream.write(des.data.name, des.data.params)
|
|
}
|
|
}
|
|
this.upQ = []
|
|
}
|
|
|
|
// Called when the server gets a packet from the downstream player (Client -> PROXY -> Backend)
|
|
readPacket (packet) {
|
|
// The downstream client conn is established & we got a packet to send to upstream server
|
|
if (this.startRelaying) {
|
|
// Upstream is still connecting/handshaking
|
|
if (!this.upstream) {
|
|
const des = this.server.deserializer.parsePacketBuffer(packet)
|
|
this.downInLog('Got downstream connected packet but upstream is not connected yet, added to q', des)
|
|
this.upQ.push(packet) // Put into a queue
|
|
return
|
|
}
|
|
|
|
// Send queued packets
|
|
this.flushUpQueue()
|
|
this.downInLog('recv', packet)
|
|
|
|
// TODO: If we fail to parse a packet, proxy it raw and log an error
|
|
const des = this.server.deserializer.parsePacketBuffer(packet)
|
|
|
|
if (debugging) { // some packet encode/decode testing stuff
|
|
this.server.deserializer.verify(des, this.server.serializer)
|
|
}
|
|
|
|
this.emit('serverbound', des.data, des)
|
|
if (des.canceled) return
|
|
|
|
switch (des.data.name) {
|
|
case 'client_cache_status':
|
|
// Force the chunk cache off.
|
|
this.upstream.queue('client_cache_status', { enabled: this.enableChunkCaching })
|
|
break
|
|
case 'set_local_player_as_initialized':
|
|
this.status = 3
|
|
// falls through
|
|
default:
|
|
// Emit the packet as-is back to the upstream server
|
|
this.downInLog('Relaying', des.data)
|
|
this.upstream.queue(des.data.name, des.data.params)
|
|
}
|
|
} else {
|
|
super.readPacket(packet)
|
|
}
|
|
}
|
|
|
|
close (reason) {
|
|
this.upstream?.close(reason)
|
|
super.close(reason)
|
|
}
|
|
}
|
|
|
|
class Relay extends Server {
|
|
/**
|
|
* Creates a new non-transparent proxy connection to a destination server
|
|
* @param {Options} options
|
|
*/
|
|
constructor (options) {
|
|
super(options)
|
|
this.RelayPlayer = options.relayPlayer || RelayPlayer
|
|
this.forceSingle = true
|
|
this.upstreams = new Map()
|
|
this.conLog = debug
|
|
this.enableChunkCaching = options.enableChunkCaching
|
|
}
|
|
|
|
// Called after a new player joins our proxy. We first create a new Client to connect to
|
|
// the remote server. Then we listen to some events and proxy them over. The queue and
|
|
// flushing logic is more of an accessory to make sure the server or client recieves
|
|
// a packet, no matter what state it's in. For example, if the client wants to send a
|
|
// packet to the server but it's not connected, it will add to the queue and send as soon
|
|
// as a connection with the server is established.
|
|
openUpstreamConnection (ds, clientAddr) {
|
|
const client = new Client({
|
|
authTitle: this.options.authTitle,
|
|
offline: this.options.destination.offline ?? this.options.offline,
|
|
username: this.options.offline ? ds.profile.name : null,
|
|
version: this.options.version,
|
|
host: this.options.destination.host,
|
|
port: this.options.destination.port,
|
|
onMsaCode: this.options.onMsaCode,
|
|
profilesFolder: this.options.profilesFolder,
|
|
autoInitPlayer: false
|
|
})
|
|
// Set the login payload unless `noLoginForward` option
|
|
if (!client.noLoginForward) client.options.skinData = ds.skinData
|
|
client.ping().then(pongData => {
|
|
client.connect()
|
|
}).catch(err => {
|
|
this.emit('error', err)
|
|
})
|
|
this.conLog('Connecting to', this.options.destination.host, this.options.destination.port)
|
|
client.outLog = ds.upOutLog
|
|
client.inLog = ds.upInLog
|
|
client.once('join', () => {
|
|
// Tell the server to disable chunk cache for this connection as a client.
|
|
// Wait a bit for the server to ack and process, the continue with proxying
|
|
// otherwise the player can get stuck in an empty world.
|
|
client.write('client_cache_status', { enabled: this.enableChunkCaching })
|
|
ds.upstream = client
|
|
ds.flushUpQueue()
|
|
this.conLog('Connected to upstream server')
|
|
client.readPacket = (packet) => ds.readUpstream(packet)
|
|
|
|
this.emit('join', /* client connected to proxy */ ds, /* backend server */ client)
|
|
})
|
|
this.upstreams.set(clientAddr.hash, client)
|
|
}
|
|
|
|
// Close a connection to a remote backend server.
|
|
closeUpstreamConnection (clientAddr) {
|
|
const up = this.upstreams.get(clientAddr.hash)
|
|
if (!up) throw Error(`unable to close non-open connection ${clientAddr.hash}`)
|
|
up.close()
|
|
this.upstreams.delete(clientAddr.hash)
|
|
this.conLog('closed upstream connection', clientAddr)
|
|
}
|
|
|
|
// Called when a new player connects to our proxy server. Once the player has authenticted,
|
|
// we can open an upstream connection to the backend server.
|
|
onOpenConnection = (conn) => {
|
|
if (this.forceSingle && this.clientCount > 0) {
|
|
this.conLog('dropping connection as single client relay', conn)
|
|
conn.close()
|
|
} else {
|
|
this.clientCount++
|
|
const player = new this.RelayPlayer(this, conn)
|
|
this.conLog('New connection from', conn.address)
|
|
this.clients[conn.address] = player
|
|
this.emit('connect', player)
|
|
player.on('login', () => {
|
|
this.openUpstreamConnection(player, conn.address)
|
|
})
|
|
}
|
|
}
|
|
|
|
// When our server is closed, make sure to kick all of the connected clients and run emitters.
|
|
close (...a) {
|
|
for (const [, v] of this.upstreams) {
|
|
v.close(...a)
|
|
}
|
|
super.close(...a)
|
|
}
|
|
}
|
|
|
|
// Too many things called 'Proxy' ;)
|
|
module.exports = { Relay }
|