From c9a6afc0b79eaeee4f2f74ce0a0f0b74d3ad5939 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 13 May 2026 23:12:00 +0800 Subject: [PATCH] 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. --- src-tauri/src/proxy/forwarder.rs | 2 +- src-tauri/src/proxy/providers/adapter.rs | 9 +++- src-tauri/src/proxy/providers/claude.rs | 67 +++++++++++++----------- src-tauri/src/proxy/providers/codex.rs | 13 +++-- src-tauri/src/proxy/providers/gemini.rs | 17 ++++-- 5 files changed, 65 insertions(+), 43 deletions(-) diff --git a/src-tauri/src/proxy/forwarder.rs b/src-tauri/src/proxy/forwarder.rs index 780d4a5bf..bef812436 100644 --- a/src-tauri/src/proxy/forwarder.rs +++ b/src-tauri/src/proxy/forwarder.rs @@ -1151,7 +1151,7 @@ impl RequestForwarder { } } - adapter.get_auth_headers(&auth) + adapter.get_auth_headers(&auth)? } else { Vec::new() }; diff --git a/src-tauri/src/proxy/providers/adapter.rs b/src-tauri/src/proxy/providers/adapter.rs index d764d3eb2..41cde627f 100644 --- a/src-tauri/src/proxy/providers/adapter.rs +++ b/src-tauri/src/proxy/providers/adapter.rs @@ -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, ProxyError>; /// 是否需要格式转换 fn needs_transform(&self, _provider: &Provider) -> bool { diff --git a/src-tauri/src/proxy/providers/claude.rs b/src-tauri/src/proxy/providers/claude.rs index bdc479e1c..c55a1064d 100644 --- a/src-tauri/src/proxy/providers/claude.rs +++ b/src-tauri/src/proxy/providers/claude.rs @@ -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, 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::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(); diff --git a/src-tauri/src/proxy/providers/codex.rs b/src-tauri/src/proxy/providers/codex.rs index eebd33704..405e20f3e 100644 --- a/src-tauri/src/proxy/providers/codex.rs +++ b/src-tauri/src/proxy/providers/codex.rs @@ -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, 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, + )]) } } diff --git a/src-tauri/src/proxy/providers/gemini.rs b/src-tauri/src/proxy/providers/gemini.rs index 7de152a97..675c3b648 100644 --- a/src-tauri/src/proxy/providers/gemini.rs +++ b/src-tauri/src/proxy/providers/gemini.rs @@ -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, ProxyError> { use http::{HeaderName, HeaderValue}; - match auth.strategy { + let hv = |s: &str| -> Result { + 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)?, )], - } + }) } }