# Decisions - 2026-02-25 m+git@andri.dk — Validate upstream TLS certificates (removed `InsecureSkipVerify`). The proxy is the user's trust boundary; it must verify upstream server identity. - 2026-02-25 m+git@andri.dk — Don't manipulate `Accept-Encoding`. The proxy forwards the client's encoding preferences to upstream and handles CSS injection based on the response `Content-Encoding`. Gzip and brotli HTML are decompressed for injection; other encodings pass through without injection. Non-HTML resources are never touched. - 2026-02-25 m+git@andri.dk — Skip CSS injection for HEAD requests. HEAD responses have no body per HTTP spec. - 2026-02-25 m+git@andri.dk — Hostname-only rules with `$options` (e.g. `||host^$third-party`) bypass the fast-path hostname map and go through compiled rules to preserve option evaluation. - 2026-02-25 m+git@andri.dk — `$match-case` rules receive both the original-case and lowercased URL so they match correctly through the optimized `ShouldBlockRequest` path. - 2026-02-25 m+git@andri.dk — Domain-indexed rule lookup: `||domain` rules are partitioned into `map[string][]*Rule` keyed by domain suffix. At match time, walk up the hostname hierarchy instead of scanning all rules. - 2026-02-25 m+git@andri.dk — Literal-skip pattern matching: use `strings.Index` to jump to candidate positions for leading literals and post-wildcard literals, avoiding byte-by-byte scanning. - 2026-02-25 m+git@andri.dk — Cache `ElementHidingForDomain` results in `sync.Map` since the ruleset is immutable after loading. - 2026-02-25 m+git@andri.dk — Serve decompressed HTML to client after CSS injection (no re-compression). Proxy-to-client hop is typically localhost. - 2026-02-25 m+git@andri.dk — Added `github.com/andybalholm/brotli` (pure Go, zero transitive runtime deps) for brotli decompression in CSS injection. First third-party dependency — warranted because brotli is the dominant encoding for HTML on modern CDNs and the stdlib has no brotli support. - 2026-02-25 m+git@andri.dk — WebSocket upgrade supported for both ws:// and wss://. Upgrade headers are re-added after hop-by-hop stripping, then bidirectional copy bridges client and upstream after 101. - 2026-02-25 m+git@andri.dk — Cert cache has no eviction. Certs are generated with 24h validity. Acceptable for personal use. - 2026-02-26 m+git@andri.dk — ~~Element hiding via HTML element replacement.~~ Superseded: element hiding is now CSS-only (see 2026-02-26 entry below). - 2026-02-26 m+git@andri.dk — Added `golang.org/x/net` (zero transitive runtime deps, Go team maintained) for HTML tokenization in src-based resource stripping. - 2026-02-26 m+git@andri.dk — Blocked CONNECT requests return 403 Forbidden instead of 204 No Content. Browsers hang on 204 because they expect a tunnel; 403 makes them fail fast. - 2026-02-26 m+git@andri.dk — Removed Playwright from the project. Playwright's screenshot and snapshot commands wait for `document.fonts.ready`, which never resolves when the MITM proxy blocks ad-network domains that serve fonts. curl is a better fit for proxy testing. - 2026-02-26 m+git@andri.dk — Re-added playwright-cli for browser testing. Previous HTTPS timeouts were caused by environment variable configuration not being picked up. Fixed by using a declarative config file (`playwright-cli-proxy.json`) with `contextOptions.ignoreHTTPSErrors` and `contextOptions.proxy` instead of env vars. - 2026-02-26 m+git@andri.dk — Elements that load external resources (script, iframe, object, embed) are stripped during HTML rewriting when their resource URL resolves to a blocked address. Defense-in-depth alongside network-level blocking: stripping the element from the DOM prevents the browser from attempting the load. `srcBlockableTags` maps tag name to URL attribute (`src` for most, `data` for object). Void elements (embed) skip the `skipUntilClose` step. Relative and protocol-relative URLs are resolved against the page origin. Inline scripts (no `src`) are not affected. - 2026-02-26 m+git@andri.dk — Disabled HTTP/2 on upstream transport (`TLSNextProto` set to empty map). Go's default `http.Transport` negotiates HTTP/2 via ALPN, which caused indefinite hangs on certain Cloudflare-hosted domains (`s3-bm-adtech.berlingskemedia.net`, `s3-common-jscdn.berlingskemedia.net`). The proxy serializes requests over HTTP/1.1 on the client side, so HTTP/2 multiplexing provides no benefit. Forcing HTTP/1.1 upstream eliminates the hang and matches what most MITM proxies do. - 2026-02-26 m+git@andri.dk — Preserve original attribute case in HTML rewriting. `golang.org/x/net/html.Tokenizer.Raw()` returns reconstructed HTML with lowercased attribute names after `TagAttr()` is called. This broke React hydration on SSR sites (e.g. `charSet` → `charset`, `viewBox` → `viewbox`). Fix: save `Raw()` output before consuming attributes and use the saved bytes for pass-through writes. - 2026-02-26 m+git@andri.dk — ~~CSS injection + element replacement for all element hiding selectors.~~ Superseded: element hiding is now CSS-only (see 2026-02-26 entry below). - 2026-02-26 m+git@andri.dk — Resource type detection for adblock content-type options (`$script`, `$image`, `$stylesheet`, `$xmlhttprequest`, `$subdocument`, `$media`, `$font`, `$object`, `$websocket`, `$document`). Previously these options were silently ignored, causing rules like `/adengine.js$script` to block all request types instead of just scripts — over-blocking that breaks sites. Detection uses `Sec-Fetch-Dest` header (primary, all modern browsers), `Accept` header heuristics (fallback), and URL file extension (last resort). Unknown resource type (no signal available) matches any type constraint for backward compatibility. `$popup` is parsed but not enforceable at proxy level (fails open). `$generichide` deferred to follow-up. - 2026-02-26 m+git@andri.dk — `-blocklist` flag supports remote URLs (`http://`, `https://`) in addition to local file paths. Downloaded once at startup using a dedicated `http.Client` with 30-second timeout. No caching or periodic refresh — same semantics as local files. Refactored `LoadFile` to use `LoadReader(io.Reader)` so all loading paths share the same line-by-line parsing. Go's `http.DefaultTransport` handles gzip decompression transparently. - 2026-02-26 m+git@andri.dk — Benchmarked `ShouldBlockRequest` with full EasyList (89K lines, 55K hostnames, 9K URL rules). Results on Apple M3: non-blocked URL 64µs, blocked-by-hostname 1.6µs, blocked-by-domain-rule 45µs, blocked-by-generic-rule 58µs. Zero allocations. No optimization needed — rule matching adds <1% to page load time. - 2026-02-26 m+git@andri.dk — Added `modernc.org/sqlite` (pure Go, no CGO) for persistent storage of passkey credentials, sessions, and user-created blocking rules. Binary grows ~7MB (9MB → 16MB). Chose pure-Go over CGO SQLite to keep zero C compiler requirement. - 2026-02-26 m+git@andri.dk — Added `fxamacker/cbor/v2` (minimal, zero transitive deps) for CBOR decoding in WebAuthn attestation parsing. Only ES256 (P-256 ECDSA) with "none" attestation is supported — covers all modern passkey authenticators. - 2026-02-26 m+git@andri.dk — Passkey (WebAuthn) authentication with no usernames or passwords. A credential IS the user identity. The credential ID is the foreign key for sessions and rules. Open registration — anyone on the network can register a passkey. Trust boundary is the network. - 2026-02-26 m+git@andri.dk — Minimal WebAuthn server implementation using stdlib crypto + fxamacker/cbor instead of a full WebAuthn library. Server generates challenges (crypto/rand), parses CBOR attestation objects to extract COSE public keys, and verifies ECDSA signatures. ~250 lines vs thousands in go-webauthn/webauthn. - 2026-02-26 m+git@andri.dk — Dual-port architecture: HTTP proxy on port 8080 (unchanged), HTTPS portal on port 8443 (new). WebAuthn requires a secure context (HTTPS), and LAN clients access the proxy from non-localhost IPs. The portal's TLS cert is signed by the proxy's own CA, which clients already trust. - 2026-02-26 m+git@andri.dk — Session-token auth for the rule management API. Portal issues a Bearer token after passkey authentication. Tokens are 32 bytes from crypto/rand, stored in SQLite with 30-day expiry. The injected script (future phase) will embed the token in a closure to prevent third-party page scripts from accessing it. - 2026-02-26 m+git@andri.dk — Bootstrap script injected inline into every HTML response (not as external `