- 新增 auth-middleware.js 验证客户端 API Key - 支持 Authorization: Bearer <key> 和 x-api-key 两种方式 - API Keys 只通过环境变量配置(安全最佳实践) - 公开路径: /, /health, /status - 可配置 /v1/models 是否需要认证 - 启动时输出认证状态日志 配置方式: AUTH_ENABLED=true API_KEYS=sk-key1,sk-key2 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
302 lines
8.9 KiB
JavaScript
302 lines
8.9 KiB
JavaScript
import express from 'express';
|
||
import { loadConfig, isDevMode, getPort, getCorsConfig } from './config.js';
|
||
import { logInfo, logError } from './logger.js';
|
||
import router from './routes.js';
|
||
import { initializeAuth } from './auth.js';
|
||
import { initializeUserAgentUpdater } from './user-agent-updater.js';
|
||
import './sls-logger.js'; // 初始化阿里云日志服务
|
||
import { sanitizeForLog } from './log-sanitizer.js';
|
||
import { authMiddleware, getAuthConfig } from './auth-middleware.js';
|
||
|
||
// ============================================================================
|
||
// 全局错误处理 - 必须在应用启动前注册
|
||
// ============================================================================
|
||
|
||
let isShuttingDown = false;
|
||
|
||
/**
|
||
* 优雅关闭服务器
|
||
*/
|
||
function gracefulShutdown(reason, exitCode = 1) {
|
||
if (isShuttingDown) {
|
||
return;
|
||
}
|
||
isShuttingDown = true;
|
||
|
||
console.error(`\n${'='.repeat(80)}`);
|
||
console.error(`🛑 Server shutting down: ${reason}`);
|
||
console.error(`${'='.repeat(80)}\n`);
|
||
|
||
// 给正在处理的请求一些时间完成
|
||
setTimeout(() => {
|
||
process.exit(exitCode);
|
||
}, 3000);
|
||
}
|
||
|
||
/**
|
||
* 处理未捕获的 Promise Rejection
|
||
*/
|
||
process.on('unhandledRejection', (reason, promise) => {
|
||
const errorInfo = {
|
||
type: 'unhandledRejection',
|
||
reason: reason instanceof Error ? reason.message : String(reason),
|
||
stack: reason instanceof Error ? reason.stack : undefined,
|
||
timestamp: new Date().toISOString()
|
||
};
|
||
|
||
console.error(`\n${'='.repeat(80)}`);
|
||
console.error('⚠️ Unhandled Promise Rejection');
|
||
console.error('='.repeat(80));
|
||
console.error(`Time: ${errorInfo.timestamp}`);
|
||
console.error(`Reason: ${errorInfo.reason}`);
|
||
if (errorInfo.stack && isDevMode()) {
|
||
console.error(`Stack: ${errorInfo.stack}`);
|
||
}
|
||
console.error('='.repeat(80) + '\n');
|
||
|
||
logError('Unhandled Promise Rejection', sanitizeForLog(errorInfo));
|
||
|
||
// 在生产环境中,unhandledRejection 可能表示严重问题,考虑退出
|
||
// 但为了服务稳定性,这里只记录不退出
|
||
// 如需更严格的处理,可取消下面的注释:
|
||
// gracefulShutdown('Unhandled Promise Rejection', 1);
|
||
});
|
||
|
||
/**
|
||
* 处理未捕获的异常
|
||
*/
|
||
process.on('uncaughtException', (error) => {
|
||
const errorInfo = {
|
||
type: 'uncaughtException',
|
||
message: error.message,
|
||
stack: error.stack,
|
||
timestamp: new Date().toISOString()
|
||
};
|
||
|
||
console.error(`\n${'='.repeat(80)}`);
|
||
console.error('💥 Uncaught Exception');
|
||
console.error('='.repeat(80));
|
||
console.error(`Time: ${errorInfo.timestamp}`);
|
||
console.error(`Error: ${errorInfo.message}`);
|
||
if (errorInfo.stack) {
|
||
console.error(`Stack: ${errorInfo.stack}`);
|
||
}
|
||
console.error('='.repeat(80) + '\n');
|
||
|
||
logError('Uncaught Exception', sanitizeForLog(errorInfo));
|
||
|
||
// uncaughtException 后进程状态不确定,必须退出
|
||
gracefulShutdown('Uncaught Exception', 1);
|
||
});
|
||
|
||
/**
|
||
* 处理进程信号
|
||
*/
|
||
process.on('SIGTERM', () => {
|
||
logInfo('Received SIGTERM signal');
|
||
gracefulShutdown('SIGTERM', 0);
|
||
});
|
||
|
||
process.on('SIGINT', () => {
|
||
logInfo('Received SIGINT signal');
|
||
gracefulShutdown('SIGINT', 0);
|
||
});
|
||
|
||
// ============================================================================
|
||
|
||
const app = express();
|
||
|
||
app.use(express.json({ limit: '50mb' }));
|
||
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
|
||
|
||
/**
|
||
* CORS 中间件 - 根据配置动态处理跨域请求
|
||
*/
|
||
app.use((req, res, next) => {
|
||
const corsConfig = getCorsConfig();
|
||
|
||
// 如果 CORS 完全禁用,直接跳过
|
||
if (!corsConfig.enabled) {
|
||
if (req.method === 'OPTIONS') {
|
||
return res.sendStatus(204);
|
||
}
|
||
return next();
|
||
}
|
||
|
||
const origin = req.headers.origin;
|
||
|
||
// 设置允许的方法和头
|
||
res.header('Access-Control-Allow-Methods', corsConfig.methods.join(', '));
|
||
res.header('Access-Control-Allow-Headers', corsConfig.headers.join(', '));
|
||
|
||
if (corsConfig.allowAll) {
|
||
// 允许所有来源(仅用于开发环境)
|
||
res.header('Access-Control-Allow-Origin', '*');
|
||
} else if (origin && corsConfig.origins.length > 0) {
|
||
// 白名单模式:验证请求来源
|
||
if (corsConfig.origins.includes(origin)) {
|
||
res.header('Access-Control-Allow-Origin', origin);
|
||
res.header('Vary', 'Origin');
|
||
}
|
||
// 不在白名单中的请求不设置 CORS 头,浏览器会拒绝
|
||
}
|
||
// 如果没有配置 origins 且不是 allowAll,则不设置任何 CORS 头
|
||
|
||
if (req.method === 'OPTIONS') {
|
||
return res.sendStatus(204);
|
||
}
|
||
next();
|
||
});
|
||
|
||
// 请求认证中间件
|
||
app.use(authMiddleware);
|
||
|
||
app.use(router);
|
||
|
||
app.get('/', (req, res) => {
|
||
res.json({
|
||
name: 'droid2api',
|
||
version: '1.0.0',
|
||
description: 'OpenAI Compatible API Proxy',
|
||
endpoints: [
|
||
'GET /v1/models',
|
||
'POST /v1/chat/completions',
|
||
'POST /v1/responses',
|
||
'POST /v1/messages',
|
||
'POST /v1/messages/count_tokens'
|
||
]
|
||
});
|
||
});
|
||
|
||
// 404 处理 - 捕获所有未匹配的路由
|
||
app.use((req, res, next) => {
|
||
const errorInfo = {
|
||
timestamp: new Date().toISOString(),
|
||
method: req.method,
|
||
url: req.originalUrl || req.url,
|
||
path: req.path,
|
||
query: req.query,
|
||
params: req.params,
|
||
body: req.body,
|
||
headers: {
|
||
'content-type': req.headers['content-type'],
|
||
'user-agent': req.headers['user-agent'],
|
||
'origin': req.headers['origin'],
|
||
'referer': req.headers['referer']
|
||
},
|
||
ip: req.ip || req.connection.remoteAddress
|
||
};
|
||
|
||
const safeQuery = sanitizeForLog(errorInfo.query);
|
||
const safeBody = sanitizeForLog(errorInfo.body);
|
||
const safeHeaders = sanitizeForLog(errorInfo.headers);
|
||
const safeIp = sanitizeForLog(errorInfo.ip);
|
||
|
||
console.error('\n' + '='.repeat(80));
|
||
console.error('❌ 非法请求地址');
|
||
console.error('='.repeat(80));
|
||
console.error(`时间: ${errorInfo.timestamp}`);
|
||
console.error(`方法: ${errorInfo.method}`);
|
||
console.error(`地址: ${errorInfo.url}`);
|
||
console.error(`路径: ${errorInfo.path}`);
|
||
|
||
if (Object.keys(errorInfo.query).length > 0) {
|
||
console.error(`查询参数: ${JSON.stringify(safeQuery, null, 2)}`);
|
||
}
|
||
|
||
if (errorInfo.body && Object.keys(errorInfo.body).length > 0) {
|
||
console.error(`请求体: ${JSON.stringify(safeBody, null, 2)}`);
|
||
}
|
||
|
||
console.error(`客户端IP: ${safeIp}`);
|
||
console.error(`User-Agent: ${safeHeaders['user-agent'] || 'N/A'}`);
|
||
|
||
if (safeHeaders.referer) {
|
||
console.error(`来源: ${safeHeaders.referer}`);
|
||
}
|
||
|
||
console.error('='.repeat(80) + '\n');
|
||
|
||
logError('Invalid request path', sanitizeForLog(errorInfo));
|
||
|
||
res.status(404).json({
|
||
error: 'Not Found',
|
||
message: `路径 ${req.method} ${req.path} 不存在`,
|
||
timestamp: errorInfo.timestamp,
|
||
availableEndpoints: [
|
||
'GET /v1/models',
|
||
'POST /v1/chat/completions',
|
||
'POST /v1/responses',
|
||
'POST /v1/messages',
|
||
'POST /v1/messages/count_tokens'
|
||
]
|
||
});
|
||
});
|
||
|
||
// 错误处理中间件
|
||
app.use((err, req, res, next) => {
|
||
logError('Unhandled error', err);
|
||
res.status(500).json({
|
||
error: 'Internal server error',
|
||
message: isDevMode() ? err.message : undefined
|
||
});
|
||
});
|
||
|
||
(async () => {
|
||
try {
|
||
loadConfig();
|
||
logInfo('Configuration loaded successfully');
|
||
logInfo(`Dev mode: ${isDevMode()}`);
|
||
|
||
// Log auth status
|
||
const authConfig = getAuthConfig();
|
||
if (authConfig.enabled) {
|
||
logInfo(`Auth enabled with ${authConfig.apiKeys.size} API key(s)`);
|
||
} else {
|
||
logInfo('Auth disabled - API endpoints are publicly accessible');
|
||
}
|
||
|
||
// Initialize User-Agent version updater
|
||
initializeUserAgentUpdater();
|
||
|
||
// Initialize auth system (load and setup API key if needed)
|
||
// This won't throw error if no auth config is found - will use client auth
|
||
await initializeAuth();
|
||
|
||
const PORT = getPort();
|
||
logInfo(`Starting server on port ${PORT}...`);
|
||
|
||
const server = app.listen(PORT)
|
||
.on('listening', () => {
|
||
logInfo(`Server running on http://localhost:${PORT}`);
|
||
logInfo('Available endpoints:');
|
||
logInfo(' GET /v1/models');
|
||
logInfo(' POST /v1/chat/completions');
|
||
logInfo(' POST /v1/responses');
|
||
logInfo(' POST /v1/messages');
|
||
logInfo(' POST /v1/messages/count_tokens');
|
||
})
|
||
.on('error', (err) => {
|
||
if (err.code === 'EADDRINUSE') {
|
||
console.error(`\n${'='.repeat(80)}`);
|
||
console.error(`ERROR: Port ${PORT} is already in use!`);
|
||
console.error('');
|
||
console.error('Please choose one of the following options:');
|
||
console.error(` 1. Stop the process using port ${PORT}:`);
|
||
console.error(` lsof -ti:${PORT} | xargs kill`);
|
||
console.error('');
|
||
console.error(' 2. Change the port in config.json:');
|
||
console.error(' Edit config.json and modify the "port" field');
|
||
console.error(`${'='.repeat(80)}\n`);
|
||
process.exit(1);
|
||
} else {
|
||
logError('Failed to start server', err);
|
||
process.exit(1);
|
||
}
|
||
});
|
||
} catch (error) {
|
||
logError('Failed to start server', error);
|
||
process.exit(1);
|
||
}
|
||
})();
|