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
@@ -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