import * as fs from "fs"; import * as http from "http"; import * as https from "https"; import * as path from "path"; import { dispatchAdmin } from "./admin-handlers"; import { getMockFilePath, getMockStatusCode, isMockRoute, } from "./route-matching"; import { state } from "./state"; import { decodeBodyByEncoding } from "./utils"; function isTargetHttps(): boolean { return state.config.targetHttps !== false; } function getTargetPort(): number { if (state.config.targetPort != null) return state.config.targetPort; return isTargetHttps() ? 443 : 80; } function upstreamRequest( options: http.RequestOptions, callback: (proxyRes: http.IncomingMessage) => void, ): http.ClientRequest { return isTargetHttps() ? https.request(options, callback) : http.request(options, callback); } export function createProxyServer(): http.Server { return http.createServer(async (clientReq, clientRes) => { const parsedUrl = new URL(`http://localhost${clientReq.url!}`); const requestPath = parsedUrl.pathname; // 管理接口:保留路径,不参与代理转发 const handled = await dispatchAdmin(requestPath, clientReq, clientRes); if (handled) return; // 检查是否为需要mock的路由 if (isMockRoute(requestPath)) { const mockFile = getMockFilePath(requestPath); const mockStatusCode = getMockStatusCode(requestPath); console.log(`[MOCK] 拦截路由: ${requestPath} -> 使用文件: ${mockFile}`); try { const mockFilePath = path.join(__dirname, "..", mockFile); // 检查文件是否存在 if (!fs.existsSync(mockFilePath)) { console.warn( `[MOCK] Mock文件不存在,回源并自动生成: ${mockFilePath}`, ); const targetPort = getTargetPort(); const options: http.RequestOptions = { hostname: state.config.targetHost, port: targetPort, method: clientReq.method, path: parsedUrl.pathname + parsedUrl.search, headers: { ...clientReq.headers, host: state.config.targetHost, }, }; const proxyReq = upstreamRequest(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(mockStatusCode, { ...proxyRes.headers, "X-Mock-Autogenerated": "true", "X-Mock-Source": mockFile, "X-Mock-Status-Code": String(mockStatusCode), }); 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 = state.config.defaultContentType || "application/json"; clientRes.writeHead(mockStatusCode, { "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-Status-Code": String(mockStatusCode), "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 = getTargetPort(); const options: http.RequestOptions = { hostname: state.config.targetHost, port: targetPort, method: clientReq.method, path: parsedUrl.pathname + parsedUrl.search, headers: { ...clientReq.headers, host: state.config.targetHost, }, }; const proxyReq = upstreamRequest(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); }); } export function getEffectiveTargetPort(): number { if (state.config.targetPort != null) return state.config.targetPort; return isTargetHttps() ? 443 : 80; } export function getEffectiveTargetHttps(): boolean { return isTargetHttps(); }