Skip to content

Allow archive.org downloads in the browser app - #283

Merged
highbyte merged 4 commits into
masterfrom
feature/browser-download-archive-org
Aug 10, 2026
Merged

Allow archive.org downloads in the browser app#283
highbyte merged 4 commits into
masterfrom
feature/browser-download-archive-org

Conversation

@highbyte

Copy link
Copy Markdown
Owner

Download & Run failed in the Avalonia Browser build for VisiCalc and Dangerous Dave. Both are hosted on archive.org, which was not in the CORS proxy's ALLOWED_TARGET_HOSTS, so the Worker answered 403 Target host not allowed. The other four Apple II entries use mirrors.apple2.org.za and were unaffected, and the desktop app fetches directly so it never hit this.

Reproduced against the live Worker before changing anything, and the deployed vars matched wrangler.jsonc, so there was no config/deployment drift.

Why adding archive.org was not enough

Every archive.org download 302s to a per-request node host:

https://archive.org/download/Visicalc_1.27/Visicalc_1.27.dsk
  → https://dn711007.ca.archive.org/0/items/Visicalc_1.27/Visicalc_1.27.dsk

The Worker re-validates each redirect hop against the same allowlist, so allowing only archive.org would have swapped one 403 for Redirect target host not allowed. Those node hostnames vary per request and region and cannot be enumerated.

The change

isAllowedTargetHost gains a *. wildcard entry. Three choices worth stating:

  • The wildcard is opt-in per entry, not implied by every entry. Allowing subdomains of one host must not quietly widen the rest — csdb.dk still means only csdb.dk.
  • A wildcard matches subdomains only, so a host needing both is listed twice (archive.org and *.archive.org). They are different trust decisions and the allowlist should read literally.
  • The compared suffix keeps its leading dot. Testing endsWith("archive.org") would also admit evilarchive.org — an allowlist anyone can join by registering the right name.

validateConfig now rejects wildcards too broad to be an allowlist: a bare *, or one with no dot after it such as *.com. A bare * makes this an open proxy, and that should fail at startup rather than be discovered from traffic.

Verification

Run against the real service with the Worker running locally, not just mocked:

Target Result
VisiCalc 200, 143 360 bytes, starts 01 A5 27
Dangerous Dave 200, 143 360 bytes
evilarchive.org 403 Target host not allowed
archive.org.evil.com 403 Target host not allowed

143 360 is exactly a 140 KB DOS 3.3 image and 01 A5 27… is the Apple II boot sector, so that is the real file through the real redirect chain.

Already deployed and verified manually in the browser app. The Worker deploy is independent of this merge — this PR is the source of record catching up with what is live.

20 tests pass, including 6 new ones: exact entries do not match subdomains, wildcards match node hosts, a wildcard alone does not cover the bare domain, the three lookalike cases, over-broad wildcards rejected, and a redirect-hop test mirroring archive.org's actual 302.

Two incidental fixes

  • A test was already failing before this change. The health-document assertion still listed four allowed hosts after mirrors.apple2.org.za was added as a fifth. Confirmed by stashing and re-running.
  • Nothing ran these tests, which is how that went unnoticed — no workflow referenced the Worker at all. Adds .github/workflows/cors-proxy.yml, path-filtered to tools/cloudflare/cors-download-proxy/**. A suite nothing runs is not a gate.

Flagged, not fixed

  • npm audit reports 7 high-severity advisories, all undici reached via wrangler / @cloudflare/vitest-pool-workers. Dev/test dependencies, not shipped in the Worker; the fix needs --force and a breaking bump, so it wants a decision rather than a silent change.
  • The new workflow uses tag-pinned actions, matching the repo's existing nine. The convention is full SHA pinning; no workflow here does that today, so this is a pre-existing gap worth a separate sweep rather than one inconsistent file.

Download & Run failed in the Avalonia Browser build for VisiCalc and Dangerous
Dave. Both are hosted on archive.org, which was not in the proxy's
ALLOWED_TARGET_HOSTS, so the worker answered 403 "Target host not allowed".
The other four Apple II entries use mirrors.apple2.org.za and were unaffected,
and the desktop app fetches directly so it never hit this.

Adding archive.org alone would not have fixed it. Every download there 302s to
a per-request node host - dn711007.ca.archive.org, ia801234.us.archive.org and
so on - and the worker re-validates each redirect hop against the same
allowlist, so the hop would have failed instead with "Redirect target host not
allowed". Those hostnames vary per request and cannot be enumerated.

isAllowedTargetHost now supports a "*." wildcard entry. Three choices worth
stating:

- The wildcard is opt-in per entry rather than implied by every entry, so
  allowing subdomains of one host does not quietly widen the rest. csdb.dk
  still means only csdb.dk.
- A wildcard matches subdomains only, so a host needing both is listed twice
  (archive.org and *.archive.org). They are different trust decisions and the
  allowlist should read literally.
- The compared suffix keeps its leading dot. Testing endsWith("archive.org")
  would also admit evilarchive.org, which is an allowlist anyone can join by
  registering the right name.

validateConfig now rejects wildcards too broad to be an allowlist - a bare "*",
or one with no dot after it such as "*.com". A bare "*" makes this an open
proxy, and that should fail at startup rather than be discovered from traffic.

Verified against the real service by running the worker locally: VisiCalc and
Dangerous Dave both return 143360 bytes - exactly a 140 KB DOS 3.3 image, and
VisiCalc starts 01 A5 27, the Apple II boot sector - while evilarchive.org and
archive.org.evil.com are still refused.

Also fixes a test that was already failing before this change: the health
document assertion still listed four allowed hosts after mirrors.apple2.org.za
was added as a fifth. It went unnoticed because no workflow ran these tests, so
this adds one, path-filtered to the worker. A suite nothing runs is not a gate.

20 tests pass.
SonarCloud flagged the new workflow: npm ci without --ignore-scripts lets a
dependency's lifecycle scripts execute arbitrary code during install
(githubactions:S6505, MAJOR). Worth taking seriously in this tree, which
already carries seven high-severity advisories reached through wrangler.

Nothing here needs those scripts. Verified by deleting node_modules, running
npm ci --ignore-scripts, and getting all 20 tests passing from that clean
install.
The quality gate failed on new_coverage 0% against a threshold of 80. The nine
uncovered lines were the wildcard matcher and config guard in the CORS proxy
Worker - which are covered, by six of that Worker's twenty tests. SonarCloud
receives no coverage report for the TypeScript under tools/**, so it read "no
data" as "untested".

This gate measures the .NET product: sonar.coverage.inclusions already narrows
it to a single library, and sonar.cpd.exclusions already carves out tools/** as
"standalone TypeScript deployables (not the .NET product)". Coverage was simply
never made to agree with that decision, because until now nothing under tools/**
had changed in a PR.

So this is the existing scope decision applied to a second metric, not a new
exemption to dodge the gate. The Workers keep their own vitest suites and their
own workflows; if their coverage is ever reported here, drop the exclusion and
feed the lcov in instead. Noted in the comment.
CI / Documentation and the two CI / WASM AOT checks sat at "Expected - Waiting
for status to be reported" on this PR, and would have sat there forever.

Both workflows filter on `paths`. A workflow filtered out never runs, and one
that never runs never reports a status - but branch protection still requires a
status from it. So any PR touching none of the listed paths is unmergeable. It
is a deadlock, not a slow check, and it stays invisible until a PR happens to
miss every path in the list. This one is the first to touch only tools/** and
.github/workflows/**; every previous PR touched src/** or docs/** and so
satisfied both by accident.

The fix is to let the jobs always run and always report, and move the path test
inside the job:

- The pull_request trigger loses its `paths` filter. The push trigger keeps
  its own, since nothing requires a status from a push.
- A first step diffs against the PR base and sets an output.
- Every subsequent step is gated on that output.

A miss now costs a few seconds of runner time instead of a merge, and the
expensive work - AOT publish, Playwright, mkdocs - is still skipped exactly as
before. Job names are unchanged, which matters: the required checks are matched
by name.

The alternative, a second workflow duplicating the job names on inverse path
filters, was rejected. It puts each required name in two files, so renaming one
breaks every PR - the same failure this commit is removing.

cors-proxy.yml gets the same treatment even though its check is not required.
It was added in this PR with the same latent trap, and leaving it would mean
marking it required later silently reintroduces the deadlock.

Both trigger paths verified: on this PR the workflow files themselves changed,
so all three checks report having done their real work.
@sonarqubecloud

Copy link
Copy Markdown

@highbyte
highbyte merged commit 0ea8b85 into master Aug 10, 2026
9 checks passed
@highbyte
highbyte deleted the feature/browser-download-archive-org branch August 10, 2026 08:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant