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 |
|
| `GET /health` | Lebenszeichen, verrät nichts über den Schlüssel |
|
||||||
| `POST /v1/mods/files` | Dateiliste auflösen, Rumpf `{ "fileIds": [...] }`, höchstens 100 je Anfrage |
|
| `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/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
|
## Einrichten
|
||||||
|
|
||||||
@@ -52,14 +59,19 @@ pm2 save
|
|||||||
pm2 startup # einmalig, für den Start nach einem Neustart
|
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
|
```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
|
Ein bloßes `pm2 restart aeromc-cf-proxy --update-env` reicht **nicht**: Die
|
||||||
Grund, warum der neue Schlüssel nicht greift.
|
`.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:
|
Nachsehen, ob er läuft:
|
||||||
|
|
||||||
|
|||||||
@@ -176,6 +176,49 @@ async function dateilisteAufloesen(req, res) {
|
|||||||
sendeJson(res, 200, await antwort.json());
|
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) {
|
async function dateiDurchreichen(req, res, url) {
|
||||||
const ziel = pruefeDownloadZiel(url.searchParams.get('url') || '');
|
const ziel = pruefeDownloadZiel(url.searchParams.get('url') || '');
|
||||||
if (!ziel) return sendeJson(res, 400, { error: 'Adresse fehlt oder zeigt nicht auf das CurseForge-CDN.' });
|
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 {
|
try {
|
||||||
if (req.method === 'POST' && url.pathname === '/v1/mods/files') return await dateilisteAufloesen(req, res);
|
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/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.' });
|
sendeJson(res, 404, { error: 'Unbekannter Weg.' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Fehler]', err.message);
|
console.error('[Fehler]', err.message);
|
||||||
|
|||||||
Reference in New Issue
Block a user