mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
fix: 修复 User-Agent 暴露问题并实现安全的 header 转发
- 移除硬编码的 'claude-relay-service/1.0.0' User-Agent,防止代理身份暴露 - 添加 _filterClientHeaders 方法过滤敏感请求头 - 实现完整的客户端 header 转发功能 - 默认 User-Agent 设置为 'claude-cli/1.0.53 (external, cli)' - 过滤 x-api-key, authorization, host 等敏感 headers - 更新所有 _makeClaudeRequest 方法支持 clientHeaders 参数 - 修改 API 路由传递 req.headers 到服务层 安全改进: - 防止代理服务身份暴露 - 提升请求透明性和安全性 - 保持客户端原始请求特征 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -50,7 +50,7 @@ router.post('/v1/messages', authenticateApiKey, async (req, res) => {
|
||||
let usageDataCaptured = false;
|
||||
|
||||
// 使用自定义流处理器来捕获usage数据
|
||||
await claudeRelayService.relayStreamRequestWithUsageCapture(req.body, req.apiKey, res, (usageData) => {
|
||||
await claudeRelayService.relayStreamRequestWithUsageCapture(req.body, req.apiKey, res, req.headers, (usageData) => {
|
||||
// 回调函数:当检测到完整usage数据时记录真实token使用量
|
||||
logger.info('🎯 Usage callback triggered with complete data:', JSON.stringify(usageData, null, 2));
|
||||
|
||||
@@ -86,7 +86,7 @@ router.post('/v1/messages', authenticateApiKey, async (req, res) => {
|
||||
apiKeyName: req.apiKey.name
|
||||
});
|
||||
|
||||
const response = await claudeRelayService.relayRequest(req.body, req.apiKey, req, res);
|
||||
const response = await claudeRelayService.relayRequest(req.body, req.apiKey, req, res, req.headers);
|
||||
|
||||
logger.info('📡 Claude API response received', {
|
||||
statusCode: response.statusCode,
|
||||
|
||||
@@ -15,7 +15,7 @@ class ClaudeRelayService {
|
||||
}
|
||||
|
||||
// 🚀 转发请求到Claude API
|
||||
async relayRequest(requestBody, apiKeyData, clientRequest, clientResponse) {
|
||||
async relayRequest(requestBody, apiKeyData, clientRequest, clientResponse, clientHeaders) {
|
||||
let upstreamRequest = null;
|
||||
|
||||
try {
|
||||
@@ -57,6 +57,7 @@ class ClaudeRelayService {
|
||||
processedBody,
|
||||
accessToken,
|
||||
proxyAgent,
|
||||
clientHeaders,
|
||||
(req) => { upstreamRequest = req; }
|
||||
);
|
||||
|
||||
@@ -190,11 +191,41 @@ class ClaudeRelayService {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 🔧 过滤客户端请求头
|
||||
_filterClientHeaders(clientHeaders) {
|
||||
// 需要移除的敏感 headers
|
||||
const sensitiveHeaders = [
|
||||
'x-api-key',
|
||||
'authorization',
|
||||
'host',
|
||||
'content-length',
|
||||
'connection',
|
||||
'proxy-authorization',
|
||||
'content-encoding',
|
||||
'transfer-encoding'
|
||||
];
|
||||
|
||||
const filteredHeaders = {};
|
||||
|
||||
// 转发客户端的非敏感 headers
|
||||
Object.keys(clientHeaders || {}).forEach(key => {
|
||||
const lowerKey = key.toLowerCase();
|
||||
if (!sensitiveHeaders.includes(lowerKey)) {
|
||||
filteredHeaders[key] = clientHeaders[key];
|
||||
}
|
||||
});
|
||||
|
||||
return filteredHeaders;
|
||||
}
|
||||
|
||||
// 🔗 发送请求到Claude API
|
||||
async _makeClaudeRequest(body, accessToken, proxyAgent, onRequest) {
|
||||
async _makeClaudeRequest(body, accessToken, proxyAgent, clientHeaders, onRequest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(this.claudeApiUrl);
|
||||
|
||||
// 获取过滤后的客户端 headers
|
||||
const filteredHeaders = this._filterClientHeaders(clientHeaders);
|
||||
|
||||
const options = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || 443,
|
||||
@@ -204,12 +235,17 @@ class ClaudeRelayService {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'anthropic-version': this.apiVersion,
|
||||
'User-Agent': 'claude-relay-service/1.0.0'
|
||||
...filteredHeaders
|
||||
},
|
||||
agent: proxyAgent,
|
||||
timeout: config.proxy.timeout
|
||||
};
|
||||
|
||||
// 如果客户端没有提供 User-Agent,使用默认值
|
||||
if (!filteredHeaders['User-Agent'] && !filteredHeaders['user-agent']) {
|
||||
options.headers['User-Agent'] = 'claude-cli/1.0.53 (external, cli)';
|
||||
}
|
||||
|
||||
if (this.betaHeader) {
|
||||
options.headers['anthropic-beta'] = this.betaHeader;
|
||||
}
|
||||
@@ -262,7 +298,7 @@ class ClaudeRelayService {
|
||||
}
|
||||
|
||||
// 🌊 处理流式响应(带usage数据捕获)
|
||||
async relayStreamRequestWithUsageCapture(requestBody, apiKeyData, responseStream, usageCallback) {
|
||||
async relayStreamRequestWithUsageCapture(requestBody, apiKeyData, responseStream, clientHeaders, usageCallback) {
|
||||
try {
|
||||
// 生成会话哈希用于sticky会话
|
||||
const sessionHash = sessionHelper.generateSessionHash(requestBody);
|
||||
@@ -282,7 +318,7 @@ class ClaudeRelayService {
|
||||
const proxyAgent = await this._getProxyAgent(accountId);
|
||||
|
||||
// 发送流式请求并捕获usage数据
|
||||
return await this._makeClaudeStreamRequestWithUsageCapture(processedBody, accessToken, proxyAgent, responseStream, usageCallback);
|
||||
return await this._makeClaudeStreamRequestWithUsageCapture(processedBody, accessToken, proxyAgent, clientHeaders, responseStream, usageCallback);
|
||||
} catch (error) {
|
||||
logger.error('❌ Claude stream relay with usage capture failed:', error);
|
||||
throw error;
|
||||
@@ -290,10 +326,13 @@ class ClaudeRelayService {
|
||||
}
|
||||
|
||||
// 🌊 发送流式请求到Claude API(带usage数据捕获)
|
||||
async _makeClaudeStreamRequestWithUsageCapture(body, accessToken, proxyAgent, responseStream, usageCallback) {
|
||||
async _makeClaudeStreamRequestWithUsageCapture(body, accessToken, proxyAgent, clientHeaders, responseStream, usageCallback) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(this.claudeApiUrl);
|
||||
|
||||
// 获取过滤后的客户端 headers
|
||||
const filteredHeaders = this._filterClientHeaders(clientHeaders);
|
||||
|
||||
const options = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || 443,
|
||||
@@ -303,12 +342,17 @@ class ClaudeRelayService {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'anthropic-version': this.apiVersion,
|
||||
'User-Agent': 'claude-relay-service/1.0.0'
|
||||
...filteredHeaders
|
||||
},
|
||||
agent: proxyAgent,
|
||||
timeout: config.proxy.timeout
|
||||
};
|
||||
|
||||
// 如果客户端没有提供 User-Agent,使用默认值
|
||||
if (!filteredHeaders['User-Agent'] && !filteredHeaders['user-agent']) {
|
||||
options.headers['User-Agent'] = 'claude-cli/1.0.53 (external, cli)';
|
||||
}
|
||||
|
||||
if (this.betaHeader) {
|
||||
options.headers['anthropic-beta'] = this.betaHeader;
|
||||
}
|
||||
@@ -446,10 +490,13 @@ class ClaudeRelayService {
|
||||
}
|
||||
|
||||
// 🌊 发送流式请求到Claude API
|
||||
async _makeClaudeStreamRequest(body, accessToken, proxyAgent, responseStream) {
|
||||
async _makeClaudeStreamRequest(body, accessToken, proxyAgent, clientHeaders, responseStream) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(this.claudeApiUrl);
|
||||
|
||||
// 获取过滤后的客户端 headers
|
||||
const filteredHeaders = this._filterClientHeaders(clientHeaders);
|
||||
|
||||
const options = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || 443,
|
||||
@@ -459,12 +506,17 @@ class ClaudeRelayService {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'anthropic-version': this.apiVersion,
|
||||
'User-Agent': 'claude-relay-service/1.0.0'
|
||||
...filteredHeaders
|
||||
},
|
||||
agent: proxyAgent,
|
||||
timeout: config.proxy.timeout
|
||||
};
|
||||
|
||||
// 如果客户端没有提供 User-Agent,使用默认值
|
||||
if (!filteredHeaders['User-Agent'] && !filteredHeaders['user-agent']) {
|
||||
options.headers['User-Agent'] = 'claude-cli/1.0.53 (external, cli)';
|
||||
}
|
||||
|
||||
if (this.betaHeader) {
|
||||
options.headers['anthropic-beta'] = this.betaHeader;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user