Compare commits

..
9 Commits
Author SHA1 Message Date
M_Viper e7f0939279 Upload via GUI (219 Dateien) 2026-11-09 13:01:12 +00:00
M_Viper cd08d72b2c Upload via GUI (219 Dateien) 2026-11-09 13:01:07 +00:00
M_Viper 2760253b81 Upload via GUI (219 Dateien) 2026-11-09 13:01:05 +00:00
M_Viper b7fad0d874 Upload via GUI (219 Dateien) 2026-11-09 13:01:02 +00:00
M_Viper f6fcc5096a Upload via GUI (219 Dateien) 2026-11-09 13:00:58 +00:00
M_Viper 32fc09f152 Upload via GUI (219 Dateien) 2026-11-09 13:00:55 +00:00
M_Viper 53aefae162 Upload via GUI (219 Dateien) 2026-11-09 13:00:48 +00:00
M_Viper 94cb1b9b62 Upload via GUI (219 Dateien) 2026-11-09 13:00:46 +00:00
M_Viper b242e64678 Upload via GUI (219 Dateien) 2026-11-09 13:00:43 +00:00
15 changed files with 108 additions and 33 deletions
+8 -2
View File
@@ -2,13 +2,19 @@
Alle nennenswerten Änderungen an AeroMC werden hier festgehalten. Alle nennenswerten Änderungen an AeroMC werden hier festgehalten.
## [1.3.2] - 2026-11-09
### Neu
- CurseForge: Die Suche im Katalog funktioniert jetzt ohne eigenen API-Key. Mods und Modpacks lassen sich direkt aus CurseForge suchen, installieren und importieren, so wie bei Modrinth
### Geändert
- Die Hinweise in den Netzwerk-Einstellungen und im Importfenster zu CurseForge sind angepasst: Ein eigener Key ist nur noch optional
## [1.3.1] - 2026-11-02 ## [1.3.1] - 2026-11-02
### Geändert ### Geändert
- CurseForge: Mods, die ihre Autoren nicht über Launcher freigeben, werden nicht mehr geladen. Beim Modpack-Import legt AeroMC stattdessen die Liste „GESPERRTE-MODS.txt“ mit Links zum Selbstladen in den Spielordner, bei einzelnen Mods erscheint eine Meldung mit dem Link - CurseForge: Mods, die ihre Autoren nicht über Launcher freigeben, werden nicht mehr geladen. Beim Modpack-Import legt AeroMC stattdessen die Liste „GESPERRTE-MODS.txt“ mit Links zum Selbstladen in den Spielordner, bei einzelnen Mods erscheint eine Meldung mit dem Link
- Stream Deck: Das Plugin ist jetzt in Fassung 1.1.1.0 dabei. Bei eingeschaltetem „Stream Deck erlauben“ aktualisiert AeroMC es von selbst, danach die Stream-Deck-Software einmal neu starten - Stream Deck: Das Plugin ist jetzt in Fassung 1.1.1.0 dabei. Bei eingeschaltetem „Stream Deck erlauben“ aktualisiert AeroMC es von selbst, danach die Stream-Deck-Software einmal neu starten
### Verbessert
- Kleinere Verbesserungen und Aktualisierungen im Hintergrund - Kleinere Verbesserungen und Aktualisierungen im Hintergrund
## [1.3.0] - 2026-10-30 ## [1.3.0] - 2026-10-30
+1 -1
View File
@@ -1,6 +1,6 @@
# AeroMC – Sicherheit # AeroMC – Sicherheit
Überblick über die Sicherheitsmaßnahmen im AeroMC Launcher (Stand: Version 1.3.1). Überblick über die Sicherheitsmaßnahmen im AeroMC Launcher (Stand: Version 1.3.2).
--- ---
@@ -5,7 +5,7 @@
}, },
"projects": { "projects": {
"C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj": { "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj": {
"version": "1.3.1", "version": "1.3.2",
"restore": { "restore": {
"projectUniqueName": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj", "projectUniqueName": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
"projectName": "AeroMC-Setup", "projectName": "AeroMC-Setup",
Binary file not shown.
@@ -14,11 +14,11 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
[assembly: System.Reflection.AssemblyCopyrightAttribute("Copyright (c) M_Viper. Alle Rechte vorbehalten.")] [assembly: System.Reflection.AssemblyCopyrightAttribute("Copyright (c) M_Viper. Alle Rechte vorbehalten.")]
[assembly: System.Reflection.AssemblyDescriptionAttribute("Installiert AeroMC")] [assembly: System.Reflection.AssemblyDescriptionAttribute("Installiert AeroMC")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.3.1.0")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.3.2.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.3.1")] [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.3.2")]
[assembly: System.Reflection.AssemblyProductAttribute("AeroMC Setup")] [assembly: System.Reflection.AssemblyProductAttribute("AeroMC Setup")]
[assembly: System.Reflection.AssemblyTitleAttribute("AeroMC Setup")] [assembly: System.Reflection.AssemblyTitleAttribute("AeroMC Setup")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.3.1.0")] [assembly: System.Reflection.AssemblyVersionAttribute("1.3.2.0")]
[assembly: System.Resources.NeutralResourcesLanguageAttribute("de-DE")] [assembly: System.Resources.NeutralResourcesLanguageAttribute("de-DE")]
// Von der MSBuild WriteCodeFragment-Klasse generiert. // Von der MSBuild WriteCodeFragment-Klasse generiert.
@@ -1 +1 @@
580c54ad965e1465e6447eef31337065bb9284a8dba224aec55fdd4c0fb93d67 2821045dd08c55a3267e014e5026ac3588e68340b7d53441249b44302c5f3e33
+1 -1
View File
@@ -418,7 +418,7 @@
"C:\\Users\\Viper\\.nuget\\packages\\": {} "C:\\Users\\Viper\\.nuget\\packages\\": {}
}, },
"project": { "project": {
"version": "1.3.1", "version": "1.3.2",
"restore": { "restore": {
"projectUniqueName": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj", "projectUniqueName": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
"projectName": "AeroMC-Setup", "projectName": "AeroMC-Setup",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"version": 2, "version": 2,
"dgSpecHash": "SW1JzBEcPG8=", "dgSpecHash": "dC0MHaIYOYg=",
"success": true, "success": true,
"projectFilePath": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj", "projectFilePath": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
"expectedPackageFiles": [ "expectedPackageFiles": [
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "aeromc", "name": "aeromc",
"version": "1.3.1", "version": "1.3.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "aeromc", "name": "aeromc",
"version": "1.3.1", "version": "1.3.2",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"dependencies": { "dependencies": {
"@xhayper/discord-rpc": "^1.3.4", "@xhayper/discord-rpc": "^1.3.4",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "aeromc", "name": "aeromc",
"productName": "AeroMC", "productName": "AeroMC",
"version": "1.3.1", "version": "1.3.2",
"description": "AeroMC – Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)", "description": "AeroMC – Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)",
"homepage": "https://m-viper.de", "homepage": "https://m-viper.de",
"launcherWebsite": "https://aeromc.viper.ipv64.net", "launcherWebsite": "https://aeromc.viper.ipv64.net",
+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 | | `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:
+47
View File
@@ -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);
+2 -2
View File
@@ -561,7 +561,7 @@
</section> </section>
<section class="settings-card"> <section class="settings-card">
<h5>CurseForge</h5> <h5>CurseForge</h5>
<small class="hint s-vorab">Nur für CurseForge-Modpacks und -Mods nötig. Modrinth funktioniert immer ohne.</small> <small class="hint s-vorab">Für CurseForge ist normalerweise nichts nötig: AeroMC nutzt die Weiterleitung unten. Ein eigener Key ist nur für den Fall gedacht, dass du sie nicht nutzen willst. Modrinth funktioniert immer ohne.</small>
<div class="s-zeile s-voll"> <div class="s-zeile s-voll">
<div class="s-text"><b>Eigener API-Key</b><small>Von CurseForge for Studios (Profil → Schlüssel erstellen, beginnt mit cfc_pat_…) oder ein Organisationsschlüssel. Nach dem Eintragen speichern.</small></div> <div class="s-text"><b>Eigener API-Key</b><small>Von CurseForge for Studios (Profil → Schlüssel erstellen, beginnt mit cfc_pat_…) oder ein Organisationsschlüssel. Nach dem Eintragen speichern.</small></div>
<div class="path-row"> <div class="path-row">
@@ -1629,7 +1629,7 @@
<div id="import-pane-modpack" class="import-pane hidden"> <div id="import-pane-modpack" class="import-pane hidden">
<p class="import-copy">Modpack aus dem Katalog installieren oder als Datei importieren.</p> <p class="import-copy">Modpack aus dem Katalog installieren oder als Datei importieren.</p>
<p class="hint">Katalog-Suche läuft über Modrinth oder CurseForge. Für CurseForge wird ein eigener API-Key in den Netzwerk-Einstellungen gebraucht (die Weiterleitung reicht nur für den Datei-Import). Datei-Import unterstützt zusätzlich CurseForge-ZIP (manifest.json).</p> <p class="hint">Katalog-Suche läuft über Modrinth oder CurseForge. Datei-Import unterstützt zusätzlich CurseForge-ZIP (manifest.json).</p>
<div class="field-row" style="margin-top:12px;align-items:flex-end"> <div class="field-row" style="margin-top:12px;align-items:flex-end">
<label class="field" style="width:150px"> <label class="field" style="width:150px">
+7
View File
@@ -4214,6 +4214,13 @@ ipcMain.handle('modpack:installFromCurseForge', async (_e, project) => {
const gameVersion = (project && project.gameVersion) || ''; const gameVersion = (project && project.gameVersion) || '';
broadcast('launch:progress', { phase: 'modpack', detail: 'Modpack-Version wird ermittelt …' }); broadcast('launch:progress', { phase: 'modpack', detail: 'Modpack-Version wird ermittelt …' });
const file = await services.curseforgeResolveModpackFile(zugang, modId, gameVersion); const file = await services.curseforgeResolveModpackFile(zugang, modId, gameVersion);
if (file && file.gesperrt) {
return {
ok: false,
message: 'Der Autor erlaubt kein Laden dieses Modpacks über Launcher. Bitte von Hand laden und über „Importieren“ einlesen: '
+ services.cfProjektSeite(modId),
};
}
if (!file || !file.url) { if (!file || !file.url) {
return { return {
ok: false, ok: false,
+17 -14
View File
@@ -586,8 +586,8 @@ function cfLoaderType(loader) {
} }
/* /*
* CurseForge-Katalogsuche. Braucht direkten API-Key (modus === 'direkt'). * CurseForge-Katalogsuche – mit eigenem API-Key (modus 'direkt') oder über die
* Die Weiterleitung deckt absichtlich nur Dateiauflösung und Download ab. * Weiterleitung (modus 'weiterleitung'); beide haben dieselben Pfade unter zugang.basis.
*/ */
/* /*
@@ -653,13 +653,13 @@ async function curseforgeSearch(zugang, {
query = '', gameVersion = '', loader = '', sort = 'popularity', query = '', gameVersion = '', loader = '', sort = 'popularity',
limit = 30, offset = 0, art = 'mod', limit = 30, offset = 0, art = 'mod',
} = {}) { } = {}) {
if (!zugang || zugang.modus !== 'direkt') { // Direkt (eigener Key) und über die Weiterleitung laufen über dieselben Pfade
if (!zugang) {
return { return {
total: 0, total: 0,
hits: [], hits: [],
needKey: true, needKey: true,
error: 'Für die CurseForge-Suche brauchst du einen eigenen API-Key in den Netzwerk-Einstellungen. ' error: 'Kein CurseForge-Zugang eingerichtet. Trage unter Optionen → Netzwerk die Adresse der Weiterleitung oder einen eigenen API-Key ein.',
+ 'Die Weiterleitung reicht für Downloads, nicht für den Katalog.',
}; };
} }
if (!CF_CLASS[art]) { if (!CF_CLASS[art]) {
@@ -668,7 +668,7 @@ async function curseforgeSearch(zugang, {
return { total: 0, hits: [], error: 'CurseForge unterstützt diese Inhaltsart hier nicht – bitte Modrinth nutzen.' }; return { total: 0, hits: [], error: 'CurseForge unterstützt diese Inhaltsart hier nicht – bitte Modrinth nutzen.' };
} }
const classId = CF_CLASS[art]; const classId = CF_CLASS[art];
const url = new URL(CURSEFORGE + '/mods/search'); const url = new URL(zugang.basis + '/mods/search');
url.searchParams.set('gameId', '432'); url.searchParams.set('gameId', '432');
url.searchParams.set('classId', String(classId)); url.searchParams.set('classId', String(classId));
if (query) url.searchParams.set('searchFilter', query); if (query) url.searchParams.set('searchFilter', query);
@@ -696,13 +696,16 @@ async function curseforgeSearch(zugang, {
return { return {
total: 0, total: 0,
hits: [], hits: [],
needKey: true, needKey: zugang.modus === 'direkt',
error: 'CurseForge lehnt den API-Key ab (HTTP ' + res.status + '). ' error: zugang.modus === 'direkt'
+ 'Bitte unter Optionen → Netzwerk einen gültigen Schlüssel von console.curseforge.com eintragen. ' ? 'CurseForge lehnt den API-Key ab (HTTP ' + res.status + '). '
+ 'Das Proxy-Token ist kein API-Key.' + 'Bitte unter Optionen → Netzwerk einen gültigen Schlüssel von console.curseforge.com eintragen. '
+ (detail ? ' (' + detail + ')' : ''), + 'Das Proxy-Token ist kein API-Key.'
+ (detail ? ' (' + detail + ')' : '')
: 'Der CurseForge-Server weist die Anfrage zurück (HTTP ' + res.status + ').',
}; };
} }
if (res.status === 429) throw new Error('Zu viele Anfragen – bitte einen Moment warten.');
throw new Error('CurseForge-Suche fehlgeschlagen: HTTP ' + res.status + (detail ? ' – ' + detail : '')); throw new Error('CurseForge-Suche fehlgeschlagen: HTTP ' + res.status + (detail ? ' – ' + detail : ''));
} }
const data = await res.json(); const data = await res.json();
@@ -750,12 +753,12 @@ function cfFileMatches(file, gameVersion, loader, art) {
} }
async function curseforgePickFile(zugang, modId, gameVersion, loader, art) { async function curseforgePickFile(zugang, modId, gameVersion, loader, art) {
if (!zugang || zugang.modus !== 'direkt') { if (!zugang) {
const err = new Error('CurseForge-Dateiauswahl braucht einen eigenen API-Key.'); const err = new Error('Kein CurseForge-Zugang eingerichtet.');
err.code = 'need-key'; err.code = 'need-key';
throw err; throw err;
} }
const url = new URL(CURSEFORGE + '/mods/' + modId + '/files'); const url = new URL(zugang.basis + '/mods/' + modId + '/files');
url.searchParams.set('pageSize', '50'); url.searchParams.set('pageSize', '50');
url.searchParams.set('index', '0'); url.searchParams.set('index', '0');
if (gameVersion) url.searchParams.set('gameVersion', gameVersion); if (gameVersion) url.searchParams.set('gameVersion', gameVersion);