From 486b1bbcb1beaae7a6c994e9f14083e9b46a056f Mon Sep 17 00:00:00 2001 From: SteamSmith <341182916+SteamSmith@users.noreply.github.com> Date: Mon, 12 Oct 2026 01:33:06 -0400 Subject: [PATCH] control: close the connection on malformed input before auth (#1259) control::serve documents that the first request on a connection must be `auth` and that anything else closes the connection. A first line that wasn't valid JSON got a parse error but left the connection open, unlike a wrong token or a non-auth request. Close it in that case too; after a successful auth a malformed line still just gets the parse error. Adds control::transport_tests::malformed_input_before_auth_closes_the_connection, which fails before this change. Co-authored-by: Claude Opus 5.5 --- crates/ui-egui/src/control.rs | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index 342998ff..65a12e19 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -788,7 +788,8 @@ pub fn serve(client: ControlClient) -> std::io::Result { continue; } let (reply, close) = match serde_json::from_str::(&line) { - Err(e) => (rpc_error(Value::Null, -32700, &format!("parse error: {e}")), false), + // Before `auth`, like any other first request, this closes the connection. + Err(e) => (rpc_error(Value::Null, -32700, &format!("parse error: {e}")), !authed), Ok(msg) => { let id = msg.get("id").cloned().unwrap_or(Value::Null); let method = msg.get("method").and_then(Value::as_str).unwrap_or(""); @@ -839,3 +840,31 @@ fn random_token() -> std::io::Result { getrandom::fill(&mut bytes).map_err(|e| std::io::Error::other(e.to_string()))?; Ok(bytes.iter().map(|b| format!("{b:02x}")).collect()) } + +#[cfg(test)] +mod transport_tests { + /// Before authenticating, a line that isn't JSON gets a parse error and closes the + /// connection, as [`super::serve`] documents for any first request other than `auth`. + #[cfg(not(target_arch = "wasm32"))] + #[test] + fn malformed_input_before_auth_closes_the_connection() { + use std::io::{BufRead, BufReader, ErrorKind, Write}; + let (_control, client) = super::attach(&egui::Context::default()); + let ep = super::serve(client).unwrap(); + let s = std::net::TcpStream::connect(("127.0.0.1", ep.port)).unwrap(); + s.set_read_timeout(Some(std::time::Duration::from_secs(5))).unwrap(); + writeln!(s.try_clone().unwrap(), "not json").unwrap(); + let mut r = BufReader::new(s); + let mut first = String::new(); + r.read_line(&mut first).unwrap(); + let reply: serde_json::Value = serde_json::from_str(&first).unwrap(); + assert_eq!(reply["error"]["code"], -32700, "{first}"); + // Closed: end of stream (or a reset), not a connection left waiting for more input. + let mut next = String::new(); + let closed = match r.read_line(&mut next) { + Ok(n) => n == 0, + Err(e) => matches!(e.kind(), ErrorKind::ConnectionReset | ErrorKind::ConnectionAborted), + }; + assert!(closed, "the connection stayed open: {next:?}"); + } +}