mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
fix(proxy): return Result from get_auth_headers to avoid panic on bad credentials
User-pasted API keys can contain control chars or CR/LF that make HeaderValue::from_str return Err; the previous unwrap inside every adapter turned such input into a process-wide panic instead of a request error. The trait now returns Result<_, ProxyError>; Claude/Codex/Gemini impls propagate ProxyError::AuthError so the client sees a 401 with the underlying parse error instead of a crash. Adds a regression test that pastes a CRLF-containing key and asserts AuthError.
This commit is contained in:
@@ -1151,7 +1151,7 @@ impl RequestForwarder {
|
||||
}
|
||||
}
|
||||
|
||||
adapter.get_auth_headers(&auth)
|
||||
adapter.get_auth_headers(&auth)?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
@@ -30,7 +30,14 @@ pub trait ProviderAdapter: Send + Sync {
|
||||
///
|
||||
/// The forwarder inserts these at the position of the original auth header
|
||||
/// so that header order is preserved.
|
||||
fn get_auth_headers(&self, auth: &AuthInfo) -> Vec<(http::HeaderName, http::HeaderValue)>;
|
||||
///
|
||||
/// Returns `ProxyError::AuthError` when the credential contains characters
|
||||
/// that cannot be encoded as an HTTP header value (e.g. control chars,
|
||||
/// CR/LF), which would otherwise panic inside `HeaderValue::from_str`.
|
||||
fn get_auth_headers(
|
||||
&self,
|
||||
auth: &AuthInfo,
|
||||
) -> Result<Vec<(http::HeaderName, http::HeaderValue)>, ProxyError>;
|
||||
|
||||
/// 是否需要格式转换
|
||||
fn needs_transform(&self, _provider: &Provider) -> bool {
|
||||
|
||||
@@ -589,33 +589,34 @@ impl ProviderAdapter for ClaudeAdapter {
|
||||
base
|
||||
}
|
||||
|
||||
fn get_auth_headers(&self, auth: &AuthInfo) -> Vec<(http::HeaderName, http::HeaderValue)> {
|
||||
fn get_auth_headers(
|
||||
&self,
|
||||
auth: &AuthInfo,
|
||||
) -> Result<Vec<(http::HeaderName, http::HeaderValue)>, ProxyError> {
|
||||
use http::{HeaderName, HeaderValue};
|
||||
// 注意:anthropic-version 由 forwarder.rs 统一处理(透传客户端值或设置默认值)
|
||||
let bearer = format!("Bearer {}", auth.api_key);
|
||||
match auth.strategy {
|
||||
let hv = |s: &str| -> Result<HeaderValue, ProxyError> {
|
||||
HeaderValue::from_str(s)
|
||||
.map_err(|e| ProxyError::AuthError(format!("invalid auth header value: {e}")))
|
||||
};
|
||||
Ok(match auth.strategy {
|
||||
AuthStrategy::Anthropic => {
|
||||
vec![(
|
||||
HeaderName::from_static("x-api-key"),
|
||||
HeaderValue::from_str(&auth.api_key).unwrap(),
|
||||
)]
|
||||
vec![(HeaderName::from_static("x-api-key"), hv(&auth.api_key)?)]
|
||||
}
|
||||
AuthStrategy::ClaudeAuth | AuthStrategy::Bearer => {
|
||||
vec![(
|
||||
HeaderName::from_static("authorization"),
|
||||
HeaderValue::from_str(&bearer).unwrap(),
|
||||
)]
|
||||
vec![(HeaderName::from_static("authorization"), hv(&bearer)?)]
|
||||
}
|
||||
AuthStrategy::Google => vec![(
|
||||
HeaderName::from_static("x-goog-api-key"),
|
||||
HeaderValue::from_str(&auth.api_key).unwrap(),
|
||||
hv(&auth.api_key)?,
|
||||
)],
|
||||
AuthStrategy::GoogleOAuth => {
|
||||
let token = auth.access_token.as_ref().unwrap_or(&auth.api_key);
|
||||
vec![
|
||||
(
|
||||
HeaderName::from_static("authorization"),
|
||||
HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
|
||||
hv(&format!("Bearer {token}"))?,
|
||||
),
|
||||
(
|
||||
HeaderName::from_static("x-goog-api-client"),
|
||||
@@ -627,10 +628,7 @@ impl ProviderAdapter for ClaudeAdapter {
|
||||
// 注意:bearer token 由 forwarder 动态注入到 auth.api_key
|
||||
// ChatGPT-Account-Id 由 forwarder 注入额外 header
|
||||
vec![
|
||||
(
|
||||
HeaderName::from_static("authorization"),
|
||||
HeaderValue::from_str(&bearer).unwrap(),
|
||||
),
|
||||
(HeaderName::from_static("authorization"), hv(&bearer)?),
|
||||
(
|
||||
HeaderName::from_static("originator"),
|
||||
HeaderValue::from_static("cc-switch"),
|
||||
@@ -641,10 +639,7 @@ impl ProviderAdapter for ClaudeAdapter {
|
||||
// 生成请求追踪 ID
|
||||
let request_id = uuid::Uuid::new_v4().to_string();
|
||||
vec![
|
||||
(
|
||||
HeaderName::from_static("authorization"),
|
||||
HeaderValue::from_str(&bearer).unwrap(),
|
||||
),
|
||||
(HeaderName::from_static("authorization"), hv(&bearer)?),
|
||||
(
|
||||
HeaderName::from_static("editor-version"),
|
||||
HeaderValue::from_static(super::copilot_auth::COPILOT_EDITOR_VERSION),
|
||||
@@ -683,17 +678,11 @@ impl ProviderAdapter for ClaudeAdapter {
|
||||
HeaderName::from_static("x-vscode-user-agent-library-version"),
|
||||
HeaderValue::from_static("electron-fetch"),
|
||||
),
|
||||
(
|
||||
HeaderName::from_static("x-request-id"),
|
||||
HeaderValue::from_str(&request_id).unwrap(),
|
||||
),
|
||||
(
|
||||
HeaderName::from_static("x-agent-task-id"),
|
||||
HeaderValue::from_str(&request_id).unwrap(),
|
||||
),
|
||||
(HeaderName::from_static("x-request-id"), hv(&request_id)?),
|
||||
(HeaderName::from_static("x-agent-task-id"), hv(&request_id)?),
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn needs_transform(&self, provider: &Provider) -> bool {
|
||||
@@ -871,7 +860,7 @@ mod tests {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
let auth = AuthInfo::new("sk-ant-test".to_string(), AuthStrategy::Anthropic);
|
||||
|
||||
let headers = adapter.get_auth_headers(&auth);
|
||||
let headers = adapter.get_auth_headers(&auth).unwrap();
|
||||
assert_eq!(headers.len(), 1);
|
||||
assert_eq!(headers[0].0.as_str(), "x-api-key");
|
||||
assert_eq!(headers[0].1.to_str().unwrap(), "sk-ant-test");
|
||||
@@ -882,7 +871,7 @@ mod tests {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
let auth = AuthInfo::new("sk-relay-test".to_string(), AuthStrategy::ClaudeAuth);
|
||||
|
||||
let headers = adapter.get_auth_headers(&auth);
|
||||
let headers = adapter.get_auth_headers(&auth).unwrap();
|
||||
assert_eq!(headers.len(), 1);
|
||||
assert_eq!(headers[0].0.as_str(), "authorization");
|
||||
assert_eq!(headers[0].1.to_str().unwrap(), "Bearer sk-relay-test");
|
||||
@@ -893,12 +882,26 @@ mod tests {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
let auth = AuthInfo::new("sk-or-test".to_string(), AuthStrategy::Bearer);
|
||||
|
||||
let headers = adapter.get_auth_headers(&auth);
|
||||
let headers = adapter.get_auth_headers(&auth).unwrap();
|
||||
assert_eq!(headers.len(), 1);
|
||||
assert_eq!(headers[0].0.as_str(), "authorization");
|
||||
assert_eq!(headers[0].1.to_str().unwrap(), "Bearer sk-or-test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_auth_headers_rejects_illegal_header_chars() {
|
||||
// 用户粘贴含 \r\n 的"脏"key 不能让进程 panic
|
||||
let adapter = ClaudeAdapter::new();
|
||||
let auth = AuthInfo::new(
|
||||
"sk-ant-bad\r\nX-Inject: 1".to_string(),
|
||||
AuthStrategy::Anthropic,
|
||||
);
|
||||
|
||||
let result = adapter.get_auth_headers(&auth);
|
||||
assert!(result.is_err(), "expected AuthError, got Ok");
|
||||
assert!(matches!(result, Err(ProxyError::AuthError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_auth_openrouter() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
|
||||
@@ -173,12 +173,17 @@ impl ProviderAdapter for CodexAdapter {
|
||||
url
|
||||
}
|
||||
|
||||
fn get_auth_headers(&self, auth: &AuthInfo) -> Vec<(http::HeaderName, http::HeaderValue)> {
|
||||
fn get_auth_headers(
|
||||
&self,
|
||||
auth: &AuthInfo,
|
||||
) -> Result<Vec<(http::HeaderName, http::HeaderValue)>, ProxyError> {
|
||||
let bearer = format!("Bearer {}", auth.api_key);
|
||||
vec![(
|
||||
let value = http::HeaderValue::from_str(&bearer)
|
||||
.map_err(|e| ProxyError::AuthError(format!("invalid auth header value: {e}")))?;
|
||||
Ok(vec![(
|
||||
http::HeaderName::from_static("authorization"),
|
||||
http::HeaderValue::from_str(&bearer).unwrap(),
|
||||
)]
|
||||
value,
|
||||
)])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -228,15 +228,22 @@ impl ProviderAdapter for GeminiAdapter {
|
||||
url
|
||||
}
|
||||
|
||||
fn get_auth_headers(&self, auth: &AuthInfo) -> Vec<(http::HeaderName, http::HeaderValue)> {
|
||||
fn get_auth_headers(
|
||||
&self,
|
||||
auth: &AuthInfo,
|
||||
) -> Result<Vec<(http::HeaderName, http::HeaderValue)>, ProxyError> {
|
||||
use http::{HeaderName, HeaderValue};
|
||||
match auth.strategy {
|
||||
let hv = |s: &str| -> Result<HeaderValue, ProxyError> {
|
||||
HeaderValue::from_str(s)
|
||||
.map_err(|e| ProxyError::AuthError(format!("invalid auth header value: {e}")))
|
||||
};
|
||||
Ok(match auth.strategy {
|
||||
AuthStrategy::GoogleOAuth => {
|
||||
let token = auth.access_token.as_ref().unwrap_or(&auth.api_key);
|
||||
vec![
|
||||
(
|
||||
HeaderName::from_static("authorization"),
|
||||
HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
|
||||
hv(&format!("Bearer {token}"))?,
|
||||
),
|
||||
(
|
||||
HeaderName::from_static("x-goog-api-client"),
|
||||
@@ -246,9 +253,9 @@ impl ProviderAdapter for GeminiAdapter {
|
||||
}
|
||||
_ => vec![(
|
||||
HeaderName::from_static("x-goog-api-key"),
|
||||
HeaderValue::from_str(&auth.api_key).unwrap(),
|
||||
hv(&auth.api_key)?,
|
||||
)],
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user