@quake2ts/server 0.0.754 → 0.0.757
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.cjs +3 -1
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +3 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/dedicated.d.ts +1 -0
- package/dist/types/dedicated.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/dedicated.ts +4 -1
- package/tests/unit/ratelimit.test.ts +106 -0
- package/tests/unit/ratelimit_full.test.ts +116 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { DedicatedServer } from '../../src/dedicated';
|
|
3
|
+
import { ClientState } from '../../src/client';
|
|
4
|
+
import { NetworkTransport } from '../../src/transport';
|
|
5
|
+
import { NetDriver } from '@quake2ts/shared';
|
|
6
|
+
import { MockNetDriver } from '@quake2ts/test-utils';
|
|
7
|
+
|
|
8
|
+
// Mock Transport (Server Side)
|
|
9
|
+
class MockServerTransport implements NetworkTransport {
|
|
10
|
+
listen(port: number): Promise<void> { return Promise.resolve(); }
|
|
11
|
+
close(): void {}
|
|
12
|
+
onConnection(cb: (driver: NetDriver, info?: any) => void): void { this.connCb = cb; }
|
|
13
|
+
onError(cb: (err: Error) => void): void {}
|
|
14
|
+
|
|
15
|
+
public connCb: ((driver: NetDriver, info?: any) => void) | null = null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('Rate Limiting', () => {
|
|
19
|
+
let server: DedicatedServer;
|
|
20
|
+
let transport: MockServerTransport;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
transport = new MockServerTransport();
|
|
24
|
+
server = new DedicatedServer({ transport, maxPlayers: 1, floodLimit: 200 });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should kick a client that sends too many commands in a second', async () => {
|
|
28
|
+
const s = server as any;
|
|
29
|
+
s.svs.initialized = true;
|
|
30
|
+
|
|
31
|
+
const driver = new MockNetDriver();
|
|
32
|
+
s.handleConnection(driver);
|
|
33
|
+
|
|
34
|
+
const client = s.svs.clients[0];
|
|
35
|
+
expect(client).toBeDefined();
|
|
36
|
+
|
|
37
|
+
client.state = ClientState.Active;
|
|
38
|
+
client.edict = {};
|
|
39
|
+
|
|
40
|
+
// Scenario 1: Flood within the window
|
|
41
|
+
client.commandCount = 201;
|
|
42
|
+
client.lastCommandTime = Date.now();
|
|
43
|
+
|
|
44
|
+
const dropSpy = vi.spyOn(s, 'dropClient');
|
|
45
|
+
|
|
46
|
+
// Execute logic (mimicking FIXED runFrame)
|
|
47
|
+
const runLogic = () => {
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
|
|
50
|
+
// FIXED ORDER
|
|
51
|
+
const limit = s.options.floodLimit ?? 200;
|
|
52
|
+
if (client.commandCount > limit) {
|
|
53
|
+
s.dropClient(client);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (now - client.lastCommandTime >= 1000) {
|
|
58
|
+
client.lastCommandTime = now;
|
|
59
|
+
client.commandCount = 0;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
runLogic();
|
|
64
|
+
expect(dropSpy).toHaveBeenCalledWith(client);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should kick even if flood is detected after window reset (Loophole Fixed)', async () => {
|
|
68
|
+
const s = server as any;
|
|
69
|
+
s.svs.initialized = true;
|
|
70
|
+
|
|
71
|
+
const driver = new MockNetDriver();
|
|
72
|
+
s.handleConnection(driver);
|
|
73
|
+
|
|
74
|
+
const client = s.svs.clients[0];
|
|
75
|
+
client.state = ClientState.Active;
|
|
76
|
+
client.edict = {};
|
|
77
|
+
|
|
78
|
+
const dropSpy = vi.spyOn(s, 'dropClient');
|
|
79
|
+
|
|
80
|
+
// Scenario 2: Flood accumulated, but window expires right as we check
|
|
81
|
+
client.commandCount = 300;
|
|
82
|
+
client.lastCommandTime = Date.now() - 1100; // Window expired
|
|
83
|
+
|
|
84
|
+
// Execute logic (mimicking FIXED runFrame)
|
|
85
|
+
const runLogic = () => {
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
|
|
88
|
+
// FIXED ORDER
|
|
89
|
+
const limit = s.options.floodLimit ?? 200;
|
|
90
|
+
if (client.commandCount > limit) {
|
|
91
|
+
s.dropClient(client);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (now - client.lastCommandTime >= 1000) {
|
|
96
|
+
client.lastCommandTime = now;
|
|
97
|
+
client.commandCount = 0;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
runLogic();
|
|
102
|
+
|
|
103
|
+
// NOW we expect it to be called
|
|
104
|
+
expect(dropSpy).toHaveBeenCalledWith(client);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { DedicatedServer } from '../../src/dedicated';
|
|
3
|
+
import { ClientState } from '../../src/client';
|
|
4
|
+
import { NetworkTransport } from '../../src/transport';
|
|
5
|
+
import { NetDriver, NetChan, ClientCommand, BinaryWriter } from '@quake2ts/shared';
|
|
6
|
+
import { MockNetDriver } from '@quake2ts/test-utils';
|
|
7
|
+
|
|
8
|
+
// Mock Transport (Server Side)
|
|
9
|
+
class MockServerTransport implements NetworkTransport {
|
|
10
|
+
listen(port: number): Promise<void> { return Promise.resolve(); }
|
|
11
|
+
close(): void {}
|
|
12
|
+
onConnection(cb: (driver: NetDriver, info?: any) => void): void { this.connCb = cb; }
|
|
13
|
+
onError(cb: (err: Error) => void): void {}
|
|
14
|
+
|
|
15
|
+
public connCb: ((driver: NetDriver, info?: any) => void) | null = null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('Full Stack Rate Limiting', () => {
|
|
19
|
+
let server: DedicatedServer;
|
|
20
|
+
let transport: MockServerTransport;
|
|
21
|
+
let driver: MockNetDriver;
|
|
22
|
+
let clientNetChan: NetChan;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
transport = new MockServerTransport();
|
|
26
|
+
server = new DedicatedServer({ transport, maxPlayers: 1, floodLimit: 200 });
|
|
27
|
+
driver = new MockNetDriver();
|
|
28
|
+
clientNetChan = new NetChan();
|
|
29
|
+
// Setup client netchan (qport 0 for simplicity)
|
|
30
|
+
clientNetChan.setup(0);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should process packets through netchan and kick client on flood', async () => {
|
|
34
|
+
const s = server as any;
|
|
35
|
+
s.svs.initialized = true;
|
|
36
|
+
|
|
37
|
+
// 1. Connect Client
|
|
38
|
+
// Manually trigger handleConnection
|
|
39
|
+
s.handleConnection(driver);
|
|
40
|
+
const client = s.svs.clients[0];
|
|
41
|
+
expect(client).toBeDefined();
|
|
42
|
+
client.state = ClientState.Active;
|
|
43
|
+
client.edict = {}; // Mock edict
|
|
44
|
+
|
|
45
|
+
// 2. Prepare a movement packet
|
|
46
|
+
const writer = new BinaryWriter();
|
|
47
|
+
writer.writeByte(ClientCommand.move);
|
|
48
|
+
writer.writeByte(0); // Checksum
|
|
49
|
+
writer.writeLong(0); // LastFrame
|
|
50
|
+
// UserCommand
|
|
51
|
+
writer.writeByte(16); // msec
|
|
52
|
+
writer.writeByte(0); // buttons
|
|
53
|
+
writer.writeAngle16(0); // angles
|
|
54
|
+
writer.writeAngle16(0);
|
|
55
|
+
writer.writeAngle16(0);
|
|
56
|
+
writer.writeShort(0); // forward
|
|
57
|
+
writer.writeShort(0); // side
|
|
58
|
+
writer.writeShort(0); // up
|
|
59
|
+
writer.writeByte(0); // impulse
|
|
60
|
+
writer.writeByte(0); // lightlevel
|
|
61
|
+
|
|
62
|
+
const packetData = writer.getData();
|
|
63
|
+
|
|
64
|
+
// 3. Flood packets
|
|
65
|
+
// Send 300 packets via NetChan
|
|
66
|
+
const packetCount = 300;
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < packetCount; i++) {
|
|
69
|
+
// NetChan wraps it
|
|
70
|
+
const netPacket = clientNetChan.transmit(packetData);
|
|
71
|
+
|
|
72
|
+
// Simulate receiving on server driver
|
|
73
|
+
// MockNetDriver.receiveMessage calls handlers
|
|
74
|
+
driver.receiveMessage(netPacket);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// At this point, packets are in client.messageQueue
|
|
78
|
+
expect(client.messageQueue.length).toBe(packetCount);
|
|
79
|
+
|
|
80
|
+
// 4. Run Server Frame
|
|
81
|
+
// This triggers SV_ReadPackets -> handleMove -> increment count -> check limit
|
|
82
|
+
// We can't call runFrame directly as it's private and loops.
|
|
83
|
+
// But we can call the methods it calls if we cast to any,
|
|
84
|
+
// OR we can spy on dropClient and emulate runFrame logic.
|
|
85
|
+
|
|
86
|
+
// Let's emulate runFrame logic exactly as implemented in dedicated.ts
|
|
87
|
+
const dropSpy = vi.spyOn(s, 'dropClient');
|
|
88
|
+
|
|
89
|
+
// Emulate SV_ReadPackets
|
|
90
|
+
s.SV_ReadPackets();
|
|
91
|
+
|
|
92
|
+
// Check if commands were processed
|
|
93
|
+
expect(client.commandCount).toBe(packetCount);
|
|
94
|
+
|
|
95
|
+
// Emulate Rate Check logic
|
|
96
|
+
const runLogic = () => {
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
|
|
99
|
+
// Logic from dedicated.ts
|
|
100
|
+
const limit = s.options.floodLimit ?? 200;
|
|
101
|
+
if (client.commandCount > limit) {
|
|
102
|
+
s.dropClient(client);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (now - client.lastCommandTime >= 1000) {
|
|
107
|
+
client.lastCommandTime = now;
|
|
108
|
+
client.commandCount = 0;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
runLogic();
|
|
113
|
+
|
|
114
|
+
expect(dropSpy).toHaveBeenCalledWith(client);
|
|
115
|
+
});
|
|
116
|
+
});
|