feat(installer): bundle URL Rewrite, ask where client IPs come from, verify installs

IIS does not set X-Forwarded-For on its own and HttpPlatformHandler connects
from loopback, so without a rewrite rule every client reads as 127.0.0.1. The
GE-Enforce IP allowlist, the dashboard visitor-location lookup and per-host
login rate limiting all stop working, silently. The rule needed URL Rewrite,
which the installer told operators to download - from an air-gapped server.

URL Rewrite now ships in the bundle, and the wizard asks which case applies,
because the two answers are mutually exclusive. Directly exposed: install it and
set X-Forwarded-For from REMOTE_ADDR, which is what stops a client spoofing its
own. Behind a proxy: leave the rule off, since REMOTE_ADDR is the proxy and
applying it would discard the real client IP.

The rule is enabled by deleting two explicit marker lines rather than by a regex
over the surrounding comment, so editing that prose cannot silently disable it.

An existing web.config is no longer overwritten. It is the one file on a server
that legitimately carries hand-edits, and replacing it reverted them without a
word - on a server where the X-Forwarded-For rule had been enabled by hand, that
alone would have turned the GE-Enforce IP allowlist off. The installer reports
what it found instead.

pip now runs with --require-hashes and --only-binary=:all:. Hash-checking is
requested explicitly rather than inferred from the lockfile, so shipping an
unhashed requirements.txt fails loudly instead of quietly dropping the check.

shopdb-admin.ps1 gains a verify command: which bundle this server was installed
from, and whether the installed packages still match what shipped.

The .iss states its compiler floor. WizardStyle uses the built-in windows11
custom style, which needs Inno Setup 6.6.0; older compilers now fail with that
sentence rather than 'WizardStyle is invalid'.
This commit is contained in:
cproudlock
2026-08-03 11:17:58 -04:00
parent 88af7fd9ce
commit 44237b5cbd
5 changed files with 286 additions and 19 deletions

View File

@@ -9,6 +9,14 @@
; that nobody tests. This file only collects operator input, runs the stages in
; order, and reports which one failed.
; Inno Setup 6.6.0 is the floor: WizardStyle below uses the built-in 'windows11'
; custom style, which 6.5 and earlier reject. Stated here so a build box on an
; older compiler fails with this sentence rather than with a bare
; "Value of [Setup] section directive WizardStyle is invalid".
#if VER < EncodeVer(6,6,0)
#error This script needs Inno Setup 6.6.0 or newer (WizardStyle=... windows11). Download it from https://jrsoftware.org/isdl.php
#endif
#define AppName "ShopDB-Flask"
; Pre-release. This has not shipped, so it is 0.x by definition. It becomes
; 1.0.0 when a real site installs from it successfully - not before.
@@ -173,6 +181,7 @@ var
DbPageReady: Boolean;
DeployPage: TInputOptionWizardPage;
DeployPageReady: Boolean;
ClientIpPage: TInputOptionWizardPage;
// Set from the streamed stage output so a failure can name its cause. Without
// this the wizard could only report "exit 1", which points at nothing.
FailCause: String;
@@ -388,6 +397,35 @@ begin
else
SitePage.Values[0] := GetComputerNameString;
SitePage.Values[1] := '8090';
// Where the real client IP comes from. Not cosmetic: IIS sends no
// X-Forwarded-For of its own, so with neither option applied every request
// reads as 127.0.0.1 and the GE-Enforce IP allowlist, the dashboard's
// visitor-location lookup and per-host login rate limiting all fail silently.
//
// The two answers are mutually exclusive, and picking the wrong one is worse
// than picking neither: the rule OVERWRITES the header with REMOTE_ADDR, which
// is exactly right when IIS faces clients (it defeats spoofing) and exactly
// wrong behind a proxy (REMOTE_ADDR is the proxy, so the real client IP is
// discarded). Hence a question rather than a default.
ClientIpPage := CreateInputOptionPage(SitePage.ID,
'Client addresses', 'How does this server see who is connecting?',
'ShopDB records the address of every request, and some features decide what '
+ 'to show based on it. Choose whichever describes this server.',
True, False);
ClientIpPage.Add('Clients connect to this server directly (installs URL Rewrite)');
ClientIpPage.Add('A proxy or load balancer sits in front of this server');
ClientIpPage.SelectedValueIndex := 0;
end;
// 'direct' installs URL Rewrite from the bundle and enables the X-Forwarded-For
// rule; 'proxy' leaves both alone because the proxy already sets the header.
function ClientIpSourceArg: String;
begin
if ClientIpPage.SelectedValueIndex = 1 then
Result := 'proxy'
else
Result := 'direct';
end;
// Full path to the 64-bit PowerShell.
@@ -900,6 +938,7 @@ begin
' -SiteHost "' + SitePage.Values[0] + '"' +
' -SitePort ' + SitePage.Values[1] +
' -OnFailure never' +
' -ClientIpSource ' + ClientIpSourceArg +
' -SitePlugins "' + SelectedPlugins + '"';
// Subpath deployment: an IIS Application under the existing site instead of a