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 <noreply@anthropic.com>
This commit is contained in:
SteamSmith 2026-10-12 01:33:06 -04:00 committed by GitHub
parent 9c8cf1a701
commit 486b1bbcb1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 30 additions and 1 deletions

View File

@ -788,7 +788,8 @@ pub fn serve(client: ControlClient) -> std::io::Result<Endpoint> {
continue;
}
let (reply, close) = match serde_json::from_str::<Value>(&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<String> {
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:?}");
}
}