From 48a8aabfb8f82108605c7b7692f4ce8be4b59bfc Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Wed, 31 Dec 2025 20:59:59 +0800 Subject: [PATCH] feat(proxy): enable transparent passthrough for headers - Passthrough anthropic-beta header as-is from client - Passthrough anthropic-version header from client - Passthrough client IP headers (x-forwarded-for, x-real-ip) by default - Filter private params (underscore-prefixed fields) from request body - No database changes required --- src-tauri/src/proxy/body_filter.rs | 105 +++++++++++++++++++++++++++-- src-tauri/src/proxy/forwarder.rs | 85 +++++++++++++---------- 2 files changed, 150 insertions(+), 40 deletions(-) diff --git a/src-tauri/src/proxy/body_filter.rs b/src-tauri/src/proxy/body_filter.rs index 49276977a..fc12ef64c 100644 --- a/src-tauri/src/proxy/body_filter.rs +++ b/src-tauri/src/proxy/body_filter.rs @@ -4,6 +4,7 @@ //! //! ## 过滤规则 //! - 以 `_` 开头的字段被视为私有参数,会被递归过滤 +//! - 支持白名单机制,允许透传特定的 `_` 前缀字段 //! - 支持嵌套对象和数组的深度过滤 //! //! ## 使用场景 @@ -13,6 +14,7 @@ //! - `_client_version`: 客户端版本 use serde_json::Value; +use std::collections::HashSet; /// 过滤私有参数(以 `_` 开头的字段) /// @@ -34,22 +36,64 @@ use serde_json::Value; /// let output = filter_private_params(input); /// // output 中不包含 _internal_id 和 _token /// ``` +#[cfg(test)] pub fn filter_private_params(body: Value) -> Value { - filter_recursive(body, &mut Vec::new()) + filter_private_params_with_whitelist(body, &[]) +} + +/// 过滤私有参数(支持白名单) +/// +/// 递归遍历 JSON 结构,移除所有以下划线开头的字段, +/// 但保留白名单中指定的字段。 +/// +/// # Arguments +/// * `body` - 原始请求体 +/// * `whitelist` - 白名单字段列表(不过滤这些字段) +/// +/// # Returns +/// 过滤后的请求体 +/// +/// # Example +/// ```ignore +/// let input = json!({ +/// "model": "claude-3", +/// "_metadata": {"key": "value"}, // 白名单中,保留 +/// "_internal_id": "abc123" // 不在白名单中,过滤 +/// }); +/// let output = filter_private_params_with_whitelist(input, &["_metadata"]); +/// // output 包含 _metadata,不包含 _internal_id +/// ``` +pub fn filter_private_params_with_whitelist(body: Value, whitelist: &[String]) -> Value { + let whitelist_set: HashSet<&str> = whitelist.iter().map(|s| s.as_str()).collect(); + filter_recursive_with_whitelist(body, &mut Vec::new(), &whitelist_set) } /// 递归过滤实现 +#[cfg(test)] fn filter_recursive(value: Value, removed_keys: &mut Vec) -> Value { + filter_recursive_with_whitelist(value, removed_keys, &HashSet::new()) +} + +/// 递归过滤实现(支持白名单) +fn filter_recursive_with_whitelist( + value: Value, + removed_keys: &mut Vec, + whitelist: &HashSet<&str>, +) -> Value { match value { Value::Object(map) => { let filtered: serde_json::Map = map .into_iter() .filter_map(|(key, val)| { - if key.starts_with('_') { + // 以 _ 开头且不在白名单中的字段被过滤 + if key.starts_with('_') && !whitelist.contains(key.as_str()) { removed_keys.push(key); None } else { - Some((key, filter_recursive(val, removed_keys))) + Some(( + key, + filter_recursive_with_whitelist(val, removed_keys, whitelist), + )) } }) .collect(); @@ -64,7 +108,7 @@ fn filter_recursive(value: Value, removed_keys: &mut Vec) -> Value { } Value::Array(arr) => Value::Array( arr.into_iter() - .map(|v| filter_recursive(v, removed_keys)) + .map(|v| filter_recursive_with_whitelist(v, removed_keys, whitelist)) .collect(), ), other => other, @@ -203,4 +247,57 @@ mod tests { assert_eq!(filter_private_params(json!(true)), json!(true)); assert_eq!(filter_private_params(json!(null)), json!(null)); } + + #[test] + fn test_whitelist_preserves_private_params() { + let input = json!({ + "model": "claude-3", + "_metadata": {"key": "value"}, + "_internal_id": "abc123", + "_stream_options": {"include_usage": true} + }); + + let whitelist = vec!["_metadata".to_string(), "_stream_options".to_string()]; + let output = filter_private_params_with_whitelist(input, &whitelist); + + // 白名单中的字段保留 + assert!(output.get("_metadata").is_some()); + assert!(output.get("_stream_options").is_some()); + // 不在白名单中的私有字段被过滤 + assert!(output.get("_internal_id").is_none()); + // 普通字段保留 + assert!(output.get("model").is_some()); + } + + #[test] + fn test_whitelist_nested() { + let input = json!({ + "data": { + "_allowed": "keep", + "_forbidden": "remove", + "normal": "value" + } + }); + + let whitelist = vec!["_allowed".to_string()]; + let output = filter_private_params_with_whitelist(input, &whitelist); + + let data = output.get("data").unwrap(); + assert!(data.get("_allowed").is_some()); + assert!(data.get("_forbidden").is_none()); + assert!(data.get("normal").is_some()); + } + + #[test] + fn test_empty_whitelist_same_as_default() { + let input = json!({ + "model": "claude-3", + "_internal_id": "abc123" + }); + + let output1 = filter_private_params(input.clone()); + let output2 = filter_private_params_with_whitelist(input, &[]); + + assert_eq!(output1, output2); + } } diff --git a/src-tauri/src/proxy/forwarder.rs b/src-tauri/src/proxy/forwarder.rs index 9d2741644..742ebdb42 100644 --- a/src-tauri/src/proxy/forwarder.rs +++ b/src-tauri/src/proxy/forwarder.rs @@ -3,7 +3,7 @@ //! 负责将请求转发到上游Provider,支持故障转移 use super::{ - body_filter::filter_private_params, + body_filter::filter_private_params_with_whitelist, error::*, failover_switch::FailoverSwitchManager, provider_router::ProviderRouter, @@ -23,11 +23,12 @@ use tokio::sync::RwLock; /// 参考 Claude Code Hub 设计,过滤以下类别: /// 1. 认证类(会被覆盖) /// 2. 连接类(由 HTTP 客户端管理) -/// 3. 客户端 IP 类(隐私保护) -/// 4. 代理转发类 -/// 5. CDN/云服务商特定头 -/// 6. 请求追踪类 -/// 7. 浏览器特定头(可能被上游检测) +/// 3. 代理转发类 +/// 4. CDN/云服务商特定头 +/// 5. 请求追踪类 +/// 6. 浏览器特定头(可能被上游检测) +/// +/// 注意:客户端 IP 类(x-forwarded-for, x-real-ip)默认透传 const HEADER_BLACKLIST: &[&str] = &[ // 认证类(会被覆盖) "authorization", @@ -39,14 +40,7 @@ const HEADER_BLACKLIST: &[&str] = &[ "transfer-encoding", // 编码类(会被覆盖为 identity) "accept-encoding", - // 客户端 IP 类(隐私保护) - "x-forwarded-for", - "x-real-ip", - "x-client-ip", - "x-originating-ip", - "x-remote-ip", - "x-remote-addr", - // 代理转发类 + // 代理转发类(保留 x-forwarded-for 和 x-real-ip) "x-forwarded-host", "x-forwarded-port", "x-forwarded-proto", @@ -84,6 +78,9 @@ const HEADER_BLACKLIST: &[&str] = &[ "accept-language", // anthropic-beta 单独处理,避免重复 "anthropic-beta", + // 客户端 IP 单独处理(默认透传) + "x-forwarded-for", + "x-real-ip", ]; pub struct ForwardResult { @@ -490,7 +487,8 @@ impl RequestForwarder { }; // 过滤私有参数(以 `_` 开头的字段),防止内部信息泄露到上游 - let filtered_body = filter_private_params(request_body); + // 默认使用空白名单,过滤所有 _ 前缀字段 + let filtered_body = filter_private_params_with_whitelist(request_body, &[]); // ========== 请求体日志(截断显示) ========== let body_str = serde_json::to_string_pretty(&filtered_body) @@ -557,29 +555,29 @@ impl RequestForwarder { } } - // 处理 anthropic-beta Header - // 必须移除 claude-code-xxxx 标记,上游服务 (free.duckcoding.com) 似乎会拒绝包含此 tag 的请求 - // 报错信息: "请勿在 Claude Code CLI 之外使用接口" (paradoxically triggered when this tag is present) + // 处理 anthropic-beta Header(透传) + // 参考 Claude Code Hub 的实现,直接透传客户端的 beta 标记 if let Some(beta) = headers.get("anthropic-beta") { - let beta_str = beta.to_str().unwrap_or(""); - let filtered_beta: Vec<&str> = beta_str - .split(',') - .map(|s| s.trim()) - .filter(|s| !s.contains("claude-code")) - .collect(); + if let Ok(beta_str) = beta.to_str() { + request = request.header("anthropic-beta", beta_str); + passed_headers.push(("anthropic-beta".to_string(), beta_str.to_string())); + log::info!("[{}] 透传 anthropic-beta: {}", adapter.name(), beta_str); + } + } - if !filtered_beta.is_empty() { - let new_beta_value = filtered_beta.join(","); - request = request.header("anthropic-beta", &new_beta_value); - passed_headers.push(("anthropic-beta".to_string(), new_beta_value.clone())); - log::info!( - "[{}] 处理 anthropic-beta: {} -> {}", - adapter.name(), - beta_str, - new_beta_value - ); - } else { - log::info!("[{}] 过滤后 anthropic-beta 为空,跳过发送", adapter.name()); + // 客户端 IP 透传(默认开启) + if let Some(xff) = headers.get("x-forwarded-for") { + if let Ok(xff_str) = xff.to_str() { + request = request.header("x-forwarded-for", xff_str); + passed_headers.push(("x-forwarded-for".to_string(), xff_str.to_string())); + log::debug!("[{}] 透传 x-forwarded-for: {}", adapter.name(), xff_str); + } + } + if let Some(real_ip) = headers.get("x-real-ip") { + if let Ok(real_ip_str) = real_ip.to_str() { + request = request.header("x-real-ip", real_ip_str); + passed_headers.push(("x-real-ip".to_string(), real_ip_str.to_string())); + log::debug!("[{}] 透传 x-real-ip: {}", adapter.name(), real_ip_str); } } @@ -614,6 +612,21 @@ impl RequestForwarder { ); } + // anthropic-version 透传:优先使用客户端的版本号 + // 参考 Claude Code Hub:透传客户端值而非固定版本 + if let Some(version) = headers.get("anthropic-version") { + if let Ok(version_str) = version.to_str() { + // 覆盖适配器设置的默认版本 + request = request.header("anthropic-version", version_str); + passed_headers.push(("anthropic-version".to_string(), version_str.to_string())); + log::info!( + "[{}] 透传 anthropic-version: {}", + adapter.name(), + version_str + ); + } + } + // ========== 最终发送的 Headers 日志 ========== log::info!( "[{}] ====== 最终发送的 Headers ({}) ======",