From bd807693e9cb285f2de95b31c66e5783264facc2 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Fri, 16 Oct 2026 01:27:50 +0700 Subject: [PATCH] M0.9: a control line that isn't JSON ends the connection The control server answered a line that wasn't JSON with an error and read on. A web page can make the browser send an HTTP request to the loopback port; its request and header lines were skipped that way and the JSON lines of its body ran as commands. The first line that isn't JSON now closes the connection before anything after it is read. Clients that send JSON lines (MCP --attach, scripts) are unaffected. --- apps/figcraft/src/control_server.rs | 8 +++++++- apps/figcraft/src/control_server_tests.rs | 20 ++++++++++++++++++-- docs/control-protocol.md | 3 ++- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/apps/figcraft/src/control_server.rs b/apps/figcraft/src/control_server.rs index 995240f50..1c91840fa 100644 --- a/apps/figcraft/src/control_server.rs +++ b/apps/figcraft/src/control_server.rs @@ -111,7 +111,13 @@ fn serve(mut out: TcpStream, queue: Queue, ctx: CtxSlot, timeout: Duration) -> s } } } - Err(e) => json!({"id": null, "ok": false, "error": format!("bad JSON: {e}")}), + Err(e) => { + // A line that isn't JSON ends the connection: a web page can make the browser send + // an HTTP request here, and its header lines must not let the JSON lines of its body + // through as commands. + writeln!(out, "{}", json!({"id": null, "ok": false, "error": format!("bad JSON: {e}")}))?; + break; + } }; writeln!(out, "{reply}")?; } diff --git a/apps/figcraft/src/control_server_tests.rs b/apps/figcraft/src/control_server_tests.rs index 2e0a7b484..d243fb916 100644 --- a/apps/figcraft/src/control_server_tests.rs +++ b/apps/figcraft/src/control_server_tests.rs @@ -53,9 +53,8 @@ fn valid_frames_keep_their_order_ids_and_parameters() { let pending = queue(); let q = pending.clone(); let worker = std::thread::spawn(move || serve(server, q, context(), Duration::from_secs(5))); - client.write_all(b"\r\n{bad JSON}\n{\"id\":\"test\",\"method\":\"engine.execute\",\"params\":{\"command\":\"shape.rectangle\"}}\r\n").unwrap(); + client.write_all(b"\r\n{\"id\":\"test\",\"method\":\"engine.execute\",\"params\":{\"command\":\"shape.rectangle\"}}\r\n").unwrap(); let mut reader = BufReader::new(client); - assert_eq!(reply(&mut reader)["ok"], false); let start = Instant::now(); let r = loop { if let Some(r) = pending.lock().unwrap().pop() { @@ -73,6 +72,23 @@ fn valid_frames_keep_their_order_ids_and_parameters() { worker.join().unwrap().unwrap(); } +#[test] +fn a_line_that_is_not_json_ends_the_connection_before_anything_after_it_runs() { + // What a browser sends when a web page posts to the port: the JSON line in its body never runs. + let (mut client, server) = pair(); + let pending = queue(); + let q = pending.clone(); + let worker = std::thread::spawn(move || serve(server, q, context(), Duration::from_secs(5))); + let body = "{\"id\":1,\"method\":\"engine.execute\",\"params\":{\"command\":\"file.save\"}}\n"; + let http = format!("POST / HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Type: text/plain\r\nContent-Length: {}\r\n\r\n{body}", body.len()); + client.write_all(http.as_bytes()).unwrap(); + let mut reader = BufReader::new(client); + assert_eq!(reply(&mut reader)["ok"], false); + assert_eq!(reader.read(&mut [0]).unwrap(), 0, "the connection is closed"); + worker.join().unwrap().unwrap(); + assert!(pending.lock().unwrap().is_empty(), "nothing was dispatched"); +} + #[test] fn full_queue_rejects_before_dispatch_and_ui_queue_stays_bounded() { let (mut client, server) = pair(); diff --git a/docs/control-protocol.md b/docs/control-protocol.md index f751c95b7..397aed5b7 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -18,7 +18,8 @@ reply: {"id": 1, "ok": true, "result": ...} Requests are queued and run on the UI thread at the start of the next frame, so replies reflect the real UI state. A request that the UI doesn't answer within 60 s gets a timeout error. A line that is -not JSON gets `{"id": null, "ok": false, "error": "bad JSON: …"}`. The implementation is in +not JSON gets `{"id": null, "ok": false, "error": "bad JSON: …"}` and ends the connection (so an +HTTP request a web page makes the browser send here never runs). The implementation is in `crates/ui-egui/src/control.rs` (methods) and `apps/figcraft/src/control_server.rs` (socket). A request line is limited to **32 MiB before the newline**, including whitespace. The reader stops