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
@@ -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>';
}