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