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);