Skip to content

Commit a460be5

Browse files
committed
refactor: refactored ARCHITECTURE.md and 5 others
- Updated ARCHITECTURE.md - Updated README.md - Updated http2_test.go
1 parent 40fc38f commit a460be5

6 files changed

Lines changed: 416 additions & 31 deletions

File tree

ARCHITECTURE.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Browser top-level target document
1111
│ ├─ in-memory tab/history/context state
1212
│ ├─ encrypted /p route activation
1313
│ └─ Go WASM kernel exports
14-
│ ├─ __go_jshttp(request) -> target HTTP/1.1 fetch
14+
│ ├─ __go_jshttp(request) -> target HTTP/2 or HTTP/1.1 fetch
1515
│ ├─ __zp_stream(options) -> target WebSocket stream
1616
│ ├─ __zp_kernel_init() -> transport readiness
1717
│ └─ __zp_cookie_set(request) -> document.cookie bridge
@@ -39,7 +39,7 @@ The relay server terminates only the browser WebSocket and yamux session. It use
3939
- The `#k` fragment is decrypted in the browser shell, removed with `history.replaceState`, and not sent to the server.
4040
- Every Service Worker-controlled request is classified. Unknown requests are blocked; there is no native `fetch(event.request)` fallback.
4141
- Target TCP connections are opened through WebSocket -> yamux -> Tor SOCKS5 DOMAINNAME. The kernel does not call `http.Transport` for target egress.
42-
- HTTPS uses uTLS over the Tor stream with HTTP/1.1 selected.
42+
- HTTPS uses uTLS over the Tor stream with ALPN selecting HTTP/2 when available and HTTP/1.1 fallback; target WebSocket upgrade pins HTTP/1.1.
4343
- Target response headers are passed through a constructor policy before the browser receives a `Response`.
4444
- Runtime code does not install anti-bot deception hooks such as `Function.prototype.toString` masking.
4545

@@ -52,7 +52,7 @@ The relay server terminates only the browser WebSocket and yamux session. It use
5252
| Service Worker | `web/sw.js` | Classifies every controlled request, blocks unknowns, manages in-memory tab/entry state, calls the WASM kernel, exposes runtime bridge APIs. |
5353
| Runtime prelude | `web/runtime-prelude.js`, `web/worker-prelude.js` | Installs target-realm containment hooks before target scripts run. Main-window WebSocket/navigation/form/history/location/storage/worker/iframe/device APIs are hooked; main-window fetch/XHR/EventSource are not runtime-polyfilled today. Worker `fetch` is bridged through `/__zp/api/fetch`. |
5454
| WASM kernel | `cmd/wasm-kernel/main.go`, `internal/swhttp/*` | Converts JS `Request`/`Response`, initializes transport, owns target HTTP and WebSocket execution. |
55-
| Transport | `internal/wsconn/*`, `internal/yamuxconn/*`, `internal/socks5/*`, `internal/utlskernel/*`, `internal/http1/*`, `internal/wsproto/*` | Browser WebSocket `net.Conn`, yamux streams, SOCKS5 DOMAINNAME CONNECT, uTLS, HTTP/1.1, target WebSocket upgrade/framing. |
55+
| Transport | `internal/wsconn/*`, `internal/yamuxconn/*`, `internal/socks5/*`, `internal/utlskernel/*`, `internal/http1/*`, `internal/wsproto/*` | Browser WebSocket `net.Conn`, yamux streams, SOCKS5 DOMAINNAME CONNECT, uTLS, HTTP/2 and HTTP/1.1 target fetch, target WebSocket upgrade/framing. |
5656
| HTML/header/cookie policy | `internal/htmltx/*`, `internal/headers/*`, `internal/cookiejar/*`, `internal/zpiso/*` | HTML transformation, safe response header constructor policy, target cookie jar, Tor isolation token derivation. |
5757
| Relay server | `cmd/zeroproxy-server/main.go` | Serves assets, accepts `/__zp/ws-pipe` with Gorilla WebSocket, and bridges yamux streams to the configured Tor SOCKS5 address. |
5858

@@ -64,8 +64,8 @@ The relay server terminates only the browser WebSocket and yamux session. It use
6464
4. A `/p/<encrypted>` document request is resolved back to the target URL. The Service Worker calls `__go_jshttp` with `X-ZP-*` internal metadata.
6565
5. The WASM kernel ensures one long-lived WebSocket connection to `/__zp/ws-pipe`, wraps it in a yamux client, and opens one yamux stream per target TCP connection.
6666
6. Each target connection performs SOCKS5 `CONNECT` with DOMAINNAME ATYP and a Tor `IsolateSOCKSAuth` username derived from the tab stream-isolation key and target site.
67-
7. HTTPS targets run uTLS over that stream with ALPN pinned to `http/1.1`.
68-
8. `internal/http1` writes an HTTP/1.1 request directly and reads the target response with `http.ReadResponse`.
67+
7. HTTPS fetch targets advertise `h2` and `http/1.1` through uTLS ALPN; target WebSocket connections advertise only `http/1.1`.
68+
8. `internal/http1` dispatches negotiated `h2` connections through `golang.org/x/net/http2.ClientConn`; HTTP/1.1 fallback writes a direct request and reads the response with `http.ReadResponse`.
6969
9. Redirects are followed inside the kernel so raw `Location` headers are not exposed to browser code.
7070
10. HTML document responses are transformed: runtime prelude is injected, document navigation URLs are rewritten to encrypted `/p/<encrypted>#k=<key>` routes, risky tags and headers are removed, and the browser receives a same-origin `Response` with ZeroProxy CSP.
7171

@@ -97,7 +97,7 @@ The Service Worker keeps tab, entry, client-context, route, and stream maps in m
9797

9898
The Go WASM kernel exposes `__zp_kernel_init`, `__go_jshttp`, `__zp_stream`, and `__zp_cookie_set` to the Service Worker. Initialization creates a browser WebSocket to `/__zp/ws-pipe`; the relay accepts it with Gorilla WebSocket and adapts binary messages to a stream-oriented `net.Conn`. A yamux client/server session runs over that connection, and per-target yamux streams are bridged by the relay to Tor SOCKS5.
9999

100-
`internal/http1` builds target HTTP/1.1 requests directly, applies the cookie jar, follows redirects up to `MaxRedirects`, and closes target connections through response body closure. HTTPS uses `internal/utlskernel`; target WebSocket support is implemented through `internal/wsproto` and the runtime `WebSocket` wrapper.
100+
`internal/http1` builds sanitized target requests, applies the cookie jar, follows redirects up to `MaxRedirects`, dispatches HTTPS fetches to HTTP/2 when ALPN selects `h2`, and falls back to direct HTTP/1.1 request/response handling otherwise. HTTP/2 client connections are pooled only within the same target authority, tab, and Tor isolation token; target WebSocket support stays HTTP/1.1 Upgrade through `internal/wsproto` and the runtime `WebSocket` wrapper.
101101

102102
`internal/swhttp.ResponseToJS` constructs JavaScript `Response` objects with a `ReadableStream` backed by the Go response body. Document HTML transformation uses `htmltx.TransformTo` through an `io.Pipe`, so transformed HTML can flow to the browser without first buffering the full document. Request/upload body conversion and browser backpressure/cancellation fidelity are still prototype-level.
103103

@@ -121,12 +121,12 @@ Overall status: **Phase 0 prototype / partial implementation**. The repository i
121121
|---|---|---|
122122
| 0. Correction directives | Partial | Top-level target document, encrypted `/p` route shape, AES-CBC+HMAC share envelope, and no anti-bot spoofing hooks are present. Strict `connect-src` is not fully implemented by `web/zp-core.js`; browser direct-egress prevention still needs E2E proof. |
123123
| 1. System goals | Partial | Client memory state, unknown-request blocking, Tor/yamux/uTLS path, and safe errors exist. Encrypted IndexedDB persistence and full escape-vector coverage are absent. |
124-
| 2. Overall architecture | Mostly implemented | Static shell, Service Worker, Go WASM kernel, relay WebSocket pipe, yamux, SOCKS5, uTLS, HTTP/1.1, HTML transform, cookie jar, and runtime prelude exist. |
124+
| 2. Overall architecture | Mostly implemented | Static shell, Service Worker, Go WASM kernel, relay WebSocket pipe, yamux, SOCKS5, uTLS, HTTP/2 and HTTP/1.1 fetch, HTML transform, cookie jar, and runtime prelude exist. |
125125
| 3. URL and encryption | Implemented | `web/zp-core.js` and `internal/shareurl` implement HKDF, AES-256-CBC, HMAC verification-before-decrypt, raw base64url, and protocol allowlists. Tests cover JS tamper rejection and Go envelope construction. |
126126
| 4. Active URL and tab state | Partial / PLAN-divergent | Active browsing uses encrypted `/p` routes and static tests reject legacy `/v` route generation. PLAN's `/v/<tab-id>/n/...` and `/v/<tab-id>/e/...` active-route model is not implemented. Tab/entry maps are in memory; title/state clone/origin map/storage namespace behavior is minimal; persistence is absent. |
127127
| 5. Service Worker boot | Mostly implemented | The shell waits for Service Worker control; `sw.js` tracks readiness and waits for `__go_jshttp`, `__zp_stream`, and `__zp_kernel_init`. |
128128
| 6. Fetch handler policy | Mostly implemented | `sw.js` classifies internal/share/runtime/subresource/unknown requests and has no `return fetch(event.request)` fallback. It does not implement the PLAN's distinct VIRTUAL_NAVIGATION and VIRTUAL_ENTRY `/v` classifiers. Subresource base recovery is simple and should be browser-tested. |
129-
| 7. Go WASM transport kernel | Mostly implemented | The kernel opens `/__zp/ws-pipe`, uses yamux streams, SOCKS5 DOMAINNAME CONNECT, uTLS, and direct HTTP/1.1. Target WebSocket upgrade/framing exists. Target response bodies are exposed to JavaScript through `ReadableStream`; request/upload body conversion is still prototype-level. |
129+
| 7. Go WASM transport kernel | Mostly implemented | The kernel opens `/__zp/ws-pipe`, uses yamux streams, SOCKS5 DOMAINNAME CONNECT, uTLS, HTTP/2 when ALPN selects `h2`, and HTTP/1.1 fallback. Target WebSocket upgrade/framing exists and remains HTTP/1.1-only. Target response bodies are exposed to JavaScript through `ReadableStream`; request/upload body conversion is still prototype-level. |
130130
| 8. HTML transform | Partial / PLAN-divergent | Tokenizer-based transform injects the runtime prelude, removes base/meta refresh/ping/preload hints, rewrites document navigation attrs to encrypted `/p` routes, handles `srcdoc`, and blocks object/embed. PLAN's `/v/<tab-id>/n/<base64url_target_url>` laundering and direct topbar injection are not implemented. Malformed-markup recovery still needs stronger proof. |
131131
| 9. Tor stream isolation | Implemented at code level | `zpiso.Token` derives site-granular HMAC tokens; SOCKS5 rejects IP literals and sends DOMAINNAME ATYP. Deployment still requires correctly configured Tor. |
132132
| 10. Response header policy | Implemented | `internal/headers` strips target CSP, cookies, reporting, Alt-Svc, Link, Refresh, Location, hop-by-hop headers, transformed lengths/encoding, and defaults to `Cache-Control: no-store`. |

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
ZeroProxy is a client-owned virtual browsing prototype that runs target pages on the proxy origin without a browser extension. Its design goal is that target-site HTTP, TLS, and WebSocket traffic leaves only through this path:
44

55
```text
6-
Service Worker -> Go WASM kernel -> WebSocket/yamux -> Tor SOCKS5 -> uTLS -> HTTP/1.1
6+
Service Worker -> Go WASM kernel -> WebSocket/yamux -> Tor SOCKS5 -> uTLS -> HTTP/2 or HTTP/1.1
77
```
88

99
The relay server terminates only the browser WebSocket/yamux pipe. Target HTTP parsing, redirects, cookies, header policy, HTML rewriting, and target WebSocket framing are owned by the Go WASM kernel and browser-side runtime.
@@ -18,7 +18,7 @@ Implemented core spine:
1818
- AES-256-CBC + HMAC-SHA256 URL envelope with HKDF-separated encryption/MAC keys and HMAC verification before decryption.
1919
- Service Worker request classifier that handles every controlled request and blocks unknown requests instead of falling back to native `fetch(event.request)`.
2020
- Go WASM exports: `__go_jshttp`, `__zp_stream`, `__zp_kernel_init`, and `__zp_cookie_set`.
21-
- A single browser WebSocket pipe carrying yamux streams to the relay server, then Tor SOCKS5 DOMAINNAME CONNECT, uTLS for HTTPS, and direct HTTP/1.1 request/response handling.
21+
- A single browser WebSocket pipe carrying yamux streams to the relay server, then Tor SOCKS5 DOMAINNAME CONNECT, uTLS for HTTPS, HTTP/2 when ALPN selects `h2`, and HTTP/1.1 fallback/direct handling.
2222
- Tokenizer-based HTML transform that injects the runtime prelude, launders document navigation URLs through encrypted `/p` routes, drops dangerous tags and headers, and handles `srcdoc`.
2323
- Runtime containment for WebSocket, `sendBeacon`, navigation, forms, history/location masking, storage facades, workers, iframes, and high-risk device/network APIs. Main-window `fetch`, XHR, and EventSource currently rely on Service Worker interception rather than runtime polyfills; worker `fetch` is bridged through `/__zp/api/fetch`.
2424
- Relay server static asset service and `/__zp/ws-pipe` WebSocket endpoint.

internal/http1/http2_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package http1
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"net"
8+
"net/http"
9+
"net/url"
10+
"testing"
11+
"time"
12+
13+
"github.com/gosuda/zeroproxy/internal/cookiejar"
14+
"golang.org/x/net/http2"
15+
)
16+
17+
func TestHTTP2RoundTripUsesClientConn(t *testing.T) {
18+
client, server := net.Pipe()
19+
checked := make(chan error, 1)
20+
serverDone := make(chan struct{})
21+
go func() {
22+
defer close(serverDone)
23+
defer server.Close()
24+
(&http2.Server{}).ServeConn(server, &http2.ServeConnOpts{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
25+
if err := validateHTTP2Request(r); err != nil {
26+
checked <- err
27+
http.Error(w, err.Error(), http.StatusBadRequest)
28+
return
29+
}
30+
checked <- nil
31+
w.Header().Set("X-H2", "yes")
32+
_, _ = io.WriteString(w, "h2-ok")
33+
})})
34+
}()
35+
36+
target, _ := url.Parse("https://example.com/h2?q=1")
37+
src, _ := http.NewRequest(http.MethodGet, target.String(), nil)
38+
src.Header.Set("Accept", "text/plain")
39+
src.Header.Set("X-ZP-Tab-Id", "secret")
40+
wireReq, err := BuildHTTP1Request(src, target, cookiejar.New())
41+
if err != nil {
42+
t.Fatal(err)
43+
}
44+
hc, err := newH2Conn(client)
45+
if err != nil {
46+
t.Fatal(err)
47+
}
48+
49+
resp, err := (&Engine{}).roundTripHTTP2(context.Background(), hc, wireReq)
50+
if err != nil {
51+
t.Fatal(err)
52+
}
53+
if err := <-checked; err != nil {
54+
t.Fatal(err)
55+
}
56+
if resp.ProtoMajor != 2 || resp.Header.Get("X-H2") != "yes" {
57+
t.Fatalf("unexpected h2 response: proto=%s headers=%#v", resp.Proto, resp.Header)
58+
}
59+
body, err := io.ReadAll(resp.Body)
60+
if err != nil {
61+
t.Fatal(err)
62+
}
63+
if string(body) != "h2-ok" {
64+
t.Fatalf("body = %q", string(body))
65+
}
66+
if err := resp.Body.Close(); err != nil {
67+
t.Fatal(err)
68+
}
69+
select {
70+
case <-serverDone:
71+
case <-time.After(time.Second):
72+
t.Fatal("HTTP/2 connection was not closed after unpooled response body close")
73+
}
74+
}
75+
76+
func validateHTTP2Request(r *http.Request) error {
77+
if r.ProtoMajor != 2 {
78+
return fmt.Errorf("proto = %s", r.Proto)
79+
}
80+
if r.Host != "example.com" {
81+
return fmt.Errorf("host = %q", r.Host)
82+
}
83+
if r.URL.RequestURI() != "/h2?q=1" {
84+
return fmt.Errorf("request URI = %q", r.URL.RequestURI())
85+
}
86+
if r.Header.Get("Accept") != "text/plain" {
87+
return fmt.Errorf("accept = %q", r.Header.Get("Accept"))
88+
}
89+
if r.Header.Get("Accept-Encoding") != "identity" {
90+
return fmt.Errorf("accept-encoding = %q", r.Header.Get("Accept-Encoding"))
91+
}
92+
if r.Header.Get("X-Zp-Tab-Id") != "" {
93+
return fmt.Errorf("internal header leaked: %#v", r.Header)
94+
}
95+
return nil
96+
}

0 commit comments

Comments
 (0)