Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
052419dd43 | ||
|
|
31d7779f45 |
@@ -26,6 +26,18 @@ Vor `npm version` wandert der Abschnitt „Unveröffentlicht" unter eine neue
|
||||
|
||||
## [Unveröffentlicht]
|
||||
|
||||
## [1.0.2] – 2026-10-04
|
||||
|
||||
### Hinzugefügt
|
||||
|
||||
- **Bremse gegen Passwortraten.** Nach fünf Fehlversuchen wird eine
|
||||
E-Mail-Adresse gesperrt, erst eine Minute, dann doppelt so lang, höchstens
|
||||
15 Minuten. Das gilt auch für Adressen, die es nicht gibt – die Sperre
|
||||
verrät also nicht, welche Konten existieren. Während der Sperre kommt auch
|
||||
das richtige Passwort nicht durch. Ausgesperrt? Ein Neustart des Containers
|
||||
setzt die Zähler zurück. Siehe E42
|
||||
- Die Containerabnahme prüft die Sperre im laufenden Abbild
|
||||
|
||||
## [1.0.1] – 2026-10-03
|
||||
|
||||
Das Docker-Abbild ist jetzt aus dem Nichts aufsetzbar.
|
||||
|
||||
@@ -471,3 +471,25 @@ Der Grund ist banal und war ein echter Fehler: Wer die dunkle Fassung
|
||||
eingestellt hatte, druckte schwarze Flächen. Eine einzelne Regel für `body`
|
||||
genügt nicht, weil die Karten, Ränder und Marken ihre Farben aus Merkmalen
|
||||
beziehen.
|
||||
|
||||
## E42 · Bremse gegen Passwortraten im Speicher, je E-Mail-Adresse
|
||||
|
||||
Nach fünf Fehlversuchen wird eine Adresse gesperrt, erst eine Minute, dann
|
||||
jeweils doppelt so lang, höchstens 15 Minuten. Eine Stunde ohne Fehlversuch
|
||||
oder eine erfolgreiche Anmeldung setzt den Zähler zurück.
|
||||
|
||||
- **Je Adresse, nicht je Konto.** Auch eine Adresse, die es nicht gibt, wird
|
||||
gesperrt. Sonst verriete die Sperre, welche Konten existieren (E6).
|
||||
- **Nicht je IP-Adresse.** Hinter dem Reverse Proxy käme die Adresse aus
|
||||
`X-Forwarded-For`, und darauf ist nur Verlass, wenn der Proxy richtig
|
||||
eingestellt ist. Bei einer Handvoll Konten lohnt das Durchprobieren vieler
|
||||
Konten mit wenigen Passwörtern ohnehin nicht.
|
||||
- **Obergrenze 15 Minuten.** Wer die Adresse des Ausbilders kennt, soll ihn
|
||||
nicht für Stunden aussperren können.
|
||||
- **Im Speicher, nicht in der Datenbank.** Es läuft genau ein Prozess. Ein
|
||||
Neustart setzt die Zähler zurück – das kann ein Angreifer nicht auslösen,
|
||||
der Betreiber aber schon, wenn er sich selbst ausgesperrt hat
|
||||
(`docker compose restart lernbar`).
|
||||
- **Gezählt wird vor der Prüfung.** Jeder Versuch gilt sofort als Fehlversuch
|
||||
und wird bei Erfolg zurückgenommen. Zwischen Sperrprüfung und Zählen liegt
|
||||
so kein `await`; sonst kämen gleichzeitige Anfragen alle an der Sperre vorbei.
|
||||
|
||||
@@ -274,7 +274,8 @@ prüfen.
|
||||
`npm run abnahme` prüft über `next start`, betrieben wird aber der Container.
|
||||
`npm run abnahme:container` baut deshalb das Abbild, startet es mit leerem
|
||||
Volume und geht den Weg aus dieser README: Lebenszeichen, erstes Konto über
|
||||
`werkzeuge/konto.js`, Anmeldung, Passwort neu setzen, Sicherung im Container.
|
||||
`werkzeuge/konto.js`, Anmeldung, Passwort neu setzen, Sperre nach
|
||||
Fehlversuchen, Sicherung im Container.
|
||||
Danach werden Container, Volume und Abbild wieder entfernt.
|
||||
|
||||
Voraussetzung: Docker läuft, Port 3398 ist frei (`LERNBAR_CONTAINER_PORT`).
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
# Abnahmeprotokoll
|
||||
|
||||
Erzeugt am 2026-09-11 17:16 Uhr
|
||||
Erzeugt am 2026-10-04 16:24 Uhr
|
||||
|
||||
Erzeugt von `scripts/pruefungen/abnahme.mjs` gegen eine **frische, leere Datenbank**.
|
||||
|
||||
@@ -54,15 +54,15 @@ Erzeugt von `scripts/pruefungen/abnahme.mjs` gegen eine **frische, leere Datenba
|
||||
|
||||
- ✓ Der Azubi legt das Wochenblatt der laufenden Woche an
|
||||
- ✓ Ein Wochenblatt lässt sich öffnen
|
||||
- ✓ Das Wochenblatt kommt als PDF heraus — HTTP 200, 2646 Bytes
|
||||
- ✓ Das Wochenblatt kommt als PDF heraus — HTTP 200, 2649 Bytes
|
||||
|
||||
## A8 · Sicherung zurückspielen
|
||||
|
||||
- ✓ Die Sicherung läuft durch — lernbar-20260911-171627.tar.gz
|
||||
- ✓ Die Sicherung läuft durch — lernbar-20261004-182433.tar.gz
|
||||
- ✓ Zum Beweis wird der Datenbestand beschädigt — 2 → 1 Konten
|
||||
- ✓ Die Rückspielung läuft durch — ohne Fehler
|
||||
- ✓ Der gelöschte Azubi ist wieder da — Tim Vogt
|
||||
- ✓ Auch der Lernverlauf kam mit zurück — 1 Einträge
|
||||
- ✓ Der überschriebene Stand wurde zur Seite gelegt, nicht gelöscht — verdraengt-20260911-171632
|
||||
- ✓ Der überschriebene Stand wurde zur Seite gelegt, nicht gelöscht — verdraengt-20261004-182436
|
||||
|
||||
**36 von 36 Prüfungen erfüllt.**
|
||||
|
||||
@@ -401,6 +401,7 @@ mitten durchgeschnitten, aufklappbare Abschnitte drucken aufgeklappt.
|
||||
|
||||
| Beobachtung | Woran es liegt |
|
||||
|---|---|
|
||||
| „Zu viele Fehlversuche" bei der Anmeldung | Nach fünf falschen Passwörtern wird die Adresse für eine Minute gesperrt, bei weiteren Fehlversuchen länger, höchstens 15 Minuten. Abwarten – oder Passwort vom Ausbilder neu setzen lassen. Hat sich das einzige Ausbilderkonto ausgesperrt: `docker compose restart lernbar` setzt die Sperre zurück. |
|
||||
| „Diese Seite gibt es nicht" bei fremden Daten | Kein Fehler, sondern die Rechteprüfung. Ob es zu einer Kennung überhaupt Daten gibt, geht Unbefugte nichts an – deshalb 404 und nicht „kein Zugriff". |
|
||||
| Die Anwendung startet nicht, nennt aber Datei und Zeile | Ein Inhaltsfehler unter `content/`. Die Meldung sagt genau, wo. `npm run inhalte:pruefen` prüft ohne Start. |
|
||||
| Ein Anhang lässt sich nicht hochladen | Über 25 MB. Bei einem Reverse Proxy zusätzlich `client_max_body_size 30m` setzen (siehe README). |
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "lernbar",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "lernbar",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"dependencies": {
|
||||
"@node-rs/argon2": "^2.0.2",
|
||||
"better-sqlite3": "^12.11.1",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lernbar",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"private": true,
|
||||
"description": "Lern-Bar – betriebliche Ausbildung Fachinformatiker Systemintegration",
|
||||
"engines": {
|
||||
|
||||
@@ -10,7 +10,8 @@ import { spawnSync } from 'node:child_process';
|
||||
* es nie, und ein frisch aufgesetzter Server stand ohne jedes Konto da.
|
||||
*
|
||||
* Dieser Lauf baut das Abbild, startet es mit leerem Volume und geht den Weg
|
||||
* aus der README: Lebenszeichen, erstes Konto, Anmeldung, Sicherung.
|
||||
* aus der README: Lebenszeichen, erstes Konto, Anmeldung, Sperre bei
|
||||
* Fehlversuchen, Sicherung.
|
||||
*
|
||||
* node scripts/pruefungen/container.mjs
|
||||
*
|
||||
@@ -90,6 +91,23 @@ async function anmelden(email, passwort) {
|
||||
return angekommen;
|
||||
}
|
||||
|
||||
/** Ein Anmeldeversuch, der die angezeigte Meldung zurückgibt. */
|
||||
async function meldungNachAnmeldung(email, passwort) {
|
||||
const kontext = await browser.newContext({ locale: 'de-DE' });
|
||||
const seite = await kontext.newPage();
|
||||
seite.setDefaultTimeout(30000);
|
||||
await seite.goto(`${BASIS}/anmelden`);
|
||||
await seite.fill('#email', email);
|
||||
await seite.fill('#passwort', passwort);
|
||||
await seite.click('button[type=submit]');
|
||||
const text = await seite
|
||||
.locator('.hinweis-fehler')
|
||||
.innerText({ timeout: 15000 })
|
||||
.catch(() => '');
|
||||
await kontext.close();
|
||||
return text;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (docker(['info'], { stdio: 'ignore' }).status !== 0) {
|
||||
console.error('Docker ist nicht erreichbar.');
|
||||
@@ -141,9 +159,25 @@ async function main() {
|
||||
pruefe(neuesPw && (await anmelden(AUSBILDER.email, neuesPw)), 'Mit dem neuen Passwort geht die Anmeldung');
|
||||
pruefe(!(await anmelden(AUSBILDER.email, AUSBILDER.pw)), 'Das alte Passwort gilt nicht mehr');
|
||||
|
||||
// --- C3 Sicherung im Container -------------------------------------------
|
||||
// --- C3 Bremse gegen Passwortraten ----------------------------------------
|
||||
|
||||
abschnitt('C3 · Sicherung im Container');
|
||||
abschnitt('C3 · Bremse gegen Passwortraten');
|
||||
|
||||
// Eine Adresse, die es nicht gibt: Die Sperre muss genauso greifen, sonst
|
||||
// verriete sie, welche Konten existieren.
|
||||
const unbekannt = 'niemand@betrieb.test';
|
||||
for (let i = 0; i < 5; i += 1) await meldungNachAnmeldung(unbekannt, `geraten-${i}`);
|
||||
const gesperrt = await meldungNachAnmeldung(unbekannt, 'geraten-6');
|
||||
pruefe(gesperrt.includes('Zu viele Fehlversuche'), 'Nach fünf Fehlversuchen wird gesperrt – auch für eine unbekannte Adresse', gesperrt);
|
||||
|
||||
pruefe(neuesPw && (await anmelden(AUSBILDER.email, neuesPw)), 'Ein anderes Konto ist davon nicht betroffen');
|
||||
|
||||
for (let i = 0; i < 5; i += 1) await meldungNachAnmeldung(AUSBILDER.email, `geraten-${i}`);
|
||||
pruefe(!(await anmelden(AUSBILDER.email, neuesPw)), 'Während der Sperre kommt auch das richtige Passwort nicht durch');
|
||||
|
||||
// --- C4 Sicherung im Container -------------------------------------------
|
||||
|
||||
abschnitt('C4 · Sicherung im Container');
|
||||
|
||||
const sicherung = imContainer('/app/werkzeuge/sicherung.sh', '/daten/sicherungen');
|
||||
pruefe(sicherung.ende === 0, 'Die Sicherung läuft im Container durch', sicherung.ausgabe.trim().split('\n').at(-1));
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { datenbank, schema } from '@/lib/db';
|
||||
import { anmeldeDrossel, wartezeitText } from '@/lib/auth/drossel';
|
||||
import { passwortPruefen } from '@/lib/auth/passwort';
|
||||
import { sitzungAnlegen, sitzungBeenden } from '@/lib/auth/sitzung';
|
||||
import { jetzt } from '@/lib/zeit';
|
||||
@@ -19,6 +20,18 @@ export async function anmelden(_vorher: Anmeldezustand, formular: FormData): Pro
|
||||
return { fehler: 'Bitte E-Mail-Adresse und Passwort eingeben.', email };
|
||||
}
|
||||
|
||||
// Während einer Sperre wird gar nicht erst geprüft – auch ein richtiges
|
||||
// Passwort kommt dann nicht durch, sonst wäre die Sperre wertlos.
|
||||
const drossel = anmeldeDrossel();
|
||||
const wartezeit = drossel.wartezeit(email);
|
||||
if (wartezeit > 0) {
|
||||
return { fehler: `Zu viele Fehlversuche. Bitte ${wartezeitText(wartezeit)} noch einmal versuchen.`, email };
|
||||
}
|
||||
// Jeder Versuch zählt sofort als Fehlversuch und wird erst bei Erfolg
|
||||
// zurückgenommen. Zwischen Prüfen und Zählen liegt so kein await – sonst
|
||||
// kämen hundert gleichzeitige Anfragen alle an der Sperre vorbei.
|
||||
drossel.fehlversuch(email);
|
||||
|
||||
const db = datenbank();
|
||||
const benutzer = db.select().from(schema.benutzer).where(eq(schema.benutzer.email, email)).get();
|
||||
|
||||
@@ -34,6 +47,8 @@ export async function anmelden(_vorher: Anmeldezustand, formular: FormData): Pro
|
||||
const stimmt = await passwortPruefen(benutzer.passwortHash, passwort);
|
||||
if (!stimmt) return abweisung;
|
||||
|
||||
drossel.erfolg(email);
|
||||
|
||||
db.update(schema.benutzer).set({ letzteAnmeldungAm: jetzt() }).where(eq(schema.benutzer.id, benutzer.id)).run();
|
||||
await sitzungAnlegen(benutzer.id);
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
Drossel,
|
||||
FREIE_VERSUCHE,
|
||||
GRUNDSPERRE_MS,
|
||||
HOECHSTSPERRE_MS,
|
||||
VERGESSEN_NACH_MS,
|
||||
sperreFuer,
|
||||
wartezeitText,
|
||||
} from './drossel';
|
||||
|
||||
/** Eine Uhr, die nur weiterläuft, wenn man sie stellt. */
|
||||
function stehendeUhr(start = 1_000_000) {
|
||||
let zeit = start;
|
||||
return {
|
||||
uhr: () => zeit,
|
||||
vor: (ms: number) => {
|
||||
zeit += ms;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function scheitern(drossel: Drossel, schluessel: string, mal: number) {
|
||||
for (let i = 0; i < mal; i += 1) drossel.fehlversuch(schluessel);
|
||||
}
|
||||
|
||||
describe('sperreFuer', () => {
|
||||
it('lässt die ersten Fehlversuche frei', () => {
|
||||
for (let n = 1; n < FREIE_VERSUCHE; n += 1) expect(sperreFuer(n)).toBe(0);
|
||||
});
|
||||
|
||||
it('sperrt ab dem fünften Fehlversuch und verdoppelt dann', () => {
|
||||
expect(sperreFuer(FREIE_VERSUCHE)).toBe(GRUNDSPERRE_MS);
|
||||
expect(sperreFuer(FREIE_VERSUCHE + 1)).toBe(2 * GRUNDSPERRE_MS);
|
||||
expect(sperreFuer(FREIE_VERSUCHE + 2)).toBe(4 * GRUNDSPERRE_MS);
|
||||
});
|
||||
|
||||
it('bleibt bei der Obergrenze stehen', () => {
|
||||
expect(sperreFuer(FREIE_VERSUCHE + 20)).toBe(HOECHSTSPERRE_MS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Drossel', () => {
|
||||
it('sperrt nach fünf Fehlversuchen, nicht vorher', () => {
|
||||
const { uhr } = stehendeUhr();
|
||||
const drossel = new Drossel(uhr);
|
||||
scheitern(drossel, 'a@x.de', FREIE_VERSUCHE - 1);
|
||||
expect(drossel.wartezeit('a@x.de')).toBe(0);
|
||||
drossel.fehlversuch('a@x.de');
|
||||
expect(drossel.wartezeit('a@x.de')).toBe(GRUNDSPERRE_MS);
|
||||
});
|
||||
|
||||
it('gibt nach Ablauf der Sperre wieder frei', () => {
|
||||
const { uhr, vor } = stehendeUhr();
|
||||
const drossel = new Drossel(uhr);
|
||||
scheitern(drossel, 'a@x.de', FREIE_VERSUCHE);
|
||||
vor(GRUNDSPERRE_MS);
|
||||
expect(drossel.wartezeit('a@x.de')).toBe(0);
|
||||
});
|
||||
|
||||
it('verlängert beim nächsten Fehlversuch nach der Sperre', () => {
|
||||
const { uhr, vor } = stehendeUhr();
|
||||
const drossel = new Drossel(uhr);
|
||||
scheitern(drossel, 'a@x.de', FREIE_VERSUCHE);
|
||||
vor(GRUNDSPERRE_MS);
|
||||
drossel.fehlversuch('a@x.de');
|
||||
expect(drossel.wartezeit('a@x.de')).toBe(2 * GRUNDSPERRE_MS);
|
||||
});
|
||||
|
||||
it('setzt nach erfolgreicher Anmeldung zurück', () => {
|
||||
const { uhr } = stehendeUhr();
|
||||
const drossel = new Drossel(uhr);
|
||||
scheitern(drossel, 'a@x.de', FREIE_VERSUCHE - 1);
|
||||
drossel.erfolg('a@x.de');
|
||||
scheitern(drossel, 'a@x.de', FREIE_VERSUCHE - 1);
|
||||
expect(drossel.wartezeit('a@x.de')).toBe(0);
|
||||
});
|
||||
|
||||
it('vergisst Fehlversuche nach einer Stunde Ruhe', () => {
|
||||
const { uhr, vor } = stehendeUhr();
|
||||
const drossel = new Drossel(uhr);
|
||||
scheitern(drossel, 'a@x.de', FREIE_VERSUCHE - 1);
|
||||
vor(VERGESSEN_NACH_MS);
|
||||
drossel.fehlversuch('a@x.de');
|
||||
expect(drossel.wartezeit('a@x.de')).toBe(0);
|
||||
});
|
||||
|
||||
it('zählt jede Adresse für sich', () => {
|
||||
const { uhr } = stehendeUhr();
|
||||
const drossel = new Drossel(uhr);
|
||||
scheitern(drossel, 'a@x.de', FREIE_VERSUCHE);
|
||||
expect(drossel.wartezeit('a@x.de')).toBeGreaterThan(0);
|
||||
expect(drossel.wartezeit('b@x.de')).toBe(0);
|
||||
});
|
||||
|
||||
it('räumt auf, lässt eine laufende Sperre aber nicht verdrängen', () => {
|
||||
const { uhr } = stehendeUhr();
|
||||
const drossel = new Drossel(uhr);
|
||||
scheitern(drossel, 'opfer@x.de', FREIE_VERSUCHE);
|
||||
for (let i = 0; i < 10_050; i += 1) drossel.fehlversuch(`erfunden-${i}@x.de`);
|
||||
expect(drossel.groesse).toBeLessThanOrEqual(10_001);
|
||||
expect(drossel.wartezeit('opfer@x.de')).toBe(GRUNDSPERRE_MS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('wartezeitText', () => {
|
||||
it('rundet auf ganze Minuten auf', () => {
|
||||
expect(wartezeitText(1)).toBe('in einer Minute');
|
||||
expect(wartezeitText(60_000)).toBe('in einer Minute');
|
||||
expect(wartezeitText(60_001)).toBe('in 2 Minuten');
|
||||
expect(wartezeitText(HOECHSTSPERRE_MS)).toBe('in 15 Minuten');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Bremse gegen Passwortraten.
|
||||
*
|
||||
* Gezählt wird je E-Mail-Adresse – auch für Adressen, die es nicht gibt.
|
||||
* Sonst verriete die Sperre, welche Konten existieren (siehe E6).
|
||||
*
|
||||
* Die ersten Fehlversuche sind frei, danach verdoppelt sich die Sperre bis
|
||||
* zu einer Obergrenze. Die Obergrenze ist Absicht: Wer fremde Adressen
|
||||
* eintippt, soll den Ausbilder nicht für Stunden aussperren können.
|
||||
*
|
||||
* Der Zähler liegt im Speicher des einen Serverprozesses. Ein Neustart setzt
|
||||
* ihn zurück – das ist zugleich der Notausgang, falls man sich selbst
|
||||
* ausgesperrt hat. Siehe E42.
|
||||
*/
|
||||
|
||||
export const FREIE_VERSUCHE = 5;
|
||||
export const GRUNDSPERRE_MS = 60_000;
|
||||
export const HOECHSTSPERRE_MS = 15 * 60_000;
|
||||
export const VERGESSEN_NACH_MS = 60 * 60_000;
|
||||
const HOECHSTZAHL_EINTRAEGE = 10_000;
|
||||
|
||||
export type Stand = { fehlversuche: number; zuletzt: number; gesperrtBis: number };
|
||||
|
||||
/** Wie lange nach dem n-ten Fehlversuch gesperrt wird. */
|
||||
export function sperreFuer(fehlversuche: number): number {
|
||||
if (fehlversuche < FREIE_VERSUCHE) return 0;
|
||||
return Math.min(GRUNDSPERRE_MS * 2 ** (fehlversuche - FREIE_VERSUCHE), HOECHSTSPERRE_MS);
|
||||
}
|
||||
|
||||
export function nachFehlversuch(stand: Stand | undefined, jetzt: number): Stand {
|
||||
const bisher = stand && jetzt - stand.zuletzt < VERGESSEN_NACH_MS ? stand.fehlversuche : 0;
|
||||
const fehlversuche = bisher + 1;
|
||||
return { fehlversuche, zuletzt: jetzt, gesperrtBis: jetzt + sperreFuer(fehlversuche) };
|
||||
}
|
||||
|
||||
export function restsperre(stand: Stand | undefined, jetzt: number): number {
|
||||
return stand ? Math.max(0, stand.gesperrtBis - jetzt) : 0;
|
||||
}
|
||||
|
||||
/** „in 3 Minuten" – aufgerundet, damit niemand zu früh wiederkommt. */
|
||||
export function wartezeitText(ms: number): string {
|
||||
const minuten = Math.max(1, Math.ceil(ms / 60_000));
|
||||
return minuten === 1 ? 'in einer Minute' : `in ${minuten} Minuten`;
|
||||
}
|
||||
|
||||
export class Drossel {
|
||||
private staende = new Map<string, Stand>();
|
||||
|
||||
constructor(private uhr: () => number = Date.now) {}
|
||||
|
||||
/** Restliche Sperre in Millisekunden, 0 heißt: darf versuchen. */
|
||||
wartezeit(schluessel: string): number {
|
||||
return restsperre(this.staende.get(schluessel), this.uhr());
|
||||
}
|
||||
|
||||
fehlversuch(schluessel: string): void {
|
||||
const jetzt = this.uhr();
|
||||
this.staende.set(schluessel, nachFehlversuch(this.staende.get(schluessel), jetzt));
|
||||
if (this.staende.size > HOECHSTZAHL_EINTRAEGE) this.aufraeumen(jetzt);
|
||||
}
|
||||
|
||||
erfolg(schluessel: string): void {
|
||||
this.staende.delete(schluessel);
|
||||
}
|
||||
|
||||
get groesse(): number {
|
||||
return this.staende.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wer massenhaft ausgedachte Adressen schickt, soll den Speicher nicht
|
||||
* füllen. Erst fällt weg, was ohnehin vergessen ist, dann das Älteste.
|
||||
* Eine laufende Sperre wird nie weggeräumt – sonst ließe sie sich mit
|
||||
* genug ausgedachten Adressen aus dem Speicher drängen.
|
||||
*/
|
||||
private aufraeumen(jetzt: number): void {
|
||||
for (const [schluessel, stand] of this.staende) {
|
||||
if (jetzt - stand.zuletzt >= VERGESSEN_NACH_MS) this.staende.delete(schluessel);
|
||||
}
|
||||
for (const [schluessel, stand] of this.staende) {
|
||||
if (this.staende.size <= HOECHSTZAHL_EINTRAEGE) break;
|
||||
if (stand.gesperrtBis <= jetzt) this.staende.delete(schluessel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Eine Drossel pro Prozess, am globalThis wie die Datenbankverbindung, damit
|
||||
// das Neuladen im Entwicklungsbetrieb die Zähler nicht verdoppelt.
|
||||
const halter = globalThis as unknown as { __lernbarDrossel?: Drossel };
|
||||
|
||||
export function anmeldeDrossel(): Drossel {
|
||||
halter.__lernbarDrossel ??= new Drossel();
|
||||
return halter.__lernbarDrossel;
|
||||
}
|
||||
Reference in New Issue
Block a user