From 572179f0c594a82d2c4cbea53569dd812dea8596 Mon Sep 17 00:00:00 2001 From: Xekep Date: Fri, 27 Feb 2026 19:10:26 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BF=D0=BE=D0=B4=D1=81=D1=87=D0=B5=D1=82=20?= =?UTF-8?q?=D0=BE=D0=BD=D0=BB=D0=B0=D0=B9=D0=BD=D0=B0=20=D0=B8=20=D0=BB?= =?UTF-8?q?=D0=B8=D0=BC=D0=B8=D1=82=20=D0=BF=D0=BE=D0=B4=D0=BA=D0=BB=D1=8E?= =?UTF-8?q?=D1=87=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/dimensions/client.ts | 4 + app/dimensions/clientcommandhandler.ts | 6 +- app/dimensions/listenserver.ts | 28 ++-- .../dimensions/clientcommandhandlerspec.ts | 22 ++- app/spec/dimensions/listenserverspec.ts | 138 ++++++++++++++++++ 5 files changed, 182 insertions(+), 16 deletions(-) create mode 100644 app/spec/dimensions/listenserverspec.ts diff --git a/app/dimensions/client.ts b/app/dimensions/client.ts index f05b2b9..a75f261 100644 --- a/app/dimensions/client.ts +++ b/app/dimensions/client.ts @@ -239,6 +239,10 @@ class Client { return this.player.name; } + public getTrackedPlayerCount(): number { + return Object.keys(this.globalTracking.names).length; + } + public handleDataSend(encodedData: Buffer): void { try { // Add Buffer Packet (incomplete packet from last data) diff --git a/app/dimensions/clientcommandhandler.ts b/app/dimensions/clientcommandhandler.ts index 9f442ea..b3dda9c 100644 --- a/app/dimensions/clientcommandhandler.ts +++ b/app/dimensions/clientcommandhandler.ts @@ -74,11 +74,7 @@ export class ClientCommandHandler { * @return Whether or not the who command was handled */ private handleWho(client: Client): boolean { - let total: number = 0; - let keys: string[] = Object.keys(client.serversDetails); - for (let i: number = 0, len = keys.length; i < len; i++) { - total += client.serversDetails[keys[i]].clientCount; - } + const total = client.getTrackedPlayerCount(); // Try to make it come after the normal response setTimeout(function() { diff --git a/app/dimensions/listenserver.ts b/app/dimensions/listenserver.ts index 94faeea..2044c72 100644 --- a/app/dimensions/listenserver.ts +++ b/app/dimensions/listenserver.ts @@ -303,8 +303,19 @@ export class ListenServer { * @param socket The socket of a new client */ private async handleSocket(socket: Net.Socket): Promise { - if ((this.options.connectionLimit.enabled && this.enforceConnectionLimit(socket)) - || this.options.connectionRateLimit.enabled && this.enforceConnectionRateLimit(socket)) { + const socketIp = socket.remoteAddress; + + if (this.options.connectionLimit.enabled && this.enforceConnectionLimit(socket)) { + socket.removeAllListeners(); + return; + } + + if (this.options.connectionRateLimit.enabled && this.enforceConnectionRateLimit(socket)) { + // If connectionLimit accepted this socket first, rollback its tracker entry + // when rate limit later rejects the same connection. + if (this.options.connectionLimit.enabled && typeof socketIp !== "undefined") { + this.decrementConnectionTracker(socketIp); + } socket.removeAllListeners(); return; } @@ -400,13 +411,13 @@ export class ListenServer { * the socket. Also checking if the ip address of this socket is blacklisted. */ private async setupNewSocket(socket: Net.Socket): Promise { + const socketIp = socket.remoteAddress; let chosenServer: RoutingServer | null = this.chooseServer(); if (chosenServer === null) { this.logging.warn(`No servers available for ListenServer[Port: ${this.port}]`); socket.destroy(); - const ip = socket.remoteAddress; - if (typeof ip !== "undefined") { - this.decrementConnectionTracker(ip); + if (typeof socketIp !== "undefined") { + this.decrementConnectionTracker(socketIp); } return; } @@ -482,9 +493,8 @@ export class ListenServer { if (index > -1) { this.checkingClients.splice(index, 1); } - const ip = clientArgs.socket.remoteAddress; - if (typeof ip !== "undefined") { - this.decrementConnectionTracker(ip); + if (typeof socketIp !== "undefined") { + this.decrementConnectionTracker(socketIp); } } }); @@ -671,7 +681,7 @@ export class ListenServer { } try { - const ip = socket.remoteAddress; + const ip = client.ip || socket.remoteAddress; if (typeof ip !== "undefined") { this.decrementConnectionTracker(ip); } diff --git a/app/spec/dimensions/clientcommandhandlerspec.ts b/app/spec/dimensions/clientcommandhandlerspec.ts index 11ec51e..ab64509 100644 --- a/app/spec/dimensions/clientcommandhandlerspec.ts +++ b/app/spec/dimensions/clientcommandhandlerspec.ts @@ -19,10 +19,10 @@ describe("ClientCommandHandler", () => { let serverB: RoutingServer; let socket: Net.Socket; let tcpServer: Net.Server; - let serversDetails; + let serversDetails: any; let globalHandlers; let servers; - let globalTracking; + let globalTracking: any; let client: Client; // @ts-ignore let server: TerrariaServer; @@ -204,6 +204,24 @@ describe("ClientCommandHandler", () => { expect(handled).toBe(false); }); + it("should use tracked player names for who count", (done: DoneFn) => { + serversDetails.servera.clientCount = 8; + serversDetails.serverb.clientCount = 9; + globalTracking.names = { one: true, two: true }; + + const handler = (data: string) => { + if (data.indexOf("There are 2 players across all Dimensions") === -1) { + return; + } + clientSocketDataHandlers = clientSocketDataHandlers.filter(h => h !== handler); + done(); + }; + clientSocketDataHandlers.push(handler); + + let command = client.globalHandlers.command.parseCommand("/who"); + client.globalHandlers.command.handle(command, client); + }); + it("should send the user a user count", (done: DoneFn) => { const handler = (data: string) => { if (data.indexOf("There are 0 players across all Dimensions") === -1) { diff --git a/app/spec/dimensions/listenserverspec.ts b/app/spec/dimensions/listenserverspec.ts new file mode 100644 index 0000000..eaef4cc --- /dev/null +++ b/app/spec/dimensions/listenserverspec.ts @@ -0,0 +1,138 @@ +import * as Net from 'net'; +import * as winston from 'winston'; +import ListenServer from '../../dimensions/listenserver.js'; +import ListenServerArgs from '../../dimensions/listenserverargs.js'; +import RoutingServer from '../../dimensions/routingserver.js'; +import ClientCommandHandler from '../../dimensions/clientcommandhandler.js'; +import ClientPacketHandler from '../../dimensions/clientpackethandler.js'; +import TerrariaServerPacketHandler from '../../dimensions/terrariaserverpackethandler.js'; +import { ConfigOptions } from '../../dimensions/configloader.js'; +import * as Language from '../../dimensions/language.js'; + +describe("ListenServer", () => { + let listenServer!: ListenServer; + let connectionsTracker!: Map; + let connectRateTracker!: Map; + + beforeEach(() => { + connectionsTracker = new Map(); + connectRateTracker = new Map(); + + const routingServer: RoutingServer = { + name: "servera", + serverIP: "127.0.0.1", + serverPort: 7777, + hidden: false, + isVanilla: false, + }; + + const options: ConfigOptions = { + socketTimeout: 0, + socketNoDelay: true, + fakeVersion: { + enabled: false, + terrariaVersion: 0 + }, + blacklist: { + enabled: false, + }, + blockInvis: false, + log: { + clientBlocked: false, + clientConnect: false, + clientDisconnect: false, + clientError: false, + clientTimeouts: false, + checkingClientConnect: false, + checkingClientDisconnect: false, + checkingClientError: false, + checkingClientTimeouts: false, + extensionLoad: false, + outputToFile: false, + outputToConsole: false, + tServerConnect: false, + tServerDisconnect: false, + tServerError: false, + extensionError: false, + }, + restApi: { + enabled: false, + port: 0 + }, + connectionLimit: { + enabled: true, + connectionLimitPerIP: 2, + kickReason: "" + }, + connectionRateLimit: { + enabled: true, + connectionRateLimitPerIP: 1 + }, + redis: { + enabled: false, + host: "localhost", + port: 6379 + }, + language: Language.english, + debuffOnSwitch: { enabled: false }, + disconnectOnKick: { type: "never" }, + hotReload: false + }; + + const args: ListenServerArgs = { + info: { + listenPort: 0, + routingServers: [routingServer] + }, + serversDetails: { + servera: { + clientCount: 0, + disabled: false, + disabledTimeout: null, + failedConnAttempts: 0 + } + }, + globalHandlers: { + command: new ClientCommandHandler(), + clientPacketHandler: new ClientPacketHandler(), + terrariaServerPacketHandler: new TerrariaServerPacketHandler(), + extensions: {} + }, + servers: { + servera: routingServer + }, + options, + globalTracking: { + names: {} + }, + logging: winston.createLogger({ silent: true }), + connectionsTracker, + connectRateTracker + }; + + listenServer = new ListenServer(args); + }); + + afterEach(() => { + listenServer.shutdown(); + }); + + it("should rollback the connection tracker when rate limit rejects a socket", async () => { + const ip = "127.0.0.1"; + connectRateTracker.set(ip, 1); + + const removeAllListeners = jasmine.createSpy("removeAllListeners"); + const destroy = jasmine.createSpy("destroy"); + const socket = { + remoteAddress: ip, + removeAllListeners, + destroy + } as unknown as Net.Socket; + + await (listenServer as any).handleSocket(socket); + + expect(connectionsTracker.has(ip)).toBe(false); + expect(removeAllListeners).toHaveBeenCalled(); + expect(destroy).toHaveBeenCalled(); + }); +});