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.
105 lines
3.6 KiB
Rust
105 lines
3.6 KiB
Rust
//! `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"
|
|
);
|
|
}
|