feat(proxy): add body and header filtering for upstream requests

This commit is contained in:
YoVinchen
2025-12-31 17:15:00 +08:00
parent 6004084644
commit cf79b09597
3 changed files with 377 additions and 4 deletions
+206
View File
@@ -0,0 +1,206 @@
//! 请求体过滤模块
//!
//! 过滤不应透传到上游的私有参数,防止内部信息泄露。
//!
//! ## 过滤规则
//! - 以 `_` 开头的字段被视为私有参数,会被递归过滤
//! - 支持嵌套对象和数组的深度过滤
//!
//! ## 使用场景
//! - `_internal_id`: 内部追踪 ID
//! - `_debug_mode`: 调试标记
//! - `_session_token`: 会话令牌
//! - `_client_version`: 客户端版本
use serde_json::Value;
/// 过滤私有参数(以 `_` 开头的字段)
///
/// 递归遍历 JSON 结构,移除所有以下划线开头的字段。
///
/// # Arguments
/// * `body` - 原始请求体
///
/// # Returns
/// 过滤后的请求体
///
/// # Example
/// ```ignore
/// let input = json!({
/// "model": "claude-3",
/// "_internal_id": "abc123",
/// "messages": [{"role": "user", "content": "hello", "_token": "secret"}]
/// });
/// let output = filter_private_params(input);
/// // output 中不包含 _internal_id 和 _token
/// ```
pub fn filter_private_params(body: Value) -> Value {
filter_recursive(body, &mut Vec::new())
}
/// 递归过滤实现
fn filter_recursive(value: Value, removed_keys: &mut Vec<String>) -> Value {
match value {
Value::Object(map) => {
let filtered: serde_json::Map<String, Value> = map
.into_iter()
.filter_map(|(key, val)| {
if key.starts_with('_') {
removed_keys.push(key);
None
} else {
Some((key, filter_recursive(val, removed_keys)))
}
})
.collect();
// 仅在有过滤时记录日志(避免每次请求都打印)
if !removed_keys.is_empty() {
log::debug!("[BodyFilter] 过滤私有参数: {removed_keys:?}");
removed_keys.clear();
}
Value::Object(filtered)
}
Value::Array(arr) => Value::Array(
arr.into_iter()
.map(|v| filter_recursive(v, removed_keys))
.collect(),
),
other => other,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_filter_top_level_private_params() {
let input = json!({
"model": "claude-3",
"_internal_id": "abc123",
"_debug": true,
"max_tokens": 1024
});
let output = filter_private_params(input);
assert!(output.get("model").is_some());
assert!(output.get("max_tokens").is_some());
assert!(output.get("_internal_id").is_none());
assert!(output.get("_debug").is_none());
}
#[test]
fn test_filter_nested_private_params() {
let input = json!({
"model": "claude-3",
"messages": [
{
"role": "user",
"content": "hello",
"_session_token": "secret"
}
],
"metadata": {
"user_id": "user-1",
"_tracking_id": "track-1"
}
});
let output = filter_private_params(input);
// 顶级字段保留
assert!(output.get("model").is_some());
assert!(output.get("messages").is_some());
assert!(output.get("metadata").is_some());
// messages 数组中的私有参数被过滤
let messages = output.get("messages").unwrap().as_array().unwrap();
assert!(messages[0].get("role").is_some());
assert!(messages[0].get("content").is_some());
assert!(messages[0].get("_session_token").is_none());
// metadata 对象中的私有参数被过滤
let metadata = output.get("metadata").unwrap();
assert!(metadata.get("user_id").is_some());
assert!(metadata.get("_tracking_id").is_none());
}
#[test]
fn test_filter_deeply_nested() {
let input = json!({
"level1": {
"level2": {
"level3": {
"keep": "value",
"_remove": "secret"
}
}
}
});
let output = filter_private_params(input);
let level3 = output
.get("level1")
.unwrap()
.get("level2")
.unwrap()
.get("level3")
.unwrap();
assert!(level3.get("keep").is_some());
assert!(level3.get("_remove").is_none());
}
#[test]
fn test_filter_array_of_objects() {
let input = json!({
"items": [
{"id": 1, "_secret": "a"},
{"id": 2, "_secret": "b"},
{"id": 3, "_secret": "c"}
]
});
let output = filter_private_params(input);
let items = output.get("items").unwrap().as_array().unwrap();
for item in items {
assert!(item.get("id").is_some());
assert!(item.get("_secret").is_none());
}
}
#[test]
fn test_no_private_params() {
let input = json!({
"model": "claude-3",
"messages": [{"role": "user", "content": "hello"}]
});
let output = filter_private_params(input.clone());
// 无私有参数时,输出应与输入相同
assert_eq!(input, output);
}
#[test]
fn test_empty_object() {
let input = json!({});
let output = filter_private_params(input);
assert_eq!(output, json!({}));
}
#[test]
fn test_primitive_values() {
// 原始值不应被修改
assert_eq!(filter_private_params(json!(42)), json!(42));
assert_eq!(filter_private_params(json!("string")), json!("string"));
assert_eq!(filter_private_params(json!(true)), json!(true));
assert_eq!(filter_private_params(json!(null)), json!(null));
}
}
+170 -4
View File
@@ -3,6 +3,7 @@
//! 负责将请求转发到上游Provider,支持故障转移
use super::{
body_filter::filter_private_params,
error::*,
failover_switch::FailoverSwitchManager,
provider_router::ProviderRouter,
@@ -17,6 +18,74 @@ use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
/// Headers 黑名单 - 不透传到上游的 Headers
///
/// 参考 Claude Code Hub 设计,过滤以下类别:
/// 1. 认证类(会被覆盖)
/// 2. 连接类(由 HTTP 客户端管理)
/// 3. 客户端 IP 类(隐私保护)
/// 4. 代理转发类
/// 5. CDN/云服务商特定头
/// 6. 请求追踪类
/// 7. 浏览器特定头(可能被上游检测)
const HEADER_BLACKLIST: &[&str] = &[
// 认证类(会被覆盖)
"authorization",
"x-api-key",
// 连接类
"host",
"content-length",
"connection",
"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-host",
"x-forwarded-port",
"x-forwarded-proto",
"forwarded",
// CDN/云服务商特定头
"cf-connecting-ip",
"cf-ipcountry",
"cf-ray",
"cf-visitor",
"true-client-ip",
"fastly-client-ip",
"x-azure-clientip",
"x-azure-fdid",
"x-azure-ref",
"akamai-origin-hop",
"x-akamai-config-log-detail",
// 请求追踪类
"x-request-id",
"x-correlation-id",
"x-trace-id",
"x-amzn-trace-id",
"x-b3-traceid",
"x-b3-spanid",
"x-b3-parentspanid",
"x-b3-sampled",
"traceparent",
"tracestate",
// 浏览器特定头(可能被上游检测为非 CLI 请求)
"sec-fetch-mode",
"sec-fetch-site",
"sec-fetch-dest",
"sec-ch-ua",
"sec-ch-ua-mobile",
"sec-ch-ua-platform",
"accept-language",
// anthropic-beta 单独处理,避免重复
"anthropic-beta",
];
pub struct ForwardResult {
pub response: Response,
pub provider: Provider,
@@ -420,6 +489,27 @@ impl RequestForwarder {
mapped_body
};
// 过滤私有参数(以 `_` 开头的字段),防止内部信息泄露到上游
let filtered_body = filter_private_params(request_body);
// ========== 请求体日志(截断显示) ==========
let body_str = serde_json::to_string_pretty(&filtered_body)
.unwrap_or_else(|_| filtered_body.to_string());
let body_preview = if body_str.len() > 2000 {
format!(
"{}...\n[截断,总长度: {} 字符]",
&body_str[..2000],
body_str.len()
)
} else {
body_str
};
log::info!(
"[{}] ====== 最终请求体 ======\n{}",
adapter.name(),
body_preview
);
log::info!(
"[{}] 转发请求: {} -> {}",
adapter.name(),
@@ -430,17 +520,74 @@ impl RequestForwarder {
// 构建请求
let mut request = self.client.post(&url);
// 请求头黑名单:仅跳过会被覆盖或可能失效的字段(认证、Host、长度)
let skip_headers = ["authorization", "x-api-key", "host", "content-length"];
// ========== 详细 Headers 日志 ==========
log::info!("[{}] ====== 客户端原始 Headers ======", adapter.name());
for (key, value) in headers {
log::info!(
"[{}] {}: {:?}",
adapter.name(),
key.as_str(),
value.to_str().unwrap_or("<binary>")
);
}
// 过滤黑名单 Headers,保护隐私并避免冲突
let mut filtered_headers: Vec<String> = Vec::new();
let mut passed_headers: Vec<(String, String)> = Vec::new();
for (key, value) in headers {
let key_str = key.as_str().to_lowercase();
if skip_headers.contains(&key_str.as_str()) {
if HEADER_BLACKLIST.contains(&key_str.as_str()) {
filtered_headers.push(key_str);
continue;
}
let value_str = value.to_str().unwrap_or("<binary>").to_string();
passed_headers.push((key.as_str().to_string(), value_str.clone()));
request = request.header(key, value);
}
if !filtered_headers.is_empty() {
log::info!(
"[{}] ====== 被过滤的 Headers ({}) ======",
adapter.name(),
filtered_headers.len()
);
for h in &filtered_headers {
log::info!("[{}] - {}", adapter.name(), h);
}
}
// 处理 anthropic-beta Header
// 必须移除 claude-code-xxxx 标记,上游服务 (free.duckcoding.com) 似乎会拒绝包含此 tag 的请求
// 报错信息: "请勿在 Claude Code CLI 之外使用接口" (paradoxically triggered when this tag is present)
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 !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());
}
}
// 禁用压缩,避免 gzip 流式响应解析错误
// 参考 CCH: undici 在连接提前关闭时会对不完整的 gzip 流抛出错误
request = request.header("accept-encoding", "identity");
passed_headers.push(("accept-encoding".to_string(), "identity".to_string()));
// 使用适配器添加认证头
if let Some(auth) = adapter.extract_auth(provider) {
log::debug!(
@@ -450,6 +597,15 @@ impl RequestForwarder {
auth.masked_key()
);
request = adapter.add_auth_headers(request, &auth);
// 记录认证头(脱敏)
passed_headers.push((
"authorization".to_string(),
format!("Bearer {}...", &auth.api_key[..8.min(auth.api_key.len())]),
));
passed_headers.push((
"x-api-key".to_string(),
format!("{}...", &auth.api_key[..8.min(auth.api_key.len())]),
));
} else {
log::error!(
"[{}] 未找到 API KeyProvider: {}",
@@ -458,9 +614,19 @@ impl RequestForwarder {
);
}
// ========== 最终发送的 Headers 日志 ==========
log::info!(
"[{}] ====== 最终发送的 Headers ({}) ======",
adapter.name(),
passed_headers.len()
);
for (k, v) in &passed_headers {
log::info!("[{}] {}: {}", adapter.name(), k, v);
}
// 发送请求
log::info!("[{}] 发送请求到: {}", adapter.name(), url);
let response = request.json(&request_body).send().await.map_err(|e| {
let response = request.json(&filtered_body).send().await.map_err(|e| {
log::error!("[{}] 请求失败: {}", adapter.name(), e);
if e.is_timeout() {
ProxyError::Timeout(format!("请求超时: {e}"))
+1
View File
@@ -2,6 +2,7 @@
//!
//! 提供本地HTTP代理服务,支持多Provider故障转移和请求透传
pub mod body_filter;
pub mod circuit_breaker;
pub mod error;
pub mod error_mapper;