incognitolm commited on
Commit
8dde1b0
·
1 Parent(s): e70ae7e

Per-IP Guest Rate Limiter

Browse files
server/cryptoUtils.js CHANGED
@@ -46,6 +46,7 @@ export function decryptJson(encryptedData) {
46
 
47
  export async function saveEncryptedJson(filePath, data) {
48
  const encrypted = encryptJson(data);
 
49
  await fs.writeFile(filePath, JSON.stringify(encrypted));
50
  }
51
 
 
46
 
47
  export async function saveEncryptedJson(filePath, data) {
48
  const encrypted = encryptJson(data);
49
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
50
  await fs.writeFile(filePath, JSON.stringify(encrypted));
51
  }
52
 
server/guestRequestLimiter.js ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { loadEncryptedJson, saveEncryptedJson } from './cryptoUtils.js';
2
+
3
+ const REQUESTS_FILE = '/data/guest_request_counts.json';
4
+ const WINDOW_MS = 24 * 60 * 60 * 1000;
5
+ const MAX_LOGGED_OUT_REQUESTS = 50;
6
+
7
+ const ipCounters = new Map();
8
+ let loaded = false;
9
+
10
+ async function loadGuestCounters() {
11
+ if (loaded) return;
12
+ loaded = true;
13
+ const data = await loadEncryptedJson(REQUESTS_FILE);
14
+ if (!data) return;
15
+ for (const [ip, entry] of Object.entries(data)) {
16
+ ipCounters.set(ip, {
17
+ count: typeof entry.count === 'number' ? entry.count : 0,
18
+ resetAt: typeof entry.resetAt === 'number' ? entry.resetAt : Date.now() + WINDOW_MS,
19
+ });
20
+ }
21
+ }
22
+
23
+ async function saveGuestCounters() {
24
+ const data = {};
25
+ for (const [ip, entry] of ipCounters) {
26
+ data[ip] = { count: entry.count, resetAt: entry.resetAt };
27
+ }
28
+ await saveEncryptedJson(REQUESTS_FILE, data);
29
+ }
30
+
31
+ function cleanupExpired() {
32
+ const now = Date.now();
33
+ for (const [ip, entry] of ipCounters) {
34
+ if (entry.resetAt <= now) {
35
+ ipCounters.delete(ip);
36
+ }
37
+ }
38
+ }
39
+
40
+ export async function initGuestRequestLimiter() {
41
+ await loadGuestCounters().catch(err => console.error('Failed to load guest request counters:', err));
42
+ cleanupExpired();
43
+ setInterval(() => {
44
+ cleanupExpired();
45
+ }, 60 * 60 * 1000);
46
+ }
47
+
48
+ export async function consumeGuestRequest(ip) {
49
+ await loadGuestCounters();
50
+ const now = Date.now();
51
+ let entry = ipCounters.get(ip);
52
+ if (!entry || entry.resetAt <= now) {
53
+ entry = { count: 0, resetAt: now + WINDOW_MS };
54
+ ipCounters.set(ip, entry);
55
+ }
56
+ if (entry.count >= MAX_LOGGED_OUT_REQUESTS) {
57
+ return false;
58
+ }
59
+ entry.count += 1;
60
+ await saveGuestCounters().catch(err => console.error('Failed to save guest request counters:', err));
61
+ return true;
62
+ }
63
+
64
+ export function getGuestRequestsRemaining(ip) {
65
+ const now = Date.now();
66
+ const entry = ipCounters.get(ip);
67
+ if (!entry || entry.resetAt <= now) return MAX_LOGGED_OUT_REQUESTS;
68
+ return Math.max(0, MAX_LOGGED_OUT_REQUESTS - entry.count);
69
+ }
server/wsHandler.js CHANGED
@@ -3,6 +3,7 @@ import { safeSend, broadcastToUser } from './helpers.js';
3
  import { LIGHTNING_BASE, PUBLIC_URL } from './config.js';
4
  import { sessionStore, deviceSessionStore } from './sessionStore.js';
5
  import { rateLimiter } from './rateLimiter.js';
 
6
  import {
7
  verifySupabaseToken, getUserSettings, saveUserSettings,
8
  getUserProfile, setUsername, getSubscriptionInfo,
@@ -13,12 +14,20 @@ import crypto from 'crypto';
13
 
14
  const activeStreams = new Map();
15
 
 
 
16
  export async function handleWsMessage(ws, msg, wsClients) {
17
  const client = wsClients.get(ws); if (!client) return;
18
  // Require turnstile verification for most message types
19
  if (!client.verified && msg.type !== 'ping' && msg.type !== 'turnstile:verify') {
20
  return safeSend(ws, { type: 'error', message: 'turnstile:required' });
21
  }
 
 
 
 
 
 
22
  const h = handlers[msg.type];
23
  if (h) return h(ws, msg, client, wsClients);
24
  safeSend(ws, { type: 'error', message: `Unknown: ${msg.type}` });
 
3
  import { LIGHTNING_BASE, PUBLIC_URL } from './config.js';
4
  import { sessionStore, deviceSessionStore } from './sessionStore.js';
5
  import { rateLimiter } from './rateLimiter.js';
6
+ import { initGuestRequestLimiter, consumeGuestRequest } from './guestRequestLimiter.js';
7
  import {
8
  verifySupabaseToken, getUserSettings, saveUserSettings,
9
  getUserProfile, setUsername, getSubscriptionInfo,
 
14
 
15
  const activeStreams = new Map();
16
 
17
+ initGuestRequestLimiter().catch(err => console.error('Failed to initialize guest request limiter:', err));
18
+
19
  export async function handleWsMessage(ws, msg, wsClients) {
20
  const client = wsClients.get(ws); if (!client) return;
21
  // Require turnstile verification for most message types
22
  if (!client.verified && msg.type !== 'ping' && msg.type !== 'turnstile:verify') {
23
  return safeSend(ws, { type: 'error', message: 'turnstile:required' });
24
  }
25
+
26
+ if (!client.userId && msg.type !== 'ping' && msg.type !== 'turnstile:verify') {
27
+ const allowed = await consumeGuestRequest(client.ip || 'unknown');
28
+ if (!allowed) return safeSend(ws, { type: 'error', message: 'Guest request limit exceeded' });
29
+ }
30
+
31
  const h = handlers[msg.type];
32
  if (h) return h(ws, msg, client, wsClients);
33
  safeSend(ws, { type: 'error', message: `Unknown: ${msg.type}` });