diff --git a/server/workspace-code.js b/server/workspace-code.js index 9c29f710..398325bb 100644 --- a/server/workspace-code.js +++ b/server/workspace-code.js @@ -78,7 +78,7 @@ try { if (!eintraege.length) { console.log("Protokoll ist leer."); break; } console.log(""); for (const e of eintraege) { - console.log(` ${e.zeitpunkt.slice(0, 19).replace("T", " ")} ${(e.aktion || "").padEnd(24)} ` + console.log(` ${e.zeitpunkt.slice(0, 19).replace("T", " ")} ${(e.aktion || "").padEnd(26)}` + `${(e.rolle || "—").padEnd(8)} ${(e.detail || "").padEnd(18)} ${e.ip || ""}`); } console.log(""); diff --git a/server/workspace.js b/server/workspace.js index 17127a45..d70712cd 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -122,6 +122,28 @@ function db() { const jetzt = () => new Date().toISOString(); +/* Die echte Besucher-IP. + + Warum das nötig ist: Die Kette lautet Besucher -> Cloudflare -> Caddy -> + Express. `trust proxy` steht auf 1, Express nimmt daher den letzten + Eintrag aus X-Forwarded-For -- und das ist die Adresse von Cloudflare, + nicht die des Besuchers. Folge: Im Protokoll stand bei jedem dieselbe + IP, und viel schlimmer -- ALLE Nutzer teilten sich einen einzigen + Sperr-Zähler. Ein paar Tippfehler von drei Leuten hätten den Zugang für + alle gesperrt. + + Cloudflare setzt die echte Adresse in CF-Connecting-IP und überschreibt + den Kopf bei jeder Anfrage, ein Besucher kann ihn also nicht fälschen. + Einschränkung: Wer die Adresse des Servers kennt und ihn unter Umgehung + von Cloudflare direkt anspricht, könnte den Kopf frei setzen. Deshalb + wird auf die Peer-Adresse zurückgefallen, sobald der Kopf fehlt -- und + beide Werte landen im Protokoll, damit so etwas auffällt. */ +function echteIp(req) { + const cf = req.get("cf-connecting-ip"); + if (cf && cf.length <= 45) return cf.trim(); + return req.ip || "?"; +} + function hashe(code, salt, N) { return scryptSync(code, salt, SCRYPT.keylen, { N, r: SCRYPT.r, p: SCRYPT.p, maxmem: 256 * 1024 * 1024 }).toString("hex"); @@ -180,7 +202,7 @@ function sitzungSetzen(res, person, req) { const bis = new Date(Date.now() + SITZUNG_STUNDEN * 3600_000).toISOString(); db().prepare( "INSERT INTO sitzungen (token_hash, person_id, erstellt, gueltig_bis, ip, browser) VALUES (?,?,?,?,?,?)" - ).run(tokenHash(token), person.id, jetzt(), bis, req.ip || null, + ).run(tokenHash(token), person.id, jetzt(), bis, echteIp(req), (req.get("user-agent") || "").slice(0, 200)); res.cookie(COOKIE, token, { @@ -212,7 +234,7 @@ workspaceRouter.use((req, res, next) => { }); workspaceRouter.post("/workspace/api/anmelden", (req, res) => { - const ip = req.ip || "?"; + const ip = echteIp(req); try { if (zuVieleVersuche(ip)) { protokolliere("anmeldung_gesperrt", { ip }); @@ -266,7 +288,7 @@ workspaceRouter.post("/workspace/api/abmelden", (req, res) => { if (token) { const person = sitzungLesen(req); db().prepare("DELETE FROM sitzungen WHERE token_hash = ?").run(tokenHash(token)); - if (person) protokolliere("abmeldung", { personId: person.id, rolle: person.rolle, ip: req.ip }); + if (person) protokolliere("abmeldung", { personId: person.id, rolle: person.rolle, ip: echteIp(req) }); } } catch { /* Abmelden darf nie scheitern. */ } res.clearCookie(COOKIE, { path: "/workspace" });