From f6fcc5096a10356ca7da8686ca5271342e94c970 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Mon, 9 Nov 2026 13:00:58 +0000 Subject: [PATCH] Upload via GUI (219 Dateien) --- server/curseforge-proxy/README.md | 20 ++++++++++--- server/curseforge-proxy/index.js | 47 +++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/server/curseforge-proxy/README.md b/server/curseforge-proxy/README.md index 5b8d7ae..acf0b58 100644 --- a/server/curseforge-proxy/README.md +++ b/server/curseforge-proxy/README.md @@ -17,6 +17,13 @@ auf dem Server, und der Launcher kennt nur die Adresse dieses Dienstes. | `GET /health` | Lebenszeichen, verrät nichts über den Schlüssel | | `POST /v1/mods/files` | Dateiliste auflösen, Rumpf `{ "fileIds": [...] }`, höchstens 100 je Anfrage | | `GET /v1/download?url=…` | Mod-Datei durchreichen; das CurseForge-CDN verlangt seit Juli 2026 ebenfalls einen Schlüssel | +| `GET /v1/mods/search` | Katalogsuche. Erlaubt sind nur `classId`, `categoryId`, `searchFilter`, `gameVersion`, `modLoaderType`, `sortField`, `sortOrder`, `pageSize` (1–50) und `index`; `gameId` ist immer Minecraft (432) | +| `GET /v1/categories` | Kategorien (nur `classId`, `gameId` ist immer 432) | +| `GET /v1/mods//files` | Dateien einer Mod (`gameVersion`, `modLoaderType`, `pageSize`, `index`) | + +Die Katalog-Wege geben ungültige oder unbekannte Parameter nicht weiter, sondern +lassen sie weg. Antworten von CurseForge werden nicht zwischengespeichert. Alles +andere (z. B. `/v1/games`) gibt es nicht und liefert `404`. ## Einrichten @@ -52,14 +59,19 @@ pm2 save pm2 startup # einmalig, für den Start nach einem Neustart ``` -Nach jeder Änderung an der `.env`: +Nach jeder Änderung an der `.env` den Eintrag entfernen und neu aus der +`ecosystem.config.js` starten: ```bash -pm2 restart aeromc-cf-proxy --update-env +pm2 delete aeromc-cf-proxy +pm2 start ecosystem.config.js +pm2 save ``` -Ohne `--update-env` behält pm2 die alten Werte, und du suchst lange nach dem -Grund, warum der neue Schlüssel nicht greift. +Ein bloßes `pm2 restart aeromc-cf-proxy --update-env` reicht **nicht**: Die +`.env` wird nur gelesen, wenn pm2 die `ecosystem.config.js` lädt. Beim Restart +behält pm2 die Werte vom ersten Start, und du suchst lange nach dem Grund, warum +der neue Schlüssel nicht greift. Nachsehen, ob er läuft: diff --git a/server/curseforge-proxy/index.js b/server/curseforge-proxy/index.js index 872beee..7d4decf 100644 --- a/server/curseforge-proxy/index.js +++ b/server/curseforge-proxy/index.js @@ -176,6 +176,49 @@ async function dateilisteAufloesen(req, res) { sendeJson(res, 200, await antwort.json()); } +/* + * Katalog-Wege: Suche, Kategorien und Dateiliste einer Mod. Es wird nur durchgereicht, + * was der Launcher dafür braucht – jeder Weg hat eine feste Liste erlaubter Parameter, + * andere werden verworfen. gameId ist immer Minecraft (432), damit sich der Dienst nicht + * für andere Spiele missbrauchen lässt. Antworten werden NICHT zwischengespeichert + * (die CurseForge-Bedingungen verbieten es, Daten aus der API zu speichern). + */ +const MC_SPIEL = '432'; +const ganz = (min, max) => (v) => /^\d{1,9}$/.test(v) && Number(v) >= min && Number(v) <= max; +const text = (maxLaenge) => (v) => v.length > 0 && v.length <= maxLaenge && !/[\u0000-\u001f]/.test(v); +const KATALOG_PARAMETER = { + suche: { + classId: ganz(0, 100000), categoryId: ganz(0, 100000), + searchFilter: text(100), gameVersion: text(32), + modLoaderType: ganz(0, 6), sortField: ganz(1, 8), + sortOrder: (v) => v === 'asc' || v === 'desc', + pageSize: ganz(1, 50), index: ganz(0, 10000), + }, + kategorien: { classId: ganz(0, 100000) }, + dateien: { gameVersion: text(32), modLoaderType: ganz(0, 6), pageSize: ganz(1, 50), index: ganz(0, 10000) }, +}; +const MAX_KATALOG_BYTES = 4 * 1024 * 1024; + +async function katalogAnfrage(res, url, cfPfad, erlaubt) { + const ziel = new URL(CF_API + cfPfad); + if (erlaubt === KATALOG_PARAMETER.suche || erlaubt === KATALOG_PARAMETER.kategorien) ziel.searchParams.set('gameId', MC_SPIEL); + for (const [name, wert] of url.searchParams) { + const pruefer = erlaubt[name]; + if (!pruefer || !pruefer(wert)) continue; // unbekannte oder ungültige Parameter einfach weglassen + ziel.searchParams.set(name, wert); + } + const antwort = await fetch(ziel, { headers: cfKopfzeilen(), signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS) }); + if (!antwort.ok) { + // Schlüsselprobleme nicht im Wortlaut weiterreichen + const code = antwort.status === 401 || antwort.status === 403 ? 502 : antwort.status; + return sendeJson(res, code, { error: 'CurseForge antwortet mit HTTP ' + antwort.status }); + } + const roh = await antwort.text(); + if (roh.length > MAX_KATALOG_BYTES) return sendeJson(res, 502, { error: 'Antwort von CurseForge zu groß.' }); + res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': Buffer.byteLength(roh) }); + res.end(roh); +} + async function dateiDurchreichen(req, res, url) { const ziel = pruefeDownloadZiel(url.searchParams.get('url') || ''); if (!ziel) return sendeJson(res, 400, { error: 'Adresse fehlt oder zeigt nicht auf das CurseForge-CDN.' }); @@ -386,6 +429,10 @@ const server = http.createServer(async (req, res) => { try { if (req.method === 'POST' && url.pathname === '/v1/mods/files') return await dateilisteAufloesen(req, res); if (req.method === 'GET' && url.pathname === '/v1/download') return await dateiDurchreichen(req, res, url); + if (req.method === 'GET' && url.pathname === '/v1/mods/search') return await katalogAnfrage(res, url, '/mods/search', KATALOG_PARAMETER.suche); + if (req.method === 'GET' && url.pathname === '/v1/categories') return await katalogAnfrage(res, url, '/categories', KATALOG_PARAMETER.kategorien); + const modDateien = req.method === 'GET' && /^\/v1\/mods\/(\d{1,9})\/files$/.exec(url.pathname); + if (modDateien) return await katalogAnfrage(res, url, '/mods/' + modDateien[1] + '/files', KATALOG_PARAMETER.dateien); sendeJson(res, 404, { error: 'Unbekannter Weg.' }); } catch (err) { console.error('[Fehler]', err.message);