mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-04 02:52:55 +08:00
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:
@@ -86,7 +86,7 @@ const harnessStatus: CursorHarnessStatus = {
|
|||||||
let detailed = true;
|
let detailed = true;
|
||||||
let portSettings = { proxy_port: 0, service_port: 0 };
|
let portSettings = { proxy_port: 0, service_port: 0 };
|
||||||
let proxySettings: ProxySettings = {
|
let proxySettings: ProxySettings = {
|
||||||
mode: "system",
|
mode: "default",
|
||||||
address: "",
|
address: "",
|
||||||
auth_enabled: false,
|
auth_enabled: false,
|
||||||
username: "",
|
username: "",
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function ProxySettingsCard({
|
|||||||
onSave: () => void;
|
onSave: () => void;
|
||||||
}) {
|
}) {
|
||||||
const custom = draft.mode === "custom";
|
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 ? (
|
const action = editing ? (
|
||||||
<div className={styles.actionGroup}>
|
<div className={styles.actionGroup}>
|
||||||
<Button size="small" disabled={saving} onClick={onCancel}>{t("取消")}</Button>
|
<Button size="small" disabled={saving} onClick={onCancel}>{t("取消")}</Button>
|
||||||
@@ -43,7 +43,7 @@ export function ProxySettingsCard({
|
|||||||
{editing ? <>
|
{editing ? <>
|
||||||
<div className={styles.row}>
|
<div className={styles.row}>
|
||||||
<strong>{t("代理方式")}</strong>
|
<strong>{t("代理方式")}</strong>
|
||||||
<div className={styles.control}><Select ariaLabel={t("代理方式")} value={draft.mode} options={[{ value: "system", label: t("使用系统代理") }, { value: "custom", label: t("自定义") }]} onChange={(mode) => onDraftChange({ ...draft, mode: mode as ProxySettingsInput["mode"] })} /></div>
|
<div className={styles.control}><Select ariaLabel={t("代理方式")} value={draft.mode} options={[{ value: "default", label: t("默认") }, { value: "custom", label: t("自定义") }]} onChange={(mode) => onDraftChange({ ...draft, mode: mode as ProxySettingsInput["mode"] })} /></div>
|
||||||
</div>
|
</div>
|
||||||
{custom && <div className={styles.customFields}>
|
{custom && <div className={styles.customFields}>
|
||||||
<div className={styles.row}>
|
<div className={styles.row}>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function SettingsPage() {
|
|||||||
const [clearScope, setClearScope] = useState<StatisticsStorageScope>("details");
|
const [clearScope, setClearScope] = useState<StatisticsStorageScope>("details");
|
||||||
const [clearing, setClearing] = useState(false);
|
const [clearing, setClearing] = useState(false);
|
||||||
const [outboundProxy, setOutboundProxy] = useState<ProxySettings | null>(null);
|
const [outboundProxy, setOutboundProxy] = useState<ProxySettings | null>(null);
|
||||||
const [proxyDraft, setProxyDraft] = useState<ProxySettingsInput>({ mode: "system", address: "", auth_enabled: false, username: "", password: "" });
|
const [proxyDraft, setProxyDraft] = useState<ProxySettingsInput>({ mode: "default", address: "", auth_enabled: false, username: "", password: "" });
|
||||||
const [editingProxy, setEditingProxy] = useState(false);
|
const [editingProxy, setEditingProxy] = useState(false);
|
||||||
const [savingProxy, setSavingProxy] = useState(false);
|
const [savingProxy, setSavingProxy] = useState(false);
|
||||||
const [tabSettings, setTabSettings] = useState<TabSettings | null>(null);
|
const [tabSettings, setTabSettings] = useState<TabSettings | null>(null);
|
||||||
|
|||||||
@@ -1944,12 +1944,12 @@
|
|||||||
{
|
{
|
||||||
"file": "features/settings/ProxySettingsCard.tsx",
|
"file": "features/settings/ProxySettingsCard.tsx",
|
||||||
"line": 29,
|
"line": 29,
|
||||||
"column": 93
|
"column": 90
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "features/settings/ProxySettingsCard.tsx",
|
"file": "features/settings/ProxySettingsCard.tsx",
|
||||||
"line": 46,
|
"line": 46,
|
||||||
"column": 169
|
"column": 166
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "features/settings/TabSettingsCard.tsx",
|
"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": {
|
"864597982c308d72": {
|
||||||
"source": "已开启静默启动",
|
"source": "已开启静默启动",
|
||||||
"kind": "text",
|
"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": {
|
"d8c47e9776cf1082": {
|
||||||
"source": "主菜单",
|
"source": "主菜单",
|
||||||
"kind": "text",
|
"kind": "text",
|
||||||
|
|||||||
@@ -203,6 +203,7 @@
|
|||||||
"83fcfb4c1f2c1641": "Fetch models",
|
"83fcfb4c1f2c1641": "Fetch models",
|
||||||
"842b9f11cdd96bda": "Launch at login",
|
"842b9f11cdd96bda": "Launch at login",
|
||||||
"843ac7e15a5047a7": "Confirm legacy model configuration import",
|
"843ac7e15a5047a7": "Confirm legacy model configuration import",
|
||||||
|
"844b8cc8dff7c1d8": "Default",
|
||||||
"864597982c308d72": "Silent start enabled",
|
"864597982c308d72": "Silent start enabled",
|
||||||
"86de7c4ee8fa7689": "Sync models",
|
"86de7c4ee8fa7689": "Sync models",
|
||||||
"8716e1344b0daddb": "Cursor official",
|
"8716e1344b0daddb": "Cursor official",
|
||||||
@@ -332,7 +333,6 @@
|
|||||||
"d6b1f203680f5496": "Leave blank to use adaptive thinking",
|
"d6b1f203680f5496": "Leave blank to use adaptive thinking",
|
||||||
"d766536c18e8e990": "Plugin runtime {version} is installed and ready to use.",
|
"d766536c18e8e990": "Plugin runtime {version} is installed and ready to use.",
|
||||||
"d7e266bdc8064193": "Group name",
|
"d7e266bdc8064193": "Group name",
|
||||||
"d86fa42c3848c680": "Use system proxy",
|
|
||||||
"d8c47e9776cf1082": "Main menu",
|
"d8c47e9776cf1082": "Main menu",
|
||||||
"d8c589c455675b46": "Prompt settings saved",
|
"d8c589c455675b46": "Prompt settings saved",
|
||||||
"da521d1c1cbd36af": "Authorization is required to install the certificate",
|
"da521d1c1cbd36af": "Authorization is required to install the certificate",
|
||||||
|
|||||||
@@ -203,6 +203,7 @@
|
|||||||
"83fcfb4c1f2c1641": "获取模型",
|
"83fcfb4c1f2c1641": "获取模型",
|
||||||
"842b9f11cdd96bda": "开机启动",
|
"842b9f11cdd96bda": "开机启动",
|
||||||
"843ac7e15a5047a7": "确认导入旧版模型配置",
|
"843ac7e15a5047a7": "确认导入旧版模型配置",
|
||||||
|
"844b8cc8dff7c1d8": "默认",
|
||||||
"864597982c308d72": "已开启静默启动",
|
"864597982c308d72": "已开启静默启动",
|
||||||
"86de7c4ee8fa7689": "同步模型",
|
"86de7c4ee8fa7689": "同步模型",
|
||||||
"8716e1344b0daddb": "Cursor 官方",
|
"8716e1344b0daddb": "Cursor 官方",
|
||||||
@@ -332,7 +333,6 @@
|
|||||||
"d6b1f203680f5496": "留空使用 adaptive thinking",
|
"d6b1f203680f5496": "留空使用 adaptive thinking",
|
||||||
"d766536c18e8e990": "插件运行时 {version} 已安装,可以开始使用插件。",
|
"d766536c18e8e990": "插件运行时 {version} 已安装,可以开始使用插件。",
|
||||||
"d7e266bdc8064193": "分组名称",
|
"d7e266bdc8064193": "分组名称",
|
||||||
"d86fa42c3848c680": "使用系统代理",
|
|
||||||
"d8c47e9776cf1082": "主菜单",
|
"d8c47e9776cf1082": "主菜单",
|
||||||
"d8c589c455675b46": "提示词设置已保存",
|
"d8c589c455675b46": "提示词设置已保存",
|
||||||
"da521d1c1cbd36af": "需要授权安装证书",
|
"da521d1c1cbd36af": "需要授权安装证书",
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export interface StatisticsStorage {
|
|||||||
|
|
||||||
export type StatisticsStorageScope = "details" | "all";
|
export type StatisticsStorageScope = "details" | "all";
|
||||||
|
|
||||||
export type ProxyMode = "system" | "custom";
|
export type ProxyMode = "default" | "custom";
|
||||||
|
|
||||||
export interface ProxySettings {
|
export interface ProxySettings {
|
||||||
mode: ProxyMode;
|
mode: ProxyMode;
|
||||||
|
|||||||
@@ -683,6 +683,13 @@ impl ControlService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_proxy_settings(&self, settings: ProxySettingsInput) -> Result<ProxySettings> {
|
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?;
|
let settings = self.store.set_proxy_settings(settings).await?;
|
||||||
self.clients.invalidate().await;
|
self.clients.invalidate().await;
|
||||||
Ok(settings)
|
Ok(settings)
|
||||||
|
|||||||
@@ -90,6 +90,10 @@ impl CursorHarness {
|
|||||||
*self.inner.backend_addr.write() = Some(addr);
|
*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<()> {
|
pub async fn cleanup_stale_settings(&self) -> Result<()> {
|
||||||
settings::clear_stale_managed_settings()
|
settings::clear_stale_managed_settings()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ impl ProxyRuntime {
|
|||||||
pub fn url(&self) -> Option<String> {
|
pub fn url(&self) -> Option<String> {
|
||||||
self.running().then(|| self.url.clone()).flatten()
|
self.running().then(|| self.url.clone()).flatten()
|
||||||
}
|
}
|
||||||
|
pub fn port(&self) -> Option<u16> {
|
||||||
|
if self.running() {
|
||||||
|
self.port
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn start(
|
pub async fn start(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|||||||
+115
-11
@@ -4,7 +4,12 @@ use std::{sync::Arc, time::Duration};
|
|||||||
|
|
||||||
use tokio::sync::RwLock;
|
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)]
|
#[derive(Clone)]
|
||||||
pub struct NetworkClients {
|
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.
|
// only offer legacy TLS 1.2 cipher suites unsupported by rustls.
|
||||||
let mut builder = reqwest::Client::builder().use_native_tls();
|
let mut builder = reqwest::Client::builder().use_native_tls();
|
||||||
if settings.mode.is_custom() {
|
if settings.mode.is_custom() {
|
||||||
let mut proxy = reqwest::Proxy::all(&settings.address)?;
|
builder = builder.proxy(custom_proxy(&settings)?);
|
||||||
if settings.auth_enabled {
|
|
||||||
proxy = proxy.basic_auth(&settings.username, &settings.password);
|
|
||||||
}
|
|
||||||
builder = builder.no_proxy().proxy(proxy);
|
|
||||||
}
|
}
|
||||||
Ok(builder)
|
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 settings = store.proxy_settings_secret().await?;
|
||||||
let mut builder = reqwest::blocking::Client::builder().use_native_tls();
|
let mut builder = reqwest::blocking::Client::builder().use_native_tls();
|
||||||
if settings.mode.is_custom() {
|
if settings.mode.is_custom() {
|
||||||
let mut proxy = reqwest::Proxy::all(&settings.address)?;
|
builder = builder.proxy(custom_proxy(&settings)?);
|
||||||
if settings.auth_enabled {
|
|
||||||
proxy = proxy.basic_auth(&settings.username, &settings.password);
|
|
||||||
}
|
|
||||||
builder = builder.no_proxy().proxy(proxy);
|
|
||||||
}
|
}
|
||||||
Ok(builder)
|
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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ pub struct PortSettings {
|
|||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum ProxyMode {
|
pub enum ProxyMode {
|
||||||
#[default]
|
#[default]
|
||||||
System,
|
Default,
|
||||||
Custom,
|
Custom,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,3 +362,22 @@ impl Store {
|
|||||||
Ok(settings)
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user