v0.29.7 — SteamVR merge + résilience DL (404 retry, force fresh, SHA purge)

SteamVR + Vive Business Streaming settings merge :
 - Nouveau ISteamVrSettingsDeployer : si _steamvr/steamvr.vrsettings est
   dans le ZIP, deep-merge récursif des blocs racine dans le fichier user
   (clé absente → ajout, deux objets → recurse, sinon → replace ; les
   sous-clés cible non touchées sont préservées — crucial pour "trackers")
 - Auto-localisation via registre Steam (HKCU/HKLM) + fallback Program Files
 - Phase pré-check (CheckMergeNeededAsync) : skip silencieux si la cible
   est déjà à jour (deep equality) → pas de kill SteamVR/VBS inutile, pas
   de popup à l'opérateur. Si changements nécessaires → popup OK/Annuler
   avec nombre de blocs qui changeront + liste des process à fermer.
 - Liste de kill construite depuis les health checks de type "Process"
   (l'opérateur connaît déjà sa stack VR) + guardians (Vive Business
   Streaming en tête car il relance SteamVR auto)
 - Settings UI : section dédiée avec opt-in, override path, liste process
   éditable + affichage du path canonique attendu

Résilience téléchargements :
 - 404 mid-DL traité comme 403/410 (URL refresh trigger) au niveau segment :
   on extrait le nouveau filename via /download-url server-side, retry transparent
 - Auto-retry install une fois après refresh manifest sur 404 : l'opérateur
   ne voit rien si la cause était un manifest local stale
 - Si retry échoue aussi en 404 → message ciblé "manifest serveur stale"
   (problème côté serveur, pas client)
 - Sur SHA-256 mismatch : auto-purge du cache LAN local (.zip + .sha256)
   + message d'erreur dédié avec source du DL (peer ou OVH) + nouveau menu
   "↻ Forcer re-téléchargement" pour purger état + cache manuellement
 - Détection client-side de l'incohérence "signed URL filename != manifest
   URL filename" avant DL : abort immédiat plutôt que 14 Go pour rien
 - IZipCacheStore.InvalidateAsync : nouvelle API pour purger un cache par
   version (utilisée par SHA mismatch handler + Force Fresh menu)

Bug serveur (DownloadUrl.php) :
 - L'endpoint construisait l'URL signée avec un template hardcodé
   `/builds/proserve-{version}.zip`, ignorant complètement
   download.url du manifest. Conséquence : un opérateur qui rename
   son ZIP pour buster le cache CDN OVH (ex. proserve-full-1.5.4.zip)
   voyait toutes ses releases retournent du 404 silencieux côté client.
 - Fix : on lit basename(parse_url(entry.download.url).path), whitelist
   sur le filename, vérif is_file() avant de signer, erreur 500 explicite
   si manifest et filesystem désynchros.

Strings (5 langues) :
 - ~20 nouvelles : SteamVR popups + status, SHA mismatch dialog + source
   labels, force fresh confirm + menu, manifest stale + auto-retry status

Bumps : 0.29.6 (déployé hors-commit pendant la session) → 0.29.7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 14:45:01 +02:00
parent 01793eb32d
commit 50da755a92
21 changed files with 1768 additions and 27 deletions

View File

@@ -95,6 +95,46 @@ final class DownloadUrl
]);
}
// Extrait le NOM DE FICHIER depuis l'URL du manifest (= source de vérité).
// Avant : on utilisait un template hardcodé `proserve-{version}.zip`,
// ce qui ignorait toute personnalisation du nom (ex. opérateur qui rename
// en `proserve-full-1.5.4.zip` pour buster un cache CDN). Conséquence :
// l'endpoint retournait une URL signée pointant vers un fichier inexistant
// → 404 systématique côté client sans aucune indication serveur.
// Maintenant : on lit `download.url` du manifest, on extrait le filename
// via parse_url + basename, on vérifie qu'il existe physiquement dans
// /builds/, et seulement après on signe.
$manifestUrl = $entry['download']['url'] ?? '';
if ($manifestUrl === '') {
Response::error('manifest_incomplete',
"L'entrée manifest pour v{$version} est sans download.url", 500);
}
$parsedUrl = parse_url($manifestUrl);
$urlPath = $parsedUrl['path'] ?? '';
$filename = basename($urlPath);
// Whitelist défensive sur le filename : caractères safe + suffixe .zip.
// Évite path traversal et autres injections via un manifest corrompu.
if ($filename === '' || !preg_match('/^[a-zA-Z0-9._-]+\.zip$/', $filename)) {
Response::error('manifest_invalid_filename',
"Nom de fichier invalide extrait du manifest pour v{$version} : '{$filename}'", 500);
}
// Vérifie que le fichier existe physiquement avant de signer une URL morte.
// Cas concret de bug remonté côté client : l'opérateur rename le ZIP dans
// /builds/ mais oublie de mettre à jour versions.json (ou inverse). Au
// lieu d'envoyer le client en 404 silencieux, on retourne une erreur
// serveur claire qui apparaît dans les logs PHP + client.
$buildsDir = dirname(__DIR__, 2) . '/builds';
$physicalPath = $buildsDir . '/' . $filename;
if (!is_file($physicalPath)) {
Response::error('file_missing',
"Le fichier ZIP « {$filename} » référencé par le manifest pour v{$version} est absent du dossier /builds/. Vérifie que le manifest et le filesystem sont synchros.",
500,
[
'manifestUrl' => $manifestUrl,
'expectedPath' => $physicalPath,
]);
}
// Génère l'URL HMAC-signée
// TTL : 6 h. Compromis entre :
// - sécurité (limite la fenêtre de replay si une URL fuit)
@@ -102,7 +142,7 @@ final class DownloadUrl
// Pour une connexion plus lente, le client sait auto-refresher l'URL
// pendant le DL (cf DownloadManager → 403 retry avec nouvelle URL).
$baseUrl = rtrim($config['base_url'], '/');
$relPath = '/builds/proserve-' . $version . '.zip';
$relPath = '/builds/' . $filename;
$exp = time() + 21600; // 6 h
$secret = $config['hmac_secret'] ?? '';
if ($secret === '') {