import * as http from "http"; import * as https from "https"; import * as fs from "fs"; import * as path from "path"; import * as zlib from "zlib"; // 配置文件路径 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]; } function decodeBodyByEncoding( bodyBuffer: Buffer, contentEncoding?: string, ): Buffer { const encoding = (contentEncoding || "").toLowerCase().trim(); try { if (encoding.includes("gzip")) { return zlib.gunzipSync(bodyBuffer); } if (encoding.includes("br")) { return zlib.brotliDecompressSync(bodyBuffer); } if (encoding.includes("deflate")) { return zlib.inflateSync(bodyBuffer); } } catch (error) { console.warn("[MOCK] 解压上游响应失败,按原始内容写入文件", error); } return bodyBuffer; } // 代理服务器 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.warn(`[MOCK] Mock文件不存在,回源并自动生成: ${mockFilePath}`); 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, }, }; const proxyReq = https.request(options, (proxyRes) => { const chunks: Buffer[] = []; proxyRes.on("data", (chunk: Buffer) => { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); }); proxyRes.on("end", () => { const bodyBuffer = Buffer.concat(chunks); try { const decodedBuffer = decodeBodyByEncoding( bodyBuffer, Array.isArray(proxyRes.headers["content-encoding"]) ? proxyRes.headers["content-encoding"][0] : proxyRes.headers["content-encoding"], ); const contentType = String(proxyRes.headers["content-type"] || ""); const toWrite = contentType.includes("application/json") || contentType.includes("text/") || contentType.includes("application/xml") || contentType.includes("application/javascript") ? decodedBuffer.toString("utf-8") : decodedBuffer; fs.mkdirSync(path.dirname(mockFilePath), { recursive: true }); fs.writeFileSync(mockFilePath, toWrite); console.log(`[MOCK] 已自动写入mock文件: ${mockFilePath}`); } catch (writeErr) { console.error(`[MOCK] 自动写入mock文件失败: ${mockFilePath}`, writeErr); } clientRes.writeHead(proxyRes.statusCode || 200, { ...proxyRes.headers, "X-Mock-Autogenerated": "true", "X-Mock-Source": mockFile, }); clientRes.end(bodyBuffer); }); }); proxyReq.on("error", (err) => { console.error("Proxy request error:", err); clientRes.writeHead(500, { "Content-Type": "application/json" }); clientRes.end( JSON.stringify({ error: "Proxy error", route: requestPath, file: mockFile, message: err.message, timestamp: new Date().toISOString(), }), ); }); clientReq.pipe(proxyReq); 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(`========================================`); });