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