406 lines
12 KiB
TypeScript
406 lines
12 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;
|
||
/** 代理转发的目标 HTTPS 端口;缺省 443 */
|
||
targetPort?: number;
|
||
}
|
||
|
||
interface ConfigFile {
|
||
routes: RouteConfig;
|
||
config: AppConfig;
|
||
}
|
||
|
||
// 存储当前的路由配置
|
||
let MOCK_ROUTES: RouteConfig = {};
|
||
let CONFIG: AppConfig = {
|
||
cacheConfig: true,
|
||
reloadOnChange: true,
|
||
defaultContentType: "application/json",
|
||
proxyPort: 443,
|
||
targetHost: "localhost",
|
||
targetPort: 443,
|
||
};
|
||
|
||
// 过滤掉以 # 开头的路由(视为注释,不参与 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: 443,
|
||
targetHost: "localhost",
|
||
targetPort: 443,
|
||
},
|
||
};
|
||
|
||
// 确保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",
|
||
targetPort: 443,
|
||
};
|
||
}
|
||
}
|
||
|
||
// 验证mock文件是否存在
|
||
function validateMockFiles() {
|
||
console.log(`[CONFIG] 验证mock文件...`);
|
||
let missingFiles: Array<{
|
||
route: string;
|
||
filePath: string;
|
||
fullPath: string;
|
||
}> = [];
|
||
|
||
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;
|
||
|
||
// 管理接口:保留路径,不参与代理转发
|
||
if (requestPath === "/__reload-config") {
|
||
if (clientReq.method !== "POST") {
|
||
clientRes.writeHead(405, {
|
||
"Content-Type": "application/json",
|
||
Allow: "POST",
|
||
});
|
||
clientRes.end(
|
||
JSON.stringify({
|
||
success: false,
|
||
error: "Method Not Allowed",
|
||
allow: ["POST"],
|
||
}),
|
||
);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const oldCount = Object.keys(MOCK_ROUTES).length;
|
||
loadConfig();
|
||
const newCount = Object.keys(MOCK_ROUTES).length;
|
||
|
||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||
clientRes.end(
|
||
JSON.stringify({
|
||
success: true,
|
||
message: "Configuration reloaded successfully",
|
||
routesCount: newCount,
|
||
routesChanged: newCount - oldCount,
|
||
}),
|
||
);
|
||
console.log(
|
||
`[ADMIN] 通过API重新加载配置 (路由数: ${oldCount} -> ${newCount})`,
|
||
);
|
||
} catch (error) {
|
||
clientRes.writeHead(500, { "Content-Type": "application/json" });
|
||
clientRes.end(
|
||
JSON.stringify({
|
||
success: false,
|
||
error: (error as Error).message,
|
||
}),
|
||
);
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (requestPath === "/__config") {
|
||
if (clientReq.method !== "GET") {
|
||
clientRes.writeHead(405, {
|
||
"Content-Type": "application/json",
|
||
Allow: "GET",
|
||
});
|
||
clientRes.end(
|
||
JSON.stringify({
|
||
success: false,
|
||
error: "Method Not Allowed",
|
||
allow: ["GET"],
|
||
}),
|
||
);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||
clientRes.end(
|
||
JSON.stringify(
|
||
{
|
||
routes: MOCK_ROUTES,
|
||
config: CONFIG,
|
||
timestamp: new Date().toISOString(),
|
||
totalRoutes: Object.keys(MOCK_ROUTES).length,
|
||
},
|
||
null,
|
||
2,
|
||
),
|
||
);
|
||
} catch (error) {
|
||
clientRes.writeHead(500, { "Content-Type": "application/json" });
|
||
clientRes.end(
|
||
JSON.stringify({
|
||
success: false,
|
||
error: (error as Error).message,
|
||
}),
|
||
);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 检查是否为需要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 targetPort = CONFIG.targetPort ?? 443;
|
||
|
||
// 目标服务器的选项
|
||
const options: http.RequestOptions = {
|
||
hostname: CONFIG.targetHost,
|
||
port: targetPort,
|
||
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);
|
||
});
|
||
|
||
// 初始化:第一次加载配置
|
||
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", () => {
|
||
const targetPort = CONFIG.targetPort ?? 443;
|
||
console.log(`========================================`);
|
||
console.log(`代理服务器运行在: http://localhost:${CONFIG.proxyPort}`);
|
||
console.log(
|
||
`目标服务器: https://${CONFIG.targetHost}${targetPort !== 443 ? `:${targetPort}` : ""}`,
|
||
);
|
||
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(`========================================`);
|
||
});
|