diff --git a/DECISIONS.md b/DECISIONS.md index e907161..a7f95db 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -12,15 +12,28 @@ - 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 instead of CSS `display: none` injection. Matched elements are replaced with `
` using `golang.org/x/net/html` tokenizer. This strips ad content (scripts, iframes, images) from the DOM, reducing page weight. Complex selectors (descendant/sibling combinators, `:has()`, `:not()`) fall back to CSS injection. -- 2026-02-26 m+git@andri.dk — Added `golang.org/x/net` (zero transitive runtime deps, Go team maintained) for HTML tokenization in element replacement. Selectors are classified at cache time into simple (single-element matchable) and complex (needs CSS fallback). +- 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 for all element hiding selectors, not just complex ones. Previously simple selectors only got element replacement; complex selectors only got CSS injection. Now all selectors get `display: none !important` CSS (blocks JS-injected ads), and simple selectors also get element replacement (strips content from the DOM). Defense-in-depth: CSS hides immediately, replacement removes entirely. +- 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 `` + + `
Ad content
` + `` + `

Real content

` + ``)) @@ -643,20 +647,23 @@ func TestElementHidingReplacesElements(t *testing.T) { body, _ := io.ReadAll(resp.Body) bodyStr := string(body) - // Matched elements should be replaced with placeholder divs - if !strings.Contains(bodyStr, "") { - t.Errorf("response should contain replacement for .ad-banner, got:\n%s", bodyStr) + // CSS should be injected to hide matching elements + if !strings.Contains(bodyStr, "