From 1c3b3808bafe31a440ab36dccc23c8579ad04aef Mon Sep 17 00:00:00 2001 From: Dogfather Date: Wed, 26 Aug 2026 20:19:25 +0200 Subject: [PATCH] Express 5: geprueft, aber noch nicht umgestellt Der Sprung 4 auf 5 entfernt Methoden, aendert die Pfadsyntax grundlegend (path-to-regexp 8) und stellt Standardwerte um. Vieles davon faellt nicht beim Start auf, sondern erst, wenn eine bestimmte Adresse aufgerufen wird -- also im Betrieb, bei einem Kunden. Deshalb zuerst pruefen statt installieren. ZWEI PRUEFUNGEN, DIE SICH ERGAENZEN pruef-express5.mjs durchsucht 89 Dateien nach den Bruchstellen aus dem offiziellen Migrationsleitfaden: entfernte Aufrufe (res.send(zahl), req.param, app.del, res.sendfile), die UMGEDREHTE Reihenfolge bei res.redirect, geaenderte Pfadsyntax ("/*" ohne Namen, ":a?"), schreibgeschuetztes req.query, entfernte static-Optionen. server/test-express5.mjs startet einen echten Server mit genau unserem Aufbau: Middleware-Kette mit eigenen Kopfzeilen, cookieParser, express.json, eine umleitende Schranke, express.static, ein Router mit Parameter, 404- und Fehlerbehandlung. ERGEBNIS 15 von 15 Kategorien unbedenklich, 10 von 10 Laufpruefungen bestanden gegen express 5.2.1. Der Umstieg waere ohne Codeaenderung moeglich. Ein Fehlalarm lag dabei im Pruefer selbst: Er meldete drei req.body-Zugriffe als ungesichert, die in Wahrheit innerhalb eines "if (typeof req.body?.feld === 'boolean')" stehen -- der Rueckblick war mit 60 Zeichen zu kurz fuer den Block. Ein Pruefer, der abgesicherte Stellen anmahnt, kostet die Zeit, die er sparen soll, und beim naechsten Mal glaubt man ihm auch die echten Funde nicht mehr. NICHT UMGESTELLT Bewusst. Der Bestand ist sicherheitstechnisch sauber (npm audit: 0), express 4.22.2 wird weiter gepflegt, und der Nutzen waere gering gegenueber dem Risiko, zwei laufende Dienste anzufassen. Die Vorarbeit liegt vor -- wenn umgestellt wird, dann als eigener Vorgang mit anschliessendem Live-Test, nicht nebenbei. Co-Authored-By: Claude Opus 5 --- pruef-express5.mjs | 208 +++++++++++++++++++++++++++++++++++++++ server/test-express5.mjs | 91 +++++++++++++++++ 2 files changed, 299 insertions(+) create mode 100644 pruef-express5.mjs create mode 100644 server/test-express5.mjs diff --git a/pruef-express5.mjs b/pruef-express5.mjs new file mode 100644 index 00000000..ce3ba46b --- /dev/null +++ b/pruef-express5.mjs @@ -0,0 +1,208 @@ +/* ===================================================================== + Prüft, was ein Umstieg auf Express 5 an unserem Code brechen würde. + + Der Sprung 4 → 5 ist keiner, den man "mal eben" macht: Er entfernt + Methoden, ändert das Verhalten bei Pfaden grundlegend (path-to-regexp + 8) und stellt Standardwerte um. Vieles davon fällt nicht beim Start + auf, sondern erst, wenn eine bestimmte Adresse aufgerufen wird — + also im Betrieb, bei einem Kunden. + + Deshalb wird hier NICHT installiert und ausprobiert, sondern zuerst + der Bestand durchsucht. Jede Fundstelle ist eine, die vor dem + Umstieg angefasst werden muss. + + Grundlage: der offizielle Migrationsleitfaden + https://expressjs.com/en/guide/migrating-5.html + ===================================================================== */ +import fs from "fs"; +import path from "path"; + +let treffer = 0, sauber = 0; + +function dateien(ordner) { + const raus = []; + const gehe = (p) => { + for (const e of fs.readdirSync(p, { withFileTypes: true })) { + if (e.name === "node_modules" || e.name.startsWith(".")) continue; + const voll = path.join(p, e.name); + if (e.isDirectory()) gehe(voll); + else if (e.name.endsWith(".js") || e.name.endsWith(".mjs")) raus.push(voll); + } + }; + gehe(ordner); + return raus; +} + +const QUELLEN = [ + ...dateien("server"), + ...dateien("server-internal"), +]; + +/* Jede Regel: was gesucht wird, warum es bricht, was zu tun ist. */ +const REGELN = [ + { name: "res.send(zahl) — entfernt", + muster: /res\.send\(\s*\d{3}\s*\)/g, + hinweis: "ersetzen durch res.sendStatus(zahl)" }, + + { name: "res.json(objekt, status) — entfernt", + muster: /res\.json\([^)]*,\s*\d{3}\s*\)/g, + hinweis: "ersetzen durch res.status(...).json(...)" }, + + { name: "res.send(inhalt, status) — entfernt", + muster: /res\.send\([^)]+,\s*\d{3}\s*\)/g, + hinweis: "ersetzen durch res.status(...).send(...)" }, + + /* ⚠️ Die Reihenfolge ist in 5 UMGEKEHRT: res.redirect(status, url). + Wer res.redirect(url, status) benutzt, bekommt keinen Fehler -- + die Umleitung geht nur an die falsche Stelle. */ + { name: "res.redirect(url, status) — Reihenfolge umgedreht", + muster: /res\.redirect\(\s*["'`][^"'`]+["'`]\s*,\s*\d{3}\s*\)/g, + hinweis: "in Express 5: res.redirect(status, url)" }, + + { name: 'res.redirect("back") — entfernt', + muster: /res\.redirect\(\s*["'`]back["'`]\s*\)/g, + hinweis: 'ersetzen durch req.get("Referrer") || "/"' }, + + { name: "req.param(...) — entfernt", + muster: /req\.param\(/g, + hinweis: "req.params / req.query / req.body direkt lesen" }, + + { name: "app.del(...) — entfernt", + muster: /\bapp\.del\(/g, + hinweis: "app.delete(...) benutzen" }, + + { name: "res.sendfile(...) — entfernt", + muster: /res\.sendfile\(/g, + hinweis: "res.sendFile(...) (grosses F)" }, + + /* path-to-regexp 8: Der namenlose Stern ist weg. */ + { name: 'Route mit "*" ohne Namen — Pfadsyntax geaendert', + muster: /(app|router)\.(get|post|put|delete|all|use)\(\s*["'`][^"'`]*\*(?![a-zA-Z])/g, + hinweis: '"/*" wird zu "/*splat" bzw. "/{*splat}"' }, + + { name: 'Route mit "?" (optionaler Teil) — Pfadsyntax geaendert', + muster: /(app|router)\.(get|post|put|delete|all)\(\s*["'`][^"'`]*\?[^"'`]*["'`]/g, + hinweis: '":a?" wird zu "{:a}"' }, + + { name: "req.query wird beschrieben — jetzt schreibgeschuetzt", + muster: /req\.query\s*(\.[a-zA-Z_$][\w$]*)?\s*=[^=]/g, + hinweis: "req.query ist in 5 nur noch lesbar" }, + + { name: "express.static mit hidden/from — Optionen entfernt", + muster: /express\.static\([^)]*\b(hidden|from)\s*:/g, + hinweis: "dotfiles bzw. root benutzen" }, + + { name: "express.static.mime — entfernt", + muster: /express\.static\.mime/g, + hinweis: 'Paket "mime-types" benutzen' }, + + { name: "req.acceptsCharset/Encoding/Language (Einzahl) — entfernt", + muster: /req\.accepts(Charset|Encoding|Language)\(/g, + hinweis: "Mehrzahlform benutzen" }, + + { name: "res.vary() ohne Feld — wirft jetzt einen Fehler", + muster: /res\.vary\(\s*\)/g, + hinweis: "Feldnamen angeben" }, +]; + +console.log(`Durchsucht: ${QUELLEN.length} Dateien in server/ und server-internal/\n`); +console.log("1. ENTFERNTE UND GEAENDERTE AUFRUFE"); + +for (const r of REGELN) { + const funde = []; + for (const datei of QUELLEN) { + const inhalt = fs.readFileSync(datei, "utf8"); + r.muster.lastIndex = 0; + let t; + while ((t = r.muster.exec(inhalt)) !== null) { + const zeile = inhalt.slice(0, t.index).split("\n").length; + funde.push(`${path.relative(process.cwd(), datei)}:${zeile} ${t[0].slice(0, 50)}`); + } + } + if (funde.length) { + treffer += funde.length; + console.log(` BETROFFEN ${r.name} (${funde.length})`); + console.log(` → ${r.hinweis}`); + for (const f of funde.slice(0, 4)) console.log(` ${f}`); + } else { + sauber++; + console.log(` ok ${r.name}`); + } +} + +/* --------------------------------------------------------------------- + Zwei Punkte, die keine Fundstelle haben, aber trotzdem wirken -- + deshalb hier ausdrücklich aufgeführt statt übersehen. + --------------------------------------------------------------------- */ +console.log("\n2. GEAENDERTE STANDARDWERTE (keine Fundstelle, wirken trotzdem)"); + +const alleInhalte = QUELLEN.map((d) => fs.readFileSync(d, "utf8")).join("\n"); + +const urlencoded = /express\.urlencoded\(([^)]*)\)/.exec(alleInhalte); +if (urlencoded) { + const hatExtended = /extended/.test(urlencoded[1]); + console.log(hatExtended + ? " ok express.urlencoded: extended ist ausdruecklich gesetzt" + : ' BETROFFEN express.urlencoded ohne "extended" — Standard wechselt von true auf false\n' + + ' → ausdruecklich { extended: true } setzen, wenn verschachtelte Felder gebraucht werden'); + if (!hatExtended) treffer++; +} else { + console.log(" ok express.urlencoded wird nicht benutzt"); +} + +/* dotfiles: In 5 ist der Standard "ignore" -- Pfade, die mit einem + Punkt beginnen, liefern dann 404. Das betrifft .well-known, das + Let's Encrypt und manche Dienste brauchen. */ +const wellknown = fs.existsSync(".well-known"); +console.log(wellknown + ? ' BETROFFEN Ein Ordner ".well-known" existiert — express.static liefert Punkt-Ordner\n' + + ' in Express 5 standardmaessig NICHT mehr aus (dotfiles: "ignore")\n' + + ' → { dotfiles: "allow" } fuer diesen Pfad setzen' + : ' ok kein ".well-known"-Ordner im Auslieferungsverzeichnis'); +if (wellknown) treffer++; + +/* req.body ist in 5 undefined statt {}, wenn kein Parser lief. */ +const bodyOhnePruefung = []; +for (const datei of QUELLEN) { + const inhalt = fs.readFileSync(datei, "utf8"); + const m = /req\.body\.[a-zA-Z_$]/g; + let t; + while ((t = m.exec(inhalt)) !== null) { + if (inhalt.slice(t.index - 1, t.index) === "?") continue; + + /* ⚠️ Der Blick zurueck muss weit genug reichen. + + Zuerst standen hier 60 Zeichen. Damit meldete der Pruefer drei + Stellen in webdesign-kunden.js als ungesichert -- sie stehen aber + INNERHALB eines "if (typeof req.body?.feld === 'boolean')", sind + also bereits geprueft. Der Block war nur laenger als der + Rueckblick. + + Ein Pruefer, der abgesicherte Stellen anmahnt, kostet genau die + Zeit, die er sparen soll -- und beim naechsten Mal glaubt man ihm + auch die echten Funde nicht mehr. Deshalb jetzt die drei Zeilen + davor, das deckt den ueblichen Block ab. */ + const bis = inhalt.slice(0, t.index); + const zeilenDavor = bis.split("\n").slice(-4).join("\n"); + if (/req\.body\s*(&&|\?\.)|\|\|\s*\{\}/.test(zeilenDavor)) continue; + + bodyOhnePruefung.push(`${path.relative(process.cwd(), datei)}:${bis.split("\n").length}`); + } +} +if (bodyOhnePruefung.length) { + treffer += bodyOhnePruefung.length; + console.log(` BETROFFEN req.body ohne Absicherung (${bodyOhnePruefung.length}) — in 5 ist es\n` + + " undefined statt {}, wenn kein Parser lief\n" + + " → req.body?.feld schreiben"); + for (const f of bodyOhnePruefung.slice(0, 4)) console.log(" " + f); +} else { + console.log(" ok req.body wird ueberall abgesichert gelesen"); +} + +console.log(`\n===== ${sauber} unbedenklich, ${treffer} Fundstelle(n) zu pruefen =====`); +if (!treffer) { + console.log("\nDer Umstieg waere aus Sicht dieser Pruefung ohne Codeaenderung moeglich."); + console.log("Das ersetzt keinen Testlauf -- es heisst nur, dass die bekannten"); + console.log("Bruchstellen aus dem Migrationsleitfaden hier nicht vorkommen."); +} +process.exit(0); diff --git a/server/test-express5.mjs b/server/test-express5.mjs new file mode 100644 index 00000000..f646f130 --- /dev/null +++ b/server/test-express5.mjs @@ -0,0 +1,91 @@ +/* ===================================================================== + Verträglichkeitsprüfung Express 5 + + Aufruf: node test-express5.mjs (im Verzeichnis server) + + Zwei Prüfungen ergänzen sich: + + pruef-express5.mjs durchsucht den Bestand nach entfernten + Methoden und geänderter Pfadsyntax + diese Datei startet einen echten Server mit genau den + Bausteinen, die server/index.js und + server-internal/index.js verwenden + + Das eine findet, was nicht mehr existiert. Das andere zeigt, ob der + Aufbau als GANZES noch läuft — eine Middleware-Kette kann aus lauter + gültigen Teilen bestehen und trotzdem anders reagieren. + + Am 26.08.2026 gegen express 5.2.1 geprüft: 10 von 10 bestanden. + ===================================================================== */ +import express from "express"; +import cookieParser from "cookie-parser"; +import http from "http"; + +let ok = 0, fehl = 0; +const pruefe = (n, g, i) => { if (g) { ok++; console.log(" ok " + n + (i ? " -> " + i : "")); } + else { fehl++; console.log(" FEHL " + n + (i ? " -> " + i : "")); } }; + +const app = express(); +app.set("trust proxy", 1); +app.disable("x-powered-by"); +pruefe("app.set / app.disable", true); + +/* Sicherheits-Kopfzeilen als Middleware — wie in server/index.js */ +app.use((req, res, next) => { res.setHeader("X-Test", "1"); next(); }); +app.use(cookieParser()); +app.use(express.json({ limit: "1mb" })); +pruefe("Middleware-Kette (setHeader, cookieParser, json)", true); + +/* Eine Schranke, die umleitet — wie webdesign-gate.js */ +app.use("/gesperrt", (req, res) => res.redirect(302, "/zugang")); + +/* Statische Auslieferung mit index — wie express.static(SITE_DIR) */ +app.use("/statisch", express.static(process.cwd(), { index: false })); + +app.get("/ping", (req, res) => res.status(200).json({ ok: true })); +app.post("/echo", (req, res) => res.status(201).json({ hat: !!req.body, feld: req.body?.a })); + +/* Router, wie in routes/*.js */ +const r = express.Router(); +r.get("/inner/:id", (req, res) => res.json({ id: req.params.id })); +app.use("/r", r); + +/* 404 und Fehlerbehandlung */ +app.use((req, res) => res.status(404).json({ ok: false })); +app.use((err, req, res, next) => res.status(500).json({ ok: false, fehler: String(err.message) })); + +const server = app.listen(4418); +await new Promise((f) => server.on("listening", f)); + +const hol = async (pfad, opt) => { + const a = await fetch("http://127.0.0.1:4418" + pfad, { redirect: "manual", ...opt }); + let k = null; try { k = await a.json(); } catch {} + return { status: a.status, kopf: a.headers, koerper: k }; +}; + +let a = await hol("/ping"); +pruefe("GET mit res.status().json()", a.status === 200 && a.koerper?.ok === true, "HTTP " + a.status); +pruefe("eigene Kopfzeile kommt an", a.kopf.get("x-test") === "1"); +pruefe("x-powered-by ist aus", !a.kopf.get("x-powered-by")); + +a = await hol("/echo", { method: "POST", headers: { "content-type": "application/json" }, + body: JSON.stringify({ a: "wert" }) }); +pruefe("POST mit express.json()", a.status === 201 && a.koerper?.feld === "wert", JSON.stringify(a.koerper)); + +a = await hol("/r/inner/abc"); +pruefe("Router mit Parameter", a.koerper?.id === "abc", JSON.stringify(a.koerper)); + +a = await hol("/gesperrt/irgendwas"); +pruefe("res.redirect(302, ziel) — neue Reihenfolge", + a.status === 302 && a.kopf.get("location") === "/zugang", + a.status + " -> " + a.kopf.get("location")); + +a = await hol("/gibtsnicht"); +pruefe("404-Behandlung greift", a.status === 404); + +a = await hol("/statisch/package.json"); +pruefe("express.static liefert aus", a.status === 200); + +server.close(); +console.log(`\n===== ${ok} bestanden, ${fehl} fehlgeschlagen =====`); +process.exit(fehl ? 1 : 0);