Upload via GUI (219 Dateien)
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user