Skip to content

Commit ce332c9

Browse files
committed
feat(runtime): created HTTP API wrappers and virtual history entry tracking
- Add ZPXMLHttpRequest, ZPEventSource, and fetch wrapper routing through /__zp/api/fetch for network containment. - Add activeEntryId for tracking current history entry independent of boot.entryId. - Add updateVirtualHash, setVirtualLocation, and commitVirtualHistory for unified navigation handling. - Route main-window fetch, XHR, and EventSource through runtime APIs instead of Service Worker interception alone. - Route sendBeacon through fetchThroughRuntime for consistent network handling.
1 parent a4f74d8 commit ce332c9

10 files changed

Lines changed: 332 additions & 44 deletions

File tree

ARCHITECTURE.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Browser top-level target document
1616
│ ├─ __zp_kernel_init() -> transport readiness
1717
│ └─ __zp_cookie_set(request) -> document.cookie bridge
1818
├─ /__zp/runtime-prelude.js
19-
│ ├─ WebSocket wrapper and sendBeacon target rewrite; main-window fetch/XHR/EventSource rely on Service Worker fetch interception
19+
│ ├─ fetch/XHR/EventSource/WebSocket/sendBeacon wrappers routed through same-origin runtime APIs
2020
│ ├─ navigation, form, history, location, and getter masking hooks
2121
│ ├─ storage namespace facades
2222
│ ├─ worker and iframe containment hooks
@@ -52,7 +52,7 @@ The relay server terminates only the browser WebSocket and yamux session. It use
5252
| Static shell | `web/index.html`, `web/zp-core.js` | Service Worker registration, target URL canonicalization, share URL encryption/decryption, initial target open. |
5353
| Share URL envelope | `web/zp-core.js`, `internal/shareurl/*` | Compatible JavaScript and Go implementations of `/p/<encrypted>#k=<key>` using AES-256-CBC, HMAC-SHA256, HKDF, and raw base64url. |
5454
| Service Worker | `web/sw.js` | Classifies every controlled request, blocks unknowns, manages in-memory tab/entry state, requires per-tab capability tokens on privileged runtime bridge messages, calls the WASM kernel, exposes runtime bridge APIs. |
55-
| 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. Phase 2 adds runtime membrane helpers (`__zp_get`, `__zp_set`, `__zp_call`, `__zp_construct`, `__zp_getOwnPropertyDescriptor`, `__zp_ownKeys`) plus fail-closed dynamic compilation guards. Worker `fetch` is bridged through `/__zp/api/fetch`. Patched function source strings, Canvas/Audio extraction, and speech voices receive basic masking to reduce runtime self-fingerprinting, not to provide full anti-bot spoofing. |
55+
| Runtime prelude | `web/runtime-prelude.js`, `web/worker-prelude.js` | Installs target-realm containment hooks before target scripts run. Main-window fetch/XHR/EventSource/WebSocket/sendBeacon, navigation/form/history/location/storage/worker/iframe/device APIs are hooked; main-window and worker `fetch` bridge through `/__zp/api/fetch` to satisfy strict proxy-origin CSP while preserving the Tor/yamux/uTLS transport path. Phase 2 adds runtime membrane helpers (`__zp_get`, `__zp_set`, `__zp_call`, `__zp_construct`, `__zp_getOwnPropertyDescriptor`, `__zp_ownKeys`) plus fail-closed dynamic compilation guards. Patched function source strings, Canvas/Audio extraction, and speech voices receive basic masking to reduce runtime self-fingerprinting, not to provide full anti-bot spoofing. |
5656
| WASM kernel | `cmd/wasm-kernel/main.go`, `internal/swhttp/*` | Converts JS `Request`/`Response`, initializes transport, owns target HTTP and WebSocket execution. |
5757
| Transport | `internal/wsconn/*`, `internal/yamuxconn/*`, `internal/socks5/*`, `internal/utlskernel/*`, `internal/zphttp/*`, `internal/wsproto/*` | Browser WebSocket `net.Conn`, yamux streams, SOCKS5 DOMAINNAME CONNECT, uTLS, HTTP/2 and HTTP/1.1 target fetch, target WebSocket upgrade/framing. |
5858
| HTML/header/cookie policy | `internal/htmltx/*`, `internal/headers/*`, `internal/cookiejar/*`, `internal/zpiso/*` | HTML transformation, Phase 2 inline script/event-handler hook points, safe response header constructor policy, target cookie jar, Tor isolation token derivation. |
@@ -109,7 +109,7 @@ The Go WASM kernel exposes `__zp_kernel_init`, `__go_jshttp`, `__zp_stream`, and
109109

110110
`internal/headers.ConstructorPolicy` strips target-controlled policy, storage, network-control, hop-by-hop, redirect, and transformed-body headers before constructing a browser `Response`. It defaults cache behavior to `Cache-Control: no-store`.
111111

112-
`web/runtime-prelude.js` installs hooks for high-risk browser APIs from inside the target realm. Main-window WebSocket, `sendBeacon`, navigation, forms, history/location masking, storage facades, Worker/SharedWorker constructors, service worker registration blocking, high-risk device/network API blockers, and synchronous iframe containment are present. Click navigation handles normal anchors plus script-created elements that carry a URL-valued `href` property, which covers sites that navigate from button click handlers. Main-window `fetch`, XHR, and EventSource currently rely on Service Worker fetch interception instead of dedicated runtime wrappers; worker `fetch` is wrapped by `web/worker-prelude.js` through `/__zp/api/fetch`. The runtime also masks patched function source strings, jitters Canvas/Audio readout APIs, and fixes `speechSynthesis.getVoices()` to a Windows Chrome-shaped list as a basic self-fingerprint defense.
112+
`web/runtime-prelude.js` installs hooks for high-risk browser APIs from inside the target realm. Main-window fetch, XHR, EventSource, WebSocket, `sendBeacon`, navigation, forms, history/location masking, storage facades, Worker/SharedWorker constructors, service worker registration blocking, high-risk device/network API blockers, and synchronous iframe containment are present. Click navigation handles normal anchors, hash-only virtual navigation, and script-created elements that carry a URL-valued `href` property. The runtime also masks patched function source strings for its virtual location and network wrappers where target scripts commonly inspect `toString()`.
113113

114114
Browser `window.location` cannot be made indistinguishable from the target origin from ordinary page JavaScript in a same-origin proxy document: many `Location` properties are browser-owned/unforgeable and the real address bar origin remains the proxy origin. ZeroProxy therefore uses best-effort getter masking plus navigation traps, and treats Service Worker/CSP classification as the security boundary.
115115

@@ -121,7 +121,7 @@ Overall status: **Phase 0 prototype / partial implementation**. The repository i
121121

122122
| PLAN.md section | Current status | Evidence / gap |
123123
|---|---|---|
124-
| 0. Correction directives | Partial | Top-level target document, encrypted `/p` route shape, AES-CBC+HMAC share envelope, limited runtime self-fingerprint masking, and synchronous iframe clean-realm containment are present. Strict `connect-src` is not fully implemented by `web/zp-core.js`; browser direct-egress prevention still needs broader E2E proof. |
124+
| 0. Correction directives | Partial | Top-level target document, encrypted `/p` route shape, AES-CBC+HMAC share envelope, limited runtime self-fingerprint masking, strict `connect-src`, direct-egress browser E2E coverage for current wrappers, and synchronous iframe clean-realm containment are present. Broader adversarial coverage is still required. |
125125
| 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. |
126126
| 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. |
127127
| 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. |
@@ -133,7 +133,7 @@ Overall status: **Phase 0 prototype / partial implementation**. The repository i
133133
| 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. |
134134
| 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`. |
135135
| 11. Phase 0/2 CSP | Mostly implemented | The shell and server apply strict proxy-origin CSP headers. The server's `zeroCSP` and `web/zp-core.js` both restrict `connect-src` to `'self'` plus the proxy WebSocket origin; target responses no longer permit `connect-src *`. Script CSP still carries temporary Phase 2 compatibility exceptions for inline bootstrap and OXC/WASM initialization. |
136-
| 12. Runtime prelude | Partial | WebSocket, `sendBeacon`, navigation/form/history/location, storage, worker, iframe, device blockers, basic patched-function/Canvas/Audio/speech masking, synchronous iframe containment, Phase 2 membrane helpers, fail-closed `Function`/string-timer guards, and dynamic HTML/event-handler neutralization hooks exist. Main-window fetch/XHR/EventSource runtime wrappers are absent; those requests rely on Service Worker fetch interception. XHR/EventSource/WebSocket and fingerprinting-surface fidelity are prototype-level, and direct `location.href` defense relies on layered CSP/SW enforcement where descriptors cannot be replaced. |
136+
| 12. Runtime prelude | Partial | Main-window fetch/XHR/EventSource/WebSocket/sendBeacon, navigation/form/history/location, storage, worker, iframe, device blockers, basic patched-function/Canvas/Audio/speech masking, synchronous iframe containment, Phase 2 membrane helpers, fail-closed `Function`/string-timer guards, and dynamic HTML/event-handler neutralization hooks exist. XHR/EventSource/WebSocket and fingerprinting-surface fidelity remain prototype-level, and direct `location.href` defense relies on layered CSP/SW enforcement where descriptors cannot be replaced. |
137137
| 13. Worker containment | Partial | Worker/SharedWorker constructors, data/blob workers, service worker registration blocking, worklet addModule wrapping, and worker prelude exist. Worker APIs are not all routed with browser-native fidelity; several are blocked. |
138138
| 14. Dynamic iframe containment | Partial | Iframe creation/insertion/src/srcdoc hooks and synchronous `contentWindow`/`contentDocument` containment exist for clean about:blank realms. Broader browser coverage is still needed for adversarial descriptor and navigation edge cases. |
139139
| 15. History/location | Partial | `pushState`, `replaceState`, `popstate`, scroll restore, bound `location.assign`/`replace` navigation helpers, click-time navigation capture, and best-effort getter masking are present. Native `window.location` cannot be fully spoofed to another origin in a standard same-origin proxy document, so browser descriptor edge cases need E2E coverage. |
@@ -146,7 +146,6 @@ Overall status: **Phase 0 prototype / partial implementation**. The repository i
146146
- Active browsing does not switch from `/p/<encrypted>#k=<key>` to PLAN's `/v/<tab-id>/e/<entry-id>` stable history entries. Current active and shared document routes both stay on encrypted `/p` paths.
147147
- New document navigations are not encoded as PLAN's `/v/<tab-id>/n/<base64url_target_url>` transient routes. Static HTML navigation laundering calls `internal/shareurl.New`, so links/forms/frames become fresh encrypted `/p` share routes.
148148
- The PLAN topbar/virtual address bar injection was intentionally removed; `internal/htmltx/topbar.go` documents that target pages stay on `/p` routes while runtime getters mask target location values.
149-
- Main-window fetch/XHR/EventSource runtime wrappers from PLAN §12 are not implemented. Controlled network requests are expected to be caught by the Service Worker fetch handler; worker `fetch` is bridged separately by `worker-prelude.js`.
150149
- PLAN's strict `connect-src 'self' <proxy-websocket-origin>` has been tightened in `web/zp-core.js`; target responses no longer emit `connect-src *`. Continued `'unsafe-eval'` / `'wasm-unsafe-eval'` is a Phase 2 compatibility exception for inline bootstrap and OXC/WASM initialization.
151150
- Target response bodies stream into JavaScript `Response` objects, but request/upload body handling, broader dynamic iframe edge cases, encrypted IndexedDB persistence, and required browser E2E escape tests are not acceptance-grade.
152151

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Implemented core spine:
2020
- Go WASM exports: `__go_jshttp`, `__zp_stream`, `__zp_kernel_init`, and `__zp_cookie_set`.
2121
- 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`.
23-
- 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`. Runtime-to-Service-Worker control messages carry a closure-held per-tab capability token. The runtime also applies basic self-fingerprint masking for patched function source strings, Canvas/Audio extraction jitter, and speech voice lists; broad anti-bot spoofing is not a project goal.
23+
- Runtime containment for main-window `fetch`, XHR, EventSource, WebSocket, `sendBeacon`, navigation, forms, history/location masking, storage facades, workers, iframes, and high-risk device/network APIs. Main-window and worker `fetch` paths are bridged through `/__zp/api/fetch` so strict `connect-src 'self'` does not block target API calls before the Service Worker can route them. Runtime-to-Service-Worker control messages carry a closure-held per-tab capability token. The runtime also applies basic self-fingerprint masking for patched function source strings, Canvas/Audio extraction jitter, and speech voice lists; broad anti-bot spoofing is not a project goal.
2424
- Phase 2 JavaScript rewriting is wired through an OXC parser/WASM service: target-response CSP no longer permits `connect-src *`, external and inline script sources are parsed before execution, dangerous global/window/location access is rewritten to runtime membrane helpers, parse/transform failures fail closed, and dynamic compilation paths such as `Function`, constructor-constructor escapes, string timers, and blob/data worker scripts are blocked when they cannot be rewritten synchronously.
2525
- Relay server static asset service and `/__zp/ws-pipe` WebSocket endpoint.
2626
- Go and JavaScript share URL implementations that use the same envelope format.

cmd/zeroproxy-server/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,5 +281,5 @@ func zeroCSP(r *http.Request) string {
281281
if host == "" {
282282
host = "proxy.example"
283283
}
284-
return "default-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'; style-src * 'unsafe-inline' blob: data:; img-src * blob: data:; font-src * blob: data:; media-src * blob: data:; connect-src 'self' " + wsScheme + host + "; frame-src 'self' blob: data:; child-src 'self' blob: data:; worker-src 'self' blob:; object-src 'none'; base-uri 'none'; form-action 'self'; navigate-to 'self'; manifest-src 'self'"
284+
return "default-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'; style-src * 'unsafe-inline' blob: data:; img-src * blob: data:; font-src * blob: data:; media-src * blob: data:; connect-src 'self' " + wsScheme + host + "; frame-src 'self' blob: data:; child-src 'self' blob: data:; worker-src 'self' blob:; object-src 'none'; base-uri 'none'; form-action 'self'; manifest-src 'self'"
285285
}

test/e2e/proxy.test.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -638,12 +638,20 @@ test('browser traffic uses test SOCKS5 and covers proxied runtime integrations',
638638
button.click();
639639
out.eventHandlerLocation = window.__eventHandlerLocation || '';
640640
button.remove();
641+
const loc = __zp_get(globalThis, 'window').location;
642+
out.locationReplaceSource = loc.replace.toString();
643+
loc.hash = '#zp-fragment';
644+
out.virtualHash = loc.hash;
645+
out.virtualHref = loc.href;
641646
return out;
642647
}, targetPort);
643-
assert.match(escapeMatrix.fetch, /^(ok:\d+|blocked:)/);
644-
assert.match(escapeMatrix.xhr, /^(ok:\d+|blocked:)/);
645-
assert.match(escapeMatrix.eventSource, /^(ok:sse-ok|blocked:)/);
648+
assert.equal(escapeMatrix.fetch, 'ok:404');
649+
assert.equal(escapeMatrix.xhr, 'ok:404');
650+
assert.equal(escapeMatrix.eventSource, 'ok:sse-ok');
646651
assert.equal(escapeMatrix.websocket, 'echo:direct');
652+
assert.equal(escapeMatrix.locationReplaceSource, 'function replace() { [native code] }');
653+
assert.equal(escapeMatrix.virtualHash, '#zp-fragment');
654+
assert.match(escapeMatrix.virtualHref, /#zp-fragment$/);
647655
assert.equal(escapeMatrix.stringTimer, 'Blocked by ZeroProxy policy');
648656
assert.notEqual(escapeMatrix.blobWorker, 'ran');
649657
assert.notEqual(escapeMatrix.dataWorker, 'ran');

test/js/compat-pipeline.test.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ const fs = require('node:fs');
44

55
const read = path => fs.readFileSync(path, 'utf8');
66

7-
test('window fetch, XHR, and EventSource are not replaced by runtime transport shims', () => {
7+
test('window fetch, XHR, and EventSource route through runtime transport shims', () => {
88
const rt = read('web/runtime-prelude.js');
9-
assert.equal(rt.includes("define(root, 'fetch'"), false);
10-
assert.equal(rt.includes("define(root, 'XMLHttpRequest'"), false);
11-
assert.equal(rt.includes("define(root, 'EventSource'"), false);
12-
assert.equal(rt.includes('ZPXMLHttpRequest'), false);
13-
assert.equal(rt.includes('/__zp/api/fetch'), false);
14-
assert.match(rt, /Native\.fetch\(target\.href/);
9+
assert.match(rt, /define\(root, 'fetch'/);
10+
assert.match(rt, /define\(root, 'XMLHttpRequest'/);
11+
assert.match(rt, /define\(root, 'EventSource'/);
12+
assert.ok(rt.includes('ZPXMLHttpRequest'));
13+
assert.ok(rt.includes('ZPEventSource'));
14+
assert.ok(rt.includes('/__zp/api/fetch'));
15+
assert.match(rt, /fetchThroughRuntime\(target\.href/);
1516
});
1617

1718
test('runtime navigation uses bound Location methods and catches expando href clicks', () => {

test/js/static-policy.test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ test('phase 2 script rewriting pipeline is fail-closed', () => {
139139
assert.equal(/script-src \*/.test(core), false);
140140
assert.equal(/script-src \*/.test(server), false);
141141
assert.match(server, /connect-src 'self'/);
142+
assert.equal(core.includes('navigate-to'), false);
143+
assert.equal(server.includes('navigate-to'), false);
142144
assert.ok(sw.includes('MAX_REQUEST_BODY_BYTES'));
143145
assert.ok(sw.includes('REQUEST_BODY_TOO_LARGE'));
144146
assert.ok(fs.readFileSync('internal/swhttp/bridge_js.go', 'utf8').includes('GetBody'));

web/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<!doctype html>
22
<meta charset="utf-8">
33
<meta name="viewport" content="width=device-width,initial-scale=1">
4-
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; connect-src 'self'; form-action 'self'; navigate-to 'self'; base-uri 'none'; object-src 'none'">
4+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; connect-src 'self'; form-action 'self'; base-uri 'none'; object-src 'none'">
55
<title>ZeroProxy</title>
66
<style>
77
body{font:16px system-ui,sans-serif;margin:2rem;max-width:52rem} form{display:flex;gap:.5rem} input{flex:1;padding:.6rem} button{padding:.6rem .9rem}.err{color:#991b1b}

web/js-rewriter.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@
197197
if (node.type !== 'MemberExpression' && node.type !== 'OptionalMemberExpression') return null;
198198
const name = propName(node.property, node.computed);
199199
if (name === 'location' && isVirtualWindowExpr(node.object)) return { base: render(node.object), prop: propCode(node.property, node.computed) };
200-
if (name === 'href' && isVirtualLocationExpr(node.object)) return { base: render(node.object), prop: propCode(node.property, node.computed) };
200+
if ((name === 'href' || name === 'hash') && isVirtualLocationExpr(node.object)) return { base: render(node.object), prop: propCode(node.property, node.computed) };
201201
if (node.computed && (isVirtualWindowExpr(node.object) || isVirtualLocationExpr(node.object))) return { base: render(node.object), prop: propCode(node.property, node.computed) };
202202
return null;
203203
}

0 commit comments

Comments
 (0)