Merge pull request #532 from alectimison-maker/fix/dynamic-script-data-urls
fix(js): execute dynamic data URL scripts
This commit is contained in:
2 files changed
+186
-5
No files matched your search
@@ -117,3 +117,134 @@ async fn dynamic_external_scripts_execute_and_fire_load() {
|
|||||||
"dynamic scripts must execute and fire load before navigation settles"
|
"dynamic scripts must execute and fire load before navigation settles"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn dynamic_data_scripts_execute_before_chained_load_handlers() {
|
||||||
|
let mut ctx = CdpContext::new();
|
||||||
|
let page_id = ctx.create_page();
|
||||||
|
let session_id = "session-1";
|
||||||
|
ctx.sessions.insert(session_id.to_string(), page_id);
|
||||||
|
|
||||||
|
cdp(
|
||||||
|
&mut ctx,
|
||||||
|
1,
|
||||||
|
"Page.navigate",
|
||||||
|
json!({
|
||||||
|
"url": "data:text/html,<html><head></head><body></body></html>",
|
||||||
|
"waitUntil": "load"
|
||||||
|
}),
|
||||||
|
session_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cdp(
|
||||||
|
&mut ctx,
|
||||||
|
2,
|
||||||
|
"Runtime.evaluate",
|
||||||
|
json!({
|
||||||
|
"expression": r#"(function () {
|
||||||
|
var state = {
|
||||||
|
aExec: false,
|
||||||
|
aLoad: false,
|
||||||
|
bExec: false,
|
||||||
|
bLoad: false,
|
||||||
|
cExec: false,
|
||||||
|
cLoad: false
|
||||||
|
};
|
||||||
|
window.__dataScriptState = state;
|
||||||
|
|
||||||
|
var a = document.createElement('script');
|
||||||
|
a.src = 'data:,window.__dataScriptState.aExec=true';
|
||||||
|
a.onload = function () {
|
||||||
|
state.aLoad = true;
|
||||||
|
var b = document.createElement('script');
|
||||||
|
b.src = 'data:text/html,' + encodeURIComponent('window.__dataScriptState.bExec=true');
|
||||||
|
b.onload = function () {
|
||||||
|
state.bLoad = true;
|
||||||
|
var c = document.createElement('script');
|
||||||
|
c.src = 'data:text/javascript;base64,' +
|
||||||
|
btoa('window.__dataScriptState.cExec=true').replace(/=+$/, '') +
|
||||||
|
'#ignored-fragment';
|
||||||
|
c.onload = function () { state.cLoad = true; };
|
||||||
|
document.head.appendChild(c);
|
||||||
|
};
|
||||||
|
document.head.appendChild(b);
|
||||||
|
};
|
||||||
|
document.head.appendChild(a);
|
||||||
|
return 'kicked';
|
||||||
|
})()"#,
|
||||||
|
"returnByValue": true,
|
||||||
|
}),
|
||||||
|
session_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
ctx.pages[0].settle(500).await;
|
||||||
|
|
||||||
|
let result = cdp(
|
||||||
|
&mut ctx,
|
||||||
|
3,
|
||||||
|
"Runtime.evaluate",
|
||||||
|
json!({
|
||||||
|
"expression": "JSON.stringify(window.__dataScriptState)",
|
||||||
|
"returnByValue": true,
|
||||||
|
}),
|
||||||
|
session_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(
|
||||||
|
result["result"]["value"],
|
||||||
|
r#"{"aExec":true,"aLoad":true,"bExec":true,"bLoad":true,"cExec":true,"cLoad":true}"#,
|
||||||
|
"data URL script bodies must execute before each chained load handler"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn invalid_dynamic_data_script_fires_error_not_load() {
|
||||||
|
let mut ctx = CdpContext::new();
|
||||||
|
let page_id = ctx.create_page();
|
||||||
|
let session_id = "session-1";
|
||||||
|
ctx.sessions.insert(session_id.to_string(), page_id);
|
||||||
|
|
||||||
|
cdp(
|
||||||
|
&mut ctx,
|
||||||
|
1,
|
||||||
|
"Page.navigate",
|
||||||
|
json!({"url": "data:text/html,<html><head></head><body></body></html>", "waitUntil": "load"}),
|
||||||
|
session_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cdp(
|
||||||
|
&mut ctx,
|
||||||
|
2,
|
||||||
|
"Runtime.callFunctionOn",
|
||||||
|
json!({
|
||||||
|
"functionDeclaration": r#"function () {
|
||||||
|
window.__invalidDataScript = { error: false, load: false };
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.src = 'data:text/javascript;base64,!';
|
||||||
|
script.onerror = function () { window.__invalidDataScript.error = true; };
|
||||||
|
script.onload = function () { window.__invalidDataScript.load = true; };
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}"#,
|
||||||
|
"awaitPromise": true,
|
||||||
|
}),
|
||||||
|
session_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||||
|
let result = cdp(
|
||||||
|
&mut ctx,
|
||||||
|
3,
|
||||||
|
"Runtime.evaluate",
|
||||||
|
json!({
|
||||||
|
"expression": "JSON.stringify(window.__invalidDataScript)",
|
||||||
|
"returnByValue": true,
|
||||||
|
}),
|
||||||
|
session_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(result["result"]["value"], r#"{"error":true,"load":false}"#);
|
||||||
|
}
|
||||||
Vendored
+55
-5
@@ -18,7 +18,7 @@
|
|||||||
'__obscura_hw', '__obscura_mem',
|
'__obscura_hw', '__obscura_mem',
|
||||||
'__documentReadyState__', '__currentUrl',
|
'__documentReadyState__', '__currentUrl',
|
||||||
// internal helpers (var-declared throughout the file)
|
// internal helpers (var-declared throughout the file)
|
||||||
'__processDynScriptQueue', '_markNative', '_fpRand', '_fpNoise',
|
'__processDynScriptQueue', '_decodeDataScriptUrl', '_markNative', '_fpRand', '_fpNoise',
|
||||||
'_fpCache', '_getFp', '_fp', '_splitAsciiWhitespace',
|
'_fpCache', '_getFp', '_fp', '_splitAsciiWhitespace',
|
||||||
'_getElementsByClassName', '_docEncoding', '_docIsUtf8',
|
'_getElementsByClassName', '_docEncoding', '_docIsUtf8',
|
||||||
'_isSpecialScheme', '_applyDocQueryEncoding', '_anchorBase',
|
'_isSpecialScheme', '_applyDocQueryEncoding', '_anchorBase',
|
||||||
@@ -163,6 +163,51 @@ let _fpSeed = 0;
|
|||||||
// when SPAs dynamically insert multiple <script module> tags at once.
|
// when SPAs dynamically insert multiple <script module> tags at once.
|
||||||
let __dynScriptQueue = [];
|
let __dynScriptQueue = [];
|
||||||
let __dynScriptBusy = false;
|
let __dynScriptBusy = false;
|
||||||
|
function _decodeDataScriptUrl(url) {
|
||||||
|
const comma = url.indexOf(',');
|
||||||
|
if (!url.startsWith('data:') || comma < 5) {
|
||||||
|
throw new TypeError('Invalid dynamic script data URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = url.slice(5, comma);
|
||||||
|
const fragment = url.indexOf('#', comma + 1);
|
||||||
|
const payload = url.slice(comma + 1, fragment < 0 ? url.length : fragment);
|
||||||
|
if (meta.split(';').some(part => part.toLowerCase() === 'base64')) {
|
||||||
|
let encoded = payload.replace(/[\r\n\t\f ]/g, '');
|
||||||
|
const remainder = encoded.length % 4;
|
||||||
|
if (remainder === 1 || !/^[A-Za-z0-9+/]*={0,2}$/.test(encoded) || /=/.test(encoded.slice(0, -2))) {
|
||||||
|
throw new TypeError('Invalid dynamic script data URL base64');
|
||||||
|
}
|
||||||
|
if (remainder > 0) encoded += '='.repeat(4 - remainder);
|
||||||
|
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(encoded)) {
|
||||||
|
throw new TypeError('Invalid dynamic script data URL base64');
|
||||||
|
}
|
||||||
|
return new TextDecoder().decode(_base64ToUint8Array(encoded));
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytes = [];
|
||||||
|
for (let i = 0; i < payload.length; i++) {
|
||||||
|
const code = payload.charCodeAt(i);
|
||||||
|
if (code === 0x25 && i + 2 < payload.length) {
|
||||||
|
const hi = _hexv(payload.charCodeAt(i + 1));
|
||||||
|
const lo = _hexv(payload.charCodeAt(i + 2));
|
||||||
|
if (hi >= 0 && lo >= 0) {
|
||||||
|
bytes.push(hi * 16 + lo);
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (code < 0x80) {
|
||||||
|
bytes.push(code);
|
||||||
|
} else {
|
||||||
|
const character = String.fromCodePoint(payload.codePointAt(i));
|
||||||
|
if (character.length === 2) i++;
|
||||||
|
const encoded = new TextEncoder().encode(character);
|
||||||
|
for (let j = 0; j < encoded.length; j++) bytes.push(encoded[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||||
|
}
|
||||||
async function __processDynScriptQueue() {
|
async function __processDynScriptQueue() {
|
||||||
if (__dynScriptBusy) return;
|
if (__dynScriptBusy) return;
|
||||||
__dynScriptBusy = true;
|
__dynScriptBusy = true;
|
||||||
@@ -176,11 +221,16 @@ async function __processDynScriptQueue() {
|
|||||||
if (task.isModule) {
|
if (task.isModule) {
|
||||||
await import(task.url);
|
await import(task.url);
|
||||||
} else {
|
} else {
|
||||||
const raw = await Deno.core.ops.op_fetch_url(task.url, "GET", "{}", "", task.pageOrigin, "no-cors");
|
let body;
|
||||||
const parsed = JSON.parse(raw);
|
if (task.url.startsWith('data:')) {
|
||||||
if (parsed.body) {
|
body = _decodeDataScriptUrl(task.url);
|
||||||
|
} else {
|
||||||
|
const raw = await Deno.core.ops.op_fetch_url(task.url, "GET", "{}", "", task.pageOrigin, "no-cors");
|
||||||
|
body = JSON.parse(raw).body;
|
||||||
|
}
|
||||||
|
if (body) {
|
||||||
globalThis.__currentScriptNid = task.nid;
|
globalThis.__currentScriptNid = task.nid;
|
||||||
try { (0, eval)(parsed.body); }
|
try { (0, eval)(body); }
|
||||||
catch(e) { console.error('Dynamic script error (' + task.url + '):', e.message); }
|
catch(e) { console.error('Dynamic script error (' + task.url + '):', e.message); }
|
||||||
finally { globalThis.__currentScriptNid = task.prevNid || 0; }
|
finally { globalThis.__currentScriptNid = task.prevNid || 0; }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user