Three deliverables shipped together so the next deployment cycle has a
clean distribution story.
(1) Auto-update of the launcher itself
---------------------------------------
- Models: RemoteManifest gains an optional `launcher` section
(LauncherInfo: version, minRequired, download {url,size,sha256},
releaseNotesUrl). Server-side, sign-manifest.php passes it through
unchanged; admins edit versions.json with the new launcher entry +
upload PSLauncher-X.Y.Z.exe to /builds/launcher/.
- Core: ILauncherSelfUpdater compares assembly version against
manifest.launcher.version using the existing SemVer parser, and
reuses DownloadManager (Range/resume/sha256 already proven on the
game ZIPs) to download the new exe into
%LocalAppData%/PSLauncher/selfupdate/.
- New project PSLauncher.Updater (~34 MB self-contained console exe):
spawned by the main app with --target / --source / --pid / --launch.
Waits for the main process to exit (or for the file lock to release),
backs up the current exe to .bak, copies the new file in place, and
restarts. .bak survives the swap so the user can roll back manually.
- App.csproj now declares Version=0.5.0 — currently shipped baseline.
PSLauncher.App.csproj sets a fixed AssemblyVersion so reflection-based
comparison works deterministically.
- MainViewModel.PromptLauncherUpdate: dialog after CheckForUpdates if
the manifest advertises a newer launcher. Download with progress in
the existing footer, then Application.Shutdown() so the Updater can
do its job.
(2) Inno Setup installer
------------------------
installer/PSLauncher.iss + build-installer.ps1 produce a single
PSLauncher-Setup-X.Y.Z.exe (~80 MB) that installs into
Program Files\ASTERION VR\PSLauncher\, drops both PSLauncher.exe and
PSLauncher.Updater.exe side by side (the updater MUST live next to
the target), creates Start Menu + optional Desktop shortcuts, and
registers a clean uninstall entry. The user's %LocalAppData%
(license, logs, cache) is intentionally untouched on uninstall — same
license survives a reinstall.
build-installer.ps1 chains dotnet publish for both projects and ISCC
in one command. README explains the bump-version workflow.
(3) HMAC-signed download URLs
-----------------------------
- New PHP route GET /api/download-url/{version} (Authorization: Bearer
<licenseKey> or ?key=...). Validates the license, checks
download_entitlement_until >= minLicenseDate of the version, and
returns a HMAC-signed URL (path|exp|licId, hash_hmac SHA-256, valid
1 h) + sha256 + sizeBytes for verification.
- /builds/.htaccess routes every *.zip request to gate.php. gate.php
validates exp, lic, sig (constant-time hash_equals), then streams
the file with Range: support so the launcher's resume keeps working.
Audit log gets a download_url_issued entry per request.
- Client-side wired transparently: LicenseService gains
GetSignedDownloadUrlAsync(version) that GETs the endpoint with the
decrypted license key from DPAPI. MainViewModel calls it before
every download; if the endpoint returns 404/401/network-error, the
client falls back to the manifest's plain download.url (graceful
degradation for setups that haven't deployed gate.php yet).
Note on PHP streaming for 14 GB ZIPs: gate.php uses set_time_limit(0)
+ ignore_user_abort(true) + 1 MiB chunked fread with periodic flush.
Works on OVH mutualisé but holds a PHP-FPM slot for the duration. If
parallel downloads scale past a few clients, switch to
mod_xsendfile or migrate /builds/ to Cloudflare R2 with native
S3-presigned URLs and remove the gate entirely.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PS_Launcher — Côté serveur
Contenu de ce dossier à uploader sous www/PS_Launcher/ sur le mutualisé OVH.
Arborescence finale après upload
www/
└── PS_Launcher/
├── .htaccess
├── api/ ← API consommée par le launcher
│ ├── config.php ← À CRÉER (copie de config.example.php)
│ ├── index.php
│ ├── lib/{Response,Db,Crypto}.php
│ └── routes/{Manifest,Releasenotes,ValidateLicense}.php
├── admin/ ← Backoffice web (auth par mot de passe)
│ ├── .htaccess
│ ├── index.php (dashboard)
│ ├── login.php / logout.php
│ ├── licenses.php / versions.php / audit.php
│ ├── lib/{Auth,Layout}.php
│ └── assets/style.css
├── manifest/versions.json
├── releasenotes/1.4.X.md
├── builds/ ← ZIPs uploadés en SFTP
├── migrations/001_init.sql ← Schéma MySQL initial
└── tools/
├── generate-keypair.php
├── issue-license.php
└── sign-manifest.php
Setup initial (à faire une fois)
1. Crée la base MySQL
Manager OVH → Hébergements → Bases de données → Créer. Note le DSN, user, password.
2. Joue le schéma
PhpMyAdmin (manager OVH) ou en SSH :
mysql -h <host> -u <user> -p <db> < www/PS_Launcher/migrations/001_init.sql
3. Configure le serveur
- Copie
api/config.example.php→api/config.php - Remplis la section
db,base_url - Génère les clés Ed25519 :
Recopie
cd ~/www/PS_Launcher php tools/generate-keypair.phpprivate_key_hexetpublic_key_hexdansapi/config.phpsectioned25519. Recopie aussipublic_key_hexdanssrc/PSLauncher.Core/Resources/server-pubkey.txtcôté projet C# et recompile le launcher.
4. Configure le mot de passe admin
php -r "echo password_hash('TonMotDePasseFort', PASSWORD_DEFAULT);"
Recopie le hash dans api/config.php → admin_password_hash.
5. Test la chaîne
https://tondomaine.com/PS_Launcher/api/health → JSON status: ok
https://tondomaine.com/PS_Launcher/admin/login.php → page de connexion admin
Workflow de release (via le backoffice)
- Connecte-toi sur
https://tondomaine.com/PS_Launcher/admin/. - Onglet Versions → formulaire « Ajouter une version » : numéro, date min de license, release notes Markdown.
- Upload le ZIP correspondant via SFTP dans
www/PS_Launcher/builds/proserve-{version}.zip. - Bouton 🔁 Sync (sign-manifest) : calcule SHA-256, met à jour
latest, signe le manifest avec Ed25519. - Au prochain « Vérifier les MAJ » côté launcher, la nouvelle version apparaît.
Workflow de release (alternative manuelle SSH)
# 1. Édite manifest/versions.json (ou utilise le backoffice)
# 2. Upload le ZIP en SFTP dans builds/
# 3. Resigne :
cd ~/www/PS_Launcher
php tools/sign-manifest.php
Émettre une license
Via le backoffice (recommandé) : onglet Licenses → formulaire « Émettre ». La clé apparaît une seule fois — copie-la pour le client.
Via SSH :
php tools/issue-license.php "ACME Corp" 2027-12-31 1
Convention du contenu du ZIP
Le client extrait le ZIP dans installRoot/Proserve v{version}/. Le ZIP peut soit contenir
un dossier racine Proserve v{version}/, soit le contenu directement — le launcher détecte
automatiquement le préfixe commun et le strippe.
Test API depuis ton poste
curl.exe https://tondomaine.com/PS_Launcher/api/health
curl.exe https://tondomaine.com/PS_Launcher/api/manifest
curl.exe https://tondomaine.com/PS_Launcher/api/releasenotes/1.4.6
Sécurité
api/config.phpest gitignored. Ne jamais le commit.- Toutes les URLs
/api/*forcent HTTPS via.htaccess. - Les routes /api/* renvoient toujours du JSON (pas de page Apache 500 HTML).
- L'admin est protégé par session + CSRF + mot de passe bcrypt.
- Les licenses sont stockées via UNIQUE en clair (pour la lookup constant-time côté serveur), mais ne sont jamais loguées en clair côté audit.
- Côté client : la clé license est chiffrée DPAPI scope CurrentUser dans
%LocalAppData%. - Les réponses serveur (manifest, validation license) sont signées Ed25519 — le launcher embarque la clé publique et refuse toute réponse mal signée.