incognitolm commited on
Commit ·
8dde1b0
1
Parent(s): e70ae7e
Per-IP Guest Rate Limiter
Browse files- server/cryptoUtils.js +1 -0
- server/guestRequestLimiter.js +69 -0
- server/wsHandler.js +9 -0
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}` });
|