Commit Graph

5 Commits

Author SHA1 Message Date
38194c6f0c Fix : require Crypto.php pour la route manifest + accepter id v8hex pour releasenotes
Deux bugs introduits par v0.27.x dans api/index.php :

1. La route /api/manifest ne require pas lib/Crypto.php → erreur 500
   « Class PSLauncher\Crypto not found » dès qu'on tente la re-signature
   du manifest filtré (depuis v0.27.0). Conséquence : le client voit
   tout cassé, pas seulement les versions filtrées.

2. La regex de route /api/releasenotes/X.Y.Z n'accepte que les versions
   au format SemVer. Avec v0.27.1, les release notes sont addressables
   par id stable (v + 8 hex chars), donc le manifest pointe vers
   /api/releasenotes/va3f2c891 qui matchait pas la regex → 404.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 09:12:55 +02:00
b7de228bc9 v0.6 + v1.0: HMAC download URLs, launcher self-update, Inno Setup installer
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>
2026-05-02 09:38:13 +02:00
7a29dbb049 v0.4: licensing — server validation, DPAPI cache, Ed25519 signed responses
UX bonus: clicking "Reprendre" on an interrupted download skips the
release-notes confirmation dialog (the user already approved when they
first clicked Installer).

Server side
-----------
- migrations/001_init.sql: licenses, license_machines, rate_limit,
  audit_log on InnoDB/utf8mb4. Foreign keys, unique on license_key, slot
  uniqueness per (license_id, machine_id).
- api/lib/Db.php: thin PDO singleton (exception mode, prepared, no emul).
- api/lib/Crypto.php: Ed25519 sign/verify via libsodium (sodium_crypto_*),
  HMAC-SHA-256 helper for v0.6, canonicalJson() that strips `signature`
  before serializing — must match exactly the encoding done client-side
  before verify.
- api/routes/ValidateLicense.php: POST /license/validate. Looks up the
  key, walks the machine slot logic (insert or update last_seen),
  enforces max_machines, returns a payload signed Ed25519 + status of
  valid/expired/revoked/machine_limit_exceeded/invalid. Audit logs every
  outcome. Rate-limit 10/min/IP via the rate_limit table.
- tools/generate-keypair.php: prints a fresh sodium keypair so the
  operator drops the hex into config.php and the public_key_hex into the
  launcher resource.
- tools/issue-license.php: PRSRV-XXXX-XXXX-XXXX-XXXX generator (32-char
  unambiguous alphabet), inserts the license, prints the key once.
- tools/sign-manifest.php: now also signs the manifest itself with
  Ed25519 after computing the per-zip sha256s.
- config.example.php: schema rewritten with sections db / hmac / ed25519
  / jwt / rate-limit. config.php remains gitignored.

Client side
-----------
- Models/License.cs: LicenseValidationRequest + LicenseValidationResponse
  with CanDownload(VersionManifest) — entitlement_until vs version's
  minLicenseDate. The status valid|expired|revoked|machine_limit_exceeded
  flow is preserved end-to-end.
- Core/Licensing/LicenseService.cs:
  * machineId = SHA-256 of HKLM/Software/Microsoft/Cryptography/MachineGuid
    + UserName (stable, no PII leak)
  * online ValidateAsync calls /license/validate with launcher version
  * embedded server-pubkey.txt drives Ed25519 verification of the
    response (skipped gracefully if pubkey not yet provisioned)
  * SaveCached / GetCached use DPAPI CurrentUser scope on the license
    key; the cleartext key never touches disk
  * GetCached has a 7-day offline grace window after the last successful
    validation, so going offline doesn't lock the user out
- Core/Resources/server-pubkey.txt: EmbeddedResource. Default content is
  a comment, which the service treats as "no pubkey configured" and
  bypasses verification. Operator pastes the real hex post-deploy and
  rebuilds.
- Core/PSLauncher.Core.csproj: Polly, NSec.Cryptography (Ed25519),
  System.Security.Cryptography.ProtectedData (DPAPI).
- App/Views/OnboardingDialog.xaml(.cs): first-launch / "🔑 Activer"
  modal. Calls LicenseService, displays status messages with red
  foreground on errors and green-tinted secondary text otherwise.
- ViewModels/VersionRowViewModel.cs: new LicenseAllowsDownload property.
  Install button label switches to "🔒 License insuffisante" when the
  user's entitlement_until precedes the version's minLicenseDate;
  CanInstall is false in that case so the click is a no-op too.
- ViewModels/MainViewModel.cs: loads the cached license at startup (no
  network call), surfaces it as LicenseSummary in the top bar, exposes
  ActivateLicenseCommand to (re)open the onboarding dialog. RebuildList
  applies the per-version license filter so older installed versions
  remain launchable but newer-than-license ones can't be downloaded.
- Views/MainWindow.xaml: top bar gains a "🔑 Activer / changer" button
  next to the license summary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 10:12:37 +02:00
c501115612 Server: portable rewrite + JSON-only error handler
The previous .htaccess used PATH_INFO (api/index.php/$1) which OVH mutualisé
does not always allow, producing an Apache 500 HTML page that masked our own
JSON error reporting.

Switch to query-string routing (?route=...): same effect, works everywhere.
Add a global exception handler in index.php that emits JSON errors only —
no more opaque Apache 500.

Add a /api/debug endpoint that reports PHP version, sodium availability,
pdo_mysql, and whether versions.json is found. Useful for diagnosing
shared-hosting setup before adding license logic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 09:07:22 +02:00
1c8c6803e8 Initial scaffolding: PS_Launcher v0.2
Client (C# / .NET 8 / WPF, MVVM via CommunityToolkit.Mvvm):
- PSLauncher.App: WPF UI dark theme (Epic-style sidebar + hero + big play button)
  with UpdateAvailableDialog rendering Markdown release notes via Markdig.Wpf.
- PSLauncher.Core: services for installation registry (scans Proserve v{X.Y.Z}/),
  process launcher, manifest fetch, SHA-256 integrity, HTTP download with
  progress, ZIP install via .tmp + atomic rename, update orchestrator.
- PSLauncher.Models: RemoteManifest, InstalledVersion, LocalConfig DTOs.

Server (PHP 8 for OVH mutualisé, deployed under www/PS_Launcher/):
- Front controller + routes /manifest and /releasenotes/{version}.
- Static signed-manifest workflow with tools/sign-manifest.php CLI to
  recompute SHA-256 and sizeBytes after each ZIP upload.
- .htaccess: HTTPS redirect, rewrite, security headers.
- config.example.php template; real config.php is gitignored.

Cohabiting versions: each release lives in its own Proserve v{version}/ folder
under installRoot. Old versions are never deleted automatically.

Roadmap: v0.3 = HTTP Range resume + Polly retry + state.json,
v0.4 = MySQL license + Ed25519 signatures + DPAPI, v0.5 = settings/UX polish.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 08:54:45 +02:00