ff05edbe7efef7f1513ddd37ef15e69ce3b9b9e0
16 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 500f7d12e6 |
v0.29.10 — Admin UI refonte (modals + onglets), emails de release, 4-digit versions
Admin backoffice — UI redesign : - Pages versions.php + licenses.php : remplace les <details> inline qui débordaient horizontalement par UN bouton « ✎ Modifier » par row qui ouvre une modal <dialog> avec onglets. 5 tabs versions (Méta, Notes, BÊTA, Channels, Avancé) + 6 tabs licenses (Prolonger, Slots, Channel, BÊTA, Lock, Contacts, Machines). Délégation JS unique pour les onglets. - Bouton 📋 Copier la clé license dans chaque row (Clipboard API + feedback visuel ✓ vert 1.5s). Évite le détour par phpMyAdmin pour transmettre la clé aux clients. - Overlay « hashing en cours » plein écran sur tous les boutons de hash (3-5 min sur OVH pour 13 Go ZIP). Spinner CSS + message contextualisé par scope (bulk vs single). - Date de release passée de datetime-local à date (l'heure n'a pas de sens UX), avec défaut = aujourd'hui pour release_at et aujourd'hui-1an pour min_license_date (= license standard couvre les releases sur 1 an). Emails de notification release : - Migration 004 : colonne contact_emails TEXT NULL sur licenses (CSV) - Onglet « Contacts » sur la modal licenses pour saisir les emails par license (parsing tolérant : CSV, ligne par ligne, point-virgule) - Bouton « ✉ Notifier » par version : POST notify_release filtre les licenses éligibles (channel match + min_license_date + can_see_betas pour BÊTA) et envoie un email HTML à chaque contact (dédup global) - Template email table-based + bgcolor (compat Outlook/Word engine), navy foncé #0F172A, logo Asterion en CID embed (= affichage direct sans demande de permission Outlook), bouton download installer centré (align="center" + margin auto), release notes en <pre> - Mailer.php helper : parse emails, multipart/related avec attachments inline, fallback execCommand pour clipboard 4-digit version support (X.Y.Z.B) : - SemVer Parse/CompareTo/ToString gèrent 3 ou 4 digits ; Build absent = 0 implicite (1.5.4 == 1.5.4.0 < 1.5.4.13). HasExplicitBuild préserve le format d'origine au round-trip. - Regex InstallationRegistry étendue avec (?:\.\d+)? → reconnaît « PROSERVE v1.5.4.13 » côte-à-côte avec « PROSERVE v1.5.4 » sur disque - Server-side : versions.php, launcher.php, DownloadUrl.php, api/index.php, Releasenotes.php — toutes les regex de validation acceptent le 4ᵉ digit - Use case : dev/test iterations cohabitant avec leur release stable Bugs fixes : - migrate.php : strip ligne par ligne les commentaires SQL avant le check is-empty. Sans ça, le PREMIER chunk d'un fichier migration (= header + premier ALTER) commençait par `--` et était silencieusement skip → ALTER jamais appliqué. Affectait migrations 003, 004. - SignManifest::getOrComputeSha256 ignorait son cache interne quand force demandé par le caller, retournant l'ancien hash en 0 ms même après re-upload SFTP (avec mtime préservé). Propage maintenant le flag $force. Bouton « 🔁 Hash » per-row force maintenant un re-calcul systématique. - DownloadManager : 416 (Range Not Satisfiable) ajouté aux URL-refresh triggers, avec HEAD probe pour comparer taille serveur vs manifest → message d'erreur explicite si ZIP tronqué. Bps display lissé sur une fenêtre glissante de 12 samples (3 s) → plus de clignotement quand un segment finit / Polly retry. SHA mismatch popup enrichi avec les deux SHAs (attendu vs calculé) extraits via regex de l'exception. - DownloadUrl.php : signature de l'URL utilisait un template hardcodé /builds/proserve-{version}.zip, ignorant tout rename serveur. Lit maintenant download.url du manifest et signe le filename réel. Strings i18n (5 langues) : - ~15 nouveaux : SHA mismatch enrichi avec sources, 416 size mismatch, stale manifest, manifest refreshed auto-retry, force fresh menu Bumps : 0.29.7 → 0.29.10 (4-digit support + accumulated UI fixes). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 50da755a92 |
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>
|
|||
| efb53f079e |
v0.28.10 — Mode auto + settings lock par license + fix post-install state
Mode auto (auto-launch d'une version désignée au démarrage et après exit) : - Master toggle + version désignée via bouton AUTO (orange) sur chaque row - Countdown 5 s configurable avec popup d'annulation (StartAutoLaunchCountdown) - Args CLI passés à PROSERVE : opérateur tape juste le nom (autoconnect, login), le launcher ajoute -/=/quotes. Format produit identique à un .bat. Fix : passage de ArgumentList à Arguments string pour éviter le quotage auto .NET sur les args contenant '=' (cassait FParse::Value). - Garde-fou anti double-lancement : refuse de lancer si _runningProserve vivant OU IProcessLauncher.IsRunning() détecte une instance externe. Silent pour les triggers auto, popup pour les clics manuels. - Option « Attendre santé système » : countdown différé tant que les health checks ne sont pas tous OK (phase 1 du dialog avec liste des pending). Settings lock par license (remplace l'ancien lock global manifest) : - Colonne settings_lock_password_hash sur table licenses (migration 003) - Backoffice : bouton 🔒 par license pour set/clear hash SHA-256 - Payload /license/validate inclut settingsLockPasswordHash (v3 canonique) - 3-tier fallback signature : v3 → v2 → legacy pour compat cache offline - SettingsLockService : in-memory unlock state, gate l'expander Avancés - SettingsLockDialog : prompt mdp, persiste déverrouillé jusqu'au restart Fix post-install : la row restait en visuel « Installing » jusqu'au prochain Check Updates. Reset explicite row.State=InstalledIdle + _activeRow=null AVANT le RebuildList post-install pour purger l'instance orpheline. Migrations : - 002_channel_betas.sql réécrit en ALTER simples (DELIMITER cassait migrate.php qui split sur ';\n') - migrate.php tolère « Duplicate column/key name » comme idempotent Strings (5 langues, ~25 nouvelles) : - Renomme « Relance automatique » → « Lancement automatique » (dialog utilisé pour les 3 entry points : clic, startup, post-exit) - Tooltips auto-mode, settings lock prompts, health wait phase, etc. Bumps : 0.27.4 → 0.28.10 (csproj + .iss). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 67678fe173 |
Manifest : dédup par version, l'entry channel-spécifique gagne sur default
Cas reporté : un client firefighter recevait DEUX entries v1.5.3 (default + firefighter) parce que la sémantique additive est correcte pour la visibilité, mais ne dédupe pas. Le launcher faisait alors un ToDictionary(v => v.Version) qui collisionne sur "1.5.3" et garde silencieusement le premier (la default), donc le mauvais ZIP s'affichait dans la UI. Fix : Manifest.php groupe par version après le filtre, et pour chaque groupe à plusieurs entries pick l'entry la plus spécifique au client : 1. priorité à celle taggée avec le channel propre du client (firefighter) 2. sinon l'entry default sert de fallback Sémantique pour un user firefighter : - v1.5.3 (default) + v1.5.3 (firefighter) → renvoie uniquement firefighter - v1.4.0 (default uniquement) → renvoie default (visible) - v1.5.3 (firefighter uniquement) → renvoie firefighter Fix purement server-side, pas de modif client requise. Le launcher recevra naturellement un seul ZIP par version, le bon. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 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> |
|||
| 72f99e0c56 |
v0.27.1 — Versions : permettre numéro identique sur channels différents
Cas d'usage rapporté : « je peux avoir une version 1.5.3 pour les
pompiers et une version 1.5.3 pour la police ». Le check anti-doublon
sur le numéro de version bloquait la création — c'était un faux check
puisque la duplication SUR DES CHANNELS DIFFÉRENTS est légitime.
Changements :
1. ID stable interne : chaque entry de version reçoit un `id` immutable
généré à la création (`v` + 8 hex aléatoires). Ce id devient l'identifiant
primaire dans les forms et URLs admin. Le numéro de version reste un
libellé human-readable, possiblement dupliqué.
2. Migration douce : loadManifest() ajoute un id aux entrées qui n'en
ont pas, et auto-save pour persister. Stable aux reloads suivants.
Les manifests legacy fonctionnent sans aucune action manuelle.
3. Check anti-doublon déplacé sur le NOM DE ZIP au lieu du numéro de
version. Deux entries v1.5.3 sont autorisées si elles pointent vers
des ZIPs différents (proserve-1.5.3-police.zip + proserve-1.5.3-pompier.zip).
Une vraie ambiguïté sur disque (deux entries → même ZIP) est rejetée.
4. Toutes les actions row-level (edit_meta, edit_notes, set_beta,
set_channels, toggle_available, delete, set_skip_hash, sync_one)
identifient l'entry par `id` au lieu de `version`. Forms HTML mis
à jour en conséquence.
5. Release notes : addressables par id stable. Stockées en
releasenotes/{id}.md. Compat ascendante : si un fichier {id}.md
n'existe pas, l'admin lit le legacy {version}.md (pour les notes
créées avant v0.27.1). L'endpoint API releasenotes/{key} accepte
les deux formats.
6. Suppression d'une entry : nettoie le fichier {id}.md correspondant.
7. SignManifest::run filtre toujours par version string : si plusieurs
entries partagent un numéro de version, toutes sont re-hashées via
« 🔁 Hash » sur une row. Comportement correct (chaque entry a sa
propre URL/ZIP, donc son hash spécifique), juste moins efficient.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 49a3b855af |
v0.27.0 — Channels comme TAGS sur versions (refactor du modèle)
Changement majeur du modèle de channels en réponse au feedback :
« c'est trop compliqué. Il faudrait juste pouvoir tout laisser dans
builds, et taguer une version dans un canal ou un autre voir plusieurs
canaux. ». Le précédent design (1 fichier manifest par channel +
sous-dossier builds/{channel}/) imposait une duplication de structure
et empêchait une version d'être visible sur plusieurs channels à la fois.
NOUVEAU MODÈLE :
- 1 seul fichier manifest/versions.json
- 1 seul dossier builds/ flat (les ZIPs ont des noms libres pour éviter
les collisions homonymes — proserve-1.5.3.zip, proserve-1.5.3-police.zip…)
- Chaque entry de version a un champ `channels: ["default", "police"]`
qui dit qui peut la voir
- Sémantique additive : un client sur channel "police" voit les versions
taggées "default" + celles taggées "police". "default" = public.
- Une version peut être taggée sur plusieurs channels en une fois.
NOUVEAU REGISTRE channels.json :
Le registre des channels (avec name + label + description) vit dans
manifest/channels.json. Le channel "default" est implicite et ne peut
pas être supprimé. Nouvelle page admin Channels (CRUD) pour gérer la
liste, avec garde-fou anti-suppression : compte les versions taggées
et les licenses attribuées avant d'autoriser la suppression.
SERVEUR :
- api/routes/Manifest.php : reçoit ?channel=X depuis la license,
filtre versions où channels[] contient X ou "default", re-signe avec
la clé privée Ed25519 à la volée. Coût ~1ms par requête.
- admin/versions.php : refactor — plus de session "channel actif", plus
de switcher en haut, plus de prefix builds/{channel}/. Add form a
des checkboxes channels[]. Nouveau bouton "Channels" par-row pour
retager.
- admin/licenses.php : dropdown channel alimenté depuis channels.json
au lieu de scanner les fichiers manifest.
- tools/SignManifest.php : revert du constructeur channel-aware,
toujours sur versions.json + builds/ flat.
CLIENT :
- VersionManifest.Channels (List<string>) ajouté pour debug, mais le
filtrage est server-side donc le client n'a rien à faire de ce champ
côté UX.
- L'envoi du ?channel=X depuis la license signée fonctionne déjà
depuis v0.26.0, pas de changement nécessaire.
MIGRATION :
Les fichiers manifest/versions-{X}.json créés en v0.26.0 deviennent
inutiles. Si tu en avais (test "police" par exemple), copie les
versions concernées dans versions.json et tague-les avec les bons
channels via la nouvelle UI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| c371a79f93 |
v0.26.0 — Channels per-license + tag BÊTA sur versions
DEUX features liées :
1. CHANNELS : chaque license peut être attribuée à un manifest distinct
(« channel »). Permet de servir des versions différentes selon le
client. Le serveur lit ?channel=X et sert manifest/versions-{X}.json
avec fallback transparent sur versions.json. NULL = default.
2. BÊTA : nouveau flag isBeta + betaNotes par version dans le manifest.
Visible uniquement par les licenses avec can_see_betas=1. Affichage
d'une pill orange « BÊTA » sur la row + tooltip avec les notes pour
les testeurs. Les installations locales déjà présentes restent
visibles même si l'accès BÊTA est retiré ensuite (on n'efface pas
le disque du client).
DB :
002_channel_betas.sql ajoute channel + can_see_betas sur licenses.
Idempotent (ALTER TABLE IF NOT EXISTS), zero data migration.
Serveur PHP :
- ValidateLicense.php signe channel + canSeeBetas dans la réponse
(ordre des clés CRITIQUE pour matcher le canonical client).
- Manifest.php : whitelist regex anti-traversal sur ?channel=, fallback
silencieux sur versions.json si channel inconnu (évite leak de la
liste de channels par probing).
- SignManifest.php prend un channel optionnel → l'admin peut signer
chaque manifest indépendamment.
- admin/licenses.php : dropdown channel + checkbox bêta sur create,
bouton détails repliable par-row pour edit.
- admin/versions.php : channel switcher en tête, badge BÊTA sur chaque
row, dialog repliable « Bêta » avec checkbox + notes des testeurs.
Client C# :
- License.Channel + License.CanSeeBetas (dans le canonical signé).
- VersionManifest.IsBeta + BetaNotes.
- ManifestService prend un channelProvider via DI, lu depuis license
cachée à chaque fetch (lazy, pas de circular dep).
- MainViewModel.RebuildList filtre les versions IsBeta si !CanSeeBetas
(mais conserve les installées locales — on ne retire pas l'accès
rétroactivement à ce qui est déjà sur disque).
- VersionRowViewModel : props IsBeta / BetaNotes / BetaTooltip.
- MainWindow.xaml : pill orange à côté du n° version pour le featured
et les rows compactes, tooltip dynamique avec les notes testeurs.
Backward compat signature :
Anciennes licenses cachées (signées sans channel/canSeeBetas) sont
toujours validées via un fallback canonical legacy dans VerifySignature.
Sans ce fallback, le passage à v0.26 invaliderait toutes les caches
hors-ligne et bloquerait les users en mobilité.
Migration côté admin : jouer 002_channel_betas.sql sur la base, déployer
les fichiers PHP, créer manifest/versions-{channel}.json pour les
nouveaux channels (l'admin versions.php propose un input « Créer/utiliser
un nouveau channel »). Les licenses existantes restent en channel=NULL
= default = comportement actuel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 962f5a8ce0 |
Server: faster releases (cache hashes), per-version skip, longer signed-URL TTL
- gate.php : ETag now derived from size+mtime instead of md5_file($zip). Previously the gate hashed the entire 14 GB ZIP on every HEAD/GET call (including each of 8 parallel segments) → 30s-5min preparation lag for clients before the first byte. Now <1ms per request. - SignManifest : disk-backed cache for SHA-256 keyed by (path,size,mtime). Re-signing 5×14 GB versions used to take ~25 min, now ~1s when nothing changed. New "Force re-hash" toggle in admin to ignore the cache. - versions.php : per-row "🔁 Hash" button to sign a single version, plus a "⚙ Hash" dropdown to toggle hashAlgorithm:none for builds where the user accepts skipping client-side verification (manifest stays signed Ed25519, only the per-ZIP SHA-256 verification is bypassed). - DownloadUrl.php : signed-URL TTL bumped 1h → 6h to cover slow ADSL users who need >1h to finish a 14 GB download. - .gitignore : track server/builds/.htaccess + gate.php (still ignore the actual ZIP/exe binaries). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 466755ddc2 |
DownloadUrl: tolerate Apache FastCGI stripping the Authorization header
OVH mutualisé serves PHP via FastCGI which strips the Authorization
header by default for security. The user's curl test of GET
/api/download-url/{ver} returned 401 even with -H "Authorization:
Bearer <key>" because $_SERVER['HTTP_AUTHORIZATION'] was empty server-side.
Fix on two layers:
1. .htaccess at root re-injects the header into the Apache env so PHP
can read it via the standard $_SERVER variable. The mod_rewrite
one-liner `RewriteRule ^ - [E=HTTP_AUTHORIZATION:%1]` is the de-
facto FastCGI workaround.
2. DownloadUrl.php now reads from any of: $_SERVER['HTTP_AUTHORIZATION'],
$_SERVER['REDIRECT_HTTP_AUTHORIZATION'] (passthrough variant),
apache_request_headers(), getallheaders(). Belt and braces — works
regardless of host config. Falls back to ?key= as before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 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>
|
|||
| 1a3910ef9d |
license: force UTC + fixed-Z date format on both sides
Symptom: "Réponse serveur non authentifiée (signature invalide)" on
license activation. Root cause: PHP \DateTimeInterface::ATOM emits the
server's local timezone offset (e.g. +02:00 on a CET host), while the
C# canonicalizer emitted +00:00 after ToUniversalTime(). Same instant,
different string, different bytes, signature mismatch.
Server (ValidateLicense.php):
- All timestamps now built with `new DateTimeZone('UTC')` and formatted
as 'Y-m-d\TH:i:s\Z' — fixed string, no offset variation.
- Reads issued_at / download_entitlement_until from MySQL as UTC; the
display is consistent with what the client sees.
Client (LicenseService.cs):
- FormatDateAtom now produces "yyyy-MM-ddTHH:mm:ssZ" with literal Z and
handles null safely (previous version would have produced "Z" alone
for a null input thanks to string + null concatenation).
Both sides therefore agree on the canonical bytes for any datetime,
including across daylight savings transitions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 92f4fd16e8 |
Backoffice: PHP admin web UI for licenses, versions, audit
Self-contained admin under /PS_Launcher/admin/ on the same OVH host. No
JS framework, no Composer deps — just PHP 8.3 + sessions + a small CSS.
Auth & infrastructure
---------------------
- admin/lib/Auth.php: session + CSRF helper. Login via password_hash /
password_verify. session_regenerate_id on successful login.
config.example.php gains admin_password_hash, generated by:
php -r "echo password_hash('PWD', PASSWORD_DEFAULT);"
- admin/lib/Layout.php: shared header/footer/nav, formatBytes,
csrfField helpers.
- admin/.htaccess: noindex / X-Frame-Options DENY / X-Content-Type
nosniff / blocks lib/*.php from web access.
- admin/assets/style.css: matches launcher's dark theme — same
Brush.* palette mapped to CSS vars, vivid green/blue/amber status
pills consistent with the WPF UI.
Pages
-----
- index.php (Dashboard): KPIs (active/expired/revoked licenses, machines
seen 30d, validations 24h), manifest signature status, last 10
audit_log entries.
- licenses.php: full CRUD.
* Émettre: owner, expiration date, max machines, internal notes →
generates a PRSRV-XXXX-XXXX-XXXX-XXXX key, displays it ONCE in a
green callout (DB stores the key; the message stays only on this
request, never shown again).
* Prolonger (per-row, expandable form), Revoke / Unrevoke,
Reset machines (frees all slots for that license).
* Status badge: active / expired / revoked.
- versions.php: edit the manifest from the web.
* Add a version: number + release date + minLicenseDate + release
notes Markdown (creates releasenotes/{version}.md). Sets default
download URL to {base_url}/builds/proserve-{version}.zip.
* Per-row Méta (edit minLicenseDate / releasedAt), Notes (edit md
inline), toggle availableForDownload, Delete entry.
* 🔁 Sync (sign-manifest) button: shells out to
`php tools/sign-manifest.php` and shows its stdout — recomputes
sha256/sizeBytes for every uploaded ZIP, bumps `latest`, signs
Ed25519. Visual indicators on each row: zip presence, hash
computed yes/no, signature status.
* Lists orphan ZIPs in builds/ that no manifest entry references.
- audit.php: paginated audit_log viewer (100/page) with event-type
filter dropdown. JOINs licenses to show owner_name. Color-codes
events (validate_ok green, expired amber, invalid/revoked red).
Server README rewritten to document the full setup flow:
1. Create MySQL DB, run migrations/001_init.sql
2. Copy config.example.php → config.php, fill db credentials
3. php tools/generate-keypair.php → paste into config.php and into the
client's Resources/server-pubkey.txt
4. Set admin_password_hash in config.php
5. Login at /PS_Launcher/admin/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 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>
|
|||
| 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> |
|||
| 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>
|