From 86668c3de9b87e45a9878a18cd2dab5014973795 Mon Sep 17 00:00:00 2001 From: shaw Date: Mon, 22 Sep 2025 13:53:56 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8D1.1.147=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=90=AF=E5=8A=A8=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/validators/clientDefinitions.js | 69 ++++++++ src/validators/clientValidator.js | 143 +++++++++++++++++ src/validators/clients/claudeCodeValidator.js | 140 +++++++++++++++++ src/validators/clients/codexCliValidator.js | 147 ++++++++++++++++++ src/validators/clients/geminiCliValidator.js | 105 +++++++++++++ 5 files changed, 604 insertions(+) create mode 100644 src/validators/clientDefinitions.js create mode 100644 src/validators/clientValidator.js create mode 100644 src/validators/clients/claudeCodeValidator.js create mode 100644 src/validators/clients/codexCliValidator.js create mode 100644 src/validators/clients/geminiCliValidator.js diff --git a/src/validators/clientDefinitions.js b/src/validators/clientDefinitions.js new file mode 100644 index 00000000..3c549987 --- /dev/null +++ b/src/validators/clientDefinitions.js @@ -0,0 +1,69 @@ +/** + * 客户端定义配置 + * 定义所有支持的客户端类型和它们的属性 + */ + +const CLIENT_DEFINITIONS = { + CLAUDE_CODE: { + id: 'claude_code', + name: 'Claude Code', + displayName: 'Claude Code CLI', + description: 'Claude Code command-line interface', + userAgentPattern: /^claude-cli\/[\d.]+([-\w]*)?\s+\(external,\s*cli\)$/i, + requiredHeaders: ['x-app', 'anthropic-beta', 'anthropic-version'], + restrictedPaths: ['/api/v1/messages', '/claude/v1/messages'], + icon: '🤖' + }, + + GEMINI_CLI: { + id: 'gemini_cli', + name: 'Gemini CLI', + displayName: 'Gemini Command Line Tool', + description: 'Google Gemini API command-line interface', + userAgentPattern: /^GeminiCLI\/v?[\d.]+/i, + requiredPaths: ['/gemini'], + validatePaths: ['generateContent'], + icon: '💎' + }, + + CODEX_CLI: { + id: 'codex_cli', + name: 'Codex CLI', + displayName: 'Codex Command Line Tool', + description: 'Cursor/Codex command-line interface', + userAgentPattern: /^(codex_vscode|codex_cli_rs)\/[\d.]+/i, + requiredHeaders: ['originator', 'session_id'], + restrictedPaths: ['/openai', '/azure'], + icon: '🔷' + } +} + +// 导出客户端ID枚举 +const CLIENT_IDS = { + CLAUDE_CODE: 'claude_code', + GEMINI_CLI: 'gemini_cli', + CODEX_CLI: 'codex_cli' +} + +// 获取所有客户端定义 +function getAllClientDefinitions() { + return Object.values(CLIENT_DEFINITIONS) +} + +// 根据ID获取客户端定义 +function getClientDefinitionById(clientId) { + return Object.values(CLIENT_DEFINITIONS).find((client) => client.id === clientId) +} + +// 检查客户端ID是否有效 +function isValidClientId(clientId) { + return Object.values(CLIENT_IDS).includes(clientId) +} + +module.exports = { + CLIENT_DEFINITIONS, + CLIENT_IDS, + getAllClientDefinitions, + getClientDefinitionById, + isValidClientId +} diff --git a/src/validators/clientValidator.js b/src/validators/clientValidator.js new file mode 100644 index 00000000..54a87634 --- /dev/null +++ b/src/validators/clientValidator.js @@ -0,0 +1,143 @@ +/** + * 客户端验证器 + * 用于验证请求是否来自特定的客户端 + */ + +const logger = require('../utils/logger') +const { CLIENT_DEFINITIONS, getAllClientDefinitions } = require('./clientDefinitions') +const ClaudeCodeValidator = require('./clients/claudeCodeValidator') +const GeminiCliValidator = require('./clients/geminiCliValidator') +const CodexCliValidator = require('./clients/codexCliValidator') + +/** + * 客户端验证器类 + */ +class ClientValidator { + /** + * 获取客户端验证器 + * @param {string} clientId - 客户端ID + * @returns {Object|null} 验证器实例 + */ + static getValidator(clientId) { + switch (clientId) { + case 'claude_code': + return ClaudeCodeValidator + case 'gemini_cli': + return GeminiCliValidator + case 'codex_cli': + return CodexCliValidator + default: + logger.warn(`Unknown client ID: ${clientId}`) + return null + } + } + + /** + * 获取所有支持的客户端ID列表 + * @returns {Array} 客户端ID列表 + */ + static getSupportedClients() { + return ['claude_code', 'gemini_cli', 'codex_cli'] + } + + /** + * 验证单个客户端 + * @param {string} clientId - 客户端ID + * @param {Object} req - Express请求对象 + * @returns {boolean} 验证结果 + */ + static validateClient(clientId, req) { + const validator = this.getValidator(clientId) + + if (!validator) { + logger.warn(`No validator found for client: ${clientId}`) + return false + } + + try { + return validator.validate(req) + } catch (error) { + logger.error(`Error validating client ${clientId}:`, error) + return false + } + } + + /** + * 验证请求是否来自允许的客户端列表中的任一客户端 + * @param {Array} allowedClients - 允许的客户端ID列表 + * @param {Object} req - Express请求对象 + * @returns {Object} 验证结果对象 + */ + static validateRequest(allowedClients, req) { + const userAgent = req.headers['user-agent'] || '' + const clientIP = req.ip || req.connection?.remoteAddress || 'unknown' + + // 记录验证开始 + logger.api(`🔍 Starting client validation for User-Agent: "${userAgent}"`) + logger.api(` Allowed clients: ${allowedClients.join(', ')}`) + logger.api(` Request from IP: ${clientIP}`) + + // 遍历所有允许的客户端进行验证 + for (const clientId of allowedClients) { + const validator = this.getValidator(clientId) + + if (!validator) { + logger.warn(`Skipping unknown client ID: ${clientId}`) + continue + } + + logger.debug(`Checking against ${validator.getName()}...`) + + try { + if (validator.validate(req)) { + // 验证成功 + logger.api(`✅ Client validated: ${validator.getName()} (${clientId})`) + logger.api(` Matched User-Agent: "${userAgent}"`) + + return { + allowed: true, + matchedClient: clientId, + clientName: validator.getName(), + clientInfo: Object.values(CLIENT_DEFINITIONS).find((def) => def.id === clientId) + } + } + } catch (error) { + logger.error(`Error during validation for ${clientId}:`, error) + continue + } + } + + // 没有匹配的客户端 + logger.api(`❌ No matching client found for User-Agent: "${userAgent}"`) + return { + allowed: false, + matchedClient: null, + reason: 'No matching client found' + } + } + + /** + * 获取客户端信息 + * @param {string} clientId - 客户端ID + * @returns {Object} 客户端信息 + */ + static getClientInfo(clientId) { + const validator = this.getValidator(clientId) + if (!validator) { + return null + } + + return validator.getInfo() + } + + /** + * 获取所有可用的客户端信息 + * @returns {Array} 客户端信息数组 + */ + static getAvailableClients() { + // 直接从 CLIENT_DEFINITIONS 返回所有客户端信息 + return getAllClientDefinitions() + } +} + +module.exports = ClientValidator diff --git a/src/validators/clients/claudeCodeValidator.js b/src/validators/clients/claudeCodeValidator.js new file mode 100644 index 00000000..4216c9ec --- /dev/null +++ b/src/validators/clients/claudeCodeValidator.js @@ -0,0 +1,140 @@ +const logger = require('../../utils/logger') +const { CLIENT_DEFINITIONS } = require('../clientDefinitions') + +/** + * Claude Code CLI 验证器 + * 验证请求是否来自 Claude Code CLI + */ +class ClaudeCodeValidator { + /** + * 获取客户端ID + */ + static getId() { + return CLIENT_DEFINITIONS.CLAUDE_CODE.id + } + + /** + * 获取客户端名称 + */ + static getName() { + return CLIENT_DEFINITIONS.CLAUDE_CODE.name + } + + /** + * 获取客户端描述 + */ + static getDescription() { + return CLIENT_DEFINITIONS.CLAUDE_CODE.description + } + + /** + * 获取客户端图标 + */ + static getIcon() { + return CLIENT_DEFINITIONS.CLAUDE_CODE.icon || '🤖' + } + + /** + * 验证请求是否来自 Claude Code CLI + * @param {Object} req - Express 请求对象 + * @returns {boolean} 验证结果 + */ + static validate(req) { + try { + const userAgent = req.headers['user-agent'] || '' + const path = req.path || '' + + // 1. 先检查是否是 Claude Code 的 User-Agent + // 格式: claude-cli/1.0.86 (external, cli) + const claudeCodePattern = /^claude-cli\/[\d\.]+([-\w]*)?\s+\(external,\s*cli\)$/i + if (!claudeCodePattern.test(userAgent)) { + // 不是 Claude Code 的请求,此验证器不处理 + return false + } + + // 2. Claude Code 检测到,对于特定路径进行额外的严格验证 + if (!path.includes('messages')) { + // 其他路径,只要 User-Agent 匹配就认为是 Claude Code + logger.debug(`Claude Code detected for path: ${path}, allowing access`) + return true + } + + // 3. 检查必需的头部(值不为空即可) + const xApp = req.headers['x-app'] + const anthropicBeta = req.headers['anthropic-beta'] + const anthropicVersion = req.headers['anthropic-version'] + + if (!xApp || xApp.trim() === '') { + logger.debug('Claude Code validation failed - missing or empty x-app header') + return false + } + + if (!anthropicBeta || anthropicBeta.trim() === '') { + logger.debug('Claude Code validation failed - missing or empty anthropic-beta header') + return false + } + + if (!anthropicVersion || anthropicVersion.trim() === '') { + logger.debug('Claude Code validation failed - missing or empty anthropic-version header') + return false + } + + logger.debug(`Claude Code headers - x-app: ${xApp}, anthropic-beta: ${anthropicBeta}, anthropic-version: ${anthropicVersion}`) + + // 4. 验证 body 中的 metadata.user_id + if (!req.body || !req.body.metadata || !req.body.metadata.user_id) { + logger.debug('Claude Code validation failed - missing metadata.user_id in body') + return false + } + + const userId = req.body.metadata.user_id + // 格式: user_{64位字符串}_account__session_{哈希值} + // user_d98385411c93cd074b2cefd5c9831fe77f24a53e4ecdcd1f830bba586fe62cb9_account__session_17cf0fd3-d51b-4b59-977d-b899dafb3022 + const userIdPattern = /^user_[a-fA-F0-9]{64}_account__session_[\w-]+$/ + + if (!userIdPattern.test(userId)) { + logger.debug(`Claude Code validation failed - invalid user_id format: ${userId}`) + + // 提供更详细的错误信息 + if (!userId.startsWith('user_')) { + logger.debug('user_id must start with "user_"') + } else { + const parts = userId.split('_') + if (parts.length < 4) { + logger.debug('user_id format is incomplete') + } else if (parts[1].length !== 64) { + logger.debug(`user hash must be 64 characters, got ${parts[1].length}`) + } else if (parts[2] !== 'account' || parts[3] !== '' || parts[4] !== 'session') { + logger.debug('user_id must contain "_account__session_"') + } + } + return false + } + + // 5. 额外日志记录(用于调试) + logger.debug(`Claude Code validation passed - UA: ${userAgent}, userId: ${userId}`) + + // 所有必要检查通过 + return true + + } catch (error) { + logger.error('Error in ClaudeCodeValidator:', error) + // 验证出错时默认拒绝 + return false + } + } + + /** + * 获取验证器信息 + */ + static getInfo() { + return { + id: this.getId(), + name: this.getName(), + description: this.getDescription(), + icon: CLIENT_DEFINITIONS.CLAUDE_CODE.icon + } + } +} + +module.exports = ClaudeCodeValidator \ No newline at end of file diff --git a/src/validators/clients/codexCliValidator.js b/src/validators/clients/codexCliValidator.js new file mode 100644 index 00000000..aff09fbf --- /dev/null +++ b/src/validators/clients/codexCliValidator.js @@ -0,0 +1,147 @@ +const logger = require('../../utils/logger') +const { CLIENT_DEFINITIONS } = require('../clientDefinitions') + +/** + * Codex CLI 验证器 + * 验证请求是否来自 Codex CLI + */ +class CodexCliValidator { + /** + * 获取客户端ID + */ + static getId() { + return CLIENT_DEFINITIONS.CODEX_CLI.id + } + + /** + * 获取客户端名称 + */ + static getName() { + return CLIENT_DEFINITIONS.CODEX_CLI.name + } + + /** + * 获取客户端描述 + */ + static getDescription() { + return CLIENT_DEFINITIONS.CODEX_CLI.description + } + + /** + * 验证请求是否来自 Codex CLI + * @param {Object} req - Express 请求对象 + * @returns {boolean} 验证结果 + */ + static validate(req) { + try { + const userAgent = req.headers['user-agent'] || '' + const originator = req.headers['originator'] || '' + const sessionId = req.headers['session_id'] + + // 1. 基础 User-Agent 检查 + // Codex CLI 的 UA 格式: + // - codex_vscode/0.35.0 (Windows 10.0.26100; x86_64) unknown (Cursor; 0.4.10) + // - codex_cli_rs/0.38.0 (Ubuntu 22.4.0; x86_64) WindowsTerminal + const codexCliPattern = /^(codex_vscode|codex_cli_rs)\/[\d\.]+/i + const uaMatch = userAgent.match(codexCliPattern) + + if (!uaMatch) { + logger.debug(`Codex CLI validation failed - UA mismatch: ${userAgent}`) + return false + } + + // 2. 对于特定路径,进行额外的严格验证 + // 对于 /openai 和 /azure 路径需要完整验证 + const strictValidationPaths = ['/openai', '/azure'] + const needsStrictValidation = req.path && strictValidationPaths.some(path => req.path.startsWith(path)) + + if (!needsStrictValidation) { + // 其他路径,只要 User-Agent 匹配就认为是 Codex CLI + logger.debug(`Codex CLI detected for path: ${req.path}, allowing access`) + return true + } + + // 3. 验证 originator 头必须与 UA 中的客户端类型匹配 + const clientType = uaMatch[1].toLowerCase() + if (originator.toLowerCase() !== clientType) { + logger.debug( + `Codex CLI validation failed - originator mismatch. UA: ${clientType}, originator: ${originator}` + ) + return false + } + + // 4. 检查 session_id - 必须存在且长度大于20 + if (!sessionId || sessionId.length <= 20) { + logger.debug(`Codex CLI validation failed - session_id missing or too short: ${sessionId}`) + return false + } + + // 5. 对于 /openai/responses 和 /azure/response 路径,额外检查 body 中的 instructions 字段 + if ( + req.path && + (req.path.includes('/openai/responses') || req.path.includes('/azure/response')) + ) { + if (!req.body || !req.body.instructions) { + logger.debug(`Codex CLI validation failed - missing instructions in body for ${req.path}`) + return false + } + + const expectedPrefix = + 'You are Codex, based on GPT-5. You are running as a coding agent in the Codex CLI' + if (!req.body.instructions.startsWith(expectedPrefix)) { + logger.debug(`Codex CLI validation failed - invalid instructions prefix for ${req.path}`) + logger.debug(`Expected: "${expectedPrefix}..."`) + logger.debug(`Received: "${req.body.instructions.substring(0, 100)}..."`) + return false + } + + // 额外检查 model 字段应该是 gpt-5-codex + if (req.body.model && req.body.model !== 'gpt-5-codex') { + logger.debug(`Codex CLI validation warning - unexpected model: ${req.body.model}`) + // 只记录警告,不拒绝请求 + } + } + + // 所有必要检查通过 + logger.debug(`Codex CLI validation passed for UA: ${userAgent}`) + return true + } catch (error) { + logger.error('Error in CodexCliValidator:', error) + // 验证出错时默认拒绝 + return false + } + } + + /** + * 比较版本号 + * @returns {number} -1: v1 < v2, 0: v1 = v2, 1: v1 > v2 + */ + static compareVersions(v1, v2) { + const parts1 = v1.split('.').map(Number) + const parts2 = v2.split('.').map(Number) + + for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { + const part1 = parts1[i] || 0 + const part2 = parts2[i] || 0 + + if (part1 < part2) return -1 + if (part1 > part2) return 1 + } + + return 0 + } + + /** + * 获取验证器信息 + */ + static getInfo() { + return { + id: this.getId(), + name: this.getName(), + description: this.getDescription(), + icon: CLIENT_DEFINITIONS.CODEX_CLI.icon + } + } +} + +module.exports = CodexCliValidator diff --git a/src/validators/clients/geminiCliValidator.js b/src/validators/clients/geminiCliValidator.js new file mode 100644 index 00000000..ea8e60e7 --- /dev/null +++ b/src/validators/clients/geminiCliValidator.js @@ -0,0 +1,105 @@ +const logger = require('../../utils/logger') +const { CLIENT_DEFINITIONS } = require('../clientDefinitions') + +/** + * Gemini CLI 验证器 + * 验证请求是否来自 Gemini CLI + */ +class GeminiCliValidator { + /** + * 获取客户端ID + */ + static getId() { + return CLIENT_DEFINITIONS.GEMINI_CLI.id + } + + /** + * 获取客户端名称 + */ + static getName() { + return CLIENT_DEFINITIONS.GEMINI_CLI.name + } + + /** + * 获取客户端描述 + */ + static getDescription() { + return CLIENT_DEFINITIONS.GEMINI_CLI.description + } + + /** + * 获取客户端图标 + */ + static getIcon() { + return CLIENT_DEFINITIONS.GEMINI_CLI.icon || '💎' + } + + /** + * 验证请求是否来自 Gemini CLI + * @param {Object} req - Express 请求对象 + * @returns {boolean} 验证结果 + */ + static validate(req) { + try { + const userAgent = req.headers['user-agent'] || '' + const path = req.originalUrl || '' + + // 1. 必须是 /gemini 开头的路径 + if (!path.startsWith('/gemini')) { + // 非 /gemini 路径不属于 Gemini + return false + } + + // 2. 对于 /gemini 路径,检查是否包含 generateContent + if (path.includes('generateContent')) { + // 包含 generateContent 的路径需要验证 User-Agent + const geminiCliPattern = /^GeminiCLI\/v?[\d\.]+/i + if (!geminiCliPattern.test(userAgent)) { + logger.debug(`Gemini CLI validation failed - UA mismatch for generateContent: ${userAgent}`) + return false + } + } + + // 所有必要检查通过 + logger.debug(`Gemini CLI validation passed for path: ${path}`) + return true + } catch (error) { + logger.error('Error in GeminiCliValidator:', error) + // 验证出错时默认拒绝 + return false + } + } + + /** + * 比较版本号 + * @returns {number} -1: v1 < v2, 0: v1 = v2, 1: v1 > v2 + */ + static compareVersions(v1, v2) { + const parts1 = v1.split('.').map(Number) + const parts2 = v2.split('.').map(Number) + + for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { + const part1 = parts1[i] || 0 + const part2 = parts2[i] || 0 + + if (part1 < part2) return -1 + if (part1 > part2) return 1 + } + + return 0 + } + + /** + * 获取验证器信息 + */ + static getInfo() { + return { + id: this.getId(), + name: this.getName(), + description: this.getDescription(), + icon: CLIENT_DEFINITIONS.GEMINI_CLI.icon + } + } +} + +module.exports = GeminiCliValidator