Anfragebremse für /submit und /testimonials/submit (die letzten zwei ungebremsten Routen)
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]>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
/* =====================================================================
|
||||
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;
|
||||
@@ -0,0 +1,104 @@
|
||||
/* =====================================================================
|
||||
kontingent.js — eine Anfragebremse pro IP, ohne Fremdpaket
|
||||
|
||||
WOZU
|
||||
|
||||
Öffentliche Routen, die in die Datenbank schreiben oder Dateien
|
||||
annehmen, brauchen eine Obergrenze pro Absender. Sonst füllt ein
|
||||
einzelnes Skript die Datenbank mit Müll oder die Platte mit Bildern.
|
||||
|
||||
Es gab diese Bremse schon — aber nur an einer Stelle (der Bild-Upload
|
||||
in routes/testimonials.js) und dort fest verdrahtet. Zwei weitere
|
||||
öffentliche Schreibrouten (/submit, /testimonials/submit) hatten
|
||||
keine. Statt das Muster ein drittes Mal zu kopieren, steht es nun
|
||||
einmal hier und wird überall gleich benutzt.
|
||||
|
||||
ZWEI VERBESSERUNGEN GEGENÜBER DEM ORIGINAL
|
||||
|
||||
1. Getrennte Töpfe je Zweck: Der Bild-Upload und die Bewerbung teilen
|
||||
sich NICHT dasselbe Kontingent. Wer ein Bild hochlädt, verbraucht
|
||||
damit keinen seiner Bewerbungsversuche.
|
||||
|
||||
2. Die Zählung räumt sich selbst auf. Das Original ließ jede IP, die
|
||||
je eine Anfrage geschickt hatte, für immer im Speicher stehen — ein
|
||||
langsames Leck. Hier werden abgelaufene Einträge bei Gelegenheit
|
||||
entfernt.
|
||||
|
||||
BEWUSSTE GRENZE
|
||||
|
||||
Der Zähler liegt im Arbeitsspeicher, nicht in der Datenbank. Ein
|
||||
Neustart setzt ihn zurück, und bei mehreren Prozessen zählte jeder für
|
||||
sich. Beides ist hier unkritisch: Es gibt genau einen Prozess, und ein
|
||||
Neustart, der das Kontingent leert, schadet niemandem — im Gegenteil,
|
||||
ein zu Unrecht Gebremster käme dann wieder durch. Für eine echte,
|
||||
prozessübergreifende Bremse wäre die Datenbank der Ort; der Aufwand
|
||||
lohnt hier (noch) nicht.
|
||||
|
||||
HINTER CADDY
|
||||
|
||||
Die IP kommt aus req.ip. Das ist nur dann die ECHTE Besucher-IP, wenn
|
||||
express dem Proxy vertraut — app.set("trust proxy", 1) ist in index.js
|
||||
gesetzt (mit Begründung in lib/auth.js). Ohne das wäre req.ip für alle
|
||||
Besucher 127.0.0.1 und die Bremse träfe alle gemeinsam.
|
||||
===================================================================== */
|
||||
|
||||
const STUNDE = 60 * 60 * 1000;
|
||||
|
||||
/* Ein Topf je Zweck. Schlüssel ist der Zweckname, Wert eine Map von
|
||||
IP -> { anzahl, fensterStart }. */
|
||||
const toepfe = new Map();
|
||||
|
||||
/* Aufräumen ist beiläufig, nicht auf die Uhr getaktet: Bei jedem N-ten
|
||||
Aufruf wird der eigene Topf einmal von abgelaufenen Einträgen befreit.
|
||||
Das kostet nichts extra (kein Timer, der auch im Leerlauf läuft) und
|
||||
hält den Speicher trotzdem klein. */
|
||||
let aufrufeSeitReinigung = 0;
|
||||
const REINIGEN_ALLE = 500;
|
||||
|
||||
function reinige(topf, jetzt, fensterMs) {
|
||||
for (const [ip, e] of topf) {
|
||||
if (jetzt - e.fensterStart > fensterMs) topf.delete(ip);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fragt die Bremse und zählt den Versuch zugleich.
|
||||
*
|
||||
* @param {string} zweck getrennter Topf, z.B. "bewerbung", "stimme"
|
||||
* @param {string} ip req.ip des Aufrufers
|
||||
* @param {object} [opt]
|
||||
* @param {number} [opt.maxProFenster=20] erlaubte Anfragen je Fenster
|
||||
* @param {number} [opt.fensterMs=1h] Länge des Fensters
|
||||
* @returns {boolean} true = erlaubt (und gezählt), false = Grenze erreicht
|
||||
*/
|
||||
export function kontingentFrei(zweck, ip, opt = {}) {
|
||||
const maxProFenster = opt.maxProFenster ?? 20;
|
||||
const fensterMs = opt.fensterMs ?? STUNDE;
|
||||
const schluessel = ip || "unbekannt";
|
||||
|
||||
let topf = toepfe.get(zweck);
|
||||
if (!topf) { topf = new Map(); toepfe.set(zweck, topf); }
|
||||
|
||||
const jetzt = Date.now();
|
||||
if (++aufrufeSeitReinigung >= REINIGEN_ALLE) {
|
||||
aufrufeSeitReinigung = 0;
|
||||
reinige(topf, jetzt, fensterMs);
|
||||
}
|
||||
|
||||
const eintrag = topf.get(schluessel);
|
||||
if (!eintrag || jetzt - eintrag.fensterStart > fensterMs) {
|
||||
/* Neues Fenster: erster Versuch zählt sofort. */
|
||||
topf.set(schluessel, { anzahl: 1, fensterStart: jetzt });
|
||||
return true;
|
||||
}
|
||||
if (eintrag.anzahl >= maxProFenster) return false;
|
||||
eintrag.anzahl += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Nur für Tests: alle Töpfe leeren, damit ein Testfall nicht den nächsten
|
||||
beeinflusst. */
|
||||
export function _zuruecksetzen() {
|
||||
toepfe.clear();
|
||||
aufrufeSeitReinigung = 0;
|
||||
}
|
||||
@@ -11,9 +11,16 @@ import { logAction } from "../lib/audit.js";
|
||||
import { json } from "../lib/http.js";
|
||||
import { bewerbungAlsTicketAnlegen } from "../lib/ticketanizer.js";
|
||||
import { bewerbungAlsDiscordNachrichtPosten } from "../lib/discord-webhooks.js";
|
||||
import { kontingentFrei } from "../lib/kontingent.js";
|
||||
|
||||
const ALLOWED_TYPES = ["creator", "scout", "kooperation", "modi"];
|
||||
|
||||
/* Öffentliche Route, keine Anmeldung: ohne Bremse könnte ein Skript die
|
||||
Bewerbungstabelle mit Müll fluten. 20/Stunde/IP ist großzügig genug
|
||||
für mehrere echte Bewerbungen hinter einem geteilten Anschluss
|
||||
(Familie, Mobilfunk-NAT) und stoppt trotzdem eine maschinelle Flut. */
|
||||
const BEWERBUNG_MAX_PRO_STUNDE = 20;
|
||||
|
||||
function can(session, key) {
|
||||
if (!session) return false;
|
||||
if (session.isOwner) return true;
|
||||
@@ -29,6 +36,10 @@ function viewPermissionForType(type) {
|
||||
}
|
||||
|
||||
export async function submitApplication(req, res) {
|
||||
const ip = req.ip || req.socket?.remoteAddress || "unbekannt";
|
||||
if (!kontingentFrei("bewerbung", ip, { maxProFenster: BEWERBUNG_MAX_PRO_STUNDE })) {
|
||||
return json(res, { ok: false, error: "Zu viele Bewerbungen in kurzer Zeit. Bitte versuch es später noch einmal." }, 429);
|
||||
}
|
||||
const body = req.body;
|
||||
const { type, data } = body || {};
|
||||
if (!ALLOWED_TYPES.includes(type) || typeof data !== "object" || !data) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { generateId, nowIso } from "../lib/crypto.js";
|
||||
import { hasPermission } from "../lib/permissions.js";
|
||||
import { json } from "../lib/http.js";
|
||||
import { logAction } from "../lib/audit.js";
|
||||
import { kontingentFrei } from "../lib/kontingent.js";
|
||||
|
||||
function can(session, key) {
|
||||
if (!session) return false;
|
||||
@@ -61,24 +62,13 @@ function istEigenesHochgeladenesBild(wert) {
|
||||
return /^[a-f0-9-]{36}\.(jpe?g|png|webp)$/i.test(datei);
|
||||
}
|
||||
|
||||
/* Einfache Missbrauchsbremse für den öffentlichen Bild-Upload (kein Login
|
||||
nötig). Ohne sie könnte jemand den Endpunkt in einer Schleife aufrufen
|
||||
und die Festplatte des Servers vollschreiben. Bewusst schlicht im
|
||||
Arbeitsspeicher gehalten: es gibt hier nur einen Serverprozess, und ein
|
||||
Neustart (der den Zähler leert) ist kein Schaden. */
|
||||
/* Zwei Obergrenzen pro IP (Stunde). Beide über denselben Baustein
|
||||
lib/kontingent.js, in getrennten Töpfen -- ein Bild-Upload verbraucht
|
||||
also kein Kontingent für Einreichungen und umgekehrt.
|
||||
Upload strenger (10), weil er Speicher auf der Platte belegt; die
|
||||
reine Text-Einreichung großzügiger (20). */
|
||||
const UPLOAD_MAX_PRO_STUNDE = 10;
|
||||
const uploadZaehler = new Map(); // ip -> { anzahl, fensterStart }
|
||||
function uploadKontingentFrei(ip) {
|
||||
const jetzt = Date.now();
|
||||
const eintrag = uploadZaehler.get(ip);
|
||||
if (!eintrag || jetzt - eintrag.fensterStart > 60 * 60 * 1000) {
|
||||
uploadZaehler.set(ip, { anzahl: 1, fensterStart: jetzt });
|
||||
return true;
|
||||
}
|
||||
if (eintrag.anzahl >= UPLOAD_MAX_PRO_STUNDE) return false;
|
||||
eintrag.anzahl += 1;
|
||||
return true;
|
||||
}
|
||||
const STIMME_MAX_PRO_STUNDE = 20;
|
||||
|
||||
/* Öffentlich (ohne Login): Profilbild für die eigene Einreichung hochladen.
|
||||
Die Datei ist danach zwar unter ihrer zufälligen Adresse abrufbar, taucht
|
||||
@@ -87,7 +77,7 @@ function uploadKontingentFrei(ip) {
|
||||
Größe begrenzt multer (nur JPG/PNG/WebP, max. 5 MB, zufälliger Name). */
|
||||
export async function uploadEigenesTestimonialBild(req, res) {
|
||||
const ip = req.ip || req.socket?.remoteAddress || "unbekannt";
|
||||
if (!uploadKontingentFrei(ip)) {
|
||||
if (!kontingentFrei("stimme-upload", ip, { maxProFenster: UPLOAD_MAX_PRO_STUNDE })) {
|
||||
return json(res, { ok: false, error: "Zu viele Uploads in kurzer Zeit. Bitte versuch es später nochmal." }, 429);
|
||||
}
|
||||
if (!req.file) return json(res, { ok: false, error: "Keine Bilddatei erhalten." }, 400);
|
||||
@@ -125,6 +115,10 @@ function vollesFeld(row) {
|
||||
Schutzschicht gegen Missbrauch ist die manuelle Freigabe durch Dogi,
|
||||
kein Eintrag wird je ungesehen veröffentlicht. */
|
||||
export async function submitTestimonial(req, res) {
|
||||
const ip = req.ip || req.socket?.remoteAddress || "unbekannt";
|
||||
if (!kontingentFrei("stimme", ip, { maxProFenster: STIMME_MAX_PRO_STUNDE })) {
|
||||
return json(res, { ok: false, error: "Zu viele Nachrichten in kurzer Zeit. Bitte versuch es später noch einmal." }, 429);
|
||||
}
|
||||
const body = req.body || {};
|
||||
const name = String(body.name || "").trim().slice(0, 60);
|
||||
const tiktokUsername = String(body.tiktokUsername || "").trim().replace(/^@/, "").slice(0, 40) || null;
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/* =====================================================================
|
||||
test-kontingent-routen.mjs — die Anfragebremse an den echten Routen
|
||||
|
||||
Prüft nicht den Baustein selbst (das tut pruef-kontingent.mjs im
|
||||
Projektstamm), sondern dass die beiden bisher ungebremsten Routen
|
||||
submitApplication und submitTestimonial ihn WIRKLICH benutzen:
|
||||
ab der Grenze kommt 429, und verschiedene IPs stören sich nicht.
|
||||
|
||||
Läuft gegen eine Wegwerf-Datenbank, ruft die Handler direkt mit
|
||||
Attrappen auf -- gleiches Muster wie test-webdesign-anfragen.mjs.
|
||||
|
||||
Aufruf (aus server-internal/): node test-kontingent-routen.mjs
|
||||
===================================================================== */
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
process.env.DB_PATH = join(mkdtempSync(join(tmpdir(), "kont-test-")), "test.db");
|
||||
|
||||
const { initDb } = await import("./db.js");
|
||||
initDb();
|
||||
|
||||
const { submitApplication } = await import("./routes/applications.js");
|
||||
const { submitTestimonial } = await import("./routes/testimonials.js");
|
||||
|
||||
let ok = 0, schlecht = 0;
|
||||
const pruefe = (name, bedingung, zusatz = "") => {
|
||||
if (bedingung) { ok++; console.log(" ok " + name); }
|
||||
else { schlecht++; console.log(" FEHLT " + name + " " + zusatz); }
|
||||
};
|
||||
|
||||
function antwortAttrappe() {
|
||||
const a = { code: 200, daten: null };
|
||||
a.status = (c) => { a.code = c; return a; };
|
||||
a.json = (d) => { a.daten = d; return a; };
|
||||
return a;
|
||||
}
|
||||
function req(koerper, ip) {
|
||||
return { body: koerper, params: {}, query: {}, headers: {}, ip, socket: { remoteAddress: ip } };
|
||||
}
|
||||
|
||||
/* Eine gültige Bewerbung bzw. Stimme, damit NICHT eine Inhaltsprüfung
|
||||
(Pflichtfeld o.ä.) fälschlich die 429-Messung verfälscht. */
|
||||
const bewerbung = () => ({ type: "creator", data: { name: "Test Person", warum: "Ich will dabei sein und mithelfen." } });
|
||||
const stimme = () => ({ name: "Test Person", tiktokUsername: "testi", nachricht: "Das Team ist wirklich super, ich bin gern dabei!" });
|
||||
|
||||
async function feuere(handler, koerperFn, ip, anzahl) {
|
||||
let durch = 0, gesperrt = 0;
|
||||
for (let i = 0; i < anzahl; i++) {
|
||||
const res = antwortAttrappe();
|
||||
await handler(req(koerperFn(), ip), res);
|
||||
if (res.code === 429) gesperrt++;
|
||||
else durch++;
|
||||
}
|
||||
return { durch, gesperrt };
|
||||
}
|
||||
|
||||
console.log("\n=== Bewerbungen: 20 pro Stunde, dann 429 ===");
|
||||
{
|
||||
const r = await feuere(submitApplication, bewerbung, "198.51.100.1", 25);
|
||||
pruefe("genau 20 kommen durch, der Rest wird gebremst", r.durch === 20 && r.gesperrt === 5, `durch=${r.durch}, gesperrt=${r.gesperrt}`);
|
||||
}
|
||||
|
||||
console.log("\n=== Stimmen: 20 pro Stunde, dann 429 ===");
|
||||
{
|
||||
const r = await feuere(submitTestimonial, stimme, "198.51.100.2", 25);
|
||||
pruefe("genau 20 kommen durch, der Rest wird gebremst", r.durch === 20 && r.gesperrt === 5, `durch=${r.durch}, gesperrt=${r.gesperrt}`);
|
||||
}
|
||||
|
||||
console.log("\n=== Getrennte Töpfe: erschöpfte Bewerbung sperrt keine Stimme ===");
|
||||
{
|
||||
await feuere(submitApplication, bewerbung, "198.51.100.3", 25); // Bewerbung erschöpfen
|
||||
const r = await feuere(submitTestimonial, stimme, "198.51.100.3", 1); // Stimme derselben IP
|
||||
pruefe("dieselbe IP darf noch eine Stimme abgeben", r.durch === 1 && r.gesperrt === 0);
|
||||
}
|
||||
|
||||
console.log("\n=== Verschiedene IPs stören sich nicht ===");
|
||||
{
|
||||
await feuere(submitApplication, bewerbung, "198.51.100.4", 25); // eine IP erschöpfen
|
||||
const r = await feuere(submitApplication, bewerbung, "198.51.100.5", 1); // andere IP
|
||||
pruefe("eine frische IP kommt durch", r.durch === 1 && r.gesperrt === 0);
|
||||
}
|
||||
|
||||
console.log(`\n===== ${ok} bestanden, ${schlecht} fehlgeschlagen =====`);
|
||||
process.exitCode = schlecht ? 1 : 0;
|
||||
Reference in New Issue
Block a user