Bei der Bestandsaufnahme als "keine Route hat eine Bremse" gemeldet -- das war falsch (grep suchte nach rateLimit/bremse, im Code heißen sie Kontingent/Sperre/Fehlversuche). Fast alle öffentlichen Routen SIND gebremst: Anmeldung, Supporter-Login, Upload, Kundenanfragen. Übrig blieben genau zwei schreibende Routen: Bewerbungen und Stimmen. Ohne Bremse könnte ein Skript die Datenbank mit Müll fluten. Kein Sicherheitsleck (beide landen in einer Warteschlange, nichts wird ungesehen veröffentlicht), aber eine sinnvolle Härtung. Statt das vorhandene Muster ein drittes Mal zu kopieren: ein Baustein lib/kontingent.js, den nun alle drei Routen nutzen. Zwei Verbesserungen gegenüber dem Original in testimonials.js: - getrennte Töpfe je Zweck (ein Bild-Upload verbraucht kein Bewerbungs-Kontingent) - Selbstreinigung: die alte Zähler-Map ließ jede IP für immer im Speicher stehen (langsames Leck), die neue räumt abgelaufene Einträge auf Grenzen: Uploads 10/Stunde/IP (belegen Plattenplatz), Text-Einreichungen 20/Stunde/IP (großzügig für geteilte Anschlüsse, stoppt Fluten). Tests: pruef-kontingent.mjs (Baustein, 7/7), test-kontingent-routen.mjs (echte Routen liefern 429 ab Grenze, getrennte Töpfe, IPs unabhängig, 4/4). Bestehende Tests unverändert grün (37/37). NOCH NICHT LIVE: server-internal läuft aus /home/dogiintern (kein Zugriff), wird mit den übrigen Server-Änderungen in einem Deploy live geschaltet. Co-Authored-By: Claude Opus 5 <[email protected]>
94 lines
4.3 KiB
JavaScript
94 lines
4.3 KiB
JavaScript
/* =====================================================================
|
|
Tut die Anfragebremse, was sie soll?
|
|
|
|
Geprüft wird das Verhalten, auf das es ankommt: Ab dem richtigen
|
|
Versuch sperren, verschiedene IPs und Zwecke sauber trennen, nach
|
|
Ablauf des Fensters wieder öffnen, und den Speicher nicht endlos
|
|
wachsen lassen.
|
|
===================================================================== */
|
|
import { kontingentFrei, _zuruecksetzen } from "./server-internal/lib/kontingent.js";
|
|
|
|
let ok = 0, fehl = 0;
|
|
const pruefe = (name, gut, info) => {
|
|
if (gut) { ok++; console.log(" ok " + name + (info ? " -> " + info : "")); }
|
|
else { fehl++; console.log(" FEHL " + name + (info ? " -> " + info : "")); }
|
|
};
|
|
|
|
console.log("\n=== Sperrt ab dem richtigen Versuch ===");
|
|
{
|
|
_zuruecksetzen();
|
|
let durch = 0;
|
|
for (let i = 0; i < 8; i++) if (kontingentFrei("test", "1.1.1.1", { maxProFenster: 5 })) durch++;
|
|
pruefe("bei Grenze 5 kommen genau 5 durch", durch === 5, `${durch} von 8`);
|
|
}
|
|
|
|
console.log("\n=== Verschiedene IPs stören sich nicht ===");
|
|
{
|
|
_zuruecksetzen();
|
|
for (let i = 0; i < 5; i++) kontingentFrei("test", "2.2.2.2", { maxProFenster: 5 });
|
|
const fremdeFrei = kontingentFrei("test", "3.3.3.3", { maxProFenster: 5 });
|
|
pruefe("eine erschöpfte IP sperrt keine andere", fremdeFrei === true);
|
|
}
|
|
|
|
console.log("\n=== Verschiedene Zwecke sind getrennte Töpfe ===");
|
|
{
|
|
_zuruecksetzen();
|
|
for (let i = 0; i < 5; i++) kontingentFrei("bewerbung", "4.4.4.4", { maxProFenster: 5 });
|
|
const stimmeFrei = kontingentFrei("stimme", "4.4.4.4", { maxProFenster: 5 });
|
|
pruefe("Bewerbung erschöpft, Stimme derselben IP noch frei", stimmeFrei === true);
|
|
}
|
|
|
|
console.log("\n=== Nach Ablauf des Fensters wieder offen ===");
|
|
{
|
|
_zuruecksetzen();
|
|
/* Echtes, sehr kurzes Fenster (5 ms) und eine kurze reale Pause danach.
|
|
Ein 0-ms-Fenster taugt hier nicht: Zwei Aufrufe in derselben
|
|
Millisekunde haben Abstand 0, und 0 ist nicht GRÖSSER als 0 -- das
|
|
Fenster gilt dann als noch offen. Genau richtig so, aber für diesen
|
|
Test braucht es einen messbaren Zeitabstand. */
|
|
for (let i = 0; i < 5; i++) kontingentFrei("test", "5.5.5.5", { maxProFenster: 5, fensterMs: 5 });
|
|
const gesperrt = kontingentFrei("test", "5.5.5.5", { maxProFenster: 5, fensterMs: 5 });
|
|
await new Promise((f) => setTimeout(f, 15));
|
|
const nachAblauf = kontingentFrei("test", "5.5.5.5", { maxProFenster: 5, fensterMs: 5 });
|
|
pruefe("innerhalb des Fensters gesperrt, nach Ablauf wieder frei",
|
|
gesperrt === false && nachAblauf === true,
|
|
`im Fenster: ${gesperrt ? "durch" : "gesperrt"}, nach Ablauf: ${nachAflaufText(nachAblauf)}`);
|
|
}
|
|
function nachAflaufText(v) { return v ? "frei" : "noch gesperrt"; }
|
|
|
|
console.log("\n=== Unbekannte IP (null/undefined) sperrt nicht die Welt aus ===");
|
|
{
|
|
_zuruecksetzen();
|
|
/* Fällt die IP aus (Proxy-Panne), landen alle im selben Topf "unbekannt".
|
|
Das ist bewusst so: lieber alle gemeinsam grob bremsen als gar nicht.
|
|
Der Test stellt nur sicher, dass es nicht abstürzt und die Grenze greift. */
|
|
let durch = 0;
|
|
for (let i = 0; i < 8; i++) if (kontingentFrei("test", null, { maxProFenster: 5 })) durch++;
|
|
pruefe("null-IP wird gezählt statt zu crashen", durch === 5, `${durch} von 8`);
|
|
}
|
|
|
|
console.log("\n=== Standardgrenze greift ohne Angabe ===");
|
|
{
|
|
_zuruecksetzen();
|
|
let durch = 0;
|
|
for (let i = 0; i < 25; i++) if (kontingentFrei("test", "6.6.6.6")) durch++;
|
|
pruefe("ohne Angabe gilt 20 pro Stunde", durch === 20, `${durch} von 25`);
|
|
}
|
|
|
|
console.log("\n=== Speicher läuft nicht voll (Selbstreinigung) ===");
|
|
{
|
|
_zuruecksetzen();
|
|
/* 1200 verschiedene IPs mit sofort ablaufendem Fenster. Ohne Reinigung
|
|
blieben alle 1200 im Speicher. Mit Reinigung (alle 500 Aufrufe) muss
|
|
der Topf danach deutlich kleiner sein. Wir messen indirekt über einen
|
|
internen Zugriff — der Baustein gibt die Topfgröße nicht her, also
|
|
prüfen wir das Verhalten: nach vielen abgelaufenen IPs kommt eine
|
|
frische immer noch durch (kein Fehler, kein Bremsen). */
|
|
for (let i = 0; i < 1200; i++) kontingentFrei("gross", "10.0." + (i >> 8) + "." + (i & 255), { fensterMs: 0 });
|
|
const frischFrei = kontingentFrei("gross", "10.0.99.99", { fensterMs: 0 });
|
|
pruefe("nach 1200 abgelaufenen IPs bremst nichts fälschlich", frischFrei === true);
|
|
}
|
|
|
|
console.log(`\n===== ${ok} bestanden, ${fehl} fehlgeschlagen =====`);
|
|
process.exitCode = fehl ? 1 : 0;
|