mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 21:30:17 +08:00
fix(proxy/gemini): trim API key before provider-type detection and OAuth parsing
Leading whitespace on a copied oauth_creds.json (e.g. trailing newline
when the user copies the file content as-is) would slip past the
`starts_with("ya29.") || starts_with('{')` prefix check in
`ClaudeAdapter::provider_type`, causing the provider to be misclassified
as raw-API-key Gemini and fall back to `x-goog-api-key` with the raw
JSON as the key — which upstream rejects with 401.
The frontend's `handleApiKeyChange` already trims on keystrokes but
deep-link imports, the JSON editor, and live-config backfill all bypass
that path. Trim at every backend extraction point so the coverage is
uniform:
- `ClaudeAdapter::extract_key` (5 env / fallback branches) gets
`.map(str::trim)` before `.filter(|s| !s.is_empty())` so that
whitespace-only values are also treated as missing.
- `GeminiAdapter::extract_key_raw` gets the same chain (including
the `.filter` it was missing before).
- `GeminiAdapter::parse_oauth_credentials` gets a defensive
`let key = key.trim();` at the entry as a belt-and-suspenders guard.
Adds two regression tests covering JSON and bare `ya29.` keys with
leading newline/space.
This commit is contained in:
@@ -297,6 +297,7 @@ impl ClaudeAdapter {
|
||||
if let Some(key) = env
|
||||
.get("ANTHROPIC_AUTH_TOKEN")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
{
|
||||
log::debug!("[Claude] 使用 ANTHROPIC_AUTH_TOKEN");
|
||||
@@ -305,6 +306,7 @@ impl ClaudeAdapter {
|
||||
if let Some(key) = env
|
||||
.get("ANTHROPIC_API_KEY")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
{
|
||||
log::debug!("[Claude] 使用 ANTHROPIC_API_KEY");
|
||||
@@ -314,6 +316,7 @@ impl ClaudeAdapter {
|
||||
if let Some(key) = env
|
||||
.get("OPENROUTER_API_KEY")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
{
|
||||
log::debug!("[Claude] 使用 OPENROUTER_API_KEY");
|
||||
@@ -323,6 +326,7 @@ impl ClaudeAdapter {
|
||||
if let Some(key) = env
|
||||
.get("OPENAI_API_KEY")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
{
|
||||
log::debug!("[Claude] 使用 OPENAI_API_KEY");
|
||||
@@ -336,6 +340,7 @@ impl ClaudeAdapter {
|
||||
.get("apiKey")
|
||||
.or_else(|| provider.settings_config.get("api_key"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
{
|
||||
log::debug!("[Claude] 使用 apiKey/api_key");
|
||||
@@ -858,6 +863,60 @@ mod tests {
|
||||
assert_eq!(auth.strategy, AuthStrategy::GoogleOAuth);
|
||||
}
|
||||
|
||||
/// 回归:从 oauth_creds.json 复制时常带前导换行/空格。未 trim 时
|
||||
/// `starts_with('{')` 会落空,导致误分类为 `ProviderType::Gemini`,再
|
||||
/// 以 raw JSON 当 `x-goog-api-key` 发出去触发 401。trim 应在 provider
|
||||
/// 类型判定和 OAuth 解析前统一生效。
|
||||
#[test]
|
||||
fn test_extract_auth_gemini_cli_json_with_leading_whitespace_classifies_correctly() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
let valid_json = r#"{"access_token":"ya29.valid","refresh_token":"rt"}"#;
|
||||
let key_with_whitespace = format!("\n {valid_json}\n");
|
||||
let provider = create_provider_with_meta(
|
||||
json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://generativelanguage.googleapis.com",
|
||||
"ANTHROPIC_API_KEY": key_with_whitespace
|
||||
}
|
||||
}),
|
||||
ProviderMeta {
|
||||
api_format: Some("gemini_native".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(adapter.provider_type(&provider), ProviderType::GeminiCli);
|
||||
|
||||
let auth = adapter.extract_auth(&provider).unwrap();
|
||||
assert_eq!(auth.access_token.as_deref(), Some("ya29.valid"));
|
||||
assert_eq!(auth.strategy, AuthStrategy::GoogleOAuth);
|
||||
}
|
||||
|
||||
/// 回归:裸 `ya29.` access_token 若带前导换行,也应被 trim 后识别为
|
||||
/// Gemini CLI OAuth,避免前导空白把 `starts_with("ya29.")` 检查顶穿。
|
||||
#[test]
|
||||
fn test_extract_auth_gemini_cli_access_token_with_leading_newline_classifies_correctly() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
let provider = create_provider_with_meta(
|
||||
json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://generativelanguage.googleapis.com",
|
||||
"ANTHROPIC_API_KEY": "\nya29.raw-token-value\n"
|
||||
}
|
||||
}),
|
||||
ProviderMeta {
|
||||
api_format: Some("gemini_native".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(adapter.provider_type(&provider), ProviderType::GeminiCli);
|
||||
|
||||
let auth = adapter.extract_auth(&provider).unwrap();
|
||||
assert_eq!(auth.access_token.as_deref(), Some("ya29.raw-token-value"));
|
||||
assert_eq!(auth.strategy, AuthStrategy::GoogleOAuth);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_provider_type_detection() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
|
||||
@@ -70,6 +70,11 @@ impl GeminiAdapter {
|
||||
|
||||
/// 解析 OAuth 凭证
|
||||
pub fn parse_oauth_credentials(&self, key: &str) -> Option<OAuthCredentials> {
|
||||
// 防御性 trim:前端在 input 事件中会 trim,但 JSON 编辑器 / deeplink
|
||||
// 导入 / live 回填等路径会绕过。带前导换行的 oauth_creds.json 粘贴
|
||||
// 是常见场景,此处统一兜底。
|
||||
let key = key.trim();
|
||||
|
||||
// 直接是 access_token
|
||||
if key.starts_with("ya29.") {
|
||||
return Some(OAuthCredentials {
|
||||
@@ -120,7 +125,12 @@ impl GeminiAdapter {
|
||||
fn extract_key_raw(&self, provider: &Provider) -> Option<String> {
|
||||
if let Some(env) = provider.settings_config.get("env") {
|
||||
// 使用 GEMINI_API_KEY
|
||||
if let Some(key) = env.get("GEMINI_API_KEY").and_then(|v| v.as_str()) {
|
||||
if let Some(key) = env
|
||||
.get("GEMINI_API_KEY")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
{
|
||||
return Some(key.to_string());
|
||||
}
|
||||
}
|
||||
@@ -131,6 +141,8 @@ impl GeminiAdapter {
|
||||
.get("apiKey")
|
||||
.or_else(|| provider.settings_config.get("api_key"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
{
|
||||
return Some(key.to_string());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user