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
This commit is contained in:
YoVinchen
2025-12-31 20:59:59 +08:00
parent cf79b09597
commit 48a8aabfb8
2 changed files with 150 additions and 40 deletions
+101 -4
View File
@@ -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<String>) -> Value {
filter_recursive_with_whitelist(value, removed_keys, &HashSet::new())
}
/// 递归过滤实现(支持白名单)
fn filter_recursive_with_whitelist(
value: Value,
removed_keys: &mut Vec<String>,
whitelist: &HashSet<&str>,
) -> Value {
match value {
Value::Object(map) => {
let filtered: serde_json::Map<String, Value> = 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<String>) -> 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);
}
}
+49 -36
View File
@@ -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 ({}) ======",