diff --git a/README.md b/README.md index c41ade8..a97a2e0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ -# ToolCTRL — Landing Page +# ToolCTRL — Website -Eine-Seiten-Website im Visitenkarten-Stil für den Messeauftritt: Landing Page -plus Lead-Formular zur Kontaktsammlung. Monochrom (weiß / schwarz), an die -ToolCTRL Brand Identity angelehnt, ohne externe Abhängigkeiten. +Mehrseitige Website im Visitenkarten-Stil: Landing Page mit Lead-Formular, +TechDay-2026-Seite, AI-GameJam-Seite mit Datei-Upload sowie Impressum und +Datenschutz. Monochrom (weiß / schwarz), an die ToolCTRL Brand Identity +angelehnt. Der Server läuft ohne npm-Pakete (nur Node-Standardbibliothek). ## Starten @@ -10,35 +11,85 @@ ToolCTRL Brand Identity angelehnt, ohne externe Abhängigkeiten. node server.js ``` -Der Server sucht automatisch einen freien Port (zuerst `4317`) und gibt die -URL aus, z. B. `http://127.0.0.1:4317`. Stoppen mit `Strg + C`. +Der Server sucht automatisch einen freien Port (zuerst `4317`) und gibt lokale +und Netzwerk-URL aus. Stoppen mit `Strg + C`. -Voraussetzung: Node.js (getestet mit v26). Keine Installation, keine `npm`-Pakete. +Voraussetzung: Node.js (getestet mit v26). ## Aufbau ``` -public/ statische Website (HTML, CSS, JS, Assets) - index.html Landing Page + Kontaktformular - styles.css Designsystem - script.js Formular-Logik & Mobile-Navigation - assets/ Logos, Emblem, Favicon, Icon-Set -server.js abhängigkeitsfreier Node-HTTP-Server + Lead-Endpunkt -leads/ gesammelte Anfragen (wird automatisch angelegt) - leads.csv Anfragen als CSV — direkt in Excel öffenbar - leads.ndjson dieselben Anfragen als JSON pro Zeile +public/ statische Website + index.html Landing Page + Kontaktformular + techday.html TechDay Bruchsal 2026 + gamejam.html AI GameJam (eigenes Styling, Spiel-Upload) + impressum.html Impressum + datenschutz.html Datenschutzerklärung + styles.css Designsystem + script.js Formular-Logik & Mobile-Navigation + assets/ Logos, Emblem, Favicon, Icons, Schriften, Mockups +server.js abhängigkeitsfreier Node-HTTP-Server + API +leads/ gesammelte Kontaktanfragen (wird automatisch angelegt) +gamejam/ GameJam-Einreichungen, Metadaten (wird automatisch angelegt) ``` +`leads/`, `gamejam/` und hochgeladene Spiele sind per `.gitignore` vom Repo +ausgeschlossen. + ## Leads -Jede abgesendete Anfrage landet sofort lokal auf der Platte — in `leads.csv` -**und** `leads.ndjson`. So geht auf der Messe keine Anfrage verloren, auch -ohne Internet. Ein Honeypot-Feld fängt einfache Spam-Bots ab. +`POST /api/lead` (JSON, Pflichtfelder `name`, `email`, `nachricht`). Jede +Anfrage landet sofort lokal in `leads/leads.csv` **und** `leads.ndjson` — so +geht auf der Messe nichts verloren. Ein Honeypot-Feld fängt einfache Bots ab. -Endpunkt: `POST /api/lead` (JSON). Pflichtfelder: `name`, `email`, `nachricht`. +## GameJam-Upload & Virenscan (ClamAV) + +`POST /api/gamejam/upload` nimmt eine `.html`-Datei entgegen (max. 5 MB). +**Jede hochgeladene Datei wird vor dem Speichern per ClamAV auf Viren +geprüft.** Befund → Ablehnung; ist der Scanner nicht erreichbar, wird der +Upload ebenfalls abgelehnt (fail-closed, kein ungeprüftes Speichern). + +Der Server spricht den ClamAV-Daemon `clamd` direkt über sein Socket-Protokoll +an — keine zusätzliche Abhängigkeit. Voraussetzung: ClamAV ist installiert und +`clamd` läuft. Beim Start meldet der Server `Virenscan: aktiv` bzw. eine +Warnung, falls `clamd` nicht erreichbar ist. + +### ClamAV einrichten — macOS + +```bash +brew install clamav +cd "$(brew --prefix)/etc/clamav" +cp freshclam.conf.sample freshclam.conf +cp clamd.conf.sample clamd.conf +# In BEIDEN Dateien die Zeile "Example" entfernen oder auskommentieren. +# In clamd.conf zusätzlich diese zwei Zeilen aktivieren (Raute entfernen): +# TCPSocket 3310 +# TCPAddr 127.0.0.1 +freshclam # Virendefinitionen laden (einmalig) +clamd # Daemon starten +``` + +### ClamAV einrichten — Linux (Debian/Ubuntu) + +```bash +sudo apt install clamav clamav-daemon +# In /etc/clamav/clamd.conf: TCPSocket 3310 und TCPAddr 127.0.0.1 setzen +sudo systemctl restart clamav-daemon +``` + +### Konfiguration + +Host/Port und An/Aus stehen in `server.js` im Objekt `CLAMAV`. Mit +`enabled: false` läuft der Upload ohne Virenscan (nur für lokale Tests +ohne ClamAV gedacht). + +### Funktion testen + +Eine Datei mit der EICAR-Testsignatur hochladen — `clamd` meldet dann +`Eicar-Test-Signature FOUND`, der Upload wird abgelehnt. ## Hinweis für die Messe -Für den QR-Code am Stand sollte der Rechner mit dem Server im selben WLAN -erreichbar sein. Soll die Seite öffentlich laufen, hinter einen Reverse-Proxy -mit HTTPS setzen oder bei einem Hoster bereitstellen — `tool-ctrl.de`. +Für den QR-Code am Stand muss der Rechner im selben WLAN erreichbar sein. +Soll die Seite öffentlich laufen, hinter einen Reverse-Proxy mit HTTPS setzen +oder bei einem Hoster bereitstellen — `tool-ctrl.de`. diff --git a/public/index.html b/public/index.html index b9f04f6..6637141 100644 --- a/public/index.html +++ b/public/index.html @@ -4,7 +4,7 @@ ToolCTRL — from vision to version - + @@ -46,14 +46,14 @@

- Software und Automatisierung,
- die einfach läuft. + Moderne, durchdachte All-in-One-Software für Branchen, die zu + lange übersehen wurden.

- ToolCTRL plant, baut und betreibt individuelle Software — von der - Prozessautomatisierung über interne Tools bis zur Integration - bestehender Systeme. Schnell umgesetzt, sauber konstruiert, - zuverlässig im Betrieb. + Ganze Branchen arbeiten noch mit veralteten, zusammengestückelten + Insellösungen. Wir bei ToolCTRL bauen die Alternative: Software mit + echtem Fokus auf Bedienung und Gestaltung, die gut funktioniert, + klar aussieht und sich schnell bedienen lässt.

Projekt besprechen diff --git a/public/styles.css b/public/styles.css index 11a0645..97b5ab5 100644 --- a/public/styles.css +++ b/public/styles.css @@ -283,9 +283,9 @@ a { color: inherit; text-decoration: none; } padding: 96px 32px 104px; } .hero-title { - font-size: clamp(36px, 4.6vw, 62px); - line-height: 1.05; - letter-spacing: -.022em; + font-size: clamp(30px, 3.6vw, 48px); + line-height: 1.12; + letter-spacing: -.02em; font-weight: 700; margin: 0 0 24px; } diff --git a/server.js b/server.js index aa4d1ac..7812d6d 100644 --- a/server.js +++ b/server.js @@ -3,7 +3,8 @@ Abhängigkeitsfrei (nur Node-Standardbibliothek). - Liefert /public - POST /api/lead — Kontakt-Leads - - POST /api/gamejam/upload — GameJam: Spiel einreichen (Datei-Upload) + - POST /api/gamejam/upload — GameJam: Spiel einreichen (Datei-Upload, + wird per ClamAV auf Viren geprüft) - GET /api/gamejam/games — GameJam: eingereichte Spiele ============================================================ */ @@ -14,6 +15,7 @@ const fs = require("fs"); const path = require("path"); const crypto = require("crypto"); const os = require("os"); +const net = require("net"); const ROOT = __dirname; const PUBLIC_DIR = path.join(ROOT, "public"); @@ -27,6 +29,17 @@ const SUBMISSIONS_JSON = path.join(GAMEJAM_DIR, "submissions.ndjson"); const GAMES_DIR = path.join(PUBLIC_DIR, "gamejam-games"); const MAX_UPLOAD = 5 * 1024 * 1024; /* 5 MB pro eingereichtem Spiel */ +/* Virenscan über ClamAV. Der Server spricht das clamd-Socket-Protokoll + direkt — keine zusätzliche Abhängigkeit. Voraussetzung: ClamAV ist + installiert und der Daemon "clamd" läuft (Einrichtung siehe README). + enabled:false schaltet den Scan ab (z. B. lokal ohne ClamAV). */ +const CLAMAV = { + enabled: true, + host: "127.0.0.1", + port: 3310, /* clamd TCPSocket-Standardport */ + timeoutMs: 15000 +}; + /* Bevorzugte Ports — der erste freie wird genutzt. */ const PORT_CANDIDATES = [4317, 4318, 4417, 5317, 8317, 0]; /* 0.0.0.0 = auf allen Schnittstellen erreichbar, auch für andere @@ -235,6 +248,65 @@ function parseMultipart(buffer, boundary) { return result; } +/* ---------- Virenscan via ClamAV (clamd INSTREAM) ---------- */ +function scanWithClamAV(buffer) { + return new Promise((resolve, reject) => { + const socket = net.createConnection({ host: CLAMAV.host, port: CLAMAV.port }); + let reply = ""; + let settled = false; + const done = (fn, arg) => { + if (settled) return; + settled = true; + socket.destroy(); + fn(arg); + }; + socket.setTimeout(CLAMAV.timeoutMs); + socket.on("timeout", () => done(reject, new Error("clamd-Zeitüberschreitung"))); + socket.on("error", (e) => done(reject, e)); + socket.on("data", (d) => { reply += d.toString("utf8"); }); + socket.on("end", () => { + const r = reply.replace(/\0/g, "").trim(); + if (/\bOK$/.test(r)) return done(resolve, { clean: true, signature: null }); + const m = r.match(/:\s*(.+)\s+FOUND$/); + if (m) return done(resolve, { clean: false, signature: m[1] }); + done(reject, new Error("Unerwartete clamd-Antwort: " + r)); + }); + socket.on("connect", () => { + socket.write("zINSTREAM\0"); + const CHUNK = 64 * 1024; + for (let i = 0; i < buffer.length; i += CHUNK) { + const part = buffer.subarray(i, i + CHUNK); + const len = Buffer.alloc(4); + len.writeUInt32BE(part.length, 0); + socket.write(len); + socket.write(part); + } + socket.write(Buffer.alloc(4)); /* 0-Länge = Stream-Ende */ + }); + }); +} + +/* Verfügbarkeits-Check: clamd PING -> PONG */ +function clamavPing() { + return new Promise((resolve) => { + const socket = net.createConnection({ host: CLAMAV.host, port: CLAMAV.port }); + let reply = ""; + let settled = false; + const done = (v) => { + if (settled) return; + settled = true; + socket.destroy(); + resolve(v); + }; + socket.setTimeout(3000); + socket.on("timeout", () => done(false)); + socket.on("error", () => done(false)); + socket.on("data", (d) => { reply += d.toString("utf8"); }); + socket.on("end", () => done(/PONG/.test(reply))); + socket.on("connect", () => socket.write("zPING\0")); + }); +} + /* ---------- GameJam: Spiel einreichen ---------- */ function handleGamejamUpload(req, res) { const ctype = req.headers["content-type"] || ""; @@ -258,7 +330,7 @@ function handleGamejamUpload(req, res) { chunks.push(chunk); }); - req.on("end", () => { + req.on("end", async () => { if (tooLarge) { return sendJSON(res, 413, { error: "Datei zu groß (max. 5 MB)." }); } @@ -284,6 +356,28 @@ function handleGamejamUpload(req, res) { return sendJSON(res, 400, { error: "Nur einzelne .html-Dateien." }); } + /* Virenscan vor dem Speichern */ + if (CLAMAV.enabled) { + let scan; + try { + scan = await scanWithClamAV(file.data); + } catch (e) { + console.error("[gamejam] Virenscan nicht möglich:", e.message); + return sendJSON(res, 503, { + error: "Sicherheitsprüfung derzeit nicht verfügbar. Bitte später erneut versuchen." + }); + } + if (!scan.clean) { + console.warn( + "[gamejam] Upload ABGELEHNT — Fund: " + scan.signature + + " (Datei: " + (file.filename || "?") + ")" + ); + return sendJSON(res, 422, { + error: "Die Datei wurde von der Sicherheitsprüfung abgelehnt." + }); + } + } + const id = Date.now().toString(36) + "-" + crypto.randomBytes(4).toString("hex"); const entry = { id: id, @@ -408,6 +502,15 @@ function listen(candidates) { } console.log(" Leads: " + LEADS_CSV); console.log(" GameJam-Einreichungen: " + SUBMISSIONS_JSON); + if (CLAMAV.enabled) { + clamavPing().then((ok) => { + console.log(" Virenscan: " + (ok + ? "aktiv (clamd " + CLAMAV.host + ":" + CLAMAV.port + ")" + : "clamd NICHT erreichbar — GameJam-Uploads werden abgelehnt!")); + }); + } else { + console.log(" Virenscan: deaktiviert (CLAMAV.enabled = false)"); + } console.log(" Stop: Strg + C"); console.log(""); });