Terminplaner: oeffentliche Schnittstelle v1 mit Missbrauchsschutz
Router /api/public/termine/v1 (config, tage, zeiten, buchungen, widerruf, Token-Seiten /termin/*, Kalender-Abo) mit eigenem 16-KB-Parser und Fehlermiddleware, CORS nach Herkunft, Honeypot und Formular-Token, Grenzen je IP (IPv6 je /64), siteKey und Adresse in limits.js, Mindestantwortzeit (TERMINE_MIN_ANTWORT_MS) und 10-Sekunden-Cache fuer tage/zeiten. Vor dem globalen express.json eingehaengt; /api/public/ ist vom Kundenansicht-Waechter ausgenommen. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
43ac05aa06
commit
2b0c05b8e9
10 files changed
+1928
-7
No files matched your search
@@ -1,11 +1,25 @@
|
||||
import { test, beforeEach } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { zaehle, zaehleAlle, darfG08, darfK21NichtZugeordnet, resetTermineLimitsForTests } from '../server/services/termine/limits.js'
|
||||
import {
|
||||
zaehle,
|
||||
zaehleAlle,
|
||||
darfG08,
|
||||
darfK21NichtZugeordnet,
|
||||
darfAnfrage,
|
||||
darfAnfrageSite,
|
||||
darfBuchungsversuch,
|
||||
darfHaltung,
|
||||
darfWiderrufsversuch,
|
||||
darfG01,
|
||||
ipSchluessel,
|
||||
resetTermineLimitsForTests,
|
||||
} from '../server/services/termine/limits.js'
|
||||
|
||||
// Grenzen im Speicher: festes Fenster ab dem ersten Ereignis, alle-oder-keine bei mehreren Regeln, Obergrenze der Schluessel.
|
||||
const T0 = new Date('2026-10-05T08:00:00.000Z')
|
||||
const STUNDE = 60 * 60 * 1000
|
||||
const TAG = 24 * STUNDE
|
||||
const MINUTE = 60 * 1000
|
||||
const plus = (ms) => new Date(T0.getTime() + ms)
|
||||
|
||||
beforeEach(() => resetTermineLimitsForTests())
|
||||
@@ -81,3 +95,75 @@ test('resetTermineLimitsForTests leert alles', () => {
|
||||
resetTermineLimitsForTests()
|
||||
assert.equal(zaehle('a', 1, TAG, T0), true)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Aufgabe 16: IP, siteKey, G01
|
||||
|
||||
test('ipSchluessel: IPv4 bleibt, IPv4-in-IPv6 wird IPv4, IPv6 zaehlt je /64 (auch in anderer Schreibweise)', () => {
|
||||
assert.equal(ipSchluessel('203.0.113.7'), '203.0.113.7')
|
||||
assert.equal(ipSchluessel('::ffff:203.0.113.7'), '203.0.113.7')
|
||||
assert.equal(ipSchluessel('::FFFF:203.0.113.7'), '203.0.113.7')
|
||||
const a = ipSchluessel('2001:db8:1:2::1')
|
||||
assert.equal(ipSchluessel('2001:0db8:0001:0002:ffff:ffff:ffff:ffff'), a, 'dieselbe /64, ausgeschrieben')
|
||||
assert.equal(ipSchluessel('2001:DB8:1:2:abcd::9'), a, 'dieselbe /64, Grossbuchstaben und Kurzform')
|
||||
assert.equal(ipSchluessel('2001:db8:1:2:0:0:0:0'), a)
|
||||
assert.notEqual(ipSchluessel('2001:db8:1:3::1'), a, 'andere /64')
|
||||
assert.notEqual(ipSchluessel('2001:db8:2:2::1'), a)
|
||||
assert.equal(ipSchluessel('fe80::1%eth0'), ipSchluessel('fe80::2'), 'Zonenkennung faellt weg')
|
||||
assert.equal(ipSchluessel('::1'), ipSchluessel('0:0:0:0:ffff:ffff:ffff:ffff'), 'Loopback ist die /64 ::')
|
||||
assert.notEqual(ipSchluessel('203.0.113.7'), ipSchluessel('203.0.113.8'))
|
||||
assert.notEqual(ipSchluessel('203.0.113.7'), a, 'IPv4 und IPv6 stossen nie zusammen')
|
||||
})
|
||||
|
||||
test('ipSchluessel: fehlende oder unlesbare Adresse ergibt einen festen Ersatzschluessel (nie einen Fehler)', () => {
|
||||
for (const wert of [undefined, null, '', ' ', 'kaputt', '1.2.3', '999.1.1.1', 42, {}]) {
|
||||
assert.equal(ipSchluessel(wert), 'unbekannt', JSON.stringify(wert))
|
||||
}
|
||||
})
|
||||
|
||||
test('darfAnfrage: 120 je IP und Minute (IPv6 je /64); darfAnfrageSite: 600 je siteKey und Minute', () => {
|
||||
assert.equal(Array.from({ length: 125 }, () => darfAnfrage('203.0.113.7', T0)).filter(Boolean).length, 120)
|
||||
assert.equal(darfAnfrage('203.0.113.8', T0), true, 'andere IP')
|
||||
assert.equal(darfAnfrage('203.0.113.7', plus(MINUTE)), true, 'nach einer Minute wieder')
|
||||
|
||||
resetTermineLimitsForTests()
|
||||
const erlaubt = Array.from({ length: 125 }, (_, i) => darfAnfrage(`2001:db8:1:2::${i + 1}`, T0))
|
||||
assert.equal(erlaubt.filter(Boolean).length, 120, 'alle Adressen der /64 teilen sich 120')
|
||||
assert.equal(darfAnfrage('2001:db8:1:3::1', T0), true)
|
||||
|
||||
resetTermineLimitsForTests()
|
||||
assert.equal(Array.from({ length: 605 }, () => darfAnfrageSite('abcdefghijklmnop', T0)).filter(Boolean).length, 600)
|
||||
assert.equal(darfAnfrageSite('bcdefghijklmnopq', T0), true)
|
||||
assert.equal(darfAnfrageSite('abcdefghijklmnop', plus(MINUTE)), true)
|
||||
})
|
||||
|
||||
test('darfBuchungsversuch: 10 je IP und 10 Minuten (IPv6 je /64); darfHaltung: 60 je siteKey und Stunde', () => {
|
||||
assert.equal(Array.from({ length: 12 }, () => darfBuchungsversuch('203.0.113.7', T0)).filter(Boolean).length, 10)
|
||||
assert.equal(darfBuchungsversuch('203.0.113.7', plus(10 * MINUTE - 1)), false)
|
||||
assert.equal(darfBuchungsversuch('203.0.113.7', plus(10 * MINUTE)), true)
|
||||
assert.equal(darfBuchungsversuch('2001:db8::1', T0) && darfBuchungsversuch('2001:db8:0:0:1::1', T0), true, 'getrennt von der IPv4')
|
||||
|
||||
resetTermineLimitsForTests()
|
||||
for (let i = 0; i < 5; i += 1) assert.equal(darfBuchungsversuch(`2001:db8:9:9::${i + 1}`, T0), true)
|
||||
for (let i = 0; i < 5; i += 1) assert.equal(darfBuchungsversuch(`2001:db8:9:9:1::${i + 1}`, T0), true)
|
||||
assert.equal(darfBuchungsversuch('2001:db8:9:9:ffff::1', T0), false, 'elf Versuche aus einer /64')
|
||||
|
||||
resetTermineLimitsForTests()
|
||||
assert.equal(Array.from({ length: 65 }, () => darfHaltung('abcdefghijklmnop', T0)).filter(Boolean).length, 60)
|
||||
assert.equal(darfHaltung('bcdefghijklmnopq', T0), true)
|
||||
assert.equal(darfHaltung('abcdefghijklmnop', plus(STUNDE)), true)
|
||||
})
|
||||
|
||||
test('darfWiderrufsversuch: 10 je IP und Stunde', () => {
|
||||
assert.equal(Array.from({ length: 12 }, () => darfWiderrufsversuch('203.0.113.7', T0)).filter(Boolean).length, 10)
|
||||
assert.equal(darfWiderrufsversuch('203.0.113.8', T0), true)
|
||||
assert.equal(darfWiderrufsversuch('203.0.113.7', plus(STUNDE)), true)
|
||||
})
|
||||
|
||||
test('darfG01: 5 je Adresse und Funktion und Tag; die Zaehler der Arten stoeren sich nicht', () => {
|
||||
assert.deepEqual(Array.from({ length: 7 }, () => darfG01('f1', 'max@example.com', T0)), [true, true, true, true, true, false, false])
|
||||
assert.equal(darfG01('f2', 'max@example.com', T0), true, 'andere Funktion')
|
||||
assert.equal(darfG01('f1', 'anna@example.com', T0), true, 'andere Adresse')
|
||||
assert.equal(darfG01('f1', 'max@example.com', plus(TAG - 1)), false)
|
||||
assert.equal(darfG01('f1', 'max@example.com', plus(TAG)), true, 'am naechsten Tag wieder')
|
||||
assert.equal(darfG08('f1', 'max@example.com', T0), true, 'G08 zaehlt getrennt von G01')
|
||||
})
|
||||
Reference in new issue
Block a user