Skip to content

Commit d1e9cda

Browse files
committed
feat(worker): enhance worker prelude with new proxy methods and link handling
1 parent f46d5ed commit d1e9cda

3 files changed

Lines changed: 191 additions & 19 deletions

File tree

test/e2e/proxy.test.js

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,28 @@ function createTargetServer(requests) {
131131
dynamicScript.id = 'dynamic-script-probe';
132132
dynamicScript.src = '/dynamic-script.js?from=createElement';
133133
document.head.appendChild(dynamicScript);
134+
try {
135+
const template = document.createElement('template');
136+
template.innerHTML = '<link rel="preconnect" href="https://preconnect.invalid">';
137+
const first = template.content.firstChild;
138+
const clone = first && first.cloneNode(true);
139+
if (clone) document.head.appendChild(clone);
140+
window.__templateLinkFixture = {
141+
childCount: template.content.childNodes.length,
142+
firstNode: first && first.localName,
143+
rel: first && first.getAttribute('rel'),
144+
href: first && first.getAttribute('href'),
145+
blockedRel: first && first.getAttribute('data-zp-blocked-rel'),
146+
blockedURL: first && first.getAttribute('data-zp-blocked-url'),
147+
cloneRel: clone && clone.getAttribute('rel'),
148+
cloneHref: clone && clone.getAttribute('href')
149+
};
150+
} catch (err) {
151+
window.__templateLinkFixture = { error: err && err.message || String(err) };
152+
}
134153
</script>
135154
<script src="/rewrite-fixture.js"></script>
155+
<script type="module" src="/module-worker.js"></script>
136156
</body></html>`);
137157
return;
138158
}
@@ -165,6 +185,18 @@ function createTargetServer(requests) {
165185
};`);
166186
return;
167187
}
188+
if (url.pathname === '/module-worker.js') {
189+
res.writeHead(200, { 'Content-Type': 'text/javascript; charset=utf-8', 'Cache-Control': 'no-store' });
190+
res.end(`const worker = new Worker(new URL('/worker-fixture.js', import.meta.url).href, { name: 'module-worker-fixture' });
191+
worker.onmessage = ev => { window.__moduleWorkerFixture = ev.data; worker.terminate(); };
192+
worker.onerror = ev => { window.__moduleWorkerFixture = { error: ev && ev.message || 'worker-error' }; };`);
193+
return;
194+
}
195+
if (url.pathname === '/worker-fixture.js') {
196+
res.writeHead(200, { 'Content-Type': 'text/javascript; charset=utf-8', 'Cache-Control': 'no-store' });
197+
res.end(`postMessage({ loaded: true, href: location.href, userAgent: navigator.userAgent, platform: navigator.platform });`);
198+
return;
199+
}
168200
if (url.pathname === '/frame-child') {
169201
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
170202
res.end(`<!doctype html><html><body><p>frame child</p><script>
@@ -452,6 +484,7 @@ test('browser traffic uses internal SOCKS5 mode and covers proxied runtime integ
452484
userAgent: navigator.userAgent,
453485
appVersion: navigator.appVersion,
454486
platform: navigator.platform,
487+
templateLink: window.__templateLinkFixture,
455488
phase2Location: window.__phase2Location,
456489
phase2DynamicFunction: window.__phase2DynamicFunction,
457490
phase2EvalLocation: window.__phase2EvalLocation,
@@ -461,6 +494,16 @@ test('browser traffic uses internal SOCKS5 mode and covers proxied runtime integ
461494
assert.equal(home.shellVisible, false);
462495
assert.equal(home.userAgent, TARGET_UA);
463496
assert.equal(home.appVersion, TARGET_UA.replace(/^Mozilla\//, ''));
497+
assert.deepEqual(home.templateLink, {
498+
childCount: 1,
499+
firstNode: 'link',
500+
rel: null,
501+
href: null,
502+
blockedRel: 'preconnect',
503+
blockedURL: 'https://preconnect.invalid',
504+
cloneRel: null,
505+
cloneHref: null,
506+
});
464507
assert.equal(home.platform, 'Win32');
465508
assert.match(home.href, new RegExp(`^http://proxy\\.localhost:${proxyPort}/p/`));
466509
assert.deepEqual(home.phase2Location, { href: `http://${targetHost}:${targetPort}/`, windowHref: `http://${targetHost}:${targetPort}/` });
@@ -480,11 +523,12 @@ test('browser traffic uses internal SOCKS5 mode and covers proxied runtime integ
480523
assert.equal(rewriteAdvanced.compoundHref, `http://${targetHost}:${targetPort}/#compound-tail`);
481524
assert.ok(requests.some(r => r.upgrade && r.url === '/ws' && r.protocol === 'zp-rewrite' && r.userAgent === TARGET_UA), `target requests: ${JSON.stringify(requests)}`);
482525
try {
483-
await page.waitForFunction(() => window.__gtmFixture && window.__gtmFixture.loaded && window.__dynamicScriptLoaded && window.__dynamicScriptLoaded.loaded, { timeout: 30000 });
526+
await page.waitForFunction(() => window.__gtmFixture && window.__gtmFixture.loaded && window.__dynamicScriptLoaded && window.__dynamicScriptLoaded.loaded && window.__moduleWorkerFixture && window.__moduleWorkerFixture.loaded, { timeout: 30000 });
484527
} catch (err) {
485528
const state = await page.evaluate(() => ({
486529
gtm: window.__gtmFixture || null,
487530
dynamic: window.__dynamicScriptLoaded || null,
531+
moduleWorker: window.__moduleWorkerFixture || null,
488532
scripts: Array.from(document.scripts).map(s => ({ id: s.id, src: s.attributes.getNamedItem('src')?.value || '', type: s.type || '', blocked: s.hasAttribute('data-zp-blocked-script') })),
489533
messages: window.__messageEvents || [],
490534
}));
@@ -493,19 +537,25 @@ test('browser traffic uses internal SOCKS5 mode and covers proxied runtime integ
493537
const dynamicScripts = await page.evaluate(() => ({
494538
gtm: window.__gtmFixture,
495539
dynamic: window.__dynamicScriptLoaded,
540+
moduleWorker: window.__moduleWorkerFixture,
496541
gtmAttr: document.getElementById('gtm-fixture')?.attributes.getNamedItem('src')?.value || '',
497542
dynamicAttr: document.getElementById('dynamic-script-probe')?.attributes.getNamedItem('src')?.value || '',
498543
messages: window.__messageEvents || [],
499544
}));
500545
assert.ok(dynamicScripts.gtm.href.startsWith(`http://${targetHost}:${targetPort}/`), dynamicScripts.gtm.href);
501546
assert.ok(dynamicScripts.dynamic.href.startsWith(`http://${targetHost}:${targetPort}/`), dynamicScripts.dynamic.href);
502547
assert.match(dynamicScripts.gtm.currentAttr, /^\/__zp\/api\/script\?/);
548+
assert.equal(dynamicScripts.moduleWorker.href, `http://${targetHost}:${targetPort}/worker-fixture.js`);
549+
assert.equal(dynamicScripts.moduleWorker.userAgent, TARGET_UA);
550+
assert.equal(dynamicScripts.moduleWorker.platform, 'Win32');
503551
assert.match(dynamicScripts.dynamic.currentAttr, /^\/__zp\/api\/script\?/);
504552
assert.match(dynamicScripts.gtmAttr, /^\/__zp\/api\/script\?/);
505553
assert.match(dynamicScripts.dynamicAttr, /^\/__zp\/api\/script\?/);
506554
assert.ok(dynamicScripts.messages.some(m => m.type === 'gtm-loaded'), `messages: ${JSON.stringify(dynamicScripts.messages)}`);
507555
assert.ok(requests.some(r => r.url.startsWith('/gtm.js') && r.userAgent === TARGET_UA), `target requests: ${JSON.stringify(requests)}`);
508556
assert.ok(requests.some(r => r.url.startsWith('/dynamic-script.js') && r.userAgent === TARGET_UA), `target requests: ${JSON.stringify(requests)}`);
557+
assert.ok(requests.some(r => r.url.startsWith('/module-worker.js') && r.userAgent === TARGET_UA), `target requests: ${JSON.stringify(requests)}`);
558+
assert.ok(requests.some(r => r.url.startsWith('/worker-fixture.js') && r.userAgent === TARGET_UA), `target requests: ${JSON.stringify(requests)}`);
509559

510560
const iframeIsolation = await page.evaluate(async target => {
511561
const blockedByPolicy = fn => {

web/runtime-prelude.js

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1226,15 +1226,26 @@
12261226
return false;
12271227
}
12281228
function isBlockedLink(el) { return el && el.localName === 'link' && isBlockedLinkRelValue(Native.getAttribute.call(el, 'rel') || ''); }
1229+
function hasSuppressedBlockedLinkRel(el) { return el && el.localName === 'link' && isBlockedLinkRelValue(Native.getAttribute.call(el, 'data-zp-blocked-rel') || ''); }
1230+
function suppressBlockedLinkRel(el, rawRel) {
1231+
Native.setAttribute.call(el, 'data-zp-blocked-rel', String(rawRel));
1232+
if (Native.removeAttribute) Native.removeAttribute.call(el, 'rel');
1233+
blockLinkURL(el, Native.getAttribute.call(el, 'href') || '');
1234+
}
12291235
function blockLinkURL(el, raw) {
12301236
if (raw != null && String(raw) !== '') Native.setAttribute.call(el, 'data-zp-blocked-url', String(raw));
12311237
urlMeta.delete(el);
12321238
if (Native.removeAttribute) Native.removeAttribute.call(el, 'href');
12331239
}
12341240
function enforceLinkPolicy(el) {
1235-
if (!isBlockedLink(el)) return;
1236-
blockLinkURL(el, Native.getAttribute.call(el, 'href') || '');
1237-
Native.setAttribute.call(el, 'data-zp-blocked-rel', Native.getAttribute.call(el, 'rel') || '');
1241+
if (!el || el.localName !== 'link') return;
1242+
const rel = Native.getAttribute.call(el, 'rel') || '';
1243+
if (isBlockedLinkRelValue(rel)) {
1244+
suppressBlockedLinkRel(el, rel);
1245+
return;
1246+
}
1247+
if (rel && Native.removeAttribute) Native.removeAttribute.call(el, 'data-zp-blocked-rel');
1248+
if (hasSuppressedBlockedLinkRel(el)) blockLinkURL(el, Native.getAttribute.call(el, 'href') || '');
12381249
}
12391250
function isNavigationTargetElement(el) {
12401251
const tag = el && el.localName;
@@ -1251,8 +1262,13 @@
12511262
const localKey = attrLocalName(key);
12521263
if (key === 'integrity' && isIntegrityBearing(this)) return setBackedIntegrity(this, v);
12531264
if (localKey === 'target' && isNavigationTargetElement(this)) return setSafeNavigationTarget(this, k, v);
1254-
if (this.localName === 'link' && localKey === 'rel') { const ret = Native.setAttribute.call(this, k, v); enforceLinkPolicy(this); return ret; }
1255-
if (this.localName === 'link' && localKey === 'href' && isBlockedLink(this)) return blockLinkURL(this, v);
1265+
if (this.localName === 'link' && localKey === 'rel') {
1266+
const value = String(v);
1267+
if (isBlockedLinkRelValue(value)) return suppressBlockedLinkRel(this, value);
1268+
if (Native.removeAttribute) Native.removeAttribute.call(this, 'data-zp-blocked-rel');
1269+
return Native.setAttribute.call(this, k, v);
1270+
}
1271+
if (this.localName === 'link' && localKey === 'href' && (isBlockedLink(this) || hasSuppressedBlockedLinkRel(this))) return blockLinkURL(this, v);
12561272
if (key.startsWith('on') && key.length > 2) return Native.setAttribute.call(this, k, rewriteEventAttribute(String(v)));
12571273
if (this.localName === 'base' && localKey === 'href') {
12581274
updateVirtualBase(v);
@@ -1392,21 +1408,33 @@
13921408
}
13931409
function installLinkProp(proto) {
13941410
if (!proto) return;
1395-
const d = propertyDescriptor(proto, 'href');
1396-
if (!d || !d.get) return;
1397-
try {
1411+
const hrefDescriptor = propertyDescriptor(proto, 'href');
1412+
if (hrefDescriptor && hrefDescriptor.get) try {
13981413
Object.defineProperty(proto, 'href', {
1399-
get() { return urlMeta.get(this) || Native.getAttribute.call(this, 'data-zp-target-url') || d.get.call(this); },
1414+
get() { return urlMeta.get(this) || Native.getAttribute.call(this, 'data-zp-target-url') || hrefDescriptor.get.call(this); },
14001415
set(v) {
1401-
if (isBlockedLink(this)) return blockLinkURL(this, v);
1416+
if (isBlockedLink(this) || hasSuppressedBlockedLinkRel(this)) return blockLinkURL(this, v);
14021417
const value = String(v);
14031418
if (hasExecutableURLScheme(value)) return blockExecutableURL(this, 'href', value);
14041419
if (isHTTPURL(value)) {
14051420
const t = targetURL(value);
14061421
urlMeta.set(this, t);
14071422
Native.setAttribute.call(this, 'data-zp-target-url', t);
14081423
}
1409-
return d.set.call(this, value);
1424+
return hrefDescriptor.set ? hrefDescriptor.set.call(this, value) : Native.setAttribute.call(this, 'href', value);
1425+
},
1426+
configurable: false
1427+
});
1428+
} catch {}
1429+
const relDescriptor = propertyDescriptor(proto, 'rel');
1430+
if (relDescriptor && relDescriptor.get) try {
1431+
Object.defineProperty(proto, 'rel', {
1432+
get() { return relDescriptor.get.call(this); },
1433+
set(v) {
1434+
const value = String(v);
1435+
if (isBlockedLinkRelValue(value)) return suppressBlockedLinkRel(this, value);
1436+
if (Native.removeAttribute) Native.removeAttribute.call(this, 'data-zp-blocked-rel');
1437+
return relDescriptor.set ? relDescriptor.set.call(this, value) : Native.setAttribute.call(this, 'rel', value);
14101438
},
14111439
configurable: false
14121440
});
@@ -1419,7 +1447,32 @@
14191447
}
14201448
function isSVGURLBearing(el, key) { return el && el.namespaceURI === 'http://www.w3.org/2000/svg' && attrLocalName(key) === 'href' && /^(a|image|use|script)$/.test(el.localName || ''); }
14211449
function isURLBearing(el, key) { const tag = el.localName; const localKey = attrLocalName(key); return localKey === 'href' && (tag === 'a' || tag === 'area' || isSVGURLBearing(el, key)) || localKey === 'action' && tag === 'form' || localKey === 'formaction' && (tag === 'input' || tag === 'button') || localKey === 'src' && (tag === 'iframe' || tag === 'frame' || tag === 'script'); }
1422-
function transformHTML(s) { return String(s).replace(/<base\b[^>]*\shref=(["'])([\s\S]*?)\1[^>]*>/ig, (_, q, href) => baseSyncScript(href)).replace(/<link\b(?=[^>]*\brel\s*=\s*(?:"[^"]*(?:modulepreload|preload|prefetch|preconnect|dns-prefetch|prerender|manifest)[^"]*"|'[^']*(?:modulepreload|preload|prefetch|preconnect|dns-prefetch|prerender|manifest)[^']*'|[^\s>]*(?:modulepreload|preload|prefetch|preconnect|dns-prefetch|prerender|manifest)[^\s>]*))[^>]*>/ig, '').replace(/(<iframe\b[^>]*\ssrcdoc=["'])([\s\S]*?)(["'])/ig, (_, p, h, q) => p + injectSrcdoc(h).replace(/"/g,'&quot;') + q).replace(/<script\b/ig, '<script type="application/x-zeroproxy-blocked" data-zp-blocked-script').replace(/\sintegrity\s*=\s*(\"[^\"]*\"|'[^']*'|[^\s>]+)/ig, (_, value) => ' ' + integrityBackupAttr + '=' + value).replace(/\s(on[a-z0-9_:-]+)\s*=\s*(\"[^\"]*\"|'[^']*'|[^\s>]+)/ig, (_, name, value) => ' data-zp-blocked-' + name.toLowerCase() + '=' + value); }
1450+
const blockedLinkTagRE = /<link\b(?=[^>]*\brel\s*=\s*(?:"[^"]*(?:modulepreload|preload|prefetch|preconnect|dns-prefetch|prerender|manifest)[^"]*"|'[^']*(?:modulepreload|preload|prefetch|preconnect|dns-prefetch|prerender|manifest)[^']*'|[^\s>]*(?:modulepreload|preload|prefetch|preconnect|dns-prefetch|prerender|manifest)[^\s>]*))[^>]*>/ig;
1451+
function escapeHTMLAttrValue(value) {
1452+
return String(value).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1453+
}
1454+
function tagAttrValue(tag, name) {
1455+
const m = new RegExp("(?:^|\\s)" + name + "\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s\"'>`=]+))", 'i').exec(tag);
1456+
return m ? m[1] ?? m[2] ?? m[3] ?? '' : '';
1457+
}
1458+
function rewriteBlockedLinkTag(tag) {
1459+
const href = tagAttrValue(tag, 'href');
1460+
const rel = tagAttrValue(tag, 'rel');
1461+
let out = tag.replace(/\s(?:href|rel|data-zp-blocked-url|data-zp-blocked-rel)\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'>`=]+)/ig, '');
1462+
out = out.replace(/\s*\/?>$/, '');
1463+
if (href) out += ' data-zp-blocked-url="' + escapeHTMLAttrValue(href) + '"';
1464+
if (rel) out += ' data-zp-blocked-rel="' + escapeHTMLAttrValue(rel) + '"';
1465+
return out + '>';
1466+
}
1467+
function transformHTML(s) {
1468+
return String(s)
1469+
.replace(/<base\b[^>]*\shref=(["'])([\s\S]*?)\1[^>]*>/ig, (_, q, href) => baseSyncScript(href))
1470+
.replace(blockedLinkTagRE, rewriteBlockedLinkTag)
1471+
.replace(/(<iframe\b[^>]*\ssrcdoc=["'])([\s\S]*?)(["'])/ig, (_, p, h, q) => p + injectSrcdoc(h).replace(/"/g,'&quot;') + q)
1472+
.replace(/<script\b/ig, '<script type="application/x-zeroproxy-blocked" data-zp-blocked-script')
1473+
.replace(/\sintegrity\s*=\s*(\"[^\"]*\"|'[^']*'|[^\s>]+)/ig, (_, value) => ' ' + integrityBackupAttr + '=' + value)
1474+
.replace(/\s(on[a-z0-9_:-]+)\s*=\s*(\"[^\"]*\"|'[^']*'|[^\s>]+)/ig, (_, name, value) => ' data-zp-blocked-' + name.toLowerCase() + '=' + value);
1475+
}
14231476
function injectSrcdoc(s) { return '<script src="/__zp/zp-core.js"><\/script><script id="__zp-boot" type="application/json">' + bootJSON() + '<\/script><script src="/__zp/runtime-prelude.js"><\/script>' + transformHTML(String(s)); }
14241477
function bootJSON() { return JSON.stringify(boot).replace(/[<>&]/g, c => c === '<' ? '\\u003c' : c === '>' ? '\\u003e' : '\\u0026'); }
14251478
function baseSyncScript(raw) { return '<script>window.__ZP_SET_BASE&&window.__ZP_SET_BASE(' + JSON.stringify(String(raw)).replace(/</g,'\\u003c') + ');<\/script>'; }
@@ -1549,7 +1602,7 @@
15491602
return parsed.href;
15501603
}
15511604
if (parsed.protocol === 'data:') return dataWorkerURL(parsed.href);
1552-
return '/__zp/worker-bootstrap.js#u=' + encodeURIComponent(targetURL(raw)) + '&tab=' + encodeURIComponent(boot.tabId);
1605+
return '/__zp/worker-bootstrap.js#u=' + encodeURIComponent(requestTargetURL(raw)) + '&tab=' + encodeURIComponent(boot.tabId);
15531606
}
15541607
function dataWorkerURL(raw) {
15551608
const comma = raw.indexOf(',');

0 commit comments

Comments
 (0)