import 'server-only' import { env } from '@/lib/env' /** * Von welchem Anschluss kommt diese Anfrage? * * Gebraucht wird das nur fuer eines: die zweite Dimension der Anmeldesperre. * Gezaehlt wird nach E-Mail UND nach Anschluss, damit es weder hilft, * dieselbe Adresse von vielen Anschluessen zu probieren, noch viele * Adressen von einem. * * ## Warum das vorher nichts geschuetzt hat * * Der Wert kam aus "x-forwarded-for", und zwar aus dem ERSTEN Eintrag: * * kopf.get('x-forwarded-for')?.split(',')[0]?.trim() * * Dieser Kopf ist eine Liste, an die jeder Zwischenknoten hinten anhaengt. * Der erste Eintrag ist deshalb der, den der Aufrufer selbst geschickt hat - * frei erfunden. Wer bei jeder Anfrage eine andere Zahl hineinschrieb, sah * fuer die Sperre wie tausend verschiedene Anschluesse aus. Die Zaehlung * nach Anschluss war damit nicht nur wirkungslos, sondern irrefuehrend: Im * Protokoll standen tausend Anschluesse, wo einer war. * * Richtig ist das andere Ende: Was der vertraute Proxy anhaengt, hat er * selbst gesehen und nicht der Aufrufer geschrieben. * * ## Und wenn kein Proxy davor steht * * Dann ist der Kopf vollstaendig fremdbestimmt und es gibt hier nichts zu * holen - also null. Keine Zaehlung nach Anschluss ist besser als eine, die * sich mit einer Kopfzeile aushebeln laesst. Deshalb muss der Betrieb * ausdruecklich sagen, dass ein Proxy davorsteht (VERTRAUE_PROXY=true). */ let gewarnt = false /** * Warnt einmal, wenn weitergereichte Koepfe ankommen, ihnen aber nicht * geglaubt wird. Genau dann fehlt die Einstellung im Betrieb. */ function einmalWarnen(): void { if (gewarnt) return gewarnt = true console.warn( '[anschluss] Es kommen "x-forwarded-for"-Koepfe an, aber VERTRAUE_PROXY ist nicht gesetzt. ' + 'Die Anmeldesperre zaehlt deshalb nur nach E-Mail-Adresse, nicht nach Anschluss. ' + 'Steht ein Reverse Proxy davor, setze VERTRAUE_PROXY=true (und, bei mehreren, ' + 'VERTRAUE_PROXY_STUFEN=). Steht keiner davor, ist das hier richtig so.', ) } /** * Liest die Adresse des Aufrufers aus den Koepfen. * * @param kopf Die Kopfzeilen der Anfrage. * @returns Die Adresse, oder null, wenn sie sich nicht vertrauenswuerdig * bestimmen laesst. */ export function anschlussAus(kopf: Headers): string | null { const weitergereicht = kopf.get('x-forwarded-for') const echterIp = kopf.get('x-real-ip') if (!env.vertraueProxy) { if (weitergereicht || echterIp) einmalWarnen() return null } if (weitergereicht) { const kette = weitergereicht .split(',') .map((teil) => teil.trim()) .filter((teil) => teil.length > 0) // Von hinten zaehlen: Der letzte Eintrag stammt vom naechsten // Zwischenknoten, der zweitletzte von dem davor. Bei einer Stufe ist // der letzte Eintrag der Aufrufer, wie der Proxy ihn gesehen hat. if (kette.length > 0) { const stelle = kette.length - env.vertraueProxyStufen return kette[Math.max(0, stelle)] ?? null } } // Manche Proxys setzen nur diesen Kopf. Er enthaelt genau eine Adresse, // also gibt es hier nichts abzuzaehlen. return echterIp?.trim() || null } /** Nur fuer Tests: setzt die einmalige Warnung zurueck. */ export function warnungZuruecksetzen(): void { gewarnt = false }