Upload via GUI (194 Dateien)

This commit is contained in:
2026-10-07 07:32:04 +00:00
parent 3ac5a6c14d
commit d976cb4595
8 changed files with 6244 additions and 6054 deletions
+4624 -4914
View File
File diff suppressed because it is too large Load Diff
+64 -54
View File
@@ -1,54 +1,64 @@
{ {
"name": "aeromc", "name": "aeromc",
"productName": "AeroMC", "productName": "AeroMC",
"version": "1.2.1", "version": "1.2.3",
"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",
"main": "src/main.js", "main": "src/main.js",
"author": "M_Viper", "author": "M_Viper",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "electron .", "start": "electron .",
"prebuild": "node scripts/write-build-config.js", "prebuild": "node scripts/pruefe-version.js && node scripts/write-build-config.js",
"prebuild:portable": "node scripts/write-build-config.js", "prebuild:portable": "node scripts/pruefe-version.js && node scripts/write-build-config.js",
"build": "electron-builder --win dir && node scripts/build-setup.js", "build": "electron-builder --win dir && node scripts/build-setup.js && node scripts/sign-release.js setup",
"build:portable": "electron-builder --win portable --x64 && node -e \"const fs=require('fs');const path=require('path');['dist/builder-debug.yml','dist/builder-effective-config.yaml','dist/win-unpacked'].forEach((target)=>fs.rmSync(target,{recursive:true,force:true}));if(fs.existsSync('dist')){const source=path.join('dist','AeroMC.exe');const target=path.join('dist','AeroMC-Portable.exe');if(fs.existsSync(target))fs.rmSync(target,{force:true});if(fs.existsSync(source))fs.renameSync(source,target);for(const name of fs.readdirSync('dist')){if((name.endsWith('.exe')&&name!=='AeroMC-Setup.exe'&&name!=='AeroMC-Portable.exe')||name.endsWith('.blockmap')){fs.rmSync(path.join('dist',name),{force:true})}}}\"" "build:portable": "electron-builder --win portable --x64 && node -e \"const fs=require('fs');const path=require('path');['dist/builder-debug.yml','dist/builder-effective-config.yaml','dist/win-unpacked'].forEach((target)=>fs.rmSync(target,{recursive:true,force:true}));if(fs.existsSync('dist')){const source=path.join('dist','AeroMC.exe');const target=path.join('dist','AeroMC-Portable.exe');if(fs.existsSync(target))fs.rmSync(target,{force:true});if(fs.existsSync(source))fs.renameSync(source,target);for(const name of fs.readdirSync('dist')){if((name.endsWith('.exe')&&name!=='AeroMC-Setup.exe'&&name!=='AeroMC-Portable.exe')||name.endsWith('.blockmap')){fs.rmSync(path.join('dist',name),{force:true})}}}\" && node scripts/sign-release.js portable"
}, },
"build": { "build": {
"appId": "com.aeromc.launcher", "appId": "com.aeromc.launcher",
"productName": "AeroMC", "productName": "AeroMC",
"artifactName": "AeroMC.${ext}", "artifactName": "AeroMC.${ext}",
"compression": "store", "compression": "store",
"files": [ "files": [
"src/**/*", "src/**/*",
"package.json", "package.json",
"LICENSE" "LICENSE"
], ],
"asarUnpack": [ "asarUnpack": [
"node_modules/loopback-capture/**/*", "node_modules/loopback-capture/**/*",
"node_modules/node-window-manager/**/*", "node_modules/node-window-manager/**/*",
"node_modules/ffmpeg-static/**/*" "node_modules/ffmpeg-static/**/*"
], ],
"win": { "win": {
"target": "dir", "target": "dir",
"icon": "src/assets/icon.ico" "icon": "src/assets/icon.ico"
} },
}, "electronFuses": {
"devDependencies": { "runAsNode": false,
"electron": "^28.0.0", "enableNodeOptionsEnvironmentVariable": false,
"electron-builder": "^24.0.0" "enableNodeCliInspectArguments": false,
}, "enableEmbeddedAsarIntegrityValidation": true,
"dependencies": { "onlyLoadAppFromAsar": true
"@xhayper/discord-rpc": "^1.3.4", }
"archiver": "^7.0.1", },
"ffmpeg-static": "^5.3.0", "devDependencies": {
"loopback-capture": "^3.0.2", "electron": "^44.6.0",
"node-window-manager": "^2.2.4", "electron-builder": "^26.15.3"
"skinview3d": "^3.4.2", },
"sql.js": "^1.14.2", "dependencies": {
"undici": "^6.28.0", "@xhayper/discord-rpc": "^1.3.4",
"unzipper": "^0.12.5" "archiver": "^7.0.1",
} "ffmpeg-static": "^5.3.0",
} "loopback-capture": "^3.0.2",
"node-window-manager": "^2.2.4",
"skinview3d": "^3.4.2",
"sql.js": "^1.14.2",
"undici": "^6.28.0",
"unzipper": "^0.12.5"
},
"overrides": {
"brace-expansion@^2.0.0": "^2.1.7"
}
}
+588 -536
View File
File diff suppressed because it is too large Load Diff
+43
View File
@@ -839,6 +839,32 @@ select.s-wert,input.s-wert{padding:6px 8px;border:1px solid var(--border);border
.erfolg-karte small{display:block;font-size:11px;color:var(--dim);line-height:1.3} .erfolg-karte small{display:block;font-size:11px;color:var(--dim);line-height:1.3}
.erfolg-karte.offen{opacity:.45;filter:grayscale(1)} .erfolg-karte.offen{opacity:.45;filter:grayscale(1)}
/* Mod-Schalter: zwei Listen wie beim Ressourcenpaket-Bildschirm */
#modal-modschalter.popout-host .modal-body{display:flex;flex-direction:column}
.ms-instanz{font-weight:400;color:var(--dim);font-size:12px;margin-left:6px}
.ms-kopf{display:flex;align-items:center;gap:12px;margin-bottom:10px}
.ms-kopf input{flex:1;max-width:320px;padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--input,#fff);color:var(--text);font-family:inherit;font-size:13px}
.ms-kopf input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px rgba(64,126,232,.25)}
.ms-spalten{flex:1 1 auto;min-height:0;display:grid;grid-template-columns:1fr 1fr;gap:12px}
.ms-spalte{display:flex;flex-direction:column;min-height:0;border:1px solid var(--border);border-radius:8px;background:rgba(127,127,127,.06)}
.ms-titel{padding:8px 12px;font-weight:600;font-size:12.5px;border-bottom:1px solid var(--border)}
.ms-liste{flex:1 1 auto;min-height:0;overflow:auto;padding:6px}
.ms-mod{display:flex;align-items:center;gap:10px;padding:7px 8px;border-radius:6px;cursor:pointer;border:1px solid transparent}
.ms-mod:hover{background:rgba(127,127,127,.12);border-color:var(--border)}
.ms-icon{width:32px;height:32px;flex:none;border-radius:6px;display:grid;place-items:center;background:rgba(127,127,127,.15);font-size:17px;overflow:hidden}
.ms-icon img{width:100%;height:100%;object-fit:cover}
.ms-info{flex:1;min-width:0}
.ms-name{font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ms-meta{font-size:11px;color:var(--dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ms-warn{font-size:11px;color:#d9822b}
.ms-pfeil{flex:none;width:28px;height:28px;border-radius:6px;border:1px solid var(--border);background:var(--win);color:var(--text);cursor:pointer;font-size:14px;opacity:.55}
.ms-mod:hover .ms-pfeil{opacity:1;border-color:var(--accent);color:var(--accent)}
.ms-neu{font-size:10.5px;font-weight:700;color:var(--accent);margin-left:6px}
.ms-leer{color:var(--dim);font-size:12px;padding:14px;text-align:center}
.ms-status{margin-right:auto}
.ui-animiert .ms-mod.kommt{animation:ms-rein .28s cubic-bezier(.2,.8,.2,1)}
@keyframes ms-rein{from{opacity:0;transform:translateX(var(--ms-von,12px))}}
/* Konto-Menü oben rechts (wie in MultiMC) */ /* Konto-Menü oben rechts (wie in MultiMC) */
.acct-pfeil{font-size:9px;opacity:.7;margin-left:-2px} .acct-pfeil{font-size:9px;opacity:.7;margin-left:-2px}
.konto-menu{min-width:220px} .konto-menu{min-width:220px}
@@ -915,3 +941,20 @@ select.s-wert,input.s-wert{padding:6px 8px;border:1px solid var(--border);border
.se-u2d[data-werkzeug="pipette"] canvas{cursor:copy} .se-u2d[data-werkzeug="pipette"] canvas{cursor:copy}
.se-fuss{align-items:center} .se-fuss{align-items:center}
.se-status{flex:1;min-width:0;margin-left:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .se-status{flex:1;min-width:0;margin-left:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Manipulationsschutz: gesperrte Instanz (Verbotszeichen, siehe integritaet.js) */
.card.gesperrt .card-icon,.card.gesperrt .card-icon-img{filter:grayscale(1) brightness(.55)}
.card-verbot{position:absolute;left:50%;top:6px;width:60px;height:60px;margin-left:-30px;opacity:.95;pointer-events:none;z-index:2;filter:drop-shadow(0 1px 2px rgba(0,0,0,.45))}
.card.gesperrt .card-name{color:#e05555}
.card.gesperrt.sel .card-name{color:#fff;background:#b02020}
.side-ico{position:relative}
.side-ico.gesperrt > :not(.side-verbot){filter:grayscale(1) brightness(.55)}
.side-ico img.side-verbot{position:absolute;left:-4px;top:-4px;width:60px;height:60px;object-fit:contain;border-radius:0;pointer-events:none}
.side-gesperrt{margin:8px 6px 2px;padding:9px 10px;border:1px solid rgba(200,0,0,.55);background:rgba(200,0,0,.10);border-radius:6px;font-size:12px}
.side-gesperrt-titel{display:flex;align-items:center;gap:6px;font-weight:700;color:#e04848;font-size:13px;margin-bottom:4px}
.side-gesperrt-titel img{width:18px;height:18px}
.side-gesperrt-text{line-height:1.35;margin:4px 0}
.side-gesperrt-text.dim{opacity:.7;font-size:11px}
.side-gesperrt-gruende{margin:4px 0 8px;padding-left:16px;max-height:110px;overflow:auto;word-break:break-word;line-height:1.35}
.side-gesperrt-gruende .dim{opacity:.7;list-style:none;margin-left:-16px}
#p-wiederherstellen{width:100%;margin:4px 0}
+3
View File
@@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAXxt1cCntDQzvej4hjyVZt6Onj6Uv3hSlneJaOxxmsds=
-----END PUBLIC KEY-----
+495 -370
View File
@@ -1,370 +1,495 @@
'use strict'; 'use strict';
/* /*
* updater.js – automatische Updates über die Gitea-Releases * updater.js – automatische Updates über die Gitea-Releases
* --------------------------------------------------------- * ---------------------------------------------------------
* Ablauf: Releases abfragen -> neueste Version mit der laufenden vergleichen -> * Ablauf: Releases abfragen -> neueste Version mit der laufenden vergleichen ->
* bei Bedarf den Windows-Installer herunterladen und starten. * bei Bedarf den Windows-Installer herunterladen und starten.
* Ein Klick des Nutzers genügt; Download, Prüfung und Start laufen automatisch. * Ein Klick des Nutzers genügt; Download, Prüfung und Start laufen automatisch.
*/ */
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const os = require('os'); const os = require('os');
const crypto = require('crypto'); const crypto = require('crypto');
const { spawn } = require('child_process'); const { spawn } = require('child_process');
const API = 'https://git.viper.ipv64.net/api/v1/repos/M_Viper/AeroMc-Launcher/releases'; const API = 'https://git.viper.ipv64.net/api/v1/repos/M_Viper/AeroMc-Launcher/releases';
const UA = 'AeroMC-Launcher'; const UA = 'AeroMC-Launcher';
// Downloads nur von hier – eine Release-Datei mit fremder Adresse wird abgelehnt
/* const ERLAUBTER_HOST = 'git.viper.ipv64.net';
* Paketart der laufenden Anwendung: 'portable', 'setup' oder 'unknown'. // Die Signaturen liegen nicht im Gitea-Release, sondern auf der WordPress-Seite
* // (Plugin aeromc-friends, includes/signaturen.php). Hochgeladen werden sie beim
* Wichtig: es wird nie geraten. Portable-Nutzer dürfen niemals einen Installer // Bauen automatisch von scripts/sign-release.js.
* bekommen und Setup-Nutzer niemals eine Portable-Datei – im Zweifel lieber const SIGNATUR_API = 'https://viper-network.de/wp-json/aeromc/v1/update-signatur';
* 'unknown' und gar kein automatisches Update anbieten.
*/ /*
let cachedKind = null; * Sicherheit: Jede Update-Datei muss mit dem privaten Release-Schlüssel signiert
function detectInstallKind() { * sein (Ed25519). Der liegt nur auf dem Rechner, auf dem gebaut wird – nie auf
if (cachedKind) return cachedKind; * dem Server. Wer den Gitea-Server oder das Konto übernimmt, kann deshalb zwar
cachedKind = computeInstallKind(); * Dateien hochladen, aber keine gültige Signatur erzeugen; der Launcher
return cachedKind; * installiert so etwas nicht. Signiert wird beim Bauen von
} * scripts/sign-release.js; die Signatur liegt auf einem zweiten Server
* (SIGNATUR_API) – woher sie kommt, ist für die Sicherheit aber egal, fälschen
function computeInstallKind() { * kann sie ohne den Schlüssel niemand.
// 1. Portable: diese Variablen setzt ausschließlich der Portable-Build von *
// electron-builder – das ist das verlässlichste Signal überhaupt. * Unterschrieben wird nicht nur der Inhalt (SHA-256), sondern auch Dateiname
if (String(process.env.PORTABLE_EXECUTABLE_FILE || '').trim()) return 'portable'; * und Version. So lässt sich weder eine Portable- als Setup-Datei ausgeben noch
if (String(process.env.PORTABLE_EXECUTABLE_DIR || '').trim()) return 'portable'; * eine alte, signierte Fassung als "neues" Update unterschieben.
if (String(process.env.PORTABLE_EXECUTABLE_APP_FILENAME || '').trim()) return 'portable'; */
const SIGNATUR_FORMAT = 'AeroMC-Update/1';
const exe = process.execPath || ''; let oeffentlicherSchluessel = null;
// 2. Notnagel über den Dateinamen, falls die Variablen einmal fehlen function releaseSchluessel() {
if (/portable/i.test(path.basename(exe))) return 'portable'; if (!oeffentlicherSchluessel) {
const pem = fs.readFileSync(path.join(__dirname, 'update-schluessel.pem'), 'utf8');
// 3. Setup: das Setup legt neben der EXE immer "Uninstall AeroMC.exe" ab. Nur wenn der oeffentlicherSchluessel = crypto.createPublicKey(pem);
// da ist, ist es wirklich eine Installation. }
if (hasUninstaller(path.dirname(exe))) return 'setup'; return oeffentlicherSchluessel;
}
// 4. Alles andere (Entwicklungsmodus, entpackter Ordner, unbekannte Kopie)
return 'unknown'; function normVersion(v) {
} return String(v || '').trim().replace(/^v/i, '');
}
function hasUninstaller(dir) {
try { // Der Text, der unterschrieben wird (identisch in scripts/sign-release.js)
return fs.readdirSync(dir).some((n) => /^unins(tall)?.*\.exe$/i.test(n)); function signaturText(datei, version, sha256) {
} catch { return Buffer.from([SIGNATUR_FORMAT, datei, normVersion(version), String(sha256).toLowerCase()].join('\n'), 'utf8');
return false; }
}
} // Prüft eine Signatur (Inhalt der .sig-Datei) gegen Name, Version und Hash
function pruefeSignatur(sig, datei, version, sha256) {
// Pfad der laufenden Portable-EXE (die Datei, die der Nutzer selbst gestartet hat) if (!sig || sig.format !== SIGNATUR_FORMAT) throw new Error('Signaturdatei hat ein unbekanntes Format.');
function portableTarget() { if (sig.datei !== datei) throw new Error('Signatur gehört zu einer anderen Datei.');
const p = String(process.env.PORTABLE_EXECUTABLE_FILE || '').trim(); if (normVersion(sig.version) !== normVersion(version)) throw new Error('Signatur gehört zu einer anderen Version.');
if (p) return p; if (String(sig.sha256 || '').toLowerCase() !== String(sha256).toLowerCase()) {
// Fallback: nur brauchbar, wenn die EXE nicht entpackt ausgeführt wird throw new Error('Die heruntergeladene Datei passt nicht zur Signatur – Update abgebrochen.');
const exe = process.execPath || ''; }
return path.basename(exe).toLowerCase().includes('portable') ? exe : ''; const ok = crypto.verify(null, signaturText(datei, version, sha256),
} releaseSchluessel(), Buffer.from(String(sig.signatur || ''), 'base64'));
if (!ok) throw new Error('Ungültige Signatur – die Datei stammt nicht vom AeroMC-Entwickler. Update abgebrochen.');
function updateDir() { }
return path.join(os.tmpdir(), 'aeromc-update');
} function erlaubteAdresse(adresse) {
try {
/* const u = new URL(String(adresse || ''));
* Räumt Reste alter Update-Vorgänge weg (Installer sind mehrere hundert MB groß). return u.protocol === 'https:' && u.hostname.toLowerCase() === ERLAUBTER_HOST;
* Dateien, die jünger als 10 Minuten sind, bleiben liegen – sie könnten gerade } catch { return false; }
* von einem laufenden Update benutzt werden. }
*/
function cleanupUpdateCache() { /*
try { * Signatur einer Release-Datei vom WordPress-Server holen.
const dir = updateDir(); * -> Signatur-Objekt, null wenn es (noch) keine gibt; wirft bei Netzwerkfehlern.
if (!fs.existsSync(dir)) return; */
for (const name of fs.readdirSync(dir)) { async function holeSignatur(datei, version) {
const p = path.join(dir, name); const adresse = `${SIGNATUR_API}?datei=${encodeURIComponent(datei)}&version=${encodeURIComponent(normVersion(version))}`;
try { const res = await fetch(adresse, { headers: { 'User-Agent': UA, Accept: 'application/json' } });
if (Date.now() - fs.statSync(p).mtimeMs < 600000) continue; if (res.status === 404) return null;
fs.rmSync(p, { force: true, recursive: true }); if (!res.ok) throw new Error('Signatur-Server antwortet mit HTTP ' + res.status);
} catch { /* gesperrt – beim nächsten Start erneut versuchen */ } const text = await res.text();
} if (text.length > 16384) throw new Error('Signatur ist ungültig.');
} catch { /* egal */ } const sig = JSON.parse(text);
} if (!sig || typeof sig !== 'object' || sig.datei !== datei || normVersion(sig.version) !== normVersion(version)) {
throw new Error('Signatur passt nicht zu diesem Update.');
// "v1.2.3-beta.2" -> [1,2,3]; führendes "v" wird ignoriert }
function parseVersion(v) { return { format: sig.format, datei: sig.datei, version: sig.version, sha256: sig.sha256, signatur: sig.signatur };
const m = String(v || '').trim().replace(/^v/i, '').match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/); }
if (!m) return null;
return [Number(m[1] || 0), Number(m[2] || 0), Number(m[3] || 0)]; function sha256Datei(datei) {
} return new Promise((resolve, reject) => {
const h = crypto.createHash('sha256');
// Vorabkennung hinter dem ersten "-", z. B. "beta" oder "beta.2" fs.createReadStream(datei).on('error', reject).on('data', (d) => h.update(d)).on('end', () => resolve(h.digest('hex')));
function parsePreRelease(v) { });
const m = String(v || '').trim().replace(/^v/i, '').match(/^\d+(?:\.\d+){0,2}-([0-9A-Za-z.\-]+)/); }
return m ? m[1].split('.') : null;
} /*
* Paketart der laufenden Anwendung: 'portable', 'setup' oder 'unknown'.
/* *
* Vergleicht die Vorabkennungen zweier gleicher Versionsnummern. * Wichtig: es wird nie geraten. Portable-Nutzer dürfen niemals einen Installer
* * bekommen und Setup-Nutzer niemals eine Portable-Datei – im Zweifel lieber
* Bewusst nicht streng nach SemVer: trägt ein Release-Tag die Vorabkennung * 'unknown' und gar kein automatisches Update anbieten.
* nicht, die die App selbst meldet (Tag "1.0.1" gegen App "1.0.1-beta"), */
* würde "1.0.1" als neuer gelten – der Launcher böte dasselbe Update nach let cachedKind = null;
* jeder Installation erneut an, eine Endlosschleife. Deshalb zählt eine function detectInstallKind() {
* Vorabkennung nur, wenn sie auf beiden Seiten vorhanden ist (beta.1 -> beta.2). if (cachedKind) return cachedKind;
*/ cachedKind = computeInstallKind();
function comparePreRelease(a, b) { return cachedKind;
if (!a || !b) return 0; }
for (let i = 0; i < Math.max(a.length, b.length); i++) {
const x = a[i]; const y = b[i]; function computeInstallKind() {
if (x === undefined) return -1; // 1. Portable: diese Variablen setzt ausschließlich der Portable-Build von
if (y === undefined) return 1; // electron-builder – das ist das verlässlichste Signal überhaupt.
const nx = /^\d+$/.test(x) ? Number(x) : null; if (String(process.env.PORTABLE_EXECUTABLE_FILE || '').trim()) return 'portable';
const ny = /^\d+$/.test(y) ? Number(y) : null; if (String(process.env.PORTABLE_EXECUTABLE_DIR || '').trim()) return 'portable';
if (nx !== null && ny !== null) { if (nx !== ny) return nx - ny; continue; } if (String(process.env.PORTABLE_EXECUTABLE_APP_FILENAME || '').trim()) return 'portable';
if (nx !== null) return -1; // Zahlen zählen weniger als Text
if (ny !== null) return 1; const exe = process.execPath || '';
if (x !== y) return x < y ? -1 : 1; // 2. Notnagel über den Dateinamen, falls die Variablen einmal fehlen
} if (/portable/i.test(path.basename(exe))) return 'portable';
return 0;
} // 3. Setup: das Setup legt neben der EXE immer "Uninstall AeroMC.exe" ab. Nur wenn der
// da ist, ist es wirklich eine Installation.
// >0 wenn a neuer als b if (hasUninstaller(path.dirname(exe))) return 'setup';
function compareVersions(a, b) {
const x = parseVersion(a); const y = parseVersion(b); // 4. Alles andere (Entwicklungsmodus, entpackter Ordner, unbekannte Kopie)
if (!x || !y) return 0; return 'unknown';
for (let i = 0; i < 3; i++) { }
if (x[i] !== y[i]) return x[i] - y[i];
} function hasUninstaller(dir) {
return comparePreRelease(parsePreRelease(a), parsePreRelease(b)); try {
} return fs.readdirSync(dir).some((n) => /^unins(tall)?.*\.exe$/i.test(n));
} catch {
/* return false;
* Paketart einer Release-Datei am Namen erkennen: 'portable', 'setup' oder null. }
* Namen, die auf beides passen (z. B. "AeroMC-Portable-Setup.exe"), gelten als }
* mehrdeutig und werden bewusst niemandem angeboten – lieber kein Update als
* das falsche. // Pfad der laufenden Portable-EXE (die Datei, die der Nutzer selbst gestartet hat)
*/ function portableTarget() {
function assetKind(name) { const p = String(process.env.PORTABLE_EXECUTABLE_FILE || '').trim();
const n = String(name || ''); if (p) return p;
if (!/\.exe$/i.test(n)) return null; // Fallback: nur brauchbar, wenn die EXE nicht entpackt ausgeführt wird
const portable = /portable/i.test(n); const exe = process.execPath || '';
const setup = /setup|install(er)?/i.test(n); return path.basename(exe).toLowerCase().includes('portable') ? exe : '';
if (portable && setup) return null; }
if (portable) return 'portable';
if (setup) return 'setup'; function updateDir() {
return null; // ohne Kennzeichnung im Namen nicht zuzuordnen return path.join(os.tmpdir(), 'aeromc-update');
} }
// passendes Windows-Artefakt im Release finden /*
function pickAsset(release, installKind) { * Räumt Reste alter Update-Vorgänge weg (Installer sind mehrere hundert MB groß).
if (installKind !== 'portable' && installKind !== 'setup') return null; * Dateien, die jünger als 10 Minuten sind, bleiben liegen – sie könnten gerade
const assets = release.assets || []; * von einem laufenden Update benutzt werden.
return assets.find((a) => assetKind(a.name) === installKind) || null; */
} function cleanupUpdateCache() {
try {
/* const dir = updateDir();
* Prüft auf Updates. if (!fs.existsSync(dir)) return;
* -> { available, currentVersion, latestVersion, notes, asset, url, noReleases } for (const name of fs.readdirSync(dir)) {
*/ const p = path.join(dir, name);
async function checkForUpdate(currentVersion) { try {
const installKind = detectInstallKind(); if (Date.now() - fs.statSync(p).mtimeMs < 600000) continue;
const res = await fetch(API + '?limit=10', { headers: { 'User-Agent': UA, Accept: 'application/json' } }); fs.rmSync(p, { force: true, recursive: true });
if (!res.ok) throw new Error('Update-Server antwortet mit HTTP ' + res.status); } catch { /* gesperrt – beim nächsten Start erneut versuchen */ }
const list = await res.json(); }
const releases = (Array.isArray(list) ? list : []).filter((r) => !r.draft); } catch { /* egal */ }
if (!releases.length) { }
return { available: false, noReleases: true, currentVersion };
} // "v1.2.3-beta.2" -> [1,2,3]; führendes "v" wird ignoriert
// neueste anhand der Versionsnummer (nicht nach Datum, damit Nachträge nicht stören) function parseVersion(v) {
releases.sort((a, b) => compareVersions(b.tag_name, a.tag_name)); const m = String(v || '').trim().replace(/^v/i, '').match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
const latest = releases[0]; if (!m) return null;
const available = compareVersions(latest.tag_name, currentVersion) > 0; return [Number(m[1] || 0), Number(m[2] || 0), Number(m[3] || 0)];
const asset = pickAsset(latest, installKind); }
return {
available, // Vorabkennung hinter dem ersten "-", z. B. "beta" oder "beta.2"
currentVersion, function parsePreRelease(v) {
latestVersion: String(latest.tag_name || '').replace(/^v/i, ''), const m = String(v || '').trim().replace(/^v/i, '').match(/^\d+(?:\.\d+){0,2}-([0-9A-Za-z.\-]+)/);
installKind, return m ? m[1].split('.') : null;
notes: latest.body || '', }
published: latest.published_at || latest.created_at || null,
asset: asset ? { name: asset.name, url: asset.browser_download_url, size: asset.size, kind: installKind } : null, /*
url: latest.html_url, * Vergleicht die Vorabkennungen zweier gleicher Versionsnummern.
}; *
} * Bewusst nicht streng nach SemVer: trägt ein Release-Tag die Vorabkennung
* nicht, die die App selbst meldet (Tag "1.0.1" gegen App "1.0.1-beta"),
/* * würde "1.0.1" als neuer gelten – der Launcher böte dasselbe Update nach
* Wirft, sobald eine Datei nicht zweifelsfrei zur laufenden Paketart gehört. * jeder Installation erneut an, eine Endlosschleife. Deshalb zählt eine
* Wird vor dem Download und noch einmal direkt vor dem Ausführen aufgerufen. * Vorabkennung nur, wenn sie auf beiden Seiten vorhanden ist (beta.1 -> beta.2).
*/ */
function assertMatchingKind(fileName, installKind) { function comparePreRelease(a, b) {
if (installKind !== 'portable' && installKind !== 'setup') { if (!a || !b) return 0;
throw new Error('Die Paketart des Launchers lässt sich nicht sicher bestimmen – bitte manuell aktualisieren.'); for (let i = 0; i < Math.max(a.length, b.length); i++) {
} const x = a[i]; const y = b[i];
const kind = assetKind(fileName); if (x === undefined) return -1;
if (!kind) { if (y === undefined) return 1;
throw new Error(`"${fileName}" lässt sich keiner Paketart zuordnen – bitte manuell aktualisieren.`); const nx = /^\d+$/.test(x) ? Number(x) : null;
} const ny = /^\d+$/.test(y) ? Number(y) : null;
if (kind !== installKind) { if (nx !== null && ny !== null) { if (nx !== ny) return nx - ny; continue; }
const soll = installKind === 'portable' ? 'eine Portable-Datei' : 'einen Installer'; if (nx !== null) return -1; // Zahlen zählen weniger als Text
const ist = kind === 'portable' ? 'eine Portable-Datei' : 'ein Installer'; if (ny !== null) return 1;
throw new Error(`Falsches Paket: Dieser Launcher braucht ${soll}, angeboten wurde ${ist}.`); if (x !== y) return x < y ? -1 : 1;
} }
} return 0;
}
/*
* Lädt den Installer herunter (mit Fortschritt) und startet ihn. // >0 wenn a neuer als b
* onProgress: ({ phase, loaded, total, percent }) function compareVersions(a, b) {
*/ const x = parseVersion(a); const y = parseVersion(b);
async function downloadAndInstall(asset, onProgress) { if (!x || !y) return 0;
if (!asset || !asset.url) throw new Error('Kein Installer im Release gefunden.'); for (let i = 0; i < 3; i++) {
const installKind = detectInstallKind(); if (x[i] !== y[i]) return x[i] - y[i];
// Paketart am echten Dateinamen prüfen, nicht am mitgelieferten Feld – }
// sonst kontrolliert sich die Angabe nur selbst. return comparePreRelease(parsePreRelease(a), parsePreRelease(b));
assertMatchingKind(asset.name, installKind); }
// Portable-Updates ersetzen später die laufende EXE – das geht nur, wenn wir sie kennen /*
const target = installKind === 'portable' ? portableTarget() : ''; * Paketart einer Release-Datei am Namen erkennen: 'portable', 'setup' oder null.
if (installKind === 'portable' && !target) { * Namen, die auf beides passen (z. B. "AeroMC-Portable-Setup.exe"), gelten als
throw new Error('Der Pfad der laufenden Portable-Datei ist unbekannt – bitte manuell aktualisieren.'); * mehrdeutig und werden bewusst niemandem angeboten – lieber kein Update als
} * das falsche.
*/
const dir = updateDir(); function assetKind(name) {
fs.mkdirSync(dir, { recursive: true }); const n = String(name || '');
let dest = path.join(dir, asset.name.replace(/[^\w.\-]/g, '_')); if (!/\.exe$/i.test(n)) return null;
try { const portable = /portable/i.test(n);
fs.rmSync(dest, { force: true }); const setup = /setup|install(er)?/i.test(n);
} catch { if (portable && setup) return null;
// Alte Datei ist gesperrt (Virenscanner prüft sie gerade, oder ein vorheriger if (portable) return 'portable';
// Installer-Versuch läuft noch) - dann eben unter neuem Namen laden, statt mit if (setup) return 'setup';
// EPERM abzubrechen. Der Rest wird bei nächster Gelegenheit von return null; // ohne Kennzeichnung im Namen nicht zuzuordnen
// cleanupUpdateCache() aufgeräumt (ab 10 Minuten Alter). }
const parsed = path.parse(dest);
dest = path.join(parsed.dir, `${parsed.name}-${Date.now()}${parsed.ext}`); // passendes Windows-Artefakt im Release finden
} function pickAsset(release, installKind) {
if (installKind !== 'portable' && installKind !== 'setup') return null;
const res = await fetch(asset.url, { headers: { 'User-Agent': UA } }); const assets = release.assets || [];
if (!res.ok) throw new Error('Download fehlgeschlagen: HTTP ' + res.status); return assets.find((a) => assetKind(a.name) === installKind) || null;
}
const total = Number(res.headers.get('content-length')) || asset.size || 0;
/*
// direkt auf die Platte schreiben statt alles im Speicher zu sammeln * Prüft auf Updates.
const out = fs.createWriteStream(dest); * -> { available, currentVersion, latestVersion, notes, asset, url, noReleases }
const hash = crypto.createHash('sha256'); */
let loaded = 0; async function checkForUpdate(currentVersion) {
let lastReport = 0; const installKind = detectInstallKind();
try { const res = await fetch(API + '?limit=10', { headers: { 'User-Agent': UA, Accept: 'application/json' } });
for await (const chunk of res.body) { if (!res.ok) throw new Error('Update-Server antwortet mit HTTP ' + res.status);
const buf = Buffer.from(chunk); const list = await res.json();
hash.update(buf); const releases = (Array.isArray(list) ? list : []).filter((r) => !r.draft);
loaded += buf.length; if (!releases.length) {
// auf den Puffer warten; der jeweils andere Listener wird mit abgeräumt, return { available: false, noReleases: true, currentVersion };
// sonst sammeln sich über den ganzen Download hunderte davon an }
if (!out.write(buf)) await new Promise((resolve, reject) => { // neueste anhand der Versionsnummer (nicht nach Datum, damit Nachträge nicht stören)
const onDrain = () => { out.off('error', onError); resolve(); }; releases.sort((a, b) => compareVersions(b.tag_name, a.tag_name));
const onError = (e) => { out.off('drain', onDrain); reject(e); }; const latest = releases[0];
out.once('drain', onDrain); const available = compareVersions(latest.tag_name, currentVersion) > 0;
out.once('error', onError); const latestVersion = normVersion(latest.tag_name);
}); let asset = pickAsset(latest, installKind);
// höchstens viermal pro Sekunde melden, sonst flutet es die Oberfläche // ohne Signatur oder von fremder Adresse wird nichts automatisch installiert
if (onProgress && Date.now() - lastReport >= 250) { let unsigniert = false;
lastReport = Date.now(); let signaturHinweis = '';
onProgress({ phase: 'download', loaded, total, percent: total ? Math.round((loaded / total) * 100) : 0 }); let sig = null;
} if (asset && !erlaubteAdresse(asset.browser_download_url)) {
} asset = null;
await new Promise((resolve, reject) => { unsigniert = true;
out.once('error', reject); } else if (asset && available) {
out.end((err) => (err ? reject(err) : resolve())); try {
}); sig = await holeSignatur(asset.name, latestVersion);
} catch (err) { } catch (err) {
out.destroy(); signaturHinweis = 'Die Signatur des Updates ließ sich gerade nicht abrufen (' + (err.message || err) + '). Bitte später erneut versuchen.';
fs.rmSync(dest, { force: true }); }
throw err; if (!sig) {
} asset = null;
unsigniert = !signaturHinweis;
if (total && loaded !== total) { }
fs.rmSync(dest, { force: true }); }
throw new Error('Download unvollständig.'); return {
} available,
currentVersion,
if (onProgress) onProgress({ phase: 'ready', loaded, total: loaded, percent: 100 }); latestVersion,
return { path: dest, bytes: loaded, sha256: hash.digest('hex'), kind: installKind, target }; installKind,
} unsigniert,
signaturHinweis,
/* notes: latest.body || '',
* Hilfsskript für Portable-Updates: wartet, bis der Launcher die eigene EXE published: latest.published_at || latest.created_at || null,
* freigibt, tauscht sie gegen die neue aus und startet sie wieder. asset: asset ? {
* "ping" statt "timeout", weil timeout ohne Konsole abbricht. name: asset.name, url: asset.browser_download_url, size: asset.size, kind: installKind,
*/ version: latestVersion, signatur: sig,
const SWAP_SCRIPT = `@echo off } : null,
setlocal url: latest.html_url,
set "SRC=%~1" };
set "DST=%~2" }
set "LOG=%~dp0update.log"
set TRIES=0 /*
:loop * Wirft, sobald eine Datei nicht zweifelsfrei zur laufenden Paketart gehört.
ping -n 2 127.0.0.1 >nul * Wird vor dem Download und noch einmal direkt vor dem Ausführen aufgerufen.
copy /y "%SRC%" "%DST%" >nul 2>&1 */
if not errorlevel 1 goto done function assertMatchingKind(fileName, installKind) {
set /a TRIES+=1 if (installKind !== 'portable' && installKind !== 'setup') {
if %TRIES% GEQ 60 goto fail throw new Error('Die Paketart des Launchers lässt sich nicht sicher bestimmen – bitte manuell aktualisieren.');
goto loop }
:done const kind = assetKind(fileName);
del /f /q "%SRC%" >nul 2>&1 if (!kind) {
start "" "%DST%" throw new Error(`"${fileName}" lässt sich keiner Paketart zuordnen – bitte manuell aktualisieren.`);
exit /b 0 }
:fail if (kind !== installKind) {
echo [%DATE% %TIME%] Konnte "%DST%" nicht ersetzen - Datei gesperrt oder schreibgeschuetzt.>>"%LOG%" const soll = installKind === 'portable' ? 'eine Portable-Datei' : 'einen Installer';
exit /b 1 const ist = kind === 'portable' ? 'eine Portable-Datei' : 'ein Installer';
`; throw new Error(`Falsches Paket: Dieser Launcher braucht ${soll}, angeboten wurde ${ist}.`);
}
/* }
* Wendet das geladene Update an.
* setup -> AeroMC-Setup mit --update starten, es überschreibt die Installation selbst /*
* portable -> Skript starten, das die laufende EXE nach dem Beenden ersetzt * Lädt den Installer herunter (mit Fortschritt) und startet ihn.
* Der Launcher muss sich danach beenden. * onProgress: ({ phase, loaded, total, percent })
*/ */
function applyUpdate(file) { async function downloadAndInstall(asset, onProgress) {
const installKind = detectInstallKind(); if (!asset || !asset.url) throw new Error('Kein Installer im Release gefunden.');
// letzte Kontrolle unmittelbar vor dem Start: passt die Datei auf der Platte const installKind = detectInstallKind();
// wirklich zu dieser Paketart? // Paketart am echten Dateinamen prüfen, nicht am mitgelieferten Feld –
assertMatchingKind(path.basename(file.path || ''), installKind); // sonst kontrolliert sich die Angabe nur selbst.
const kind = installKind; assertMatchingKind(asset.name, installKind);
if (kind !== 'portable') { if (!erlaubteAdresse(asset.url)) {
// --update: das AeroMC-Setup aktualisiert ohne Rückfragen und startet AeroMC danach neu throw new Error('Das Update kommt nicht vom AeroMC-Server – abgebrochen.');
const child = spawn(file.path, ['--update'], { detached: true, stdio: 'ignore' }); }
child.unref(); // ohne Signatur wird gar nicht erst heruntergeladen
return { started: 'installer' }; const signatur = asset.signatur;
} if (!signatur || signatur.datei !== asset.name || normVersion(signatur.version) !== normVersion(asset.version)) {
throw new Error('Signatur passt nicht zu diesem Update – abgebrochen.');
const target = file.target || portableTarget(); }
if (!target) throw new Error('Der Pfad der laufenden Portable-Datei ist unbekannt.');
// Portable-Updates ersetzen später die laufende EXE – das geht nur, wenn wir sie kennen
const script = path.join(updateDir(), 'aeromc-swap.cmd'); const target = installKind === 'portable' ? portableTarget() : '';
fs.writeFileSync(script, SWAP_SCRIPT, 'ascii'); if (installKind === 'portable' && !target) {
throw new Error('Der Pfad der laufenden Portable-Datei ist unbekannt – bitte manuell aktualisieren.');
// über "start /min", damit kein Konsolenfenster im Vordergrund stehen bleibt }
const child = spawn(process.env.COMSPEC || 'cmd.exe',
['/c', 'start', '', '/min', script, file.path, target], const dir = updateDir();
{ detached: true, stdio: 'ignore', windowsHide: true }); fs.mkdirSync(dir, { recursive: true });
child.unref(); let dest = path.join(dir, asset.name.replace(/[^\w.\-]/g, '_'));
return { started: 'swap', target }; try {
} fs.rmSync(dest, { force: true });
} catch {
// Rückwärtskompatibel: nur für die Setup-Variante // Alte Datei ist gesperrt (Virenscanner prüft sie gerade, oder ein vorheriger
function runInstaller(installerPath) { // Installer-Versuch läuft noch) - dann eben unter neuem Namen laden, statt mit
return applyUpdate({ path: installerPath, kind: 'setup' }); // EPERM abzubrechen. Der Rest wird bei nächster Gelegenheit von
} // cleanupUpdateCache() aufgeräumt (ab 10 Minuten Alter).
const parsed = path.parse(dest);
module.exports = { dest = path.join(parsed.dir, `${parsed.name}-${Date.now()}${parsed.ext}`);
checkForUpdate, }
downloadAndInstall,
applyUpdate, const res = await fetch(asset.url, { headers: { 'User-Agent': UA } });
runInstaller, if (!res.ok) throw new Error('Download fehlgeschlagen: HTTP ' + res.status);
cleanupUpdateCache,
compareVersions, const total = Number(res.headers.get('content-length')) || asset.size || 0;
parseVersion,
detectInstallKind, // direkt auf die Platte schreiben statt alles im Speicher zu sammeln
assetKind, const out = fs.createWriteStream(dest);
portableTarget, const hash = crypto.createHash('sha256');
}; let loaded = 0;
let lastReport = 0;
try {
for await (const chunk of res.body) {
const buf = Buffer.from(chunk);
hash.update(buf);
loaded += buf.length;
// auf den Puffer warten; der jeweils andere Listener wird mit abgeräumt,
// sonst sammeln sich über den ganzen Download hunderte davon an
if (!out.write(buf)) await new Promise((resolve, reject) => {
const onDrain = () => { out.off('error', onError); resolve(); };
const onError = (e) => { out.off('drain', onDrain); reject(e); };
out.once('drain', onDrain);
out.once('error', onError);
});
// höchstens viermal pro Sekunde melden, sonst flutet es die Oberfläche
if (onProgress && Date.now() - lastReport >= 250) {
lastReport = Date.now();
onProgress({ phase: 'download', loaded, total, percent: total ? Math.round((loaded / total) * 100) : 0 });
}
}
await new Promise((resolve, reject) => {
out.once('error', reject);
out.end((err) => (err ? reject(err) : resolve()));
});
} catch (err) {
out.destroy();
fs.rmSync(dest, { force: true });
throw err;
}
if (total && loaded !== total) {
fs.rmSync(dest, { force: true });
throw new Error('Download unvollständig.');
}
const sha256 = hash.digest('hex');
try {
pruefeSignatur(signatur, asset.name, asset.version, sha256);
} catch (err) {
fs.rmSync(dest, { force: true });
throw err;
}
if (onProgress) onProgress({ phase: 'ready', loaded, total: loaded, percent: 100 });
return { path: dest, bytes: loaded, sha256, signatur, name: asset.name, version: asset.version, kind: installKind, target };
}
/*
* Hilfsskript für Portable-Updates: wartet, bis der Launcher die eigene EXE
* freigibt, tauscht sie gegen die neue aus und startet sie wieder.
* "ping" statt "timeout", weil timeout ohne Konsole abbricht.
*/
const SWAP_SCRIPT = `@echo off
setlocal
set "SRC=%~1"
set "DST=%~2"
set "LOG=%~dp0update.log"
set TRIES=0
:loop
ping -n 2 127.0.0.1 >nul
copy /y "%SRC%" "%DST%" >nul 2>&1
if not errorlevel 1 goto done
set /a TRIES+=1
if %TRIES% GEQ 60 goto fail
goto loop
:done
del /f /q "%SRC%" >nul 2>&1
start "" "%DST%"
exit /b 0
:fail
echo [%DATE% %TIME%] Konnte "%DST%" nicht ersetzen - Datei gesperrt oder schreibgeschuetzt.>>"%LOG%"
exit /b 1
`;
/*
* Wendet das geladene Update an.
* setup -> AeroMC-Setup mit --update starten, es überschreibt die Installation selbst
* portable -> Skript starten, das die laufende EXE nach dem Beenden ersetzt
* Der Launcher muss sich danach beenden.
*/
async function applyUpdate(file) {
const installKind = detectInstallKind();
// letzte Kontrolle unmittelbar vor dem Start: passt die Datei auf der Platte
// wirklich zu dieser Paketart – und ist sie noch genau die signierte? (Zwischen
// Download und Start könnte sie im Temp-Ordner ausgetauscht worden sein.)
assertMatchingKind(path.basename(file.path || ''), installKind);
pruefeSignatur(file.signatur, file.name, file.version, await sha256Datei(file.path));
const kind = installKind;
if (kind !== 'portable') {
// --update: das AeroMC-Setup aktualisiert ohne Rückfragen und startet AeroMC danach neu
const child = spawn(file.path, ['--update'], { detached: true, stdio: 'ignore' });
child.unref();
return { started: 'installer' };
}
const target = file.target || portableTarget();
if (!target) throw new Error('Der Pfad der laufenden Portable-Datei ist unbekannt.');
const script = path.join(updateDir(), 'aeromc-swap.cmd');
fs.writeFileSync(script, SWAP_SCRIPT, 'ascii');
// über "start /min", damit kein Konsolenfenster im Vordergrund stehen bleibt
const child = spawn(process.env.COMSPEC || 'cmd.exe',
['/c', 'start', '', '/min', script, file.path, target],
{ detached: true, stdio: 'ignore', windowsHide: true });
child.unref();
return { started: 'swap', target };
}
module.exports = {
checkForUpdate,
downloadAndInstall,
applyUpdate,
pruefeSignatur,
signaturText,
holeSignatur,
SIGNATUR_API,
cleanupUpdateCache,
compareVersions,
parseVersion,
detectInstallKind,
assetKind,
portableTarget,
};
+181 -180
View File
@@ -1,180 +1,181 @@
<?php <?php
/** /**
* Plugin Name: AeroMC Freunde * Plugin Name: AeroMC Freunde
* Description: Backend für das Freundesystem des AeroMC Launchers - meldet, wer gerade online/im Spiel ist, verwaltet Freundschaftsanfragen, Einladungen, Kurznachrichten, per Code geteilte Instanzen und Fehlerberichte aus dem Absturz-Assistenten sowie eigene Umhänge (auch HD) aus dem Skin-Editor. Anmeldung über Mojangs Sitzungsprüfung (wie bei Minecraft-Servern) - ohne eigenes Passwort. * Description: Backend für das Freundesystem des AeroMC Launchers - meldet, wer gerade online/im Spiel ist, verwaltet Freundschaftsanfragen, Einladungen, Kurznachrichten, per Code geteilte Instanzen und Fehlerberichte aus dem Absturz-Assistenten sowie eigene Umhänge (auch HD) aus dem Skin-Editor. Anmeldung über Mojangs Sitzungsprüfung (wie bei Minecraft-Servern) - ohne eigenes Passwort.
* Version: 1.5.1 * Version: 1.5.5
* Author: M_Viper * Author: M_Viper
*/ */
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; // kein direkter Aufruf der Datei exit; // kein direkter Aufruf der Datei
} }
define('AEROMC_FRIENDS_VERSION', '1.5.1'); define('AEROMC_FRIENDS_VERSION', '1.5.5');
define('AEROMC_FRIENDS_PRESENCE_TABLE', 'aeromc_presence'); define('AEROMC_FRIENDS_PRESENCE_TABLE', 'aeromc_presence');
define('AEROMC_FRIENDS_FRIENDS_TABLE', 'aeromc_friends'); define('AEROMC_FRIENDS_FRIENDS_TABLE', 'aeromc_friends');
define('AEROMC_FRIENDS_INVITES_TABLE', 'aeromc_invites'); define('AEROMC_FRIENDS_INVITES_TABLE', 'aeromc_invites');
define('AEROMC_FRIENDS_SESSIONS_TABLE', 'aeromc_sessions'); define('AEROMC_FRIENDS_SESSIONS_TABLE', 'aeromc_sessions');
define('AEROMC_FRIENDS_SHARES_TABLE', 'aeromc_shares'); define('AEROMC_FRIENDS_SHARES_TABLE', 'aeromc_shares');
define('AEROMC_FRIENDS_MESSAGES_TABLE', 'aeromc_messages'); define('AEROMC_FRIENDS_MESSAGES_TABLE', 'aeromc_messages');
define('AEROMC_FRIENDS_REPORTS_TABLE', 'aeromc_reports'); define('AEROMC_FRIENDS_REPORTS_TABLE', 'aeromc_reports');
define('AEROMC_FRIENDS_CAPES_TABLE', 'aeromc_capes'); define('AEROMC_FRIENDS_CAPES_TABLE', 'aeromc_capes');
// Übergangsmodus: false = Anfragen ohne Anmeldung werden noch angenommen, aber // Anmeldung ist Pflicht (seit Plugin 1.5.5): Jede Anfrage "als Spieler X" braucht
// nur für Spieler, die sich noch NIE über die neue Anmeldung gemeldet haben // das Sitzungs-Token aus der Mojang-Anmeldung (Launcher ab 1.1.5). Launcher vor
// (ältere Launcher-Versionen funktionieren weiter). Sobald alle Nutzer // 1.1.5 können das Freundesystem damit nicht mehr nutzen. Nur falls doch noch
// aktualisiert haben, in der wp-config.php auf true setzen: // nötig, lässt sich der alte Übergangsmodus in der wp-config.php zurückholen:
// define('AEROMC_FRIENDS_REQUIRE_AUTH', true); // define('AEROMC_FRIENDS_REQUIRE_AUTH', false);
if (!defined('AEROMC_FRIENDS_REQUIRE_AUTH')) { if (!defined('AEROMC_FRIENDS_REQUIRE_AUTH')) {
define('AEROMC_FRIENDS_REQUIRE_AUTH', false); define('AEROMC_FRIENDS_REQUIRE_AUTH', true);
} }
/** /**
* Legt die beiden Tabellen an (bzw. bringt sie bei einem Update auf den * Legt die beiden Tabellen an (bzw. bringt sie bei einem Update auf den
* neuesten Stand - dbDelta erkennt bestehende Tabellen/Spalten selbst). * neuesten Stand - dbDelta erkennt bestehende Tabellen/Spalten selbst).
*/ */
function aeromc_friends_install() { function aeromc_friends_install() {
global $wpdb; global $wpdb;
$charset_collate = $wpdb->get_charset_collate(); $charset_collate = $wpdb->get_charset_collate();
$presence_table = $wpdb->prefix . AEROMC_FRIENDS_PRESENCE_TABLE; $presence_table = $wpdb->prefix . AEROMC_FRIENDS_PRESENCE_TABLE;
$friends_table = $wpdb->prefix . AEROMC_FRIENDS_FRIENDS_TABLE; $friends_table = $wpdb->prefix . AEROMC_FRIENDS_FRIENDS_TABLE;
$invites_table = $wpdb->prefix . AEROMC_FRIENDS_INVITES_TABLE; $invites_table = $wpdb->prefix . AEROMC_FRIENDS_INVITES_TABLE;
$sessions_table = $wpdb->prefix . AEROMC_FRIENDS_SESSIONS_TABLE; $sessions_table = $wpdb->prefix . AEROMC_FRIENDS_SESSIONS_TABLE;
$shares_table = $wpdb->prefix . AEROMC_FRIENDS_SHARES_TABLE; $shares_table = $wpdb->prefix . AEROMC_FRIENDS_SHARES_TABLE;
require_once ABSPATH . 'wp-admin/includes/upgrade.php'; require_once ABSPATH . 'wp-admin/includes/upgrade.php';
// uuid ohne Bindestriche gespeichert (siehe aeromc_friends_normalize_uuid // uuid ohne Bindestriche gespeichert (siehe aeromc_friends_normalize_uuid
// in includes/routes.php) - vermeidet doppelte Zeilen durch // in includes/routes.php) - vermeidet doppelte Zeilen durch
// unterschiedliche Schreibweisen derselben UUID // unterschiedliche Schreibweisen derselben UUID
$sql_presence = "CREATE TABLE $presence_table ( $sql_presence = "CREATE TABLE $presence_table (
uuid varchar(32) NOT NULL, uuid varchar(32) NOT NULL,
username varchar(32) NOT NULL, username varchar(32) NOT NULL,
instance_name varchar(128) DEFAULT NULL, instance_name varchar(128) DEFAULT NULL,
server_label varchar(128) DEFAULT NULL, server_label varchar(128) DEFAULT NULL,
mc_version varchar(32) DEFAULT NULL, mc_version varchar(32) DEFAULT NULL,
last_seen datetime NOT NULL, last_seen datetime NOT NULL,
PRIMARY KEY (uuid) PRIMARY KEY (uuid)
) $charset_collate;"; ) $charset_collate;";
dbDelta($sql_presence); dbDelta($sql_presence);
$sql_friends = "CREATE TABLE $friends_table ( $sql_friends = "CREATE TABLE $friends_table (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT, id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
uuid_a varchar(32) NOT NULL, uuid_a varchar(32) NOT NULL,
uuid_b varchar(32) NOT NULL, uuid_b varchar(32) NOT NULL,
status varchar(20) NOT NULL DEFAULT 'pending', status varchar(20) NOT NULL DEFAULT 'pending',
created_at datetime NOT NULL, created_at datetime NOT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
KEY uuid_a (uuid_a), KEY uuid_a (uuid_a),
KEY uuid_b (uuid_b) KEY uuid_b (uuid_b)
) $charset_collate;"; ) $charset_collate;";
dbDelta($sql_friends); dbDelta($sql_friends);
// Einladungen "Komm auf meinen Server" - gelten nur ein paar Minuten // Einladungen "Komm auf meinen Server" - gelten nur ein paar Minuten
$sql_invites = "CREATE TABLE $invites_table ( $sql_invites = "CREATE TABLE $invites_table (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT, id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
from_uuid varchar(32) NOT NULL, from_uuid varchar(32) NOT NULL,
to_uuid varchar(32) NOT NULL, to_uuid varchar(32) NOT NULL,
server varchar(128) NOT NULL, server varchar(128) NOT NULL,
mc_version varchar(32) DEFAULT NULL, mc_version varchar(32) DEFAULT NULL,
created_at datetime NOT NULL, created_at datetime NOT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
KEY to_uuid (to_uuid) KEY to_uuid (to_uuid)
) $charset_collate;"; ) $charset_collate;";
dbDelta($sql_invites); dbDelta($sql_invites);
// Anmelde-Sitzungen: nur der SHA-256 des Tokens wird gespeichert // Anmelde-Sitzungen: nur der SHA-256 des Tokens wird gespeichert
$sql_sessions = "CREATE TABLE $sessions_table ( $sql_sessions = "CREATE TABLE $sessions_table (
token_hash char(64) NOT NULL, token_hash char(64) NOT NULL,
uuid varchar(32) NOT NULL, uuid varchar(32) NOT NULL,
username varchar(32) NOT NULL, username varchar(32) NOT NULL,
created_at datetime NOT NULL, created_at datetime NOT NULL,
last_used datetime NOT NULL, last_used datetime NOT NULL,
PRIMARY KEY (token_hash), PRIMARY KEY (token_hash),
KEY uuid (uuid) KEY uuid (uuid)
) $charset_collate;"; ) $charset_collate;";
dbDelta($sql_sessions); dbDelta($sql_sessions);
// per Code geteilte Instanzen (Mod-Liste, keine Dateien) // per Code geteilte Instanzen (Mod-Liste, keine Dateien)
$sql_shares = "CREATE TABLE $shares_table ( $sql_shares = "CREATE TABLE $shares_table (
code varchar(9) NOT NULL, code varchar(9) NOT NULL,
uuid varchar(32) NOT NULL, uuid varchar(32) NOT NULL,
manifest longtext NOT NULL, manifest longtext NOT NULL,
created_at datetime NOT NULL, created_at datetime NOT NULL,
PRIMARY KEY (code), PRIMARY KEY (code),
KEY uuid (uuid) KEY uuid (uuid)
) $charset_collate;"; ) $charset_collate;";
dbDelta($sql_shares); dbDelta($sql_shares);
// Kurznachrichten zwischen Freunden (werden nach einigen Tagen gelöscht) // Kurznachrichten zwischen Freunden (werden nach einigen Tagen gelöscht)
$messages_table = $wpdb->prefix . AEROMC_FRIENDS_MESSAGES_TABLE; $messages_table = $wpdb->prefix . AEROMC_FRIENDS_MESSAGES_TABLE;
$sql_messages = "CREATE TABLE $messages_table ( $sql_messages = "CREATE TABLE $messages_table (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT, id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
from_uuid varchar(32) NOT NULL, from_uuid varchar(32) NOT NULL,
to_uuid varchar(32) NOT NULL, to_uuid varchar(32) NOT NULL,
text varchar(400) NOT NULL, text varchar(400) NOT NULL,
image_url varchar(255) DEFAULT NULL, image_url varchar(255) DEFAULT NULL,
created_at datetime NOT NULL, created_at datetime NOT NULL,
read_at datetime DEFAULT NULL, read_at datetime DEFAULT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
KEY to_uuid (to_uuid), KEY to_uuid (to_uuid),
KEY from_uuid (from_uuid) KEY from_uuid (from_uuid)
) $charset_collate;"; ) $charset_collate;";
dbDelta($sql_messages); dbDelta($sql_messages);
// Fehlerberichte aus dem Absturz-Assistenten (Ansicht: Werkzeuge → AeroMC Fehlerberichte) // Fehlerberichte aus dem Absturz-Assistenten (Ansicht: Werkzeuge → AeroMC Fehlerberichte)
$reports_table = $wpdb->prefix . AEROMC_FRIENDS_REPORTS_TABLE; $reports_table = $wpdb->prefix . AEROMC_FRIENDS_REPORTS_TABLE;
$sql_reports = "CREATE TABLE $reports_table ( $sql_reports = "CREATE TABLE $reports_table (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT, id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
created_at datetime NOT NULL, created_at datetime NOT NULL,
uuid varchar(32) DEFAULT NULL, uuid varchar(32) DEFAULT NULL,
username varchar(32) DEFAULT NULL, username varchar(32) DEFAULT NULL,
launcher_version varchar(32) DEFAULT NULL, launcher_version varchar(32) DEFAULT NULL,
mc_version varchar(32) DEFAULT NULL, mc_version varchar(32) DEFAULT NULL,
loader varchar(32) DEFAULT NULL, loader varchar(32) DEFAULT NULL,
loader_version varchar(64) DEFAULT NULL, loader_version varchar(64) DEFAULT NULL,
system varchar(128) DEFAULT NULL, system varchar(128) DEFAULT NULL,
funde text, funde text,
mods longtext, mods longtext,
bericht longtext, bericht longtext,
kommentar text, kommentar text,
status varchar(20) NOT NULL DEFAULT 'neu', status varchar(20) NOT NULL DEFAULT 'neu',
PRIMARY KEY (id), PRIMARY KEY (id),
KEY status (status) KEY status (status)
) $charset_collate;"; ) $charset_collate;";
dbDelta($sql_reports); dbDelta($sql_reports);
// eigene Umhänge (Bild liegt unter wp-content/uploads/aeromc-umhaenge) // eigene Umhänge (Bild liegt unter wp-content/uploads/aeromc-umhaenge)
$capes_table = $wpdb->prefix . AEROMC_FRIENDS_CAPES_TABLE; $capes_table = $wpdb->prefix . AEROMC_FRIENDS_CAPES_TABLE;
$sql_capes = "CREATE TABLE $capes_table ( $sql_capes = "CREATE TABLE $capes_table (
uuid varchar(32) NOT NULL, uuid varchar(32) NOT NULL,
username varchar(32) NOT NULL, username varchar(32) NOT NULL,
datei varchar(64) NOT NULL DEFAULT '', datei varchar(64) NOT NULL DEFAULT '',
breite smallint(5) unsigned NOT NULL DEFAULT 0, breite smallint(5) unsigned NOT NULL DEFAULT 0,
hoehe smallint(5) unsigned NOT NULL DEFAULT 0, hoehe smallint(5) unsigned NOT NULL DEFAULT 0,
hash varchar(40) NOT NULL DEFAULT '', hash varchar(40) NOT NULL DEFAULT '',
status varchar(16) NOT NULL DEFAULT 'aktiv', status varchar(16) NOT NULL DEFAULT 'aktiv',
updated_at datetime NOT NULL, updated_at datetime NOT NULL,
PRIMARY KEY (uuid), PRIMARY KEY (uuid),
KEY status (status) KEY status (status)
) $charset_collate;"; ) $charset_collate;";
dbDelta($sql_capes); dbDelta($sql_capes);
update_option('aeromc_friends_db_version', AEROMC_FRIENDS_VERSION); update_option('aeromc_friends_db_version', AEROMC_FRIENDS_VERSION);
} }
register_activation_hook(__FILE__, 'aeromc_friends_install'); register_activation_hook(__FILE__, 'aeromc_friends_install');
// Beim Ersetzen der Plugin-Dateien (Update per Upload) läuft der // Beim Ersetzen der Plugin-Dateien (Update per Upload) läuft der
// Aktivierungs-Hook NICHT erneut - neue Spalten/Tabellen deshalb anlegen, // Aktivierungs-Hook NICHT erneut - neue Spalten/Tabellen deshalb anlegen,
// sobald die gespeicherte DB-Version nicht mehr zur Plugin-Version passt. // sobald die gespeicherte DB-Version nicht mehr zur Plugin-Version passt.
add_action('plugins_loaded', function () { add_action('plugins_loaded', function () {
if (get_option('aeromc_friends_db_version') !== AEROMC_FRIENDS_VERSION) { if (get_option('aeromc_friends_db_version') !== AEROMC_FRIENDS_VERSION) {
aeromc_friends_install(); aeromc_friends_install();
} }
}); });
require_once plugin_dir_path(__FILE__) . 'includes/routes.php'; require_once plugin_dir_path(__FILE__) . 'includes/routes.php';
require_once plugin_dir_path(__FILE__) . 'includes/capes.php'; require_once plugin_dir_path(__FILE__) . 'includes/capes.php';
if (is_admin()) { require_once plugin_dir_path(__FILE__) . 'includes/signaturen.php';
require_once plugin_dir_path(__FILE__) . 'includes/admin.php'; if (is_admin()) {
} require_once plugin_dir_path(__FILE__) . 'includes/admin.php';
}
@@ -0,0 +1,246 @@
<?php
/**
* Update-Signaturen für den AeroMC Launcher
* -----------------------------------------
* Der Launcher installiert ein Update nur, wenn es mit dem Release-Schlüssel des
* Entwicklers unterschrieben ist (Ed25519). Die Unterschriften liegen hier statt
* im Gitea-Release; die EXE selbst kommt weiter aus Gitea.
*
* GET /wp-json/aeromc/v1/update-signatur?datei=AeroMC-Setup.exe&version=1.2.2
* öffentlich – liefert die Unterschrift (sie ist nicht geheim)
* POST /wp-json/aeromc/v1/update-signatur
* nur Administratoren (Anwendungspasswort) – vom Build-Skript
* scripts/sign-release.js automatisch aufgerufen
* GET /wp-json/aeromc/v1/update-signatur/zugang
* prüft beim Einrichten, ob der Zugang stimmt, und sagt sonst, woran es scheitert
*
* Wichtig: Der geheime Schlüssel liegt NIE auf diesem Server. Hier wird nur
* mit dem öffentlichen Schlüssel geprüft, dass eine hochgeladene Unterschrift
* echt ist – selbst ein übernommener Admin-Zugang kann keine gültige erzeugen.
*/
if (!defined('ABSPATH')) {
exit;
}
define('AEROMC_SIG_OPTION', 'aeromc_update_signaturen');
define('AEROMC_SIG_FORMAT', 'AeroMC-Update/1');
// öffentlicher Release-Schlüssel (identisch mit src/update-schluessel.pem im Launcher)
define('AEROMC_SIG_SCHLUESSEL', 'MCowBQYDK2VwAyEAXxt1cCntDQzvej4hjyVZt6Onj6Uv3hSlneJaOxxmsds=');
define('AEROMC_SIG_DATEIEN', ['AeroMC-Setup.exe', 'AeroMC-Portable.exe']);
define('AEROMC_SIG_MAX', 40); // so viele Einträge werden aufgehoben (älteste fliegen raus)
add_action('rest_api_init', function () {
register_rest_route('aeromc/v1', '/update-signatur', [
[
'methods' => 'GET',
'callback' => 'aeromc_sig_route_get',
'permission_callback' => '__return_true',
],
[
'methods' => 'POST',
'callback' => 'aeromc_sig_route_post',
'permission_callback' => 'aeromc_sig_ist_admin',
],
]);
// Zugangsprüfung mit Diagnose: sagt dem Build-Skript genau, woran eine
// Anmeldung scheitert (Header kommt nicht an, falsches Passwort, Rolle fehlt …)
register_rest_route('aeromc/v1', '/update-signatur/zugang', [
'methods' => 'GET',
'callback' => 'aeromc_sig_route_zugang',
'permission_callback' => '__return_true',
]);
});
/*
* Eigene Rolle "AeroMC Release": darf NUR Signaturen hochladen (sonst nichts
* außer dem eigenen Profil). Für das Build-Skript gedacht – so muss kein
* Administrator-Zugang auf dem Entwickler-PC liegen.
*/
define('AEROMC_SIG_RECHT', 'aeromc_signaturen_hochladen');
add_action('init', function () {
if (!get_role('aeromc_release')) {
add_role('aeromc_release', 'AeroMC Release', ['read' => true, AEROMC_SIG_RECHT => true]);
}
});
function aeromc_sig_route_zugang() {
$authorization = !empty($_SERVER['HTTP_AUTHORIZATION']) || !empty($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])
|| !empty($_SERVER['PHP_AUTH_USER']);
$eigener = !empty($_SERVER['HTTP_X_AEROMC_AUTH']);
$grund = '';
$user = is_user_logged_in() ? wp_get_current_user() : null;
if (!$user && $eigener) {
$roh = base64_decode((string) $_SERVER['HTTP_X_AEROMC_AUTH'], true);
if ($roh && strpos($roh, ':') !== false) {
list($benutzer, $passwort) = explode(':', $roh, 2);
$r = wp_authenticate_application_password(null, $benutzer, $passwort);
if ($r instanceof WP_User) {
$user = $r;
wp_set_current_user($r->ID);
} elseif (is_wp_error($r)) {
$grund = $r->get_error_code();
} else {
$grund = 'nicht_geprueft';
}
} else {
$grund = 'kopf_ungueltig';
}
}
$darf = $user && (user_can($user, AEROMC_SIG_RECHT) || user_can($user, 'manage_options'));
return new WP_REST_Response([
'ok' => (bool) $darf,
'version' => AEROMC_FRIENDS_VERSION,
'authorization_kommt_an' => $authorization,
'eigener_kopf_kommt_an' => $eigener,
'anwendungspasswoerter' => function_exists('wp_is_application_passwords_available') && wp_is_application_passwords_available(),
'benutzer' => $user ? $user->user_login : null,
'darf_hochladen' => (bool) $darf,
'grund' => $grund,
], $darf ? 200 : 401);
}
function aeromc_sig_ist_admin() {
aeromc_sig_anmelden_ueber_kopf();
return current_user_can(AEROMC_SIG_RECHT) || current_user_can('manage_options');
}
/*
* Manche Server (Apache mit FastCGI/PHP-FPM, Proxys) geben den Header
* "Authorization" nicht an PHP weiter – die normale Anmeldung per
* Anwendungspasswort kommt dann nie an. Das Build-Skript schickt dieselben
* Daten deshalb zusätzlich als "X-AeroMC-Auth: base64(benutzer:passwort)".
* Geprüft wird mit der WordPress-eigenen Funktion für Anwendungspasswörter –
* das normale Login-Passwort wird hier NICHT angenommen.
*/
function aeromc_sig_anmelden_ueber_kopf() {
if (is_user_logged_in()) {
return;
}
$kopf = isset($_SERVER['HTTP_X_AEROMC_AUTH']) ? (string) $_SERVER['HTTP_X_AEROMC_AUTH'] : '';
$roh = $kopf !== '' ? base64_decode($kopf, true) : false;
if (!$roh || strpos($roh, ':') === false) {
return;
}
list($benutzer, $passwort) = explode(':', $roh, 2);
$user = wp_authenticate_application_password(null, $benutzer, $passwort);
if ($user instanceof WP_User) {
wp_set_current_user($user->ID);
}
}
function aeromc_sig_version_norm($v) {
return preg_replace('/^v/i', '', trim((string) $v));
}
function aeromc_sig_schluessel_von($datei, $version) {
return $datei . '|' . aeromc_sig_version_norm($version);
}
function aeromc_sig_alle() {
$alle = get_option(AEROMC_SIG_OPTION, []);
return is_array($alle) ? $alle : [];
}
function aeromc_sig_route_get($request) {
$datei = (string) $request->get_param('datei');
$version = aeromc_sig_version_norm($request->get_param('version'));
$alle = aeromc_sig_alle();
$eintrag = $alle[aeromc_sig_schluessel_von($datei, $version)] ?? null;
if (!$eintrag) {
return new WP_REST_Response(['ok' => false, 'message' => 'Keine Signatur für diese Datei/Version.'], 404);
}
unset($eintrag['hochgeladen']);
$antwort = new WP_REST_Response($eintrag, 200);
$antwort->header('Cache-Control', 'no-store');
return $antwort;
}
/**
* Prüft Aufbau und – sofern PHP libsodium mitbringt – die Unterschrift selbst.
* Gibt null zurück, wenn alles stimmt, sonst die Fehlermeldung.
*/
function aeromc_sig_pruefe(array $s) {
if (($s['format'] ?? '') !== AEROMC_SIG_FORMAT) return 'Unbekanntes Format.';
if (!in_array($s['datei'] ?? '', AEROMC_SIG_DATEIEN, true)) return 'Unbekannter Dateiname.';
if (!preg_match('/^\d+\.\d+\.\d+(-[0-9A-Za-z.\-]+)?$/', aeromc_sig_version_norm($s['version'] ?? ''))) return 'Ungültige Version.';
if (!preg_match('/^[0-9a-f]{64}$/', (string) ($s['sha256'] ?? ''))) return 'Ungültige Prüfsumme.';
$unterschrift = base64_decode((string) ($s['signatur'] ?? ''), true);
if ($unterschrift === false || strlen($unterschrift) !== 64) return 'Ungültige Signatur.';
if (function_exists('sodium_crypto_sign_verify_detached')) {
$der = base64_decode(AEROMC_SIG_SCHLUESSEL);
$oeffentlich = substr($der, -32); // SPKI-Hülle (12 Byte) + 32 Byte Schlüssel
$text = implode("\n", [AEROMC_SIG_FORMAT, $s['datei'], aeromc_sig_version_norm($s['version']), $s['sha256']]);
if (!sodium_crypto_sign_verify_detached($unterschrift, $text, $oeffentlich)) {
return 'Signatur passt nicht zum Release-Schlüssel.';
}
}
return null;
}
function aeromc_sig_route_post($request) {
$s = $request->get_json_params();
if (!is_array($s)) {
return new WP_REST_Response(['ok' => false, 'message' => 'Kein JSON erhalten.'], 400);
}
$s = [
'format' => (string) ($s['format'] ?? ''),
'datei' => (string) ($s['datei'] ?? ''),
'version' => aeromc_sig_version_norm($s['version'] ?? ''),
'sha256' => strtolower((string) ($s['sha256'] ?? '')),
'signatur' => (string) ($s['signatur'] ?? ''),
];
$fehler = aeromc_sig_pruefe($s);
if ($fehler) {
return new WP_REST_Response(['ok' => false, 'message' => $fehler], 400);
}
$alle = aeromc_sig_alle();
$s['hochgeladen'] = current_time('mysql');
$alle[aeromc_sig_schluessel_von($s['datei'], $s['version'])] = $s;
// nur die neuesten Einträge behalten
uasort($alle, function ($a, $b) { return strcmp($b['hochgeladen'], $a['hochgeladen']); });
$alle = array_slice($alle, 0, AEROMC_SIG_MAX, true);
update_option(AEROMC_SIG_OPTION, $alle, false);
return ['ok' => true, 'datei' => $s['datei'], 'version' => $s['version'],
'geprueft' => function_exists('sodium_crypto_sign_verify_detached')];
}
// Übersicht im Admin-Menü (unter "AeroMC Fehler")
add_action('admin_menu', function () {
add_submenu_page('aeromc-reports', 'AeroMC Update-Signaturen', 'Update-Signaturen', 'manage_options',
'aeromc-signaturen', 'aeromc_sig_admin_seite');
}, 20);
function aeromc_sig_admin_seite() {
if (!current_user_can('manage_options')) {
return;
}
if (isset($_POST['aeromc_sig_loeschen']) && check_admin_referer('aeromc_signaturen')) {
$alle = aeromc_sig_alle();
unset($alle[(string) wp_unslash($_POST['aeromc_sig_loeschen'])]);
update_option(AEROMC_SIG_OPTION, $alle, false);
echo '<div class="notice notice-success"><p>Signatur entfernt.</p></div>';
}
echo '<div class="wrap"><h1>AeroMC Update-Signaturen</h1>';
echo '<p>Diese Unterschriften lädt das Build-Skript automatisch hoch. Der Launcher installiert ein Update nur, '
. 'wenn hier eine passende, gültige Signatur liegt. Sie sind nicht geheim – der geheime Schlüssel bleibt auf dem Entwickler-PC.</p>';
if (!function_exists('sodium_crypto_sign_verify_detached')) {
echo '<div class="notice notice-warning"><p>PHP hat kein libsodium – hochgeladene Signaturen werden hier nicht gegengeprüft '
. '(der Launcher prüft sie trotzdem selbst).</p></div>';
}
$alle = aeromc_sig_alle();
if (!$alle) {
echo '<p><em>Noch keine Signaturen vorhanden.</em></p></div>';
return;
}
echo '<table class="widefat striped"><thead><tr><th>Datei</th><th>Version</th><th>SHA-256</th><th>Hochgeladen</th><th></th></tr></thead><tbody>';
foreach ($alle as $schl => $s) {
echo '<tr><td>' . esc_html($s['datei']) . '</td><td>' . esc_html($s['version']) . '</td>'
. '<td><code>' . esc_html(substr($s['sha256'], 0, 16)) . '…</code></td><td>' . esc_html($s['hochgeladen']) . '</td><td>'
. '<form method="post" onsubmit="return confirm(\'Signatur wirklich entfernen? Dann bietet der Launcher dieses Update nicht mehr an.\');">';
wp_nonce_field('aeromc_signaturen');
echo '<input type="hidden" name="aeromc_sig_loeschen" value="' . esc_attr($schl) . '" />'
. '<button class="button button-link-delete">Entfernen</button></form></td></tr>';
}
echo '</tbody></table></div>';
}