api-proxy-mock/index.api.ts

487 lines
15 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 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(`========================================`);
});