325 lines
11 KiB
TypeScript
325 lines
11 KiB
TypeScript
import * as http from 'http';
|
||
import * as https from 'https';
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
|
||
// 配置文件路径
|
||
const CONFIG_FILE = path.join(__dirname, 'config.json');
|
||
|
||
// 定义类型
|
||
interface RouteConfig {
|
||
[route: string]: string;
|
||
}
|
||
|
||
interface AppConfig {
|
||
/** 为 false 时所有请求走代理,不命中 mock 路由;缺省为 true */
|
||
mockEnabled?: boolean;
|
||
cacheConfig: boolean;
|
||
reloadOnChange: boolean;
|
||
defaultContentType: string;
|
||
proxyPort: number;
|
||
targetHost: string;
|
||
}
|
||
|
||
interface ConfigFile {
|
||
routes: RouteConfig;
|
||
config: AppConfig;
|
||
}
|
||
|
||
// 存储当前的路由配置
|
||
let MOCK_ROUTES: RouteConfig = {};
|
||
let CONFIG: AppConfig = {
|
||
cacheConfig: true,
|
||
reloadOnChange: true,
|
||
defaultContentType: "application/json",
|
||
proxyPort: 9443,
|
||
targetHost: "devrmtapp.resmart.cn"
|
||
};
|
||
|
||
// 过滤掉以 # 开头的路由(视为注释,不参与 mock)
|
||
function filterActiveRoutes(routes: RouteConfig): RouteConfig {
|
||
const filtered: RouteConfig = {};
|
||
for (const [route, filePath] of Object.entries(routes)) {
|
||
if (route.startsWith('#')) continue;
|
||
filtered[route] = filePath;
|
||
}
|
||
return filtered;
|
||
}
|
||
|
||
// 加载配置文件
|
||
function loadConfig() {
|
||
try {
|
||
if (!fs.existsSync(CONFIG_FILE)) {
|
||
console.warn(`[CONFIG] 配置文件不存在: ${CONFIG_FILE}`);
|
||
console.warn(`[CONFIG] 正在创建默认配置文件...`);
|
||
|
||
// 创建默认配置
|
||
const defaultConfig: ConfigFile = {
|
||
routes: {
|
||
"/api2/test": "mock/test.txt"
|
||
},
|
||
config: {
|
||
cacheConfig: true,
|
||
reloadOnChange: true,
|
||
defaultContentType: "application/json",
|
||
proxyPort: 9443,
|
||
targetHost: "devrmtapp.resmart.cn"
|
||
}
|
||
};
|
||
|
||
// 确保mock目录存在
|
||
const mockDir = path.join(__dirname, 'mock');
|
||
if (!fs.existsSync(mockDir)) {
|
||
fs.mkdirSync(mockDir, { recursive: true });
|
||
}
|
||
|
||
// 保存配置文件
|
||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(defaultConfig, null, 2), 'utf-8');
|
||
console.log(`[CONFIG] 已创建默认配置文件: ${CONFIG_FILE}`);
|
||
|
||
// 加载配置(# 开头的路由视为注释,不参与 mock)
|
||
const configData: ConfigFile = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
||
MOCK_ROUTES = filterActiveRoutes(configData.routes || {});
|
||
CONFIG = configData.config || CONFIG;
|
||
} else {
|
||
const configData: ConfigFile = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
||
MOCK_ROUTES = filterActiveRoutes(configData.routes || {});
|
||
CONFIG = configData.config || CONFIG;
|
||
console.log(`[CONFIG] 配置文件已加载,共 ${Object.keys(MOCK_ROUTES).length} 个路由${CONFIG.mockEnabled !== false ? '' : '(mock 已关闭,全部走代理)'}`);
|
||
}
|
||
|
||
// 验证mock文件是否存在
|
||
validateMockFiles();
|
||
|
||
} catch (error) {
|
||
console.error(`[CONFIG] 加载配置文件失败:`, error);
|
||
// 使用默认配置
|
||
MOCK_ROUTES = {
|
||
"/api2/user/list": "mock/user_list.txt"
|
||
};
|
||
CONFIG = {
|
||
cacheConfig: true,
|
||
reloadOnChange: true,
|
||
defaultContentType: "application/json",
|
||
proxyPort: 9443,
|
||
targetHost: "devrmtapp.resmart.cn"
|
||
};
|
||
}
|
||
}
|
||
|
||
// 验证mock文件是否存在
|
||
function validateMockFiles() {
|
||
console.log(`[CONFIG] 验证mock文件...`);
|
||
let missingFiles = [];
|
||
|
||
for (const [route, filePath] of Object.entries(MOCK_ROUTES)) {
|
||
const fullPath = path.join(__dirname, filePath);
|
||
if (!fs.existsSync(fullPath)) {
|
||
missingFiles.push({ route, filePath, fullPath });
|
||
console.warn(`[CONFIG] 警告: mock文件不存在 - ${filePath} (用于路由: ${route})`);
|
||
}
|
||
}
|
||
|
||
if (missingFiles.length > 0) {
|
||
console.log(`[CONFIG] 缺少 ${missingFiles.length} 个mock文件,请创建这些文件`);
|
||
} else {
|
||
console.log(`[CONFIG] 所有mock文件验证通过`);
|
||
}
|
||
}
|
||
|
||
// 检查是否为mock路由的函数
|
||
function isMockRoute(requestPath: string): boolean {
|
||
if (CONFIG.mockEnabled === false) return false;
|
||
return MOCK_ROUTES.hasOwnProperty(requestPath);
|
||
}
|
||
|
||
// 获取mock文件路径
|
||
function getMockFilePath(requestPath: string): string {
|
||
return MOCK_ROUTES[requestPath];
|
||
}
|
||
|
||
// 代理服务器
|
||
const proxyServer = http.createServer((clientReq, clientRes) => {
|
||
// 解析客户端请求的 URL
|
||
const parsedUrl = new URL(`http://localhost${clientReq.url!}`);
|
||
const requestPath = parsedUrl.pathname;
|
||
|
||
// 检查是否为需要mock的路由
|
||
if (isMockRoute(requestPath)) {
|
||
const mockFile = getMockFilePath(requestPath);
|
||
console.log(`[MOCK] 拦截路由: ${requestPath} -> 使用文件: ${mockFile}`);
|
||
|
||
try {
|
||
// 构建完整的文件路径
|
||
const mockFilePath = path.join(__dirname, mockFile);
|
||
|
||
// 检查文件是否存在
|
||
if (!fs.existsSync(mockFilePath)) {
|
||
console.error(`[MOCK] Mock文件不存在: ${mockFilePath}`);
|
||
clientRes.writeHead(404, { 'Content-Type': 'application/json' });
|
||
clientRes.end(JSON.stringify({
|
||
error: 'Mock file not found',
|
||
route: requestPath,
|
||
file: mockFile,
|
||
timestamp: new Date().toISOString()
|
||
}));
|
||
return;
|
||
}
|
||
|
||
// 读取本地mock文件
|
||
const mockData = fs.readFileSync(mockFilePath, 'utf-8');
|
||
|
||
// 设置响应头
|
||
const contentType = CONFIG.defaultContentType || 'application/json';
|
||
clientRes.writeHead(200, {
|
||
'Content-Type': contentType,
|
||
'Access-Control-Allow-Origin': '*',
|
||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||
'Access-Control-Allow-Headers': 'Content-Type',
|
||
'X-Mock-Source': mockFile,
|
||
'X-Mock-Timestamp': new Date().toISOString()
|
||
});
|
||
|
||
// 返回mock数据
|
||
clientRes.end(mockData);
|
||
console.log(`[MOCK] 成功返回mock数据: ${mockFile}`);
|
||
} catch (error) {
|
||
console.error(`[MOCK] 读取mock文件失败: ${mockFile}`, error);
|
||
clientRes.writeHead(500, { 'Content-Type': 'application/json' });
|
||
clientRes.end(JSON.stringify({
|
||
error: 'Failed to read mock data',
|
||
route: requestPath,
|
||
file: mockFile,
|
||
message: (error as Error).message,
|
||
timestamp: new Date().toISOString()
|
||
}));
|
||
}
|
||
return; // 直接返回,不转发到目标服务器
|
||
}
|
||
|
||
// 如果不是mock路由,正常代理转发
|
||
console.log(`[PROXY] 转发请求: ${requestPath}`);
|
||
|
||
// 目标服务器的选项
|
||
const options: http.RequestOptions = {
|
||
hostname: CONFIG.targetHost,
|
||
port: 443, // HTTPS 默认端口
|
||
method: clientReq.method,
|
||
path: parsedUrl.pathname + parsedUrl.search,
|
||
headers: {
|
||
...clientReq.headers,
|
||
host: CONFIG.targetHost
|
||
}
|
||
};
|
||
|
||
// 创建到目标服务器的 HTTPS 请求
|
||
const proxyReq = https.request(options, (proxyRes) => {
|
||
// 将目标服务器的响应头复制到客户端响应
|
||
clientRes.writeHead(proxyRes.statusCode!, proxyRes.headers);
|
||
// 将目标服务器的响应数据管道传输到客户端
|
||
proxyRes.pipe(clientRes);
|
||
});
|
||
|
||
// 错误处理
|
||
proxyReq.on('error', (err) => {
|
||
console.error('Proxy request error:', err);
|
||
clientRes.writeHead(500, { 'Content-Type': 'application/json' });
|
||
clientRes.end(JSON.stringify({
|
||
error: 'Proxy error',
|
||
message: err.message,
|
||
timestamp: new Date().toISOString()
|
||
}));
|
||
});
|
||
|
||
// 将客户端请求体管道传输到代理请求
|
||
clientReq.pipe(proxyReq);
|
||
});
|
||
|
||
// 添加管理接口用于重新加载配置
|
||
proxyServer.on('request', (req, res) => {
|
||
const parsedUrl = new URL(`http://localhost${req.url!}`);
|
||
const pathname = parsedUrl.pathname;
|
||
|
||
// 管理接口:重新加载配置
|
||
if (pathname === '/__reload-config' && req.method === 'POST') {
|
||
try {
|
||
const oldCount = Object.keys(MOCK_ROUTES).length;
|
||
loadConfig();
|
||
const newCount = Object.keys(MOCK_ROUTES).length;
|
||
|
||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
success: true,
|
||
message: 'Configuration reloaded successfully',
|
||
routesCount: newCount,
|
||
routesChanged: newCount - oldCount
|
||
}));
|
||
console.log(`[ADMIN] 通过API重新加载配置 (路由数: ${oldCount} -> ${newCount})`);
|
||
} catch (error) {
|
||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
success: false,
|
||
error: (error as Error).message
|
||
}));
|
||
}
|
||
}
|
||
|
||
// 管理接口:查看当前配置
|
||
if (pathname === '/__config' && req.method === 'GET') {
|
||
try {
|
||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
routes: MOCK_ROUTES,
|
||
config: CONFIG,
|
||
timestamp: new Date().toISOString(),
|
||
totalRoutes: Object.keys(MOCK_ROUTES).length
|
||
}, null, 2));
|
||
} catch (error) {
|
||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
success: false,
|
||
error: (error as Error).message
|
||
}));
|
||
}
|
||
}
|
||
});
|
||
|
||
// 初始化:第一次加载配置
|
||
loadConfig();
|
||
|
||
// 如果配置了文件监视,则监听配置文件变化
|
||
if (CONFIG.reloadOnChange) {
|
||
fs.watchFile(CONFIG_FILE, (curr, prev) => {
|
||
console.log(`[CONFIG] 配置文件已修改,重新加载...`);
|
||
try {
|
||
const oldRoutesCount = Object.keys(MOCK_ROUTES).length;
|
||
loadConfig();
|
||
const newRoutesCount = Object.keys(MOCK_ROUTES).length;
|
||
console.log(`[CONFIG] 配置重载完成 (路由数: ${oldRoutesCount} -> ${newRoutesCount})`);
|
||
} catch (error) {
|
||
console.error(`[CONFIG] 重新加载配置文件失败:`, error);
|
||
}
|
||
});
|
||
console.log(`[CONFIG] 已启用配置文件监视: ${CONFIG_FILE}`);
|
||
}
|
||
|
||
proxyServer.listen(CONFIG.proxyPort, "0.0.0.0", () => {
|
||
console.log(`========================================`);
|
||
console.log(`代理服务器运行在: http://localhost:${CONFIG.proxyPort}`);
|
||
console.log(`目标服务器: https://${CONFIG.targetHost}`);
|
||
console.log(`配置文件: ${CONFIG_FILE}`);
|
||
console.log(`已配置Mock路由: ${Object.keys(MOCK_ROUTES).length} 个${CONFIG.mockEnabled !== false ? '' : '(当前 mockEnabled=false,未生效)'}`);
|
||
console.log(`========================================`);
|
||
console.log(`管理接口:`);
|
||
console.log(` GET http://localhost:${CONFIG.proxyPort}/__config 查看当前配置`);
|
||
console.log(` POST http://localhost:${CONFIG.proxyPort}/__reload-config 重新加载配置`);
|
||
console.log(`========================================`);
|
||
console.log(`Mock路由列表:`);
|
||
|
||
for (const [route, file] of Object.entries(MOCK_ROUTES)) {
|
||
const filePath = path.join(__dirname, file);
|
||
const exists = fs.existsSync(filePath) ? '✓' : '✗';
|
||
console.log(` ${exists} ${route} -> ${file}`);
|
||
}
|
||
console.log(`========================================`);
|
||
}); |