Files
droid2api/server.js
empty d1dc095cb1 feat: 添加请求认证中间件保护 API 端点
- 新增 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>
2025-12-27 16:14:15 +08:00

302 lines
8.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
})();