From d6a9beff2f9f877709b830ab9336c9d8e9155c23 Mon Sep 17 00:00:00 2001 From: shaw Date: Tue, 14 Oct 2025 11:24:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=80=82=E9=85=8D=E6=96=B0=E7=89=88?= =?UTF-8?q?=E6=9C=ACgemini?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/middleware/auth.js | 53 +++++++++++++++++++++++++++---- src/middleware/browserFallback.js | 34 +++++++++++++++++--- src/routes/geminiRoutes.js | 11 +++---- src/routes/openaiGeminiRoutes.js | 9 +++--- 4 files changed, 84 insertions(+), 23 deletions(-) diff --git a/src/middleware/auth.js b/src/middleware/auth.js index b18b5322..378f1565 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -65,6 +65,44 @@ const TOKEN_COUNT_PATHS = new Set([ '/droid/claude/v1/messages/count_tokens' ]) +function extractApiKey(req) { + const candidates = [ + req.headers['x-api-key'], + req.headers['x-goog-api-key'], + req.headers['authorization'], + req.headers['api-key'], + req.query?.key + ] + + for (const candidate of candidates) { + let value = candidate + + if (Array.isArray(value)) { + value = value.find((item) => typeof item === 'string' && item.trim()) + } + + if (typeof value !== 'string') { + continue + } + + let trimmed = value.trim() + if (!trimmed) { + continue + } + + if (/^Bearer\s+/i.test(trimmed)) { + trimmed = trimmed.replace(/^Bearer\s+/i, '').trim() + if (!trimmed) { + continue + } + } + + return trimmed + } + + return '' +} + function normalizeRequestPath(value) { if (!value) { return '/' @@ -95,18 +133,18 @@ const authenticateApiKey = async (req, res, next) => { try { // 安全提取API Key,支持多种格式(包括Gemini CLI支持) - const apiKey = - req.headers['x-api-key'] || - req.headers['x-goog-api-key'] || - req.headers['authorization']?.replace(/^Bearer\s+/i, '') || - req.headers['api-key'] || - req.query.key + const apiKey = extractApiKey(req) + + if (apiKey) { + req.headers['x-api-key'] = apiKey + } if (!apiKey) { logger.security(`🔒 Missing API key attempt from ${req.ip || 'unknown'}`) return res.status(401).json({ error: 'Missing API key', - message: 'Please provide an API key in the x-api-key header or Authorization header' + message: + 'Please provide an API key in the x-api-key, x-goog-api-key, or Authorization header' }) } @@ -950,6 +988,7 @@ const corsMiddleware = (req, res, next) => { 'Accept', 'Authorization', 'x-api-key', + 'x-goog-api-key', 'api-key', 'x-admin-token', 'anthropic-version', diff --git a/src/middleware/browserFallback.js b/src/middleware/browserFallback.js index df81ae38..ed82532c 100644 --- a/src/middleware/browserFallback.js +++ b/src/middleware/browserFallback.js @@ -7,12 +7,38 @@ const logger = require('../utils/logger') const browserFallbackMiddleware = (req, res, next) => { const userAgent = req.headers['user-agent'] || '' const origin = req.headers['origin'] || '' - const authHeader = req.headers['authorization'] || req.headers['x-api-key'] || '' + + const extractHeader = (value) => { + let candidate = value + + if (Array.isArray(candidate)) { + candidate = candidate.find((item) => typeof item === 'string' && item.trim()) + } + + if (typeof candidate !== 'string') { + return '' + } + + let trimmed = candidate.trim() + if (!trimmed) { + return '' + } + + if (/^Bearer\s+/i.test(trimmed)) { + trimmed = trimmed.replace(/^Bearer\s+/i, '').trim() + } + + return trimmed + } + + const apiKeyHeader = + extractHeader(req.headers['x-api-key']) || extractHeader(req.headers['x-goog-api-key']) + const normalizedKey = extractHeader(req.headers['authorization']) || apiKeyHeader // 检查是否为Chrome插件或浏览器请求 const isChromeExtension = origin.startsWith('chrome-extension://') const isBrowserRequest = userAgent.includes('Mozilla/') && userAgent.includes('Chrome/') - const hasApiKey = authHeader.startsWith('cr_') // 我们的API Key格式 + const hasApiKey = normalizedKey.startsWith('cr_') // 我们的API Key格式 if ((isChromeExtension || isBrowserRequest) && hasApiKey) { // 为Chrome插件请求添加特殊标记 @@ -23,8 +49,8 @@ const browserFallbackMiddleware = (req, res, next) => { req.headers['user-agent'] = 'claude-cli/1.0.110 (external, cli, browser-fallback)' // 确保设置正确的认证头 - if (!req.headers['authorization'] && req.headers['x-api-key']) { - req.headers['authorization'] = `Bearer ${req.headers['x-api-key']}` + if (!req.headers['authorization'] && apiKeyHeader) { + req.headers['authorization'] = `Bearer ${apiKeyHeader}` } // 添加必要的Anthropic头 diff --git a/src/routes/geminiRoutes.js b/src/routes/geminiRoutes.js index 532979cf..8ece60fd 100644 --- a/src/routes/geminiRoutes.js +++ b/src/routes/geminiRoutes.js @@ -13,13 +13,10 @@ const { updateRateLimitCounters } = require('../utils/rateLimitHelper') // 生成会话哈希 function generateSessionHash(req) { - const sessionData = [ - req.headers['user-agent'], - req.ip, - req.headers['x-api-key']?.substring(0, 10) - ] - .filter(Boolean) - .join(':') + const apiKeyPrefix = + req.headers['x-api-key']?.substring(0, 10) || req.headers['x-goog-api-key']?.substring(0, 10) + + const sessionData = [req.headers['user-agent'], req.ip, apiKeyPrefix].filter(Boolean).join(':') return crypto.createHash('sha256').update(sessionData).digest('hex') } diff --git a/src/routes/openaiGeminiRoutes.js b/src/routes/openaiGeminiRoutes.js index 54305401..a718aad2 100644 --- a/src/routes/openaiGeminiRoutes.js +++ b/src/routes/openaiGeminiRoutes.js @@ -9,11 +9,10 @@ const crypto = require('crypto') // 生成会话哈希 function generateSessionHash(req) { - const sessionData = [ - req.headers['user-agent'], - req.ip, - req.headers['authorization']?.substring(0, 20) - ] + const authSource = + req.headers['authorization'] || req.headers['x-api-key'] || req.headers['x-goog-api-key'] + + const sessionData = [req.headers['user-agent'], req.ip, authSource?.substring(0, 20)] .filter(Boolean) .join(':')