From 89db070fdbfec0f5d8b8c218db0d865251572b0d Mon Sep 17 00:00:00 2001 From: yiliang114 Date: Sun, 9 Aug 2026 17:56:43 +0800 Subject: [PATCH 1/4] fix(desktop): share the active session safely --- .../src-tauri/src/local_control.rs | 71 +++++++++++++++---- packages/desktop-shell/src-tauri/src/main.rs | 7 +- 2 files changed, 64 insertions(+), 14 deletions(-) diff --git a/packages/desktop-shell/src-tauri/src/local_control.rs b/packages/desktop-shell/src-tauri/src/local_control.rs index 9655e400272..1281445d92e 100644 --- a/packages/desktop-shell/src-tauri/src/local_control.rs +++ b/packages/desktop-shell/src-tauri/src/local_control.rs @@ -52,7 +52,11 @@ pub struct LocalControlSession { } impl LocalControlSession { - pub fn start(runtime_url: &Url, runtime_token: &str) -> Result { + pub fn start( + runtime_url: &Url, + runtime_token: &str, + current_url: &Url, + ) -> Result { let target = runtime_socket_addr(runtime_url)?; let lan_ip = primary_lan_ipv4()?; let listener = TcpListener::bind((lan_ip, 0)) @@ -66,7 +70,7 @@ impl LocalControlSession { .port(); let public_origin = format!("http://{lan_ip}:{port}"); let pair_token = random_token(); - let url = format!("{public_origin}/#token={pair_token}"); + let url = local_control_url(current_url, lan_ip, port, &pair_token)?; let qr_svg = QrCode::new(url.as_bytes()) .map_err(|error| format!("Failed to generate Local Control QR code: {error}"))? .render::() @@ -402,15 +406,33 @@ fn runtime_socket_addr(url: &Url) -> Result { Ok(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port)) } +fn local_control_url( + current_url: &Url, + lan_ip: Ipv4Addr, + port: u16, + pair_token: &str, +) -> Result { + let workspace = current_url + .query_pairs() + .find(|(name, value)| name == "workspace" && !value.is_empty()) + .map(|(_, value)| value.into_owned()); + let mut url = current_url.clone(); + url.set_host(Some(&lan_ip.to_string())) + .map_err(|error| format!("Failed to construct the Local Control URL: {error}"))?; + url.set_port(Some(port)) + .map_err(|_| "Failed to construct the Local Control URL.".to_string())?; + url.set_query(None); + if let Some(workspace) = workspace { + url.query_pairs_mut().append_pair("workspace", &workspace); + } + url.set_fragment(Some(&format!("token={pair_token}"))); + Ok(url.into()) +} + fn primary_lan_ipv4() -> Result { let routed = routed_ipv4().ok(); - let interfaces = match NetworkInterface::show() { - Ok(interfaces) => interfaces, - Err(_) => { - return routed - .ok_or_else(|| "Local Control could not find a usable IPv4 network.".to_string()) - } - }; + let interfaces = NetworkInterface::show() + .map_err(|_| "Local Control could not inspect IPv4 networks.".to_string())?; let physical = interfaces .into_iter() .filter(|interface| { @@ -441,10 +463,10 @@ fn choose_lan_ipv4( ) -> Result { physical.sort_unstable(); physical.dedup(); + physical.retain(|address| address.is_private() || address.is_link_local()); if let Some(routed) = routed.filter(|address| physical.contains(address)) { return Ok(routed); } - physical.retain(|address| address.is_private() || address.is_link_local()); match physical.as_slice() { [address] => Ok(*address), [] => Err("Local Control could not find a usable IPv4 network.".to_string()), @@ -526,8 +548,8 @@ fn lock(mutex: &Mutex) -> std::sync::MutexGuard<'_, T> { #[cfg(test)] mod tests { use super::{ - choose_lan_ipv4, find_header_end, rewrite_request, runtime_socket_addr, spawn_proxy, - Connections, + choose_lan_ipv4, find_header_end, local_control_url, rewrite_request, runtime_socket_addr, + spawn_proxy, Connections, }; use std::collections::HashMap; use std::io::{Read, Write}; @@ -541,7 +563,7 @@ mod tests { use url::Url; #[test] - fn prefers_the_physical_lan_over_a_vpn_route() { + fn selects_only_a_private_physical_lan() { assert_eq!( choose_lan_ipv4( Some("10.8.0.2".parse::().expect("VPN address")), @@ -552,6 +574,29 @@ mod tests { .parse::() .expect("expected address"), ); + assert!(choose_lan_ipv4( + Some("203.0.113.10".parse().expect("public route")), + vec!["203.0.113.10".parse().expect("public interface")], + ) + .is_err()); + } + + #[test] + fn shares_only_the_current_local_session() { + let url = local_control_url( + &Url::parse( + "http://127.0.0.1:4170/session/a%20b?workspace=work%2Ftree&token=runtime&theme=light#old", + ) + .expect("current URL"), + "192.168.1.20".parse().expect("LAN address"), + 49152, + "pair-token", + ) + .expect("Local Control URL"); + assert_eq!( + url, + "http://192.168.1.20:49152/session/a%20b?workspace=work%2Ftree#token=pair-token" + ); } #[test] diff --git a/packages/desktop-shell/src-tauri/src/main.rs b/packages/desktop-shell/src-tauri/src/main.rs index 9afa0ccf99a..84119d9e9a8 100755 --- a/packages/desktop-shell/src-tauri/src/main.rs +++ b/packages/desktop-shell/src-tauri/src/main.rs @@ -304,7 +304,12 @@ fn enable_local_control( .as_ref() .map(|runtime| (runtime.base_url().clone(), runtime.token().to_string())) .ok_or_else(|| "Start a Desktop workspace before enabling Local Control.".to_string())?; - let session = LocalControlSession::start(&runtime_url, &runtime_token)?; + let current_url = app + .get_webview_window("main") + .and_then(|window| window.url().ok()) + .filter(|url| is_same_origin(url, &runtime_url)) + .unwrap_or_else(|| runtime_url.clone()); + let session = LocalControlSession::start(&runtime_url, &runtime_token, ¤t_url)?; let info = session.info(); *local_control = Some(session); set_local_control_menu_state(&app, true); From 9370bf076312ad9bd7265c009f32bb60b7a0bb3d Mon Sep 17 00:00:00 2001 From: yiliang114 Date: Sun, 9 Aug 2026 21:05:50 +0800 Subject: [PATCH 2/4] fix(desktop): reject malformed Local Control headers --- .../src-tauri/src/local_control.rs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/desktop-shell/src-tauri/src/local_control.rs b/packages/desktop-shell/src-tauri/src/local_control.rs index 1281445d92e..6b2b5b019ac 100644 --- a/packages/desktop-shell/src-tauri/src/local_control.rs +++ b/packages/desktop-shell/src-tauri/src/local_control.rs @@ -307,7 +307,19 @@ fn rewrite_request( pair_token: &str, runtime_token: &str, ) -> Result, u16> { - let header = std::str::from_utf8(&request[..header_end]).map_err(|_| 400_u16)?; + let header_bytes = &request[..header_end]; + if header_bytes + .iter() + .enumerate() + .any(|(index, &byte)| match byte { + b'\r' => header_bytes.get(index + 1) != Some(&b'\n'), + b'\n' => index == 0 || header_bytes[index - 1] != b'\r', + _ => false, + }) + { + return Err(400); + } + let header = std::str::from_utf8(header_bytes).map_err(|_| 400_u16)?; let public_authority = public_origin.strip_prefix("http://").ok_or(500_u16)?; let pair_protocol = format!("qwen-bearer.{}", URL_SAFE_NO_PAD.encode(pair_token)); let runtime_protocol = format!("qwen-bearer.{}", URL_SAFE_NO_PAD.encode(runtime_token)); @@ -688,6 +700,20 @@ mod tests { Err(403), ); + let request = b"GET / HTTP/1.1\nHost: 127.0.0.1:4170\n\n\r\n\r\n"; + assert_eq!( + rewrite_request( + request, + find_header_end(request).expect("header"), + "http://192.168.1.10:49152", + "http://127.0.0.1:4170", + "127.0.0.1:4170", + "pair-token", + "runtime-token", + ), + Err(400), + ); + let pair = URL_SAFE_NO_PAD.encode("pair-token"); let runtime = URL_SAFE_NO_PAD.encode("runtime-token"); let request = format!( From cdee783b8e2c8914ec72e4a6916b98eed084a466 Mon Sep 17 00:00:00 2001 From: yiliang114 Date: Sun, 9 Aug 2026 21:23:02 +0800 Subject: [PATCH 3/4] test(desktop): cover Local Control boundaries --- .../src-tauri/src/local_control.rs | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/desktop-shell/src-tauri/src/local_control.rs b/packages/desktop-shell/src-tauri/src/local_control.rs index 6b2b5b019ac..fd7226b25d9 100644 --- a/packages/desktop-shell/src-tauri/src/local_control.rs +++ b/packages/desktop-shell/src-tauri/src/local_control.rs @@ -442,9 +442,15 @@ fn local_control_url( } fn primary_lan_ipv4() -> Result { - let routed = routed_ipv4().ok(); - let interfaces = NetworkInterface::show() - .map_err(|_| "Local Control could not inspect IPv4 networks.".to_string())?; + select_lan_ipv4(routed_ipv4().ok(), NetworkInterface::show().ok()) +} + +fn select_lan_ipv4( + routed: Option, + interfaces: Option>, +) -> Result { + let interfaces = + interfaces.ok_or_else(|| "Local Control could not inspect IPv4 networks.".to_string())?; let physical = interfaces .into_iter() .filter(|interface| { @@ -561,7 +567,7 @@ fn lock(mutex: &Mutex) -> std::sync::MutexGuard<'_, T> { mod tests { use super::{ choose_lan_ipv4, find_header_end, local_control_url, rewrite_request, runtime_socket_addr, - spawn_proxy, Connections, + select_lan_ipv4, spawn_proxy, Connections, }; use std::collections::HashMap; use std::io::{Read, Write}; @@ -591,13 +597,25 @@ mod tests { vec!["203.0.113.10".parse().expect("public interface")], ) .is_err()); + let routed = Ipv4Addr::new(192, 168, 1, 20); + assert_eq!( + choose_lan_ipv4(Some(routed), vec![routed, Ipv4Addr::new(192, 168, 2, 5)],) + .expect("routed LAN"), + routed, + ); + } + + #[test] + fn rejects_unverified_networks_when_interface_enumeration_fails() { + let routed = Ipv4Addr::new(192, 168, 1, 20); + assert!(select_lan_ipv4(Some(routed), None).is_err()); } #[test] fn shares_only_the_current_local_session() { let url = local_control_url( &Url::parse( - "http://127.0.0.1:4170/session/a%20b?workspace=work%2Ftree&token=runtime&theme=light#old", + "http://127.0.0.1:4170/session/a%20b?token=runtime&workspace=work%2Ftree&theme=light#old", ) .expect("current URL"), "192.168.1.20".parse().expect("LAN address"), @@ -609,6 +627,15 @@ mod tests { url, "http://192.168.1.20:49152/session/a%20b?workspace=work%2Ftree#token=pair-token" ); + + let url = local_control_url( + &Url::parse("http://127.0.0.1:4170/").expect("runtime URL"), + "192.168.1.20".parse().expect("LAN address"), + 49152, + "pair-token", + ) + .expect("Local Control URL"); + assert_eq!(url, "http://192.168.1.20:49152/#token=pair-token"); } #[test] From 912910fff092e8fcf5ce7e92744cdc6c3f5c1509 Mon Sep 17 00:00:00 2001 From: yiliang114 Date: Mon, 10 Aug 2026 00:14:43 +0800 Subject: [PATCH 4/4] test(desktop): cover bare-CR header and empty workspace guard --- .../src-tauri/src/local_control.rs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/desktop-shell/src-tauri/src/local_control.rs b/packages/desktop-shell/src-tauri/src/local_control.rs index fd7226b25d9..5e5d3b612a5 100644 --- a/packages/desktop-shell/src-tauri/src/local_control.rs +++ b/packages/desktop-shell/src-tauri/src/local_control.rs @@ -636,6 +636,19 @@ mod tests { ) .expect("Local Control URL"); assert_eq!(url, "http://192.168.1.20:49152/#token=pair-token"); + + let url = local_control_url( + &Url::parse("http://127.0.0.1:4170/session/x?workspace=") + .expect("current URL"), + "192.168.1.20".parse().expect("LAN address"), + 49152, + "pair-token", + ) + .expect("Local Control URL"); + assert_eq!( + url, + "http://192.168.1.20:49152/session/x#token=pair-token" + ); } #[test] @@ -741,6 +754,21 @@ mod tests { Err(400), ); + let request = + b"GET / HTTP/1.1\r\nHost: 192.168.1.10:49152\r\nX-Inject: a\r\r\n\r\n"; + assert_eq!( + rewrite_request( + request, + find_header_end(request).expect("header"), + "http://192.168.1.10:49152", + "http://127.0.0.1:4170", + "127.0.0.1:4170", + "pair-token", + "runtime-token", + ), + Err(400), + ); + let pair = URL_SAFE_NO_PAD.encode("pair-token"); let runtime = URL_SAFE_NO_PAD.encode("runtime-token"); let request = format!(