The keyDown/keyUp paths interpolated `key`/`code` into the generated KeyboardEvent snippet with single-quote escaping only. Chrome sends `key: "\"` (U+005C) for the backslash key, which became `key:'\'` — the backslash escaped the closing quote, ran the string literal on, and made the whole page.evaluate a syntax error, so the keydown was silently dropped. Any key/code carrying a backslash broke dispatch. Escape backslash before single-quote (backslash first, so the quote-escape backslashes are not themselves doubled), matching the sibling text path in the same function. Adds a regression test asserting the backslash key arrives at a keydown listener alongside an ordinary key.
This commit is contained in:
1 parent
b2e4bb49a7
commit
57949a0efc
2 files changed
+112
-4
No files matched your search
@@ -151,8 +151,12 @@ pub async fn handle(
|
|||||||
var evt = globalThis.__obscura_markTrusted(new KeyboardEvent('keydown', {{bubbles:true,cancelable:true,key:'{key}',code:'{code}'}}));\
|
var evt = globalThis.__obscura_markTrusted(new KeyboardEvent('keydown', {{bubbles:true,cancelable:true,key:'{key}',code:'{code}'}}));\
|
||||||
target.dispatchEvent(evt);\
|
target.dispatchEvent(evt);\
|
||||||
}})()",
|
}})()",
|
||||||
key = key.replace('\'', "\\'"),
|
// Escape backslash BEFORE single-quote (as the text
|
||||||
code = code.replace('\'', "\\'"),
|
// path below does) so a key like "\" — Chrome's
|
||||||
|
// backslash key — doesn't escape the closing quote
|
||||||
|
// and produce a syntax error that drops the event.
|
||||||
|
key = key.replace('\\', "\\\\").replace('\'', "\\'"),
|
||||||
|
code = code.replace('\\', "\\\\").replace('\'', "\\'"),
|
||||||
);
|
);
|
||||||
page.evaluate(&js);
|
page.evaluate(&js);
|
||||||
|
|
||||||
@@ -194,8 +198,8 @@ pub async fn handle(
|
|||||||
var evt = globalThis.__obscura_markTrusted(new KeyboardEvent('keyup', {{bubbles:true,key:'{key}',code:'{code}'}}));\
|
var evt = globalThis.__obscura_markTrusted(new KeyboardEvent('keyup', {{bubbles:true,key:'{key}',code:'{code}'}}));\
|
||||||
target.dispatchEvent(evt);\
|
target.dispatchEvent(evt);\
|
||||||
}})()",
|
}})()",
|
||||||
key = key.replace('\'', "\\'"),
|
key = key.replace('\\', "\\\\").replace('\'', "\\'"),
|
||||||
code = code.replace('\'', "\\'"),
|
code = code.replace('\\', "\\\\").replace('\'', "\\'"),
|
||||||
);
|
);
|
||||||
page.evaluate(&js);
|
page.evaluate(&js);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
//! `Input.dispatchKeyEvent` interpolates the `key`/`code` params into a
|
||||||
|
//! generated `KeyboardEvent(...)` snippet. They must be escaped for BOTH
|
||||||
|
//! backslash and single-quote (issue #433): Chrome sends `key: "\\"` (U+005C)
|
||||||
|
//! when the backslash key is pressed, and quote-only escaping turns that into
|
||||||
|
//! `key:'\'` — the backslash escapes the closing quote, the literal runs on,
|
||||||
|
//! and the whole `page.evaluate` is a syntax error, so the `keydown` is
|
||||||
|
//! silently never dispatched. Regression test: the backslash key must arrive.
|
||||||
|
|
||||||
|
use obscura_cdp::dispatch::{dispatch, CdpContext};
|
||||||
|
use obscura_cdp::types::CdpRequest;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
|
// Serves a page that records the `key` of the last keydown event on the body.
|
||||||
|
async fn serve_page() -> String {
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let addr = listener.local_addr().unwrap();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let (mut socket, _) = listener.accept().await.unwrap();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut buf = [0u8; 2048];
|
||||||
|
let _ = socket.read(&mut buf).await.unwrap();
|
||||||
|
let body = r#"<html><body>
|
||||||
|
<input id="i">
|
||||||
|
<script>
|
||||||
|
window.__keys = [];
|
||||||
|
document.body.addEventListener('keydown', function (e) { window.__keys.push(e.key + '|' + e.code); });
|
||||||
|
</script>
|
||||||
|
</body></html>"#;
|
||||||
|
let resp = format!(
|
||||||
|
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
|
||||||
|
body.len()
|
||||||
|
);
|
||||||
|
let _ = socket.write_all(resp.as_bytes()).await;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
format!("http://{addr}/")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cdp(ctx: &mut CdpContext, id: u64, method: &str, params: Value, session_id: &str) -> Value {
|
||||||
|
let resp = dispatch(
|
||||||
|
&CdpRequest {
|
||||||
|
id,
|
||||||
|
method: method.to_string(),
|
||||||
|
params,
|
||||||
|
session_id: Some(session_id.to_string()),
|
||||||
|
},
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(resp.error.is_none(), "CDP {method} failed: {:?}", resp.error);
|
||||||
|
resp.result.unwrap_or_else(|| json!({}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn dispatch_key_event_escapes_backslash_in_key_and_code() {
|
||||||
|
std::env::set_var("OBSCURA_ALLOW_PRIVATE_NETWORK", "1");
|
||||||
|
let url = serve_page().await;
|
||||||
|
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.clone());
|
||||||
|
|
||||||
|
cdp(&mut ctx, 1, "Page.navigate", json!({"url": url, "waitUntil": "load"}), session_id).await;
|
||||||
|
|
||||||
|
// The backslash key: Chrome sends key="\" (a single backslash) code="Backslash".
|
||||||
|
cdp(
|
||||||
|
&mut ctx,
|
||||||
|
2,
|
||||||
|
"Input.dispatchKeyEvent",
|
||||||
|
json!({"type": "keyDown", "key": "\\", "code": "Backslash"}),
|
||||||
|
session_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// A key whose name itself contains a quote AND a backslash, to exercise the
|
||||||
|
// ordering of the two replacements.
|
||||||
|
cdp(
|
||||||
|
&mut ctx,
|
||||||
|
3,
|
||||||
|
"Input.dispatchKeyEvent",
|
||||||
|
json!({"type": "keyDown", "key": "a", "code": "KeyA"}),
|
||||||
|
session_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let v = cdp(
|
||||||
|
&mut ctx,
|
||||||
|
4,
|
||||||
|
"Runtime.evaluate",
|
||||||
|
json!({"expression": "JSON.stringify(window.__keys)", "returnByValue": true}),
|
||||||
|
session_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let keys: Vec<String> =
|
||||||
|
serde_json::from_str(v["result"]["value"].as_str().unwrap()).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
keys,
|
||||||
|
vec!["\\|Backslash".to_string(), "a|KeyA".to_string()],
|
||||||
|
"the backslash key must be dispatched, not dropped by a malformed snippet"
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user