From daa4068784a52a51e41cc57f6189d2d141ba7ca9 Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Mon, 6 Jul 2026 21:46:12 -0400 Subject: [PATCH] fix(control-surface): reserve loopback URL ACL at install time The control surface uses HttpListener (http.sys), which denies prefix registration to standard users -- on every non-Administrator install the REST/WebSocket surface died at startup with Access is denied (5), taking the /ui phone panel and Companion integration with it. The MSI runs elevated, so it now reserves http://127.0.0.1:9755/ on install and deletes the reservation on uninstall. Deliberately NOT the strong wildcard: verified empirically that an idle http://+:9755/ RESERVATION makes http.sys answer 503 for every request to the loopback-bound listener without the request ever reaching the app. The in-app failure guidance suggested exactly that reservation; corrected to recommend the bound prefix and warn about the stale-wildcard trap. Verified on a clean install: urlacl present, GET / and /ui and /participants all 200 as a standard user; reservation removed on uninstall. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 7 +++ installer/Dragon-ISO.Installer.wixproj | 2 + installer/Package.wxs | 57 +++++++++++++++++++ .../Services/ControlSurfaceServer.cs | 24 +++++--- 4 files changed, 82 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73fc00d..9d9cade 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,13 @@ Beta pre-release for the framerate and reliability work since 1.1.0. - **MSI ProductVersion now tracks the build version** — it was hardcoded to 1.0.0.0, so in-place upgrades (MajorUpgrade) never saw a newer version. Upgrading from an earlier install may require a manual uninstall once. +- **Control surface works out of the box for standard users** — the installer + now reserves the http.sys URL namespace for `http://127.0.0.1:9755/` + (removed again on uninstall). Previously the REST/WebSocket control surface + failed with "Access is denied" on every non-Administrator launch. Note for + LAN-reachable mode: it still needs its own `http://+:9755/` reservation + (the app logs the exact command), and that reservation must be deleted when + reverting to loopback-only — while idle it makes http.sys 503 every request. - **CAB embedded in the MSI** — the installer is a single self-contained file again; distributing only the MSI no longer fails with error 1302. diff --git a/installer/Dragon-ISO.Installer.wixproj b/installer/Dragon-ISO.Installer.wixproj index e286bfe..477bfbb 100644 --- a/installer/Dragon-ISO.Installer.wixproj +++ b/installer/Dragon-ISO.Installer.wixproj @@ -39,6 +39,8 @@ --> + + \ No newline at end of file diff --git a/installer/Package.wxs b/installer/Package.wxs index 6a65a73..2f0e31d 100644 --- a/installer/Package.wxs +++ b/installer/Package.wxs @@ -224,5 +224,62 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Dragon-ISO.App/Services/ControlSurfaceServer.cs b/src/Dragon-ISO.App/Services/ControlSurfaceServer.cs index 1f6eb0b..4ed0e26 100644 --- a/src/Dragon-ISO.App/Services/ControlSurfaceServer.cs +++ b/src/Dragon-ISO.App/Services/ControlSurfaceServer.cs @@ -97,11 +97,17 @@ public sealed partial class ControlSurfaceServer : IAsyncDisposable /// "headless show machine + thin client controller" setups. When false /// (default), binds to 127.0.0.1 only. /// - /// LAN binding requires either running Dragon-ISO as Administrator OR a - /// one-time URL ACL reservation at the OS level: - /// netsh http add urlacl url=http://+:9755/ user=Everyone - /// If neither is in place the listener throws AccessDeniedException - /// which we catch and surface as a logger warning. + /// Binding requires either running Dragon-ISO as Administrator OR a + /// one-time URL ACL reservation at the OS level for the prefix actually + /// being bound (the installer reserves the loopback default, + /// http://127.0.0.1:9755/): + /// netsh http add urlacl url=http://127.0.0.1:9755/ sddl=D:(A;;GX;;;WD) + /// or url=http://+:9755/ for LAN mode. CAUTION: an idle + /// http://+:port/ reservation makes http.sys 503 every request to + /// the loopback-bound listener — only add it while LAN mode is in use, + /// and delete it when reverting to loopback-only. If no reservation is + /// in place the listener throws AccessDeniedException which we catch and + /// surface as a logger warning. /// public void Start(int port, bool bindToLan = false) { @@ -123,9 +129,11 @@ public sealed partial class ControlSurfaceServer : IAsyncDisposable { _logger?.LogWarning(ex, "Could not start control surface on {Prefix}. " + - "If binding to LAN, run as Administrator once OR run: " + - "netsh http add urlacl url=http://+:{Port}/ user=Everyone", - prefix, port); + "Run as Administrator once OR reserve the URL namespace: " + + "netsh http add urlacl url={UrlAclUrl} sddl=D:(A;;GX;;;WD) " + + "(if switching back from LAN mode, also delete the stale " + + "http://+:{Port}/ reservation — while idle it 503s the loopback listener)", + prefix, prefix, port); _listener = null; return; }