whistle.mock-plugins
Version:
Whistle 插件,用于快速创建 API 模拟数据
757 lines (656 loc) • 23.9 kB
JavaScript
const express = require('express');
const path = require('path');
const fs = require('fs-extra');
const Mock = require('mockjs');
const storage = require('./storage');
// 数据存储目录
const DATA_DIR = path.join(process.env.WHISTLE_PLUGIN_DATA_DIR || storage.DATA_DIR);
// 功能和接口配置文件
const FEATURES_FILE = path.join(DATA_DIR, 'features.json');
const INTERFACES_FILE = path.join(DATA_DIR, 'interfaces.json');
// 日志目录
const LOG_DIR = path.join(DATA_DIR, 'logs');
try {
fs.ensureDirSync(LOG_DIR);
console.log('Log directory created:', LOG_DIR);
} catch (err) {
console.error('Failed to create log directory:', err.message);
}
const LOG_FILE = path.join(LOG_DIR, 'plugin.log');
// 记录日志
const logMessage = (message) => {
try {
// 确保日志目录存在
if (!fs.existsSync(LOG_DIR)) {
fs.ensureDirSync(LOG_DIR);
}
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] ${message}\n`;
fs.appendFileSync(LOG_FILE, logEntry);
// 同时将日志输出到控制台,方便调试
console.log(`[mock-plugin] ${message}`);
} catch (err) {
console.error('Error writing to log file:', err.message);
}
};
// 记录日志到 logs.json 文件
const addToJsonLog = (logData) => {
try {
// 验证参数有效性
if (!logData || typeof logData !== 'object') {
console.error('无效的日志数据');
return;
}
const logsFile = path.join(DATA_DIR, 'logs.json');
// 确保日志文件存在
if (!fs.existsSync(logsFile)) {
fs.writeJsonSync(logsFile, { logs: [] }, { spaces: 2 });
}
// 读取现有日志
let logsData;
try {
logsData = fs.readJsonSync(logsFile);
if (!logsData.logs) {
logsData = { logs: [] };
}
} catch (err) {
console.error('读取日志文件错误:', err);
logsData = { logs: [] };
}
// 标准化日志数据,确保同时包含type和eventType字段
const standardizedLogData = {
...logData,
// 确保type和eventType都存在,前端可能使用eventType进行过滤
eventType: logData.eventType || 'unknown',
type: logData.eventType || 'unknown', // 复制eventType到type字段
// 如果缺少必要字段,提供默认值,防止前端过滤出错
url: logData.url || '',
method: logData.method || '',
message: logData.message || '',
status: logData.status || '',
pattern: logData.pattern || ''
};
// 添加时间戳和ID
const newLog = {
...standardizedLogData,
id: Date.now().toString(),
timestamp: new Date().toISOString()
};
// 添加新日志
logsData.logs.unshift(newLog);
// 限制日志数量,最多保留5000条 (减少内存使用)
if (logsData.logs.length > 5000) {
logsData.logs = logsData.logs.slice(0, 5000);
}
fs.writeJsonSync(logsFile, logsData, { spaces: 2 });
} catch (err) {
console.error('记录日志到JSON文件失败:', err);
}
};
// 确保数据目录存在
try {
fs.ensureDirSync(DATA_DIR);
logMessage('数据目录已确认: ' + DATA_DIR);
} catch (err) {
console.error('创建数据目录失败:', err.message);
}
// 确保配置文件存在
if (!fs.existsSync(FEATURES_FILE)) {
try {
fs.writeJsonSync(FEATURES_FILE, { features: [] }, { spaces: 2 });
logMessage('创建了功能配置文件: ' + FEATURES_FILE);
} catch (err) {
console.error('创建功能配置文件失败:', err.message);
}
} else {
// 验证文件结构
try {
const fileData = fs.readJsonSync(FEATURES_FILE);
if (!fileData || !fileData.features) {
// 如果文件存在但结构无效,重新初始化
fs.writeJsonSync(FEATURES_FILE, { features: [] }, { spaces: 2 });
logMessage('修复了损坏的features文件结构');
}
} catch (e) {
// JSON解析错误时重新初始化文件
try {
fs.writeJsonSync(FEATURES_FILE, { features: [] }, { spaces: 2 });
logMessage('修复了无法解析的features文件');
} catch (err) {
console.error('重置功能配置文件失败:', err.message);
}
}
}
if (!fs.existsSync(INTERFACES_FILE)) {
try {
fs.writeJsonSync(INTERFACES_FILE, { interfaces: [] }, { spaces: 2 });
logMessage('创建了接口配置文件: ' + INTERFACES_FILE);
} catch (err) {
console.error('创建接口配置文件失败:', err.message);
}
} else {
// 验证文件结构
try {
const fileData = fs.readJsonSync(INTERFACES_FILE);
if (!fileData || !fileData.interfaces) {
// 如果文件存在但结构无效,重新初始化
fs.writeJsonSync(INTERFACES_FILE, { interfaces: [] }, { spaces: 2 });
logMessage('修复了损坏的interfaces文件结构');
}
} catch (e) {
// JSON解析错误时重新初始化文件
try {
fs.writeJsonSync(INTERFACES_FILE, { interfaces: [] }, { spaces: 2 });
logMessage('修复了无法解析的interfaces文件');
} catch (err) {
console.error('重置接口配置文件失败:', err.message);
}
}
}
// API前缀
const RULE_VALUE_HEADER = 'x-whistle-rule-value';
const MOCK_PREFIX = 'mock://';
// 获取嵌套对象属性值的工具函数
const getNestedValue = (obj, path) => {
return path.split('.').reduce((current, key) => {
return current && current[key] !== undefined ? current[key] : undefined;
}, obj);
};
// 参数匹配功能
const isParamMatch = (req, paramMatchers) => {
if (!paramMatchers || !Array.isArray(paramMatchers) || paramMatchers.length === 0) {
return true; // 如果没有参数匹配规则,视为匹配成功
}
try {
// 合并所有可能的参数来源
let requestParams = {};
// URL查询参数
if (req.query) {
requestParams = { ...requestParams, ...req.query };
}
// POST请求体参数
if (req.body && typeof req.body === 'object') {
requestParams = { ...requestParams, ...req.body };
}
// URL路径参数(如果有的话)
if (req.params) {
requestParams = { ...requestParams, ...req.params };
}
logMessage(`参数匹配检查 - 请求参数: ${JSON.stringify(requestParams)}`);
logMessage(`参数匹配检查 - 匹配规则: ${JSON.stringify(paramMatchers)}`);
// 检查每个匹配规则
for (const matcher of paramMatchers) {
const { paramPath, paramValue, matchType = 'exact' } = matcher;
if (!paramPath) {
continue; // 跳过无效的匹配规则
}
// 获取实际参数值
const actualValue = getNestedValue(requestParams, paramPath);
logMessage(`参数匹配检查 - 路径: ${paramPath}, 期望值: ${paramValue}, 实际值: ${actualValue}, 匹配类型: ${matchType}`);
// 如果参数不存在,视为不匹配
if (actualValue === undefined || actualValue === null) {
logMessage(`参数匹配失败 - 参数 ${paramPath} 不存在`);
return false;
}
// 根据匹配类型进行比较
let isMatch = false;
const actualStr = String(actualValue);
const expectedStr = String(paramValue);
switch (matchType) {
case 'exact':
isMatch = actualStr === expectedStr;
break;
case 'contains':
isMatch = actualStr.includes(expectedStr);
break;
case 'regex':
try {
const regex = new RegExp(expectedStr);
isMatch = regex.test(actualStr);
} catch (e) {
logMessage(`参数匹配失败 - 正则表达式无效: ${expectedStr}`);
return false;
}
break;
default:
isMatch = actualStr === expectedStr;
}
if (!isMatch) {
logMessage(`参数匹配失败 - ${paramPath}: ${actualStr} 不匹配 ${expectedStr} (${matchType})`);
return false;
}
}
logMessage('参数匹配成功 - 所有规则都匹配');
return true;
} catch (error) {
logMessage(`参数匹配检查出错: ${error.message}`);
return false;
}
};
// 创建 Express 应用
const app = express();
// 解析请求体
app.use(express.json({limit: '100mb'}));
app.use(express.urlencoded({ extended: true, limit: '100mb'}));
// 注册设置路由
const settingsRouter = require('./routes/settings');
app.use('/api', settingsRouter.initRouter());
// 加载功能和接口配置
const loadConfigurations = () => {
try {
let featuresData = { features: [] };
let interfacesData = { interfaces: [] };
try {
if (fs.existsSync(FEATURES_FILE)) {
featuresData = fs.readJsonSync(FEATURES_FILE);
// 确保featuresData和features字段都存在,并且features是数组
if (!featuresData || !featuresData.features || !Array.isArray(featuresData.features)) {
logMessage('功能配置数据结构不正确,重新初始化');
featuresData = { features: [] };
}
} else {
logMessage('功能配置文件不存在,使用空数组');
}
} catch (err) {
logMessage('加载功能配置失败: ' + err.message);
featuresData = { features: [] };
}
try {
if (fs.existsSync(INTERFACES_FILE)) {
interfacesData = fs.readJsonSync(INTERFACES_FILE);
// 确保interfacesData和interfaces字段都存在,并且interfaces是数组
if (!interfacesData || !interfacesData.interfaces || !Array.isArray(interfacesData.interfaces)) {
logMessage('接口配置数据结构不正确,重新初始化');
interfacesData = { interfaces: [] };
}
} else {
logMessage('接口配置文件不存在,使用空数组');
}
} catch (err) {
logMessage('加载接口配置失败: ' + err.message);
interfacesData = { interfaces: [] };
}
return {
features: featuresData.features || [],
interfaces: interfacesData.interfaces || []
};
} catch (err) {
logMessage('加载配置失败: ' + err.message);
return { features: [], interfaces: [] };
}
};
// 匹配URL是否符合模式
const isUrlMatch = (url, pattern) => {
if (!pattern) return false;
try {
// 精确匹配
if (pattern === url) {
logMessage(`URL精确匹配成功: ${url} === ${pattern}`);
return true;
}
// 通配符匹配
if (pattern.includes('*')) {
// 需要转换为正则表达式,处理特殊字符
const regexPattern = pattern
.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&') // 转义特殊字符
.replace(/\*/g, '.*'); // 将 * 替换为 .*
const regex = new RegExp('^' + regexPattern + '$');
const result = regex.test(url);
logMessage(`URL通配符匹配${result ? '成功' : '失败'}: ${url} ${result ? '匹配' : '不匹配'} 模式 ${pattern}`);
return result;
}
// 正则表达式匹配
if (pattern.startsWith('/') && pattern.endsWith('/')) {
const regexStr = pattern.slice(1, -1);
const regex = new RegExp(regexStr);
const result = regex.test(url);
logMessage(`URL正则匹配${result ? '成功' : '失败'}: ${url} ${result ? '匹配' : '不匹配'} 模式 ${pattern}`);
return result;
}
} catch (e) {
logMessage('正则表达式匹配错误: ' + e.message);
}
logMessage(`URL不匹配任何模式: ${url} != ${pattern}`);
return false;
};
// 解析原始URL
const parseOriginalUrl = (req) => {
try {
logMessage(`开始解析原始URL,请求URL: ${req.url}`);
logMessage(`请求方法: ${req.method}`);
// 首先尝试从 originalReq 获取
if (req.originalReq) {
// 优先使用 realUrl,因为它是真实的 URL
if (req.originalReq.realUrl) {
logMessage(`从req.originalReq.realUrl获取原始URL: ${req.originalReq.realUrl}`);
return req.originalReq.realUrl;
}
// 如果 realUrl 不存在,则使用 url
if (req.originalReq.url) {
logMessage(`从req.originalReq.url获取原始URL: ${req.originalReq.url}`);
return req.originalReq.url;
}
}
// 作为备选,继续尝试从请求头获取
// 尝试从 x-whistle-real-url 头获取
const whistleRealUrl = req.headers['x-whistle-real-url'];
if (whistleRealUrl) {
logMessage(`从x-whistle-real-url头获取原始URL: ${whistleRealUrl}`);
return whistleRealUrl;
}
// 尝试从 x-forwarded-url 头获取
const forwardedUrl = req.headers['x-forwarded-url'];
if (forwardedUrl) {
logMessage(`从x-forwarded-url头获取原始URL: ${forwardedUrl}`);
return forwardedUrl;
}
// 从规则值中获取信息
const ruleValue = req.headers['x-whistle-rule-value'];
if (ruleValue) {
logMessage(`从规则值获取信息: ${ruleValue}`);
// 尝试从规则值中提取完整URL
try {
const match = /^(?:whistle\.mock-plugin:\/\/)?(.*)$/.exec(ruleValue);
if (match && match[1]) {
// 如果规则值包含URL,则直接使用
if (match[1].startsWith('http')) {
logMessage(`从规则值提取到完整URL: ${match[1]}`);
return match[1];
}
}
} catch (e) {
logMessage(`解析规则值出错: ${e.message}`);
}
} else {
logMessage(`请求头中没有x-whistle-rule-value`);
}
// 如果没有找到原始URL,使用请求URL
logMessage(`无法获取原始URL,使用请求URL: ${req.url}`);
return req.url;
} catch (err) {
logMessage(`解析原始URL失败: ${err.message}`);
return req.url;
}
};
// 获取所有请求头信息并记录
const logAllHeaders = (req) => {
try {
logMessage('===== 请求头信息 =====');
Object.keys(req.headers).forEach(headerName => {
logMessage(` ${headerName}: ${req.headers[headerName]}`);
});
logMessage('=====================');
} catch (err) {
logMessage(`记录请求头失败: ${err.message}`);
}
};
// 添加中间件记录所有请求
app.use((req, res, next) => {
try {
logMessage(`----- 新请求开始 -----`);
logMessage(`收到请求: ${req.method} ${req.url}`);
logAllHeaders(req);
next();
} catch (err) {
logMessage(`请求日志中间件错误: ${err.message}`);
next();
}
});
// 处理请求
app.use(async (req, res, next) => {
try {
logMessage(`----- 新请求开始 -----`);
logMessage(`收到请求: ${req.method} ${req.url}`);
// 记录请求头
logAllHeaders(req);
// 获取规则值
const ruleValue = req.headers['x-whistle-rule-value'];
logMessage(`规则值: ${ruleValue || '无'}`);
// 使用规则管理器处理请求
let result;
try {
// 使用当前 require 的 ruleManager
const ruleManagerModule = require('./ruleManager');
// 传递next参数,确保未匹配的请求能够继续
result = await ruleManagerModule.handleRequest(req, res, ruleValue, next);
} catch (err) {
logMessage(`规则管理器处理请求失败: ${err.message}`);
result = { handled: false };
}
if (result && result.handled) {
logMessage(`规则管理器成功处理了请求`);
// 记录匹配到的接口信息用于调试
if (result.matchedInterface) {
logMessage(`匹配的接口: ${result.matchedInterface.name}, 代理类型: ${result.matchedInterface.proxyType}`);
}
return; // 请求已处理,无需继续
}
// 如果规则管理器未处理,使用旧的处理方式
logMessage(`规则管理器未处理请求,使用旧的处理方式`);
handleLegacyRequest(req, res, next);
} catch (err) {
logMessage(`请求处理过程中发生错误: ${err.message}`);
console.error('请求处理错误:', err);
// 返回500错误
res.status(500).json({
code: 500,
message: '内部服务器错误: ' + err.message,
data: null
});
}
});
// 旧的请求处理方式,保持向后兼容
const handleLegacyRequest = (req, res, next) => {
const http = require('http');
const https = require('https');
const { Readable } = require('stream');
// 获取原始完整URL的所有可能来源
let originalUrl = null;
// 首先尝试从 originalReq 获取 (Whistle规范推荐方式)
if (req.originalReq) {
if (req.originalReq.realUrl) {
originalUrl = req.originalReq.realUrl;
logMessage(`从req.originalReq.realUrl获取原始URL: ${originalUrl}`);
} else if (req.originalReq.url) {
originalUrl = req.originalReq.url;
logMessage(`从req.originalReq.url获取原始URL: ${originalUrl}`);
}
}
// 如果 originalReq 没有提供有效URL,尝试从请求头获取
if (!originalUrl || !originalUrl.startsWith('http')) {
const whistleRealUrl = req.headers['x-whistle-real-url'];
if (whistleRealUrl && whistleRealUrl.startsWith('http')) {
originalUrl = whistleRealUrl;
logMessage(`从x-whistle-real-url头获取原始URL: ${originalUrl}`);
} else {
const forwardedUrl = req.headers['x-forwarded-url'];
if (forwardedUrl && forwardedUrl.startsWith('http')) {
originalUrl = forwardedUrl;
logMessage(`从x-forwarded-url头获取原始URL: ${originalUrl}`);
}
}
}
// 如果仍然没有找到原始URL,尝试从规则值获取
if (!originalUrl || !originalUrl.startsWith('http')) {
const ruleValue = req.headers['x-whistle-rule-value'];
if (ruleValue) {
try {
const match = /^(?:whistle\.mock-plugin:\/\/)?(.*)$/.exec(ruleValue);
if (match && match[1] && match[1].startsWith('http')) {
originalUrl = match[1];
logMessage(`从规则值提取到完整URL: ${originalUrl}`);
}
} catch (e) {
logMessage(`解析规则值出错: ${e.message}`);
}
}
}
// 如果没有找到有效的原始URL,则无法继续
if (!originalUrl || !originalUrl.startsWith('http')) {
logMessage(`没有找到有效的原始URL,无法转发请求`);
res.statusCode = 400;
res.end(JSON.stringify({
code: 400,
message: '无法确定原始请求URL',
data: null
}));
return;
}
try {
// 解析URL
const urlObj = new URL(originalUrl);
const isHttps = urlObj.protocol === 'https:';
// 准备请求选项
const options = {
protocol: urlObj.protocol,
hostname: urlObj.hostname,
port: urlObj.port || (isHttps ? 443 : 80),
path: urlObj.pathname + urlObj.search,
method: req.method,
headers: {...req.headers}
};
// 删除可能导致问题的请求头
delete options.headers.host;
delete options.headers['x-whistle-real-url'];
delete options.headers['x-whistle-rule-value'];
delete options.headers['x-forwarded-url'];
// 设置正确的Host头
options.headers.host = urlObj.host;
logMessage(`准备转发请求到: ${originalUrl}, 方法: ${req.method}`);
// 选择http或https模块
const httpModule = isHttps ? https : http;
// 创建请求并处理响应
const proxyReq = httpModule.request(options, (proxyRes) => {
logMessage(`收到来自 ${originalUrl} 的响应,状态码: ${proxyRes.statusCode}`);
// 复制响应头
Object.keys(proxyRes.headers).forEach(key => {
res.setHeader(key, proxyRes.headers[key]);
});
// 设置状态码
res.statusCode = proxyRes.statusCode;
// 传输响应体
proxyRes.pipe(res);
});
// 错误处理
proxyReq.on('error', (error) => {
logMessage(`转发请求到 ${originalUrl} 时出错: ${error.message}`);
// 如果响应尚未发送,返回错误信息
if (!res.headersSent) {
res.statusCode = 502;
res.end(JSON.stringify({
code: 502,
message: `转发请求到原始地址失败: ${error.message}`,
data: null
}));
} else {
// 如果已经发送了一部分响应,尝试结束响应
try {
res.end();
} catch (e) {
logMessage(`尝试结束已经部分发送的响应时出错: ${e.message}`);
}
}
});
// 处理超时
proxyReq.setTimeout(30000, () => {
logMessage(`转发请求到 ${originalUrl} 超时`);
proxyReq.destroy(new Error('请求超时'));
});
// 转发请求体
if (req.method !== 'GET' && req.method !== 'HEAD') {
// 如果请求有体,则转发
if (req.readable) {
req.pipe(proxyReq);
} else {
// 如果请求体已被读取,尝试重建并发送
if (req.body) {
const bodyStr = typeof req.body === 'string' ? req.body : JSON.stringify(req.body);
proxyReq.write(bodyStr);
proxyReq.end();
} else {
proxyReq.end();
}
}
} else {
// 对于GET和HEAD请求,直接结束请求
proxyReq.end();
}
} catch (error) {
logMessage(`准备转发请求时发生错误: ${error.message}`);
res.statusCode = 500;
res.end(JSON.stringify({
code: 500,
message: `内部服务器错误: ${error.message}`,
data: null
}));
}
};
// 启动服务器
const server = app.listen(0);
// 导出启动服务器的函数
module.exports = function startServer(server, options) {
// 确保之前的日志存在
try {
logMessage('------------------------------');
logMessage('启动 whistle.mock-plugin 服务器...');
logMessage('数据目录: ' + DATA_DIR);
logMessage('日志文件: ' + LOG_FILE);
// 加载并初始化规则管理器
try {
const ruleManager = require('./ruleManager');
// 初始化数据管理器
const dataManager = require('./dataManager');
dataManager.init({
baseDir: DATA_DIR,
log: logMessage
});
ruleManager.init({
server: server,
config: {
baseDir: DATA_DIR
},
dataManager: dataManager,
log: logMessage // 传递日志函数
});
logMessage('规则管理器初始化完成');
} catch (err) {
logMessage('规则管理器初始化失败: ' + err.message);
console.error('规则管理器初始化失败:', err);
}
// 输出插件版本
try {
const packagePath = path.resolve(__dirname, '../package.json');
if (fs.existsSync(packagePath)) {
const packageJson = require(packagePath);
logMessage(`插件版本: ${packageJson.name}@${packageJson.version}`);
}
} catch (e) {
logMessage('获取插件版本失败: ' + e.message);
}
// 输出Whistle信息
if (options && options.storage) {
logMessage(`Whistle存储目录: ${JSON.stringify(options.storage)}`);
}
if (options && options.type) {
logMessage(`Whistle插件类型: ${options.type}`);
}
} catch (e) {
console.error('记录启动日志失败:', e);
}
// 检查和初始化配置文件
if (!fs.existsSync(FEATURES_FILE) || !fs.existsSync(INTERFACES_FILE)) {
logMessage('初始化配置文件...');
if (!fs.existsSync(FEATURES_FILE)) {
fs.writeJsonSync(FEATURES_FILE, { features: [] }, { spaces: 2 });
logMessage('创建了features.json文件');
}
if (!fs.existsSync(INTERFACES_FILE)) {
fs.writeJsonSync(INTERFACES_FILE, { interfaces: [] }, { spaces: 2 });
logMessage('创建了interfaces.json文件');
}
}
// 记录whistle传入的options
logMessage('Whistle插件选项: ' + JSON.stringify(options));
server.on('request', app);
logMessage('whistle.mock-plugin 服务器已启动');
};