Hero-Texte an Außenkommunikationspaket angepasst
Hero-Headline auf die offizielle Underline gesetzt (moderne, durch- dachte All-in-One-Software für übersehene Branchen), Untertext aus One-Liner und Elevator-Pitch mit UI/UX-USP. Headline-Größe und Meta-Description entsprechend angepasst. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
2705f1c889
commit
80ab0f491a
4 files changed
+189
-35
No files matched your search
@@ -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("");
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user