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.
This commit is contained in:
leokun
2026-09-03 16:32:02 +08:00
parent 924b5e5926
commit 8942287a27
12 changed files with 179 additions and 38 deletions
+7
View File
@@ -683,6 +683,13 @@ impl ControlService {
}
pub async fn set_proxy_settings(&self, settings: ProxySettingsInput) -> Result<ProxySettings> {
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)
+4
View File
@@ -90,6 +90,10 @@ impl CursorHarness {
*self.inner.backend_addr.write() = Some(addr);
}
pub async fn proxy_port(&self) -> Option<u16> {
self.inner.proxy.lock().await.port()
}
pub async fn cleanup_stale_settings(&self) -> Result<()> {
settings::clear_stale_managed_settings()
}
+7
View File
@@ -33,6 +33,13 @@ impl ProxyRuntime {
pub fn url(&self) -> Option<String> {
self.running().then(|| self.url.clone()).flatten()
}
pub fn port(&self) -> Option<u16> {
if self.running() {
self.port
} else {
None
}
}
pub async fn start(
&mut self,
+115 -11
View File
@@ -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<reqwest::ClientBuilder> {
// 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<reqwest::blocking:
let settings = store.proxy_settings_secret().await?;
let mut builder = reqwest::blocking::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)
}
fn custom_proxy(settings: &ProxySettingsSecret) -> Result<reqwest::Proxy> {
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"
);
}
}
+20 -1
View File
@@ -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::<ProxyMode>("\"default\"").unwrap(),
ProxyMode::Default
);
assert!(serde_json::from_str::<ProxyMode>("\"system\"").is_err());
}
}