Commit graph

3 commits

Author SHA1 Message Date
Local Dev
bbef07c825 prometheus: 200 MiB upload cap + Caddy block fix for the "<" error
Two bugs under one roof.

The visible one: a publisher uploading anything over 10 MB on the live
site got "Network error — Unexpected token '<'" in the browser console.
Cause: the Caddy block I wrote for prometheus.x had
`request_body { max_size 2GB }` nested inside the handle block, where it
is a syntax Caddy parses but does not apply, so Caddy's own 10 MB
default was still the gate. Any oversize POST got Caddy's HTML 413 page,
which the browser's JSON parser choked on at the leading `<`.

Fix: moved `request_body { max_size … }` to the top of the prometheus.x
site block (outside `handle`), where Caddy actually honours it. The live
Caddyfile on p2pexchange is already patched + reloaded; a 50 MB upload
verified through the end-to-end path (Caddy → service → clamd → signed
scan report).

The scope one: 2 GiB was more than Prometheus should offer to publishers
at launch. Mobile apps are usually ~100 MB and even most desktop
installers come in well under 200 MiB. Dropping to 200 MiB:

  - Fits every Android APK and most desktop installers.
  - Shrinks the operator's Sia storage bill (fewer accidental Steam-size
    uploads).
  - Keeps the "publishers of larger binaries self-host and submit a
    manifest with the external URL + SHA" escape hatch — the scanner
    still fetches the URL and checks the hash, so a 5 GB game isn't
    locked out, Prometheus just doesn't eat its storage.

Changes:
  service/uploads.js       MAX_FILE_BYTES default: 2 GiB → 200 MiB
  service/pages.js         /submit step-1 subtitle: "any size up to
                           2 GiB" → "up to 200 MiB"
  deploy/prometheus-x.conf (unused angie fallback) 2g → 200m
  deploy/apps-silentmode-st.conf (unused angie fallback) 2g → 200m

PROMETHEUS_MAX_UPLOAD_BYTES env still overrides per-instance.
2026-10-22 23:20:19 +03:00
Local Dev
c9008ef3ba prometheus: VirusTotal enrichment, gated on VIRUSTOTAL_API_KEY
After clamd returns a verdict, scanStream queries VirusTotal by the file's
SHA-256 and attaches the finding under scan.virustotal. The call never
blocks, never changes the overall verdict, and never ships without a key
— enrichment is informational and strictly additive.

Shape in the signed scan report:
  scan.virustotal: {status: 'known', positives, total, scan_date, signatures}
                 | {status: 'unknown'}
                 | {status: 'rate_limited'}
                 | {status: 'error', reason}
                 | (absent)              — enrichment disabled

Rate-limited to VIRUSTOTAL_RATE_PER_MIN (default 4, matching the free
tier) via a sliding window; the service never fails the request when the
quota is spent, it just skips the enrichment that time. Hashes are
cached in-process so a resubmitted file does not re-spend quota.

The upload POST response passes the virustotal field through on both the
success and error branches so the submit UI can display it alongside the
clamd verdict.

uploads.js passes the SHA-256 it already computed into scanStream rather
than making scanStream hash a second time — the current call site is the
only one that needs the hash downstream.
2026-10-08 21:44:34 +02:00
Local Dev
c4566b4ea3 prometheus: upload + ClamAV scan + signed scan reports
POST /api/upload accepts a multipart file, spools it to a temp path while
hashing to SHA-256, hands the on-disk copy to clamd via INSTREAM, keeps
the file at uploads/<sha256>.bin on a clean verdict and discards it on
anything else. The response carries the (hash, size, verdict) tuple
signed by the service's ed25519 key — the artifact the manifest step
will attach to the APS1 record later.

For local dev on a box without ClamAV, MOCK_SCAN=clean|infected|error
short-circuits the TCP call while still draining the bytes end-to-end.
CLAMD_HOST / CLAMD_PORT / PROMETHEUS_MAX_UPLOAD_BYTES override the
defaults (127.0.0.1:3310, 2 GiB).

Service key lives at .keys/service.key (gitignored, 0600), generated on
first start if absent. The matching public key is served at
/.well-known/prometheus-service-key.json so clients can cache it per
origin. ed25519 chosen over BIP-137 because the scan report is a service
attestation, not a Bitcoin thing — the key never signs value, never
signs for a user, never touches the chain.

Canonicalisation walks the payload recursively (sorted keys at every
level, no whitespace). The first draft used JSON.stringify with a keys
array, which only serialises top-level keys from the array — nested
objects came out as {}, so the signature verified nothing useful.

/healthz pings clamd (or the mock) so monitors can tell when the scanner
is down.

The /submit page gets a working upload form that drives the endpoint
and renders the full signed response, so the whole chain is testable in
the browser as well as via curl.

One dependency added: busboy. Rolling our own multipart parser was the
obvious alternative; busboy is one small, maintained library with no
runtime deps of its own, and multipart RFC 7578 edge cases are not
places this project earns anything by rediscovering.
2026-10-05 22:41:55 +02:00