diff --git a/.gitignore b/.gitignore index ea34cc99..fd33791f 100644 --- a/.gitignore +++ b/.gitignore @@ -135,3 +135,5 @@ tools/.vid/ tools/.commit-* tools/.dreh* tools/.nach-* + +tools/.messkopf.txt diff --git a/tools/.messkopf.txt b/tools/.messkopf.txt deleted file mode 100644 index b0be30a3..00000000 --- a/tools/.messkopf.txt +++ /dev/null @@ -1,226 +0,0 @@ -/* ===================================================================== - WIE DIE REACTION AUSSIEHT (28.09.2026) - - Die Pruefung nebenan misst, was die Schnittstelle antwortet. Das - sagt nichts darueber, ob ein Mensch damit einen Abend verbringen - moechte. Dieses Werkzeug oeffnet die Seite in einem echten Browser, - schaltet sie durch alle drei Zustaende und legt Bilder ab. - - Gemessen wird dabei auch, was man sonst uebersieht: - - laeuft etwas in die Konsole? - - ragt etwas seitlich hinaus? - - ist der Saal so hoch wie das Fenster, oder muss man scrollen? - - NIE DIE ECHTE DATENBANK. Eigener Ordner, eigener Port, eigenes - Zertifikat -- und am Ende ist alles weg. - - Aufrufen mit: node server/mess-reaktion.mjs - ===================================================================== */ -import { mkdtempSync, rmSync, readFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join, dirname } from "node:path"; -import { fileURLToPath } from "node:url"; -import { request } from "node:http"; -import { execFileSync } from "node:child_process"; -import { createServer as httpsServer } from "node:https"; - -const HIER = dirname(fileURLToPath(import.meta.url)); -const ordner = mkdtempSync(join(tmpdir(), "mess-reakt-")); -process.env.WORKSPACE_DB = join(ordner, "workspace.db"); - -const PORT = 5483; -const HTTPS_PORT = PORT + 1; -process.env.PORT = `${PORT}`; -process.env.SITE_ACCESS_SECRET = "lokaler-test"; -process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; - -for (const art of ["uncaughtException", "unhandledRejection"]) { - process.on(art, (f) => { - console.error(`\nABBRUCH (${art}): ${f?.stack || f}`); - process.exit(8); - }); -} - -const express = (await import("express")).default; -const ec = express.response.cookie; -express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; -await import("./index.js"); -await new Promise((r) => setTimeout(r, 1000)); - -const CREW = "crew.dogfather-universe.com"; - -/* ---------- Leute ------------------------------------------------------- */ -const { DatabaseSync } = await import("node:sqlite"); -const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); -const d = new DatabaseSync(process.env.WORKSPACE_DB); -const kenn = randomBytes(32).toString("hex"); -d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" - + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") - .run("code_kennung_schluessel", kenn, new Date().toISOString()); -const ALT = new Date(Date.now() - 200 * 86400000).toISOString(); -function anlegen(name, rolle, code) { - const salt = randomBytes(16).toString("hex"); - const hash = scryptSync(code, salt, 64, - { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); - d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n," - + " code_kennung, aktiv, erstellt) VALUES (?,?,?,?,?,?,1,?)") - .run(name, rolle, hash, salt, 32768, - createHmac("sha256", kenn).update(code).digest("hex"), ALT); - return d.prepare("SELECT last_insert_rowid() AS id").get().id; -} -const CODES = { admin: "CODE-M00-000001", gast: "CODE-M01-000001", gast2: "CODE-M02-000001" }; -const IDS = { - admin: anlegen("DogFather", "admin", CODES.admin), - gast: anlegen("Nachtfalter", "gast", CODES.gast), - gast2: anlegen("Lenny", "gast", CODES.gast2), -}; -d.close(); - -function roh(pfad, { method = "GET", keks = "", rumpf = null } = {}) { - return new Promise((fertig, schief) => { - const daten = rumpf === null ? null : Buffer.from(JSON.stringify(rumpf)); - const a = request({ - host: "127.0.0.1", port: PORT, path: pfad, method, - headers: { - Host: CREW, Origin: `https://${CREW}`, - ...(keks ? { Cookie: keks } : {}), - ...(daten ? { "Content-Type": "application/json", "Content-Length": daten.length } : {}), - }, - }, (antwort) => { - const teile = []; - antwort.on("data", (s) => teile.push(s)); - antwort.on("end", () => { - const text = Buffer.concat(teile).toString("utf8"); - let j = null; try { j = JSON.parse(text); } catch { /* kein JSON */ } - fertig({ code: antwort.statusCode, text, j, kopf: antwort.headers }); - }); - }); - a.on("error", schief); - if (daten) a.write(daten); - a.end(); - }); -} -async function anmelden(rolle) { - const a = await roh("/workspace/api/anmelden", { - method: "POST", - rumpf: { rolle: rolle === "gast2" ? "gast" : rolle, code: CODES[rolle], alter_ok: true }, - }); - return [].concat(a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; "); -} -const k = { - admin: await anmelden("admin"), - gast: await anmelden("gast"), - gast2: await anmelden("gast2"), -}; - -/* ---------- Die Buehne fuellen ------------------------------------------ */ -await roh("/workspace/api/reaktion", { - method: "PUT", keks: k.admin, - rumpf: { - titel: "Wir schauen den neuen Trailer", - video: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", - beginnt_am: new Date(Date.now() + 7 * 60_000).toISOString(), - }, -}); - -/* ZWEI STUECKE IN DIE WARTESCHLANGE. Eine leere Liste sieht auf - einem Bildschirmfoto genauso aus wie eine kaputte -- beides ist - ein Kasten ohne Zeilen. Zwei echte Eintraege zeigen, ob Titel, - Vorschaubild und die vier Knoepfe je Zeile wirklich ankommen. */ -for (const adresse of ["https://youtu.be/9bZkp7q19f0", - "https://www.youtube.com/watch?v=kJQP7kiw5Fk"]) { - await roh("/workspace/api/reaktion/liste", { - method: "POST", keks: k.admin, rumpf: { video: adresse }, - }); -} - -const REDEN = [ - ["gast", "Bin schon daaa 🐺"], - ["gast2", "Ich auch! Hab extra Popcorn geholt"], - ["gast", "Wie lange noch?"], - ["admin", "Zwei Minuten, ich richte noch die Kamera"], - ["gast2", "Kein Stress, wir sitzen bequem"], - ["gast", "Letztes Mal war so gut, ich hab geheult"], -]; - -/* ---------- Der Vorbau -------------------------------------------------- */ -const schluesselDatei = join(ordner, "p.key"); -const zertDatei = join(ordner, "p.crt"); -execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", - "-keyout", schluesselDatei, "-out", zertDatei, "-days", "2", - "-subj", `/CN=${CREW}`, "-addext", `subjectAltName=DNS:${CREW}`], { stdio: "ignore" }); - -const vorbau = httpsServer({ - key: readFileSync(schluesselDatei), cert: readFileSync(zertDatei), -}, (von, nach) => { - const weiter = request({ - host: "127.0.0.1", port: PORT, path: von.url, method: von.method, - headers: { ...von.headers, host: `${CREW}:${HTTPS_PORT}` }, - }, (antwort) => { - nach.writeHead(antwort.statusCode || 502, antwort.headers); - antwort.pipe(nach); - }); - weiter.on("error", () => { try { nach.writeHead(502); nach.end(); } catch { /* egal */ } }); - von.pipe(weiter); -}); -await new Promise((r) => vorbau.listen(HTTPS_PORT, "127.0.0.1", r)); -const BASIS = `https://${CREW}:${HTTPS_PORT}`; - -const pw = await import( - "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); -const browser = await pw.chromium.launch({ - args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors", - /* Kamera und Mikrofon ohne Rueckfrage -- und ein bewegtes - Testbild statt eines schwarzen Rechtecks. Ohne das sieht jede - Aufnahme aus, als waere die Kamera kaputt. */ - "--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream"], -}); - -/* JEDE GEOEFFNETE SEITE HAELT ZWEI STEHENDE VERBINDUNGEN (der - Ereignisstrom der Kopfleiste und der dieser Seite). Ein Browser - laesst je Adresse nur wenige davon zu -- wer seine Seiten nicht - schliesst, nimmt den spaeteren den Platz weg. - - GEMESSEN: Die Seiten aus den ersten beiden Abschnitten blieben - offen, und danach kam Lennys Strom gar nicht mehr zustande. Die - Signale gingen ins Leere, und die Kamerafenster blieben schwarz -- - ein Messfehler, der wie ein Programmfehler aussah. */ -async function alsWer(rolle, breite, hoehe) { - const kontext = await browser.newContext({ - viewport: { width: breite, height: hoehe }, - ignoreHTTPSErrors: true, - permissions: ["camera", "microphone"], - }); - await kontext.addCookies((k[rolle] || "").split("; ").filter(Boolean).map((z) => { - const [n, ...r] = z.split("="); - return { name: n, value: r.join("="), domain: CREW, path: "/" }; - })); - const seite = await kontext.newPage(); - const konsole = []; - const anfragen = []; - seite.on("request", (r) => { if (/\/api\//.test(r.url())) anfragen.push(r.url().replace(/^https?:\/\/[^/]+/, "")); }); - seite.on("pageerror", (f) => konsole.push(String(f?.message || f))); - seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); }); - seite.on("response", (a) => { - if (a.status() >= 400) konsole.push(`${a.status()} ${a.url().replace(/^https?:\/\/[^/]+/, "")}`); - }); - return { kontext, seite, konsole, anfragen }; -} - -async function bild(seite, name) { - await seite.screenshot({ path: join(HIER, `mess-reaktion-${name}.png`) }); - console.log(` Bild: server/mess-reaktion-${name}.png`); -} - -async function vermessen(seite, wo) { - const m = await seite.evaluate(() => ({ - ueber: Math.max(0, document.documentElement.scrollWidth - document.documentElement.clientWidth), - hoch: Math.max(0, document.documentElement.scrollHeight - document.documentElement.clientHeight), - stand: document.getElementById("saal")?.dataset.stand || "?", - schild: document.getElementById("schild-wort")?.textContent || "", - })); - console.log(` ${wo}: Stand „${m.stand}", Schild „${m.schild}", ` - + `seitlich ${m.ueber} px, senkrecht ${m.hoch} px`); - return m; -} -