api-proxy-mock/src/proxy.ts
2026-06-04 12:13:28 +08:00

212 lines
6.9 KiB
TypeScript
Raw 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 * 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();
}