Upload via GUI (194 Dateien)
This commit is contained in:
Generated
+1087
-1377
File diff suppressed because it is too large
Load Diff
+17
-7
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"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",
|
||||||
@@ -11,10 +11,10 @@
|
|||||||
"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",
|
||||||
@@ -34,11 +34,18 @@
|
|||||||
"win": {
|
"win": {
|
||||||
"target": "dir",
|
"target": "dir",
|
||||||
"icon": "src/assets/icon.ico"
|
"icon": "src/assets/icon.ico"
|
||||||
|
},
|
||||||
|
"electronFuses": {
|
||||||
|
"runAsNode": false,
|
||||||
|
"enableNodeOptionsEnvironmentVariable": false,
|
||||||
|
"enableNodeCliInspectArguments": false,
|
||||||
|
"enableEmbeddedAsarIntegrityValidation": true,
|
||||||
|
"onlyLoadAppFromAsar": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "^28.0.0",
|
"electron": "^44.6.0",
|
||||||
"electron-builder": "^24.0.0"
|
"electron-builder": "^26.15.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xhayper/discord-rpc": "^1.3.4",
|
"@xhayper/discord-rpc": "^1.3.4",
|
||||||
@@ -50,5 +57,8 @@
|
|||||||
"sql.js": "^1.14.2",
|
"sql.js": "^1.14.2",
|
||||||
"undici": "^6.28.0",
|
"undici": "^6.28.0",
|
||||||
"unzipper": "^0.12.5"
|
"unzipper": "^0.12.5"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"brace-expansion@^2.0.0": "^2.1.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+54
-2
@@ -79,6 +79,8 @@ const DEFAULT_SETTINGS = {
|
|||||||
friendsEnabled: true,
|
friendsEnabled: true,
|
||||||
friendsShareServer: true, // Freunde sehen, auf welchem Server man spielt (für "Beitreten")
|
friendsShareServer: true, // Freunde sehen, auf welchem Server man spielt (für "Beitreten")
|
||||||
autoModUpdates: false,
|
autoModUpdates: false,
|
||||||
|
modSchalter: true, // „Mods verwalten“ im Pausemenü (Instanzen mit Mod-Loader)
|
||||||
|
aeromcDesign: true, // AeroMC-Look im Spiel: Ladebildschirm, Menü-Logo, Sprüche, Knöpfe, Fenstertitel
|
||||||
aeromcCapes: true, // AeroMC-Umhänge im Spiel anzeigen (Umhang-Agent beim Start) // vor jedem Start Mods über Modrinth aktualisieren
|
aeromcCapes: true, // AeroMC-Umhänge im Spiel anzeigen (Umhang-Agent beim Start) // vor jedem Start Mods über Modrinth aktualisieren
|
||||||
schnellstart: {}, // Strg+1 … Strg+5 -> Instanz-ID ({ "1": "Survival", … })
|
schnellstart: {}, // Strg+1 … Strg+5 -> Instanz-ID ({ "1": "Survival", … })
|
||||||
autoWorldBackup: true, // nach jeder Spielsitzung die zuletzt gespielte Welt sichern (letzte 5 Stände) // Freundesystem: eigene Online-Meldung + auffindbar für Anfragen
|
autoWorldBackup: true, // nach jeder Spielsitzung die zuletzt gespielte Welt sichern (letzte 5 Stände) // Freundesystem: eigene Online-Meldung + auffindbar für Anfragen
|
||||||
@@ -116,13 +118,61 @@ function settingsFile() {
|
|||||||
return path.join(USER_DATA, 'settings.json');
|
return path.join(USER_DATA, 'settings.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Geheime Einstellungen (Proxy-Passwort, API-Schlüssel, Token) stehen nicht im
|
||||||
|
* Klartext in settings.json, sondern mit Windows-DPAPI verschlüsselt – genau
|
||||||
|
* wie die Konten (siehe auth.js). Nur dieses Windows-Konto kann sie lesen.
|
||||||
|
* Der Tresor kommt erst nach app.ready (safeStorage); bis dahin bleiben die
|
||||||
|
* verschlüsselten Werte unverändert stehen und werden so auch zurückgeschrieben.
|
||||||
|
*/
|
||||||
|
const GEHEIME_FELDER = ['proxyPass', 'curseforgeApiKey', 'curseforgeProxyToken'];
|
||||||
|
const ENC = 'enc:v1:';
|
||||||
|
let tresor = null;
|
||||||
|
|
||||||
|
function bereit() {
|
||||||
|
try { return !!(tresor && tresor.available()); } catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function verschluesseln(obj) {
|
||||||
|
if (!bereit()) return obj;
|
||||||
|
const o = Object.assign({}, obj);
|
||||||
|
for (const k of GEHEIME_FELDER) {
|
||||||
|
if (typeof o[k] === 'string' && o[k] && !o[k].startsWith(ENC)) o[k] = ENC + tresor.encrypt(o[k]);
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
function entschluesseln(obj) {
|
||||||
|
if (!bereit()) return obj;
|
||||||
|
const o = Object.assign({}, obj);
|
||||||
|
for (const k of GEHEIME_FELDER) {
|
||||||
|
if (typeof o[k] === 'string' && o[k].startsWith(ENC)) {
|
||||||
|
// nicht lesbar (z. B. Sicherung von einem anderen PC) -> neu eingeben lassen
|
||||||
|
try { o[k] = tresor.decrypt(o[k].slice(ENC.length)); } catch { o[k] = ''; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wird nach app.ready aufgerufen; stellt vorhandene Klartext-Werte sofort um
|
||||||
|
function setzeTresor(t) {
|
||||||
|
tresor = t;
|
||||||
|
if (!bereit()) return;
|
||||||
|
try {
|
||||||
|
const roh = readJson(settingsFile(), null);
|
||||||
|
if (roh && GEHEIME_FELDER.some((k) => typeof roh[k] === 'string' && roh[k] && !roh[k].startsWith(ENC))) {
|
||||||
|
writeJson(settingsFile(), verschluesseln(roh));
|
||||||
|
}
|
||||||
|
} catch { /* beim nächsten Speichern erneut */ }
|
||||||
|
}
|
||||||
|
|
||||||
function getSettings() {
|
function getSettings() {
|
||||||
return Object.assign({}, DEFAULT_SETTINGS, readJson(settingsFile(), {}));
|
return Object.assign({}, DEFAULT_SETTINGS, entschluesseln(readJson(settingsFile(), {})));
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveSettings(patch) {
|
function saveSettings(patch) {
|
||||||
const merged = Object.assign({}, getSettings(), patch || {});
|
const merged = Object.assign({}, getSettings(), patch || {});
|
||||||
writeJson(settingsFile(), merged);
|
writeJson(settingsFile(), verschluesseln(merged));
|
||||||
// Falls sich das Instanzverzeichnis geändert hat: sicherstellen, dass es existiert
|
// Falls sich das Instanzverzeichnis geändert hat: sicherstellen, dass es existiert
|
||||||
ensureDir(getInstancesDir());
|
ensureDir(getInstancesDir());
|
||||||
return merged;
|
return merged;
|
||||||
@@ -510,6 +560,7 @@ function duplicateInstance(id, ohneSpieldaten) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
init,
|
init,
|
||||||
getSettings,
|
getSettings,
|
||||||
|
setzeTresor,
|
||||||
saveSettings,
|
saveSettings,
|
||||||
getInstancesDir,
|
getInstancesDir,
|
||||||
instancePath,
|
instancePath,
|
||||||
@@ -530,6 +581,7 @@ module.exports = {
|
|||||||
createInstance,
|
createInstance,
|
||||||
createOrReplaceImportedInstance,
|
createOrReplaceImportedInstance,
|
||||||
updateInstance,
|
updateInstance,
|
||||||
|
uniqueFolderId,
|
||||||
deleteInstance,
|
deleteInstance,
|
||||||
raeumePapierkorbAuf,
|
raeumePapierkorbAuf,
|
||||||
duplicateInstance,
|
duplicateInstance,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MCowBQYDK2VwAyEAXxt1cCntDQzvej4hjyVZt6Onj6Uv3hSlneJaOxxmsds=
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
+137
-12
@@ -16,6 +16,90 @@ 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';
|
||||||
|
// Die Signaturen liegen nicht im Gitea-Release, sondern auf der WordPress-Seite
|
||||||
|
// (Plugin aeromc-friends, includes/signaturen.php). Hochgeladen werden sie beim
|
||||||
|
// Bauen automatisch von scripts/sign-release.js.
|
||||||
|
const SIGNATUR_API = 'https://viper-network.de/wp-json/aeromc/v1/update-signatur';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Sicherheit: Jede Update-Datei muss mit dem privaten Release-Schlüssel signiert
|
||||||
|
* sein (Ed25519). Der liegt nur auf dem Rechner, auf dem gebaut wird – nie auf
|
||||||
|
* dem Server. Wer den Gitea-Server oder das Konto übernimmt, kann deshalb zwar
|
||||||
|
* Dateien hochladen, aber keine gültige Signatur erzeugen; der Launcher
|
||||||
|
* 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
|
||||||
|
* kann sie ohne den Schlüssel niemand.
|
||||||
|
*
|
||||||
|
* Unterschrieben wird nicht nur der Inhalt (SHA-256), sondern auch Dateiname
|
||||||
|
* und Version. So lässt sich weder eine Portable- als Setup-Datei ausgeben noch
|
||||||
|
* eine alte, signierte Fassung als "neues" Update unterschieben.
|
||||||
|
*/
|
||||||
|
const SIGNATUR_FORMAT = 'AeroMC-Update/1';
|
||||||
|
let oeffentlicherSchluessel = null;
|
||||||
|
function releaseSchluessel() {
|
||||||
|
if (!oeffentlicherSchluessel) {
|
||||||
|
const pem = fs.readFileSync(path.join(__dirname, 'update-schluessel.pem'), 'utf8');
|
||||||
|
oeffentlicherSchluessel = crypto.createPublicKey(pem);
|
||||||
|
}
|
||||||
|
return oeffentlicherSchluessel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normVersion(v) {
|
||||||
|
return String(v || '').trim().replace(/^v/i, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Der Text, der unterschrieben wird (identisch in scripts/sign-release.js)
|
||||||
|
function signaturText(datei, version, sha256) {
|
||||||
|
return Buffer.from([SIGNATUR_FORMAT, datei, normVersion(version), String(sha256).toLowerCase()].join('\n'), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüft eine Signatur (Inhalt der .sig-Datei) gegen Name, Version und Hash
|
||||||
|
function pruefeSignatur(sig, datei, version, sha256) {
|
||||||
|
if (!sig || sig.format !== SIGNATUR_FORMAT) throw new Error('Signaturdatei hat ein unbekanntes Format.');
|
||||||
|
if (sig.datei !== datei) throw new Error('Signatur gehört zu einer anderen Datei.');
|
||||||
|
if (normVersion(sig.version) !== normVersion(version)) throw new Error('Signatur gehört zu einer anderen Version.');
|
||||||
|
if (String(sig.sha256 || '').toLowerCase() !== String(sha256).toLowerCase()) {
|
||||||
|
throw new Error('Die heruntergeladene Datei passt nicht zur Signatur – Update abgebrochen.');
|
||||||
|
}
|
||||||
|
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 erlaubteAdresse(adresse) {
|
||||||
|
try {
|
||||||
|
const u = new URL(String(adresse || ''));
|
||||||
|
return u.protocol === 'https:' && u.hostname.toLowerCase() === ERLAUBTER_HOST;
|
||||||
|
} catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Signatur einer Release-Datei vom WordPress-Server holen.
|
||||||
|
* -> Signatur-Objekt, null wenn es (noch) keine gibt; wirft bei Netzwerkfehlern.
|
||||||
|
*/
|
||||||
|
async function holeSignatur(datei, version) {
|
||||||
|
const adresse = `${SIGNATUR_API}?datei=${encodeURIComponent(datei)}&version=${encodeURIComponent(normVersion(version))}`;
|
||||||
|
const res = await fetch(adresse, { headers: { 'User-Agent': UA, Accept: 'application/json' } });
|
||||||
|
if (res.status === 404) return null;
|
||||||
|
if (!res.ok) throw new Error('Signatur-Server antwortet mit HTTP ' + res.status);
|
||||||
|
const text = await res.text();
|
||||||
|
if (text.length > 16384) throw new Error('Signatur ist ungültig.');
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
return { format: sig.format, datei: sig.datei, version: sig.version, sha256: sig.sha256, signatur: sig.signatur };
|
||||||
|
}
|
||||||
|
|
||||||
|
function sha256Datei(datei) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const h = crypto.createHash('sha256');
|
||||||
|
fs.createReadStream(datei).on('error', reject).on('data', (d) => h.update(d)).on('end', () => resolve(h.digest('hex')));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Paketart der laufenden Anwendung: 'portable', 'setup' oder 'unknown'.
|
* Paketart der laufenden Anwendung: 'portable', 'setup' oder 'unknown'.
|
||||||
@@ -179,15 +263,39 @@ async function checkForUpdate(currentVersion) {
|
|||||||
releases.sort((a, b) => compareVersions(b.tag_name, a.tag_name));
|
releases.sort((a, b) => compareVersions(b.tag_name, a.tag_name));
|
||||||
const latest = releases[0];
|
const latest = releases[0];
|
||||||
const available = compareVersions(latest.tag_name, currentVersion) > 0;
|
const available = compareVersions(latest.tag_name, currentVersion) > 0;
|
||||||
const asset = pickAsset(latest, installKind);
|
const latestVersion = normVersion(latest.tag_name);
|
||||||
|
let asset = pickAsset(latest, installKind);
|
||||||
|
// ohne Signatur oder von fremder Adresse wird nichts automatisch installiert
|
||||||
|
let unsigniert = false;
|
||||||
|
let signaturHinweis = '';
|
||||||
|
let sig = null;
|
||||||
|
if (asset && !erlaubteAdresse(asset.browser_download_url)) {
|
||||||
|
asset = null;
|
||||||
|
unsigniert = true;
|
||||||
|
} else if (asset && available) {
|
||||||
|
try {
|
||||||
|
sig = await holeSignatur(asset.name, latestVersion);
|
||||||
|
} catch (err) {
|
||||||
|
signaturHinweis = 'Die Signatur des Updates ließ sich gerade nicht abrufen (' + (err.message || err) + '). Bitte später erneut versuchen.';
|
||||||
|
}
|
||||||
|
if (!sig) {
|
||||||
|
asset = null;
|
||||||
|
unsigniert = !signaturHinweis;
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
available,
|
available,
|
||||||
currentVersion,
|
currentVersion,
|
||||||
latestVersion: String(latest.tag_name || '').replace(/^v/i, ''),
|
latestVersion,
|
||||||
installKind,
|
installKind,
|
||||||
|
unsigniert,
|
||||||
|
signaturHinweis,
|
||||||
notes: latest.body || '',
|
notes: latest.body || '',
|
||||||
published: latest.published_at || latest.created_at || null,
|
published: latest.published_at || latest.created_at || null,
|
||||||
asset: asset ? { name: asset.name, url: asset.browser_download_url, size: asset.size, kind: installKind } : null,
|
asset: asset ? {
|
||||||
|
name: asset.name, url: asset.browser_download_url, size: asset.size, kind: installKind,
|
||||||
|
version: latestVersion, signatur: sig,
|
||||||
|
} : null,
|
||||||
url: latest.html_url,
|
url: latest.html_url,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -222,6 +330,15 @@ async function downloadAndInstall(asset, onProgress) {
|
|||||||
// sonst kontrolliert sich die Angabe nur selbst.
|
// sonst kontrolliert sich die Angabe nur selbst.
|
||||||
assertMatchingKind(asset.name, installKind);
|
assertMatchingKind(asset.name, installKind);
|
||||||
|
|
||||||
|
if (!erlaubteAdresse(asset.url)) {
|
||||||
|
throw new Error('Das Update kommt nicht vom AeroMC-Server – abgebrochen.');
|
||||||
|
}
|
||||||
|
// ohne Signatur wird gar nicht erst heruntergeladen
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
|
||||||
// Portable-Updates ersetzen später die laufende EXE – das geht nur, wenn wir sie kennen
|
// Portable-Updates ersetzen später die laufende EXE – das geht nur, wenn wir sie kennen
|
||||||
const target = installKind === 'portable' ? portableTarget() : '';
|
const target = installKind === 'portable' ? portableTarget() : '';
|
||||||
if (installKind === 'portable' && !target) {
|
if (installKind === 'portable' && !target) {
|
||||||
@@ -286,8 +403,16 @@ async function downloadAndInstall(asset, onProgress) {
|
|||||||
throw new Error('Download unvollständig.');
|
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 });
|
if (onProgress) onProgress({ phase: 'ready', loaded, total: loaded, percent: 100 });
|
||||||
return { path: dest, bytes: loaded, sha256: hash.digest('hex'), kind: installKind, target };
|
return { path: dest, bytes: loaded, sha256, signatur, name: asset.name, version: asset.version, kind: installKind, target };
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -323,11 +448,13 @@ exit /b 1
|
|||||||
* portable -> Skript starten, das die laufende EXE nach dem Beenden ersetzt
|
* portable -> Skript starten, das die laufende EXE nach dem Beenden ersetzt
|
||||||
* Der Launcher muss sich danach beenden.
|
* Der Launcher muss sich danach beenden.
|
||||||
*/
|
*/
|
||||||
function applyUpdate(file) {
|
async function applyUpdate(file) {
|
||||||
const installKind = detectInstallKind();
|
const installKind = detectInstallKind();
|
||||||
// letzte Kontrolle unmittelbar vor dem Start: passt die Datei auf der Platte
|
// letzte Kontrolle unmittelbar vor dem Start: passt die Datei auf der Platte
|
||||||
// wirklich zu dieser Paketart?
|
// 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);
|
assertMatchingKind(path.basename(file.path || ''), installKind);
|
||||||
|
pruefeSignatur(file.signatur, file.name, file.version, await sha256Datei(file.path));
|
||||||
const kind = installKind;
|
const kind = installKind;
|
||||||
|
|
||||||
if (kind !== 'portable') {
|
if (kind !== 'portable') {
|
||||||
@@ -351,16 +478,14 @@ function applyUpdate(file) {
|
|||||||
return { started: 'swap', target };
|
return { started: 'swap', target };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rückwärtskompatibel: nur für die Setup-Variante
|
|
||||||
function runInstaller(installerPath) {
|
|
||||||
return applyUpdate({ path: installerPath, kind: 'setup' });
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
checkForUpdate,
|
checkForUpdate,
|
||||||
downloadAndInstall,
|
downloadAndInstall,
|
||||||
applyUpdate,
|
applyUpdate,
|
||||||
runInstaller,
|
pruefeSignatur,
|
||||||
|
signaturText,
|
||||||
|
holeSignatur,
|
||||||
|
SIGNATUR_API,
|
||||||
cleanupUpdateCache,
|
cleanupUpdateCache,
|
||||||
compareVersions,
|
compareVersions,
|
||||||
parseVersion,
|
parseVersion,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
/**
|
/**
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ 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');
|
||||||
@@ -20,13 +20,13 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -175,6 +175,7 @@ add_action('plugins_loaded', function () {
|
|||||||
|
|
||||||
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';
|
||||||
|
require_once plugin_dir_path(__FILE__) . 'includes/signaturen.php';
|
||||||
if (is_admin()) {
|
if (is_admin()) {
|
||||||
require_once plugin_dir_path(__FILE__) . 'includes/admin.php';
|
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>';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user