Skip to content

Commit 3e51422

Browse files
committed
feat: enhance link handling in HTML transformation and add style probe to E2E tests
1 parent 1c07e90 commit 3e51422

3 files changed

Lines changed: 44 additions & 4 deletions

File tree

internal/htmltx/transform.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,18 @@ func rewriteToken(tok xhtml.Token, opt Options) xhtml.Token {
222222
attrs = append(attrs, a)
223223
continue
224224
}
225+
if tag == "link" && key == "href" {
226+
trimmed := strings.TrimSpace(a.Val)
227+
if target, ok := resolveTargetURL(a.Val, opt); ok {
228+
a.Val = target
229+
dataTarget = target
230+
} else if trimmed != "" && hasExecutableURLScheme(trimmed) {
231+
a.Val = shareurl.ControlPrefix + "error/POLICY_BLOCKED"
232+
attrs = append(attrs, xhtml.Attribute{Key: "data-zp-blocked-url", Val: trimmed})
233+
}
234+
attrs = append(attrs, a)
235+
continue
236+
}
225237
if strings.HasPrefix(key, "on") && len(key) > 2 {
226238
a.Val = rewriteEventHandler(a.Val, opt)
227239
attrs = append(attrs, a)
@@ -317,6 +329,22 @@ func isASCIIDigit(c byte) bool {
317329
return '0' <= c && c <= '9'
318330
}
319331

332+
func resolveTargetURL(raw string, opt Options) (target string, ok bool) {
333+
s := strings.TrimSpace(raw)
334+
if s == "" || strings.HasPrefix(s, "#") || hasExecutableURLScheme(s) {
335+
return "", false
336+
}
337+
u, err := url.Parse(s)
338+
if err != nil {
339+
return "", false
340+
}
341+
abs := opt.TargetURL.ResolveReference(u)
342+
if abs.Scheme != "http" && abs.Scheme != "https" {
343+
return "", false
344+
}
345+
return abs.String(), true
346+
}
347+
320348
func wrapAttrURL(raw string, opt Options, nav bool) (wrapped, target string, ok bool) {
321349
s := strings.TrimSpace(raw)
322350
if s == "" || strings.HasPrefix(s, "#") {

internal/htmltx/transform_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,12 @@ func TestTransformStripsIntegrityButBacksUpForRuntimeMasking(t *testing.T) {
133133
t.Fatal(err)
134134
}
135135
s := string(out)
136-
for _, want := range []string{`data-zp-integrity="sha384-script"`, `data-zp-integrity="sha256-style"`, `/zp/api/script?`} {
136+
for _, want := range []string{`data-zp-integrity="sha384-script"`, `data-zp-integrity="sha256-style"`, `/zp/api/script?`, `href="https://example.com/app.css"`, `data-zp-target-url="https://example.com/app.css"`} {
137137
if !strings.Contains(s, want) {
138138
t.Fatalf("missing %q in %s", want, s)
139139
}
140140
}
141-
for _, forbidden := range []string{` integrity="sha384-script"`, ` integrity="sha256-style"`, `data-zp-integrity="attacker"`} {
141+
for _, forbidden := range []string{` integrity="sha384-script"`, ` integrity="sha256-style"`, `data-zp-integrity="attacker"`, `href="/app.css"`} {
142142
if strings.Contains(s, forbidden) {
143143
t.Fatalf("forbidden integrity marker %q remained in %s", forbidden, s)
144144
}

test/e2e/proxy.test.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@ function createTargetServer(requests) {
119119
const url = new URL(req.url, 'http://target.local');
120120
if (url.pathname === '/') {
121121
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
122-
res.end(`<!doctype html><html><head><title>E2E Home</title></head><body>
123-
<main><h1>E2E Home</h1><a id="next" href="/next">Next page</a></main>
122+
res.end(`<!doctype html><html><head><title>E2E Home</title><link rel="stylesheet" href="/site.css"></head><body>
123+
<main id="style-probe" class="root-stylesheet-probe"><h1>E2E Home</h1><a id="next" href="/next">Next page</a></main>
124124
<script>
125125
window.__ua = navigator.userAgent;
126126
window.__platform = navigator.platform;
@@ -179,6 +179,11 @@ function createTargetServer(requests) {
179179
</body></html>`);
180180
return;
181181
}
182+
if (url.pathname === '/site.css') {
183+
res.writeHead(200, { 'Content-Type': 'text/css; charset=utf-8', 'Cache-Control': 'no-store' });
184+
res.end(`.root-stylesheet-probe{border-top:7px solid rgb(12, 34, 56); padding-left:13px}`);
185+
return;
186+
}
182187
if (url.pathname === '/gtm.js') {
183188
res.writeHead(200, { 'Content-Type': 'text/javascript; charset=utf-8', 'Cache-Control': 'no-store' });
184189
res.end(`window.__gtmFixture = {
@@ -570,6 +575,11 @@ test('browser traffic uses internal SOCKS5 mode and covers proxied runtime integ
570575
phase2Location: window.__phase2Location,
571576
phase2DynamicFunction: window.__phase2DynamicFunction,
572577
phase2EvalLocation: window.__phase2EvalLocation,
578+
styleProbe: (() => {
579+
const el = document.getElementById('style-probe');
580+
const cs = el && getComputedStyle(el);
581+
return cs && { borderTopWidth: cs.borderTopWidth, borderTopColor: cs.borderTopColor, paddingLeft: cs.paddingLeft };
582+
})(),
573583
}));
574584
assert.equal(home.title, 'E2E Home');
575585
assert.match(home.hash, /^#k=/);
@@ -591,6 +601,8 @@ test('browser traffic uses internal SOCKS5 mode and covers proxied runtime integ
591601
assert.deepEqual(home.phase2Location, { href: `http://${targetHost}:${targetPort}/`, windowHref: `http://${targetHost}:${targetPort}/` });
592602
assert.equal(home.phase2DynamicFunction, `http://${targetHost}:${targetPort}/`);
593603
assert.equal(home.phase2EvalLocation, `http://${targetHost}:${targetPort}/`);
604+
assert.deepEqual(home.styleProbe, { borderTopWidth: '7px', borderTopColor: 'rgb(12, 34, 56)', paddingLeft: '13px' });
605+
assert.ok(requests.some(r => r.url === '/site.css' && r.userAgent === TARGET_UA), `target requests: ${JSON.stringify(requests)}`);
594606
assert.ok(requests.some(r => r.url === '/' && r.userAgent === TARGET_UA), `target requests: ${JSON.stringify(requests)}`);
595607
await page.waitForFunction(() => window.__rewriteAdvanced && window.__rewriteAdvanced.wsMessage === 'echo:rewrite-script', { timeout: 30000 });
596608
const rewriteAdvanced = await page.evaluate(() => window.__rewriteAdvanced);

0 commit comments

Comments
 (0)