mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-29 09:37:37 +08:00
fix(proxy): prevent proxy recursion when system proxy points to localhost
Detect if HTTP_PROXY, HTTPS_PROXY, or ALL_PROXY environment variables point to loopback addresses (localhost, 127.0.0.1), and bypass system proxy in such cases to avoid infinite request loops.
This commit is contained in:
@@ -97,8 +97,7 @@ pub fn convert_to_opencode_format(spec: &Value) -> Result<Value, AppError> {
|
||||
}
|
||||
_ => {
|
||||
return Err(AppError::McpValidation(format!(
|
||||
"Unknown MCP type: {}",
|
||||
typ
|
||||
"Unknown MCP type: {typ}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
@@ -171,8 +170,7 @@ pub fn convert_from_opencode_format(spec: &Value) -> Result<Value, AppError> {
|
||||
}
|
||||
_ => {
|
||||
return Err(AppError::McpValidation(format!(
|
||||
"Unknown OpenCode MCP type: {}",
|
||||
typ
|
||||
"Unknown OpenCode MCP type: {typ}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
@@ -230,16 +228,16 @@ pub fn import_from_opencode(config: &mut MultiAppConfig) -> Result<usize, AppErr
|
||||
let unified_spec = match convert_from_opencode_format(&spec) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log::warn!("Skip invalid OpenCode MCP server '{}': {}", id, e);
|
||||
errors.push(format!("{}: {}", id, e));
|
||||
log::warn!("Skip invalid OpenCode MCP server '{id}': {e}");
|
||||
errors.push(format!("{id}: {e}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Validate the converted spec
|
||||
if let Err(e) = validate_server_spec(&unified_spec) {
|
||||
log::warn!("Skip invalid MCP server '{}' after conversion: {}", id, e);
|
||||
errors.push(format!("{}: {}", id, e));
|
||||
log::warn!("Skip invalid MCP server '{id}' after conversion: {e}");
|
||||
errors.push(format!("{id}: {e}"));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -248,7 +246,7 @@ pub fn import_from_opencode(config: &mut MultiAppConfig) -> Result<usize, AppErr
|
||||
if !existing.apps.opencode {
|
||||
existing.apps.opencode = true;
|
||||
changed += 1;
|
||||
log::info!("MCP server '{}' enabled for OpenCode", id);
|
||||
log::info!("MCP server '{id}' enabled for OpenCode");
|
||||
}
|
||||
} else {
|
||||
// New server: default to only OpenCode enabled
|
||||
@@ -271,7 +269,7 @@ pub fn import_from_opencode(config: &mut MultiAppConfig) -> Result<usize, AppErr
|
||||
},
|
||||
);
|
||||
changed += 1;
|
||||
log::info!("Imported new MCP server '{}' from OpenCode", id);
|
||||
log::info!("Imported new MCP server '{id}' from OpenCode");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ pub fn write_opencode_config(config: &Value) -> Result<(), AppError> {
|
||||
// 复用统一的原子写入逻辑(兼容 Windows 上目标文件已存在的情况)
|
||||
write_json_file(&path, config)?;
|
||||
|
||||
log::debug!("OpenCode config written to {:?}", path);
|
||||
log::debug!("OpenCode config written to {path:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ pub fn get_typed_providers() -> Result<IndexMap<String, OpenCodeProviderConfig>,
|
||||
result.insert(id, config);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to parse provider '{}': {}", id, e);
|
||||
log::warn!("Failed to parse provider '{id}': {e}");
|
||||
// Skip invalid providers but continue
|
||||
}
|
||||
}
|
||||
@@ -219,4 +219,3 @@ pub fn remove_mcp_server(id: &str) -> Result<(), AppError> {
|
||||
|
||||
write_opencode_config(&config)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
use crate::provider::ProviderProxyConfig;
|
||||
use once_cell::sync::OnceCell;
|
||||
use reqwest::Client;
|
||||
use std::env;
|
||||
use std::net::IpAddr;
|
||||
use std::sync::RwLock;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -163,16 +165,8 @@ pub fn get() -> Client {
|
||||
.and_then(|lock| lock.read().ok())
|
||||
.map(|c| c.clone())
|
||||
.unwrap_or_else(|| {
|
||||
// 如果还没初始化,创建一个默认客户端(配置与 build_client 一致)
|
||||
// 不调用 no_proxy(),让 reqwest 自动检测系统代理
|
||||
log::warn!("[GlobalProxy] [GP-004] Client not initialized, using fallback");
|
||||
Client::builder()
|
||||
.timeout(Duration::from_secs(600))
|
||||
.connect_timeout(Duration::from_secs(30))
|
||||
.pool_max_idle_per_host(10)
|
||||
.tcp_keepalive(Duration::from_secs(60))
|
||||
.build()
|
||||
.unwrap_or_default()
|
||||
build_client(None).unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -220,9 +214,16 @@ fn build_client(proxy_url: Option<&str>) -> Result<Client, String> {
|
||||
builder = builder.proxy(proxy);
|
||||
log::debug!("[GlobalProxy] Proxy configured: {}", mask_url(url));
|
||||
} else {
|
||||
// 未设置全局代理时,不调用 no_proxy(),让 reqwest 自动检测系统代理
|
||||
// reqwest 会自动读取 HTTP_PROXY、HTTPS_PROXY 等环境变量
|
||||
log::debug!("[GlobalProxy] Following system proxy (no explicit proxy configured)");
|
||||
// 未设置全局代理时,让 reqwest 自动检测系统代理(环境变量)
|
||||
// 若系统代理指向本机,禁用系统代理避免自环
|
||||
if system_proxy_points_to_loopback() {
|
||||
builder = builder.no_proxy();
|
||||
log::warn!(
|
||||
"[GlobalProxy] System proxy points to localhost, bypassing to avoid recursion"
|
||||
);
|
||||
} else {
|
||||
log::debug!("[GlobalProxy] Following system proxy (no explicit proxy configured)");
|
||||
}
|
||||
}
|
||||
|
||||
builder
|
||||
@@ -230,6 +231,50 @@ fn build_client(proxy_url: Option<&str>) -> Result<Client, String> {
|
||||
.map_err(|e| format!("Failed to build HTTP client: {e}"))
|
||||
}
|
||||
|
||||
fn system_proxy_points_to_loopback() -> bool {
|
||||
const KEYS: [&str; 6] = [
|
||||
"HTTP_PROXY",
|
||||
"http_proxy",
|
||||
"HTTPS_PROXY",
|
||||
"https_proxy",
|
||||
"ALL_PROXY",
|
||||
"all_proxy",
|
||||
];
|
||||
|
||||
KEYS.iter()
|
||||
.filter_map(|key| env::var(key).ok())
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.any(|value| proxy_points_to_loopback(&value))
|
||||
}
|
||||
|
||||
fn proxy_points_to_loopback(value: &str) -> bool {
|
||||
fn host_is_loopback(host: &str) -> bool {
|
||||
if host.eq_ignore_ascii_case("localhost") {
|
||||
return true;
|
||||
}
|
||||
host.parse::<IpAddr>()
|
||||
.map(|ip| ip.is_loopback())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
if let Ok(parsed) = url::Url::parse(value) {
|
||||
if let Some(host) = parsed.host_str() {
|
||||
return host_is_loopback(host);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
let with_scheme = format!("http://{value}");
|
||||
if let Ok(parsed) = url::Url::parse(&with_scheme) {
|
||||
if let Some(host) = parsed.host_str() {
|
||||
return host_is_loopback(host);
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// 隐藏 URL 中的敏感信息(用于日志)
|
||||
pub fn mask_url(url: &str) -> String {
|
||||
if let Ok(parsed) = url::Url::parse(url) {
|
||||
@@ -346,6 +391,12 @@ pub fn get_for_provider(proxy_config: Option<&ProviderProxyConfig>) -> Client {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
fn env_lock() -> &'static Mutex<()> {
|
||||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
LOCK.get_or_init(|| Mutex::new(()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mask_url() {
|
||||
@@ -394,4 +445,40 @@ mod tests {
|
||||
let result = build_client(Some("invalid-scheme://127.0.0.1:7890"));
|
||||
assert!(result.is_err(), "Should reject invalid proxy scheme");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_proxy_points_to_loopback() {
|
||||
assert!(proxy_points_to_loopback("http://127.0.0.1:7890"));
|
||||
assert!(proxy_points_to_loopback("socks5://localhost:1080"));
|
||||
assert!(proxy_points_to_loopback("127.0.0.1:7890"));
|
||||
assert!(!proxy_points_to_loopback("http://192.168.1.10:7890"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_proxy_points_to_loopback() {
|
||||
let _guard = env_lock().lock().unwrap();
|
||||
|
||||
let keys = [
|
||||
"HTTP_PROXY",
|
||||
"http_proxy",
|
||||
"HTTPS_PROXY",
|
||||
"https_proxy",
|
||||
"ALL_PROXY",
|
||||
"all_proxy",
|
||||
];
|
||||
|
||||
for key in &keys {
|
||||
std::env::remove_var(key);
|
||||
}
|
||||
|
||||
std::env::set_var("HTTP_PROXY", "http://127.0.0.1:7890");
|
||||
assert!(system_proxy_points_to_loopback());
|
||||
|
||||
std::env::set_var("HTTP_PROXY", "http://10.0.0.2:7890");
|
||||
assert!(!system_proxy_points_to_loopback());
|
||||
|
||||
for key in &keys {
|
||||
std::env::remove_var(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,17 +501,13 @@ pub(crate) fn remove_opencode_provider_from_live(provider_id: &str) -> Result<()
|
||||
// Check if OpenCode config directory exists
|
||||
if !opencode_config::get_opencode_dir().exists() {
|
||||
log::debug!(
|
||||
"OpenCode config directory doesn't exist, skipping removal of '{}'",
|
||||
provider_id
|
||||
"OpenCode config directory doesn't exist, skipping removal of '{provider_id}'"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
opencode_config::remove_provider(provider_id)?;
|
||||
log::info!(
|
||||
"OpenCode provider '{}' removed from live config",
|
||||
provider_id
|
||||
);
|
||||
log::info!("OpenCode provider '{provider_id}' removed from live config");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -535,10 +531,7 @@ pub fn import_opencode_providers_from_live(state: &AppState) -> Result<usize, Ap
|
||||
for (id, config) in providers {
|
||||
// Skip if already exists in database
|
||||
if existing.contains_key(&id) {
|
||||
log::debug!(
|
||||
"OpenCode provider '{}' already exists in database, skipping",
|
||||
id
|
||||
);
|
||||
log::debug!("OpenCode provider '{id}' already exists in database, skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -546,7 +539,7 @@ pub fn import_opencode_providers_from_live(state: &AppState) -> Result<usize, Ap
|
||||
let settings_config = match serde_json::to_value(&config) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
log::warn!("Failed to serialize OpenCode provider '{}': {}", id, e);
|
||||
log::warn!("Failed to serialize OpenCode provider '{id}': {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@@ -561,12 +554,12 @@ pub fn import_opencode_providers_from_live(state: &AppState) -> Result<usize, Ap
|
||||
|
||||
// Save to database
|
||||
if let Err(e) = state.db.save_provider("opencode", &provider) {
|
||||
log::warn!("Failed to import OpenCode provider '{}': {}", id, e);
|
||||
log::warn!("Failed to import OpenCode provider '{id}': {e}");
|
||||
continue;
|
||||
}
|
||||
|
||||
imported += 1;
|
||||
log::info!("Imported OpenCode provider '{}' from live config", id);
|
||||
log::info!("Imported OpenCode provider '{id}' from live config");
|
||||
}
|
||||
|
||||
Ok(imported)
|
||||
|
||||
@@ -323,7 +323,12 @@ impl SkillService {
|
||||
.ok_or_else(|| anyhow!("Skill not found: {id}"))?;
|
||||
|
||||
// 从所有应用目录删除
|
||||
for app in [AppType::Claude, AppType::Codex, AppType::Gemini, AppType::OpenCode] {
|
||||
for app in [
|
||||
AppType::Claude,
|
||||
AppType::Codex,
|
||||
AppType::Gemini,
|
||||
AppType::OpenCode,
|
||||
] {
|
||||
let _ = Self::remove_from_app(&skill.directory, &app);
|
||||
}
|
||||
|
||||
@@ -382,7 +387,12 @@ impl SkillService {
|
||||
|
||||
let mut unmanaged: HashMap<String, UnmanagedSkill> = HashMap::new();
|
||||
|
||||
for app in [AppType::Claude, AppType::Codex, AppType::Gemini, AppType::OpenCode] {
|
||||
for app in [
|
||||
AppType::Claude,
|
||||
AppType::Codex,
|
||||
AppType::Gemini,
|
||||
AppType::OpenCode,
|
||||
] {
|
||||
let app_dir = match Self::get_app_skills_dir(&app) {
|
||||
Ok(d) => d,
|
||||
Err(_) => continue,
|
||||
@@ -464,7 +474,12 @@ impl SkillService {
|
||||
let mut source_path: Option<PathBuf> = None;
|
||||
let mut found_in: Vec<String> = Vec::new();
|
||||
|
||||
for app in [AppType::Claude, AppType::Codex, AppType::Gemini, AppType::OpenCode] {
|
||||
for app in [
|
||||
AppType::Claude,
|
||||
AppType::Codex,
|
||||
AppType::Gemini,
|
||||
AppType::OpenCode,
|
||||
] {
|
||||
if let Ok(app_dir) = Self::get_app_skills_dir(&app) {
|
||||
let skill_path = app_dir.join(&dir_name);
|
||||
if skill_path.exists() {
|
||||
@@ -985,7 +1000,12 @@ pub fn migrate_skills_to_ssot(db: &Arc<Database>) -> Result<usize> {
|
||||
let mut discovered: HashMap<String, SkillApps> = HashMap::new();
|
||||
|
||||
// 扫描各应用目录
|
||||
for app in [AppType::Claude, AppType::Codex, AppType::Gemini, AppType::OpenCode] {
|
||||
for app in [
|
||||
AppType::Claude,
|
||||
AppType::Codex,
|
||||
AppType::Gemini,
|
||||
AppType::OpenCode,
|
||||
] {
|
||||
let app_dir = match SkillService::get_app_skills_dir(&app) {
|
||||
Ok(d) => d,
|
||||
Err(_) => continue,
|
||||
|
||||
Reference in New Issue
Block a user