Skip to content

Commit 3d040fd

Browse files
committed
Add IP session limiting
1 parent 74959eb commit 3d040fd

6 files changed

Lines changed: 57 additions & 7 deletions

File tree

web/frontend/src/App.svelte

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
<script lang="ts">
22
import { onMount, afterUpdate } from "svelte";
33
import Modal from "./lib/Modal.svelte";
4+
import IPLimited from "./lib/IPLimited.svelte";
45
import Console from "./lib/Console.svelte";
56
import Editor from "./lib/Editor.svelte";
67
import ScriptViewer from "./lib/ScriptViewer.svelte";
78
import { isEditorOpen, sessionState, scriptMap, sessionMetadata } from "./lib/stores";
89
910
let session = { id: null };
1011
let socket: WebSocket | null = null;
11-
let view: "modal" | "loading" | "not-found" | "console" = "modal";
12+
let view: "modal" | "loading" | "not-found" | "console" | "ip-limited" = "modal";
13+
let ipLimit = 2;
1214
let readonlyLogs: string | null = null;
1315
let editorPanel: HTMLElement;
1416
let consolePanel: HTMLElement;
@@ -129,8 +131,10 @@
129131

130132
<ScriptViewer />
131133
<main class="flex flex-row h-screen overflow-hidden">
132-
{#if view === "modal"}
133-
<Modal on:startsession={(e) => connectWebSocket(e.detail.sessionId, e.detail.sessionType)} />
134+
{#if view === "ip-limited"}
135+
<IPLimited limit={ipLimit} />
136+
{:else if view === "modal"}
137+
<Modal on:startsession={(e) => connectWebSocket(e.detail.sessionId, e.detail.sessionType)} on:iplimited={(e) => { ipLimit = e.detail.limit; view = "ip-limited"; }} />
134138
{:else if view === "loading"}
135139
<div class="flex items-center justify-center w-full h-full bg-gray-900">
136140
<span class="text-gray-500 font-mono text-sm">Loading session...</span>
@@ -161,7 +165,8 @@
161165
</button>
162166
</div>
163167
{#if $isEditorOpen}
164-
<div id="resizer" on:mousedown={handleMouseDown}></div>
168+
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
169+
<div id="resizer" on:mousedown={handleMouseDown} role="separator" aria-orientation="vertical"></div>
165170
<div bind:this={editorPanel} class="h-full" style="width: 33%;">
166171
<Editor {socket} />
167172
</div>

web/frontend/src/lib/Console.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@
230230
<div class="mt-auto p-4">
231231
<div class="flex items-center border-t border-gray-700 pt-3">
232232
<span class="{inactive ? 'text-gray-600' : 'text-green-400'} mr-2 shrink-0">&gt;</span>
233+
<!-- svelte-ignore a11y-autofocus -->
233234
<input type="text" bind:this={commandInput} on:keydown={handleKeydown} class="w-full bg-transparent border-none focus:ring-0 focus:outline-none {inactive ? 'text-gray-600 placeholder-gray-600 cursor-not-allowed' : 'text-gray-200 placeholder-gray-500'}" placeholder="{inactive ? 'Session ended' : 'Enter command...'}" autocomplete="off" autofocus disabled>
234235
</div>
235236
</div>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script lang="ts">
2+
export let limit: number = 2;
3+
</script>
4+
5+
<div class="fixed inset-0 bg-gray-900 bg-opacity-80 flex items-center justify-center z-50 transition-opacity duration-300">
6+
<div class="bg-gray-800 p-8 rounded-lg shadow-2xl transform scale-95 transition-transform duration-300 w-full max-w-md">
7+
<svg class="h-10 w-10 text-red-400 mx-auto mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
8+
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
9+
</svg>
10+
<h2 class="text-2xl font-bold mb-2 text-white">Session Limit Reached</h2>
11+
<p class="text-gray-400">You already have {limit} active session{limit === 1 ? "" : "s"} running. Please wait for an existing session to end before starting a new one.</p>
12+
</div>
13+
</div>

web/frontend/src/lib/Modal.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
1818
if (data.status === "READY") {
1919
dispatch("startsession", { sessionId: data.sessionId, sessionType: containerType });
20+
} else if (data.status === "IP_LIMIT") {
21+
dispatch("iplimited", { limit: data.limit });
22+
inQueue = false;
2023
} else if (data.status === "QUEUED") {
2124
queuePosition = data.position;
2225
pollQueueStatus(data.ticketId);

web/frontend/src/lib/ScriptViewer.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@
3535
<svelte:window on:keydown={handleKeydown} />
3636

3737
{#if $viewingScript}
38-
<div class="fixed inset-0 bg-gray-900 bg-opacity-80 flex items-center justify-center z-[60]" on:click={handleBackdropClick}>
38+
<div class="fixed inset-0 bg-gray-900 bg-opacity-80 flex items-center justify-center z-[60]" on:click={handleBackdropClick} on:keydown={handleKeydown} role="dialog" aria-modal="true" aria-label="Script viewer" tabindex="-1">
3939
<div class="bg-gray-800 rounded-lg shadow-2xl w-full max-w-2xl max-h-[80vh] flex flex-col border border-gray-700/50">
4040
<div class="flex justify-between items-center px-4 py-3 border-b border-gray-700">
4141
<span class="font-mono text-sm text-indigo-400">{$viewingScript.name}</span>
42-
<button on:click={close} class="text-gray-400 hover:text-white transition-colors">
42+
<button on:click={close} class="text-gray-400 hover:text-white transition-colors" aria-label="Close script viewer">
4343
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
4444
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
4545
</svg>

web/src/index.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,11 +428,15 @@ export class GmodSixtyFour extends BaseSession {}
428428
export class GmodPrerelease extends BaseSession {}
429429
export class GmodDev extends BaseSession {}
430430

431+
const MAX_SESSIONS_PER_IP = 2;
432+
431433
export class QueueDO extends DurableObject<Env> {
432434
activeSessions: Map<string, string>; // sessionId → type
435+
sessionIPs: Map<string, string>; // sessionId → IP
433436
waitingQueue: {
434437
ticketId: string;
435438
sessionType: string;
439+
ip: string;
436440
resolve: (value: string) => void
437441
}[];
438442
resolvedTickets: Map<string, { sessionId: string; sessionType: string }>;
@@ -441,6 +445,7 @@ export class QueueDO extends DurableObject<Env> {
441445
constructor(ctx: DurableObjectState, env: Env) {
442446
super(ctx, env);
443447
this.activeSessions = new Map();
448+
this.sessionIPs = new Map();
444449
this.waitingQueue = [];
445450
this.resolvedTickets = new Map();
446451
this.maxPerType = {
@@ -452,6 +457,14 @@ export class QueueDO extends DurableObject<Env> {
452457

453458
}
454459

460+
activeSessionCountForIP(ip: string): number {
461+
let count = 0;
462+
for (const [sessionId, sessionIp] of this.sessionIPs) {
463+
if (sessionIp === ip && this.activeSessions.has(sessionId)) count++;
464+
}
465+
return count;
466+
}
467+
455468
activeCountForType(type: string): number {
456469
let count = 0;
457470
for (const t of this.activeSessions.values()) {
@@ -472,13 +485,26 @@ export class QueueDO extends DurableObject<Env> {
472485

473486
if (url.pathname === "/api/request-session") {
474487
const sessionType = url.searchParams.get("type") || "public";
488+
const clientIP = request.headers.get("CF-Connecting-IP") || "unknown";
489+
490+
if (clientIP !== "unknown" && this.activeSessionCountForIP(clientIP) >= MAX_SESSIONS_PER_IP) {
491+
return new Response(JSON.stringify({
492+
status: "IP_LIMIT",
493+
limit: MAX_SESSIONS_PER_IP,
494+
}), {
495+
status: 429,
496+
headers: { "Content-Type": "application/json" },
497+
});
498+
}
499+
475500
if (this.hasCapacity(sessionType)) {
476501
const sessionId = crypto.randomUUID();
477502
this.activeSessions.set(sessionId, sessionType);
503+
this.sessionIPs.set(sessionId, clientIP);
478504
return new Response(JSON.stringify({ status: "READY", sessionId }), { headers: { "Content-Type": "application/json" }, });
479505
} else {
480506
const ticketId = crypto.randomUUID();
481-
this.waitingQueue.push({ ticketId, sessionType, resolve: (sessionId: string) => {
507+
this.waitingQueue.push({ ticketId, sessionType, ip: clientIP, resolve: (sessionId: string) => {
482508
this.resolvedTickets.set(ticketId, { sessionId, sessionType });
483509
}});
484510
const position = this.waitingQueue.filter(w => w.sessionType === sessionType).length;
@@ -542,13 +568,15 @@ export class QueueDO extends DurableObject<Env> {
542568
const { sessionId } = await request.json<{sessionId: string}>();
543569
const closedType = this.activeSessions.get(sessionId);
544570
this.activeSessions.delete(sessionId);
571+
this.sessionIPs.delete(sessionId);
545572

546573
if (closedType) {
547574
const idx = this.waitingQueue.findIndex(w => w.sessionType === closedType);
548575
if (idx !== -1) {
549576
const nextInLine = this.waitingQueue.splice(idx, 1)[0];
550577
const newSessionId = crypto.randomUUID();
551578
this.activeSessions.set(newSessionId, nextInLine.sessionType);
579+
this.sessionIPs.set(newSessionId, nextInLine.ip);
552580
nextInLine.resolve(newSessionId);
553581
}
554582
}

0 commit comments

Comments
 (0)