From 8942287a27b7c39b45f15ce773aebdc708d4df6e Mon Sep 17 00:00:00 2001 From: leokun Date: Thu, 3 Sep 2026 16:32:02 +0800 Subject: [PATCH] refactor(proxy): change proxy mode from "system" to "default" across the application - Updated the default proxy mode in `api.ts`, `ProxySettingsCard.tsx`, and `SettingsPage.tsx` to "default". - Adjusted related translations in `catalog.json`, `en-US.json`, and `zh-CN.json`. - Modified the `ProxyMode` enum in `settings.rs` to reflect the change from "system" to "default". - Enhanced proxy handling in the server code to support the new default mode. --- apps/desktop/src/demo/api.ts | 2 +- .../features/settings/ProxySettingsCard.tsx | 4 +- .../src/features/settings/SettingsPage.tsx | 2 +- apps/desktop/src/i18n/generated/catalog.json | 38 +++--- apps/desktop/src/i18n/locales/en-US.json | 2 +- apps/desktop/src/i18n/locales/zh-CN.json | 2 +- apps/desktop/src/shared/api.ts | 2 +- server/src/control/service.rs | 7 + server/src/local_app/mod.rs | 4 + server/src/local_app/proxy.rs | 7 + server/src/network.rs | 126 ++++++++++++++++-- server/src/store/settings.rs | 21 ++- 12 files changed, 179 insertions(+), 38 deletions(-) diff --git a/apps/desktop/src/demo/api.ts b/apps/desktop/src/demo/api.ts index 5577482..270081a 100644 --- a/apps/desktop/src/demo/api.ts +++ b/apps/desktop/src/demo/api.ts @@ -86,7 +86,7 @@ const harnessStatus: CursorHarnessStatus = { let detailed = true; let portSettings = { proxy_port: 0, service_port: 0 }; let proxySettings: ProxySettings = { - mode: "system", + mode: "default", address: "", auth_enabled: false, username: "", diff --git a/apps/desktop/src/features/settings/ProxySettingsCard.tsx b/apps/desktop/src/features/settings/ProxySettingsCard.tsx index a58e043..ce8ab58 100644 --- a/apps/desktop/src/features/settings/ProxySettingsCard.tsx +++ b/apps/desktop/src/features/settings/ProxySettingsCard.tsx @@ -26,7 +26,7 @@ export function ProxySettingsCard({ onSave: () => void; }) { const custom = draft.mode === "custom"; - const modeLabel = (mode: ProxySettingsInput["mode"]) => mode === "system" ? t("使用系统代理") : t("自定义"); + const modeLabel = (mode: ProxySettingsInput["mode"]) => mode === "default" ? t("默认") : t("自定义"); const action = editing ? (
@@ -43,7 +43,7 @@ export function ProxySettingsCard({ {editing ? <>
{t("代理方式")} -
onDraftChange({ ...draft, mode: mode as ProxySettingsInput["mode"] })} />
{custom &&
diff --git a/apps/desktop/src/features/settings/SettingsPage.tsx b/apps/desktop/src/features/settings/SettingsPage.tsx index 307a687..4f05143 100644 --- a/apps/desktop/src/features/settings/SettingsPage.tsx +++ b/apps/desktop/src/features/settings/SettingsPage.tsx @@ -31,7 +31,7 @@ export function SettingsPage() { const [clearScope, setClearScope] = useState("details"); const [clearing, setClearing] = useState(false); const [outboundProxy, setOutboundProxy] = useState(null); - const [proxyDraft, setProxyDraft] = useState({ mode: "system", address: "", auth_enabled: false, username: "", password: "" }); + const [proxyDraft, setProxyDraft] = useState({ mode: "default", address: "", auth_enabled: false, username: "", password: "" }); const [editingProxy, setEditingProxy] = useState(false); const [savingProxy, setSavingProxy] = useState(false); const [tabSettings, setTabSettings] = useState(null); diff --git a/apps/desktop/src/i18n/generated/catalog.json b/apps/desktop/src/i18n/generated/catalog.json index 3447646..2b53197 100644 --- a/apps/desktop/src/i18n/generated/catalog.json +++ b/apps/desktop/src/i18n/generated/catalog.json @@ -1944,12 +1944,12 @@ { "file": "features/settings/ProxySettingsCard.tsx", "line": 29, - "column": 93 + "column": 90 }, { "file": "features/settings/ProxySettingsCard.tsx", "line": 46, - "column": 169 + "column": 166 }, { "file": "features/settings/TabSettingsCard.tsx", @@ -2956,6 +2956,23 @@ } ] }, + "844b8cc8dff7c1d8": { + "source": "默认", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "features/settings/ProxySettingsCard.tsx", + "line": 29, + "column": 80 + }, + { + "file": "features/settings/ProxySettingsCard.tsx", + "line": 46, + "column": 129 + } + ] + }, "864597982c308d72": { "source": "已开启静默启动", "kind": "text", @@ -4808,23 +4825,6 @@ } ] }, - "d86fa42c3848c680": { - "source": "使用系统代理", - "kind": "text", - "placeholders": [], - "refs": [ - { - "file": "features/settings/ProxySettingsCard.tsx", - "line": 29, - "column": 79 - }, - { - "file": "features/settings/ProxySettingsCard.tsx", - "line": 46, - "column": 128 - } - ] - }, "d8c47e9776cf1082": { "source": "主菜单", "kind": "text", diff --git a/apps/desktop/src/i18n/locales/en-US.json b/apps/desktop/src/i18n/locales/en-US.json index da87697..67f09cc 100644 --- a/apps/desktop/src/i18n/locales/en-US.json +++ b/apps/desktop/src/i18n/locales/en-US.json @@ -203,6 +203,7 @@ "83fcfb4c1f2c1641": "Fetch models", "842b9f11cdd96bda": "Launch at login", "843ac7e15a5047a7": "Confirm legacy model configuration import", + "844b8cc8dff7c1d8": "Default", "864597982c308d72": "Silent start enabled", "86de7c4ee8fa7689": "Sync models", "8716e1344b0daddb": "Cursor official", @@ -332,7 +333,6 @@ "d6b1f203680f5496": "Leave blank to use adaptive thinking", "d766536c18e8e990": "Plugin runtime {version} is installed and ready to use.", "d7e266bdc8064193": "Group name", - "d86fa42c3848c680": "Use system proxy", "d8c47e9776cf1082": "Main menu", "d8c589c455675b46": "Prompt settings saved", "da521d1c1cbd36af": "Authorization is required to install the certificate", diff --git a/apps/desktop/src/i18n/locales/zh-CN.json b/apps/desktop/src/i18n/locales/zh-CN.json index f5357a3..9e6ccc7 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.json +++ b/apps/desktop/src/i18n/locales/zh-CN.json @@ -203,6 +203,7 @@ "83fcfb4c1f2c1641": "获取模型", "842b9f11cdd96bda": "开机启动", "843ac7e15a5047a7": "确认导入旧版模型配置", + "844b8cc8dff7c1d8": "默认", "864597982c308d72": "已开启静默启动", "86de7c4ee8fa7689": "同步模型", "8716e1344b0daddb": "Cursor 官方", @@ -332,7 +333,6 @@ "d6b1f203680f5496": "留空使用 adaptive thinking", "d766536c18e8e990": "插件运行时 {version} 已安装,可以开始使用插件。", "d7e266bdc8064193": "分组名称", - "d86fa42c3848c680": "使用系统代理", "d8c47e9776cf1082": "主菜单", "d8c589c455675b46": "提示词设置已保存", "da521d1c1cbd36af": "需要授权安装证书", diff --git a/apps/desktop/src/shared/api.ts b/apps/desktop/src/shared/api.ts index 2710754..03acece 100644 --- a/apps/desktop/src/shared/api.ts +++ b/apps/desktop/src/shared/api.ts @@ -120,7 +120,7 @@ export interface StatisticsStorage { export type StatisticsStorageScope = "details" | "all"; -export type ProxyMode = "system" | "custom"; +export type ProxyMode = "default" | "custom"; export interface ProxySettings { mode: ProxyMode; diff --git a/server/src/control/service.rs b/server/src/control/service.rs index 707ffe0..5cfc15d 100644 --- a/server/src/control/service.rs +++ b/server/src/control/service.rs @@ -683,6 +683,13 @@ impl ControlService { } pub async fn set_proxy_settings(&self, settings: ProxySettingsInput) -> Result { + if settings.mode.is_custom() { + let local_proxy_port = match self.cursor_harness.proxy_port().await { + Some(port) => port, + None => self.store.port_settings().await?.proxy_port, + }; + crate::network::reject_self_proxy(&settings.address, local_proxy_port)?; + } let settings = self.store.set_proxy_settings(settings).await?; self.clients.invalidate().await; Ok(settings) diff --git a/server/src/local_app/mod.rs b/server/src/local_app/mod.rs index e55f30b..7b45db6 100644 --- a/server/src/local_app/mod.rs +++ b/server/src/local_app/mod.rs @@ -90,6 +90,10 @@ impl CursorHarness { *self.inner.backend_addr.write() = Some(addr); } + pub async fn proxy_port(&self) -> Option { + self.inner.proxy.lock().await.port() + } + pub async fn cleanup_stale_settings(&self) -> Result<()> { settings::clear_stale_managed_settings() } diff --git a/server/src/local_app/proxy.rs b/server/src/local_app/proxy.rs index 70ba5fc..5cfce3a 100644 --- a/server/src/local_app/proxy.rs +++ b/server/src/local_app/proxy.rs @@ -33,6 +33,13 @@ impl ProxyRuntime { pub fn url(&self) -> Option { self.running().then(|| self.url.clone()).flatten() } + pub fn port(&self) -> Option { + if self.running() { + self.port + } else { + None + } + } pub async fn start( &mut self, diff --git a/server/src/network.rs b/server/src/network.rs index b790dae..0c02fac 100644 --- a/server/src/network.rs +++ b/server/src/network.rs @@ -4,7 +4,12 @@ use std::{sync::Arc, time::Duration}; use tokio::sync::RwLock; -use crate::{store::Store, Result}; +use crate::{ + store::{ProxySettingsSecret, Store}, + Error, Result, +}; + +const LOCAL_NO_PROXY: &str = "localhost,127.0.0.0/8,::1"; #[derive(Clone)] pub struct NetworkClients { @@ -94,11 +99,7 @@ pub async fn client_builder(store: &Store) -> Result { // only offer legacy TLS 1.2 cipher suites unsupported by rustls. let mut builder = reqwest::Client::builder().use_native_tls(); if settings.mode.is_custom() { - let mut proxy = reqwest::Proxy::all(&settings.address)?; - if settings.auth_enabled { - proxy = proxy.basic_auth(&settings.username, &settings.password); - } - builder = builder.no_proxy().proxy(proxy); + builder = builder.proxy(custom_proxy(&settings)?); } Ok(builder) } @@ -111,11 +112,114 @@ pub async fn blocking_client_builder(store: &Store) -> Result Result { + let mut proxy = reqwest::Proxy::all(&settings.address)? + .no_proxy(reqwest::NoProxy::from_string(LOCAL_NO_PROXY)); + if settings.auth_enabled { + proxy = proxy.basic_auth(&settings.username, &settings.password); + } + Ok(proxy) +} + +pub fn reject_self_proxy(address: &str, local_proxy_port: u16) -> Result<()> { + if local_proxy_port == 0 { + return Ok(()); + } + let url = url::Url::parse(address) + .map_err(|error| Error::Config(format!("invalid proxy address: {error}")))?; + if url.port_or_known_default() == Some(local_proxy_port) && url_host_is_loopback(&url) { + return Err(Error::Config( + "proxy address cannot point to the Cursor BYOK local proxy".into(), + )); + } + Ok(()) +} + +fn url_host_is_loopback(url: &url::Url) -> bool { + match url.host() { + Some(url::Host::Domain(host)) => { + host.trim_end_matches('.').eq_ignore_ascii_case("localhost") + } + Some(url::Host::Ipv4(address)) => address.is_loopback(), + Some(url::Host::Ipv6(address)) => address.is_loopback(), + None => false, + } +} + +#[cfg(test)] +mod tests { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + + use super::*; + use crate::store::ProxyMode; + + fn custom_settings(address: String) -> ProxySettingsSecret { + ProxySettingsSecret { + mode: ProxyMode::Custom, + address, + auth_enabled: false, + username: String::new(), + password: String::new(), + } + } + + #[test] + fn rejects_only_own_loopback_proxy_port() { + for address in [ + "http://localhost:15721", + "http://localhost.:15721", + "http://127.0.0.2:15721", + "http://[::1]:15721", + ] { + assert!(reject_self_proxy(address, 15721).is_err(), "{address}"); + } + assert!(reject_self_proxy("http://127.0.0.1:7890", 15721).is_ok()); + assert!(reject_self_proxy("http://192.168.1.2:15721", 15721).is_ok()); + assert!(reject_self_proxy("http://127.0.0.1:15721", 0).is_ok()); + } + + #[tokio::test] + async fn custom_proxy_bypasses_loopback_destinations() { + let proxy_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let proxy_address = proxy_listener.local_addr().unwrap(); + let target_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let target_address = target_listener.local_addr().unwrap(); + let target = tokio::spawn(async move { + let (mut socket, _) = target_listener.accept().await.unwrap(); + let mut request = [0_u8; 1024]; + let _ = socket.read(&mut request).await.unwrap(); + socket + .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok") + .await + .unwrap(); + }); + let settings = custom_settings(format!("http://{proxy_address}")); + let client = reqwest::Client::builder() + .proxy(custom_proxy(&settings).unwrap()) + .build() + .unwrap(); + + let body = client + .get(format!("http://{target_address}")) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + assert_eq!(body, "ok"); + target.await.unwrap(); + assert!( + tokio::time::timeout(Duration::from_millis(50), proxy_listener.accept()) + .await + .is_err(), + "loopback destination unexpectedly reached the configured proxy" + ); + } +} diff --git a/server/src/store/settings.rs b/server/src/store/settings.rs index 73278e7..2ac8954 100644 --- a/server/src/store/settings.rs +++ b/server/src/store/settings.rs @@ -27,7 +27,7 @@ pub struct PortSettings { #[serde(rename_all = "snake_case")] pub enum ProxyMode { #[default] - System, + Default, Custom, } @@ -362,3 +362,22 @@ impl Store { Ok(settings) } } + +#[cfg(test)] +mod tests { + use super::ProxyMode; + + #[test] + fn default_proxy_mode_uses_the_default_wire_value() { + assert_eq!(ProxyMode::default(), ProxyMode::Default); + assert_eq!( + serde_json::to_string(&ProxyMode::default()).unwrap(), + "\"default\"" + ); + assert_eq!( + serde_json::from_str::("\"default\"").unwrap(), + ProxyMode::Default + ); + assert!(serde_json::from_str::("\"system\"").is_err()); + } +}