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;
|
let usageDataCaptured = false;
|
||||||
|
|
||||||
// 使用自定义流处理器来捕获usage数据
|
// 使用自定义流处理器来捕获usage数据
|
||||||
await claudeRelayService.relayStreamRequestWithUsageCapture(req.body, req.apiKey, res, (usageData) => {
|
await claudeRelayService.relayStreamRequestWithUsageCapture(req.body, req.apiKey, res, req.headers, (usageData) => {
|
||||||
// 回调函数:当检测到完整usage数据时记录真实token使用量
|
// 回调函数:当检测到完整usage数据时记录真实token使用量
|
||||||
logger.info('🎯 Usage callback triggered with complete data:', JSON.stringify(usageData, null, 2));
|
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
|
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', {
|
logger.info('📡 Claude API response received', {
|
||||||
statusCode: response.statusCode,
|
statusCode: response.statusCode,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class ClaudeRelayService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🚀 转发请求到Claude API
|
// 🚀 转发请求到Claude API
|
||||||
async relayRequest(requestBody, apiKeyData, clientRequest, clientResponse) {
|
async relayRequest(requestBody, apiKeyData, clientRequest, clientResponse, clientHeaders) {
|
||||||
let upstreamRequest = null;
|
let upstreamRequest = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -57,6 +57,7 @@ class ClaudeRelayService {
|
|||||||
processedBody,
|
processedBody,
|
||||||
accessToken,
|
accessToken,
|
||||||
proxyAgent,
|
proxyAgent,
|
||||||
|
clientHeaders,
|
||||||
(req) => { upstreamRequest = req; }
|
(req) => { upstreamRequest = req; }
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -190,11 +191,41 @@ class ClaudeRelayService {
|
|||||||
return null;
|
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
|
// 🔗 发送请求到Claude API
|
||||||
async _makeClaudeRequest(body, accessToken, proxyAgent, onRequest) {
|
async _makeClaudeRequest(body, accessToken, proxyAgent, clientHeaders, onRequest) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const url = new URL(this.claudeApiUrl);
|
const url = new URL(this.claudeApiUrl);
|
||||||
|
|
||||||
|
// 获取过滤后的客户端 headers
|
||||||
|
const filteredHeaders = this._filterClientHeaders(clientHeaders);
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
hostname: url.hostname,
|
hostname: url.hostname,
|
||||||
port: url.port || 443,
|
port: url.port || 443,
|
||||||
@@ -204,12 +235,17 @@ class ClaudeRelayService {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${accessToken}`,
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
'anthropic-version': this.apiVersion,
|
'anthropic-version': this.apiVersion,
|
||||||
'User-Agent': 'claude-relay-service/1.0.0'
|
...filteredHeaders
|
||||||
},
|
},
|
||||||
agent: proxyAgent,
|
agent: proxyAgent,
|
||||||
timeout: config.proxy.timeout
|
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) {
|
if (this.betaHeader) {
|
||||||
options.headers['anthropic-beta'] = this.betaHeader;
|
options.headers['anthropic-beta'] = this.betaHeader;
|
||||||
}
|
}
|
||||||
@@ -262,7 +298,7 @@ class ClaudeRelayService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🌊 处理流式响应(带usage数据捕获)
|
// 🌊 处理流式响应(带usage数据捕获)
|
||||||
async relayStreamRequestWithUsageCapture(requestBody, apiKeyData, responseStream, usageCallback) {
|
async relayStreamRequestWithUsageCapture(requestBody, apiKeyData, responseStream, clientHeaders, usageCallback) {
|
||||||
try {
|
try {
|
||||||
// 生成会话哈希用于sticky会话
|
// 生成会话哈希用于sticky会话
|
||||||
const sessionHash = sessionHelper.generateSessionHash(requestBody);
|
const sessionHash = sessionHelper.generateSessionHash(requestBody);
|
||||||
@@ -282,7 +318,7 @@ class ClaudeRelayService {
|
|||||||
const proxyAgent = await this._getProxyAgent(accountId);
|
const proxyAgent = await this._getProxyAgent(accountId);
|
||||||
|
|
||||||
// 发送流式请求并捕获usage数据
|
// 发送流式请求并捕获usage数据
|
||||||
return await this._makeClaudeStreamRequestWithUsageCapture(processedBody, accessToken, proxyAgent, responseStream, usageCallback);
|
return await this._makeClaudeStreamRequestWithUsageCapture(processedBody, accessToken, proxyAgent, clientHeaders, responseStream, usageCallback);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Claude stream relay with usage capture failed:', error);
|
logger.error('❌ Claude stream relay with usage capture failed:', error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -290,10 +326,13 @@ class ClaudeRelayService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🌊 发送流式请求到Claude API(带usage数据捕获)
|
// 🌊 发送流式请求到Claude API(带usage数据捕获)
|
||||||
async _makeClaudeStreamRequestWithUsageCapture(body, accessToken, proxyAgent, responseStream, usageCallback) {
|
async _makeClaudeStreamRequestWithUsageCapture(body, accessToken, proxyAgent, clientHeaders, responseStream, usageCallback) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const url = new URL(this.claudeApiUrl);
|
const url = new URL(this.claudeApiUrl);
|
||||||
|
|
||||||
|
// 获取过滤后的客户端 headers
|
||||||
|
const filteredHeaders = this._filterClientHeaders(clientHeaders);
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
hostname: url.hostname,
|
hostname: url.hostname,
|
||||||
port: url.port || 443,
|
port: url.port || 443,
|
||||||
@@ -303,12 +342,17 @@ class ClaudeRelayService {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${accessToken}`,
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
'anthropic-version': this.apiVersion,
|
'anthropic-version': this.apiVersion,
|
||||||
'User-Agent': 'claude-relay-service/1.0.0'
|
...filteredHeaders
|
||||||
},
|
},
|
||||||
agent: proxyAgent,
|
agent: proxyAgent,
|
||||||
timeout: config.proxy.timeout
|
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) {
|
if (this.betaHeader) {
|
||||||
options.headers['anthropic-beta'] = this.betaHeader;
|
options.headers['anthropic-beta'] = this.betaHeader;
|
||||||
}
|
}
|
||||||
@@ -446,10 +490,13 @@ class ClaudeRelayService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🌊 发送流式请求到Claude API
|
// 🌊 发送流式请求到Claude API
|
||||||
async _makeClaudeStreamRequest(body, accessToken, proxyAgent, responseStream) {
|
async _makeClaudeStreamRequest(body, accessToken, proxyAgent, clientHeaders, responseStream) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const url = new URL(this.claudeApiUrl);
|
const url = new URL(this.claudeApiUrl);
|
||||||
|
|
||||||
|
// 获取过滤后的客户端 headers
|
||||||
|
const filteredHeaders = this._filterClientHeaders(clientHeaders);
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
hostname: url.hostname,
|
hostname: url.hostname,
|
||||||
port: url.port || 443,
|
port: url.port || 443,
|
||||||
@@ -459,12 +506,17 @@ class ClaudeRelayService {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${accessToken}`,
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
'anthropic-version': this.apiVersion,
|
'anthropic-version': this.apiVersion,
|
||||||
'User-Agent': 'claude-relay-service/1.0.0'
|
...filteredHeaders
|
||||||
},
|
},
|
||||||
agent: proxyAgent,
|
agent: proxyAgent,
|
||||||
timeout: config.proxy.timeout
|
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) {
|
if (this.betaHeader) {
|
||||||
options.headers['anthropic-beta'] = this.betaHeader;
|
options.headers['anthropic-beta'] = this.betaHeader;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user