Upload via GUI (219 Dateien)

This commit is contained in:
2026-11-09 13:00:58 +00:00
parent 32fc09f152
commit f6fcc5096a
2 changed files with 63 additions and 4 deletions
+16 -4
View File
@@ -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/<ID>/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:
+47
View File
@@ -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);