diff --git a/src/proxy/ProxyServer.js b/src/proxy/ProxyServer.js index 66a8133..d974687 100644 --- a/src/proxy/ProxyServer.js +++ b/src/proxy/ProxyServer.js @@ -6,14 +6,28 @@ const { disableInboundChatValidation } = require('../utils/chatRelay'); const log = createLogger('ProxyServer'); class ProxyServer { - constructor(config, onClientConnect, worldState) { + /** + * @param {object} config + * @param {(client: object) => void} onClientConnect + * @param {import('../state/WorldStateCache').WorldStateCache} worldState + * @param {() => { ok: boolean, reason?: string }} [canAcceptClient] + */ + constructor(config, onClientConnect, worldState, canAcceptClient) { this.config = config; this.onClientConnect = onClientConnect; this.worldState = worldState; + this.canAcceptClient = canAcceptClient; this.server = null; this.activeClient = null; } + /** Clear the single-client slot if it belongs to this connection. */ + releaseClient(client) { + if (this.activeClient === client) { + this.activeClient = null; + } + } + start() { this.server = mc.createServer({ host: this.config.proxy.host || '0.0.0.0', @@ -31,8 +45,26 @@ class ProxyServer { }, }); - // Replay upstream server's raw config packets before minecraft-protocol's parsed registry. this.server.on('login', (client) => { + wrapClientEnd(client); + + const slot = this.canAcceptClient?.() ?? { + ok: !this.activeClient, + reason: 'Only one client can connect at a time.', + }; + if (!slot.ok || this.activeClient) { + const reason = slot.reason || 'Only one client can connect at a time.'; + log.warn(`Rejecting login for ${client.username || 'client'}: ${reason}`); + client.end(reason); + return; + } + + this.activeClient = client; + client.on('end', () => { + log.info(`Client disconnected: ${client.username}`); + this.releaseClient(client); + }); + client.prependOnceListener('login_acknowledged', () => { const packets = this.worldState.getRawConfigPacketsForReplay(); if (packets.length === 0) return; @@ -49,24 +81,15 @@ class ProxyServer { }); this.server.on('playerJoin', (client) => { - wrapClientEnd(client); disableInboundChatValidation(client); - log.info(`Client ready: ${client.username}`); - if (this.activeClient) { - client.end('Another client is already connected.'); + if (this.activeClient !== client) { + log.warn(`Rejecting playerJoin for ${client.username} — not the active client`); + client.end('Only one client can connect at a time.'); return; } - this.activeClient = client; - - client.on('end', () => { - log.info(`Client disconnected: ${client.username}`); - if (this.activeClient === client) { - this.activeClient = null; - } - }); - + log.info(`Client ready: ${client.username}`); this.onClientConnect(client); }); diff --git a/src/session/SessionManager.js b/src/session/SessionManager.js index 4916b10..5532d7b 100644 --- a/src/session/SessionManager.js +++ b/src/session/SessionManager.js @@ -37,7 +37,12 @@ class SessionManager { // Core components this.worldState = new WorldStateCache(config); this.serverConn = new ServerConnection(config, this.worldState); - this.proxyServer = new ProxyServer(config, (client) => this._onClientConnect(client), this.worldState); + this.proxyServer = new ProxyServer( + config, + (client) => this._onClientConnect(client), + this.worldState, + () => this._clientSlotStatus(), + ); this.replayer = new StateReplayer(this.worldState, this.serverConn); // Current client bridge (if in CLIENT_MODE) @@ -207,19 +212,42 @@ class SessionManager { } } + /** + * Whether a new Java client may take the single proxy slot. + * @returns {{ ok: boolean, reason?: string }} + */ + _clientSlotStatus() { + if (this.currentClient || this.proxyServer.activeClient) { + return { ok: false, reason: 'Only one client can connect at a time.' }; + } + if (this.state === State.INIT) { + return { + ok: false, + reason: 'Proxy is still connecting to the server. Try again in a moment.', + }; + } + if (this.state !== State.BOT_MODE) { + return { ok: false, reason: 'Another client session is active.' }; + } + return { ok: true }; + } + + _rejectClient(client, reason) { + log.warn(`Rejecting ${client.username}: ${reason}`); + client.end(reason); + this.proxyServer.releaseClient(client); + } + /** * Handle a new Java client connection from the proxy server. */ async _onClientConnect(client) { - if (this.state === State.INIT) { - log.warn('Client connected but bot is not ready yet — rejecting'); - client.end('Proxy is still connecting to the server. Try again in a moment.'); + if (this.proxyServer.activeClient !== client) { + this._rejectClient(client, 'Only one client can connect at a time.'); return; } - - if (this.state === State.HANDOFF || this.state === State.CLIENT_MODE) { - log.warn('Client connected but another client is active — rejecting'); - client.end('Another client session is active.'); + if (this.state !== State.BOT_MODE || this.currentClient) { + this._rejectClient(client, 'Another client session is active.'); return; } @@ -279,8 +307,10 @@ class SessionManager { this.clientBridge.stop(); this.clientBridge = null; } + if (this.currentClient) { + this.proxyServer.releaseClient(this.currentClient); + } this.currentClient = null; - this.proxyServer.activeClient = null; } /**