UNPKG

whistle.mock-plugins

Version:

Whistle 插件,用于快速创建 API 模拟数据

440 lines (364 loc) 14.4 kB
const fs = require('fs-extra'); const path = require('path'); const url = require('url'); const storage = require('./storage'); // 数据存储目录 const DATA_DIR = path.join(process.env.WHISTLE_PLUGIN_DATA_DIR || storage.DATA_DIR); // 接口配置文件 const INTERFACES_FILE = path.join(DATA_DIR, 'interfaces.json'); // 添加功能模块配置文件 const FEATURES_FILE = path.join(DATA_DIR, 'features.json'); // 记录日志 const log = (message) => { console.log(`[mock-plugin:rulesServer] ${message}`); }; // 加载功能模块配置 const loadFeatures = () => { try { if (!fs.existsSync(FEATURES_FILE)) { log('功能模块配置文件不存在'); return []; } const data = fs.readJsonSync(FEATURES_FILE); // 处理不同的数据结构 let features = []; if (data && Array.isArray(data)) { // 如果直接是数组 features = data; } else if (data && data.features && Array.isArray(data.features)) { // 如果是 {features: [...]} 格式 features = data.features; } else { log('未能识别功能模块数据结构'); return []; } log(`总共发现 ${features.length} 个功能模块配置`); // 过滤出启用状态的功能模块 const enabledFeatures = features.filter(feature => feature.active === true); log(`找到 ${enabledFeatures.length} 个启用的功能模块`); return enabledFeatures; } catch (err) { log(`加载功能模块配置失败: ${err.message}`); return []; } }; // 加载接口配置 const loadInterfaces = () => { try { if (!fs.existsSync(INTERFACES_FILE)) { log('接口配置文件不存在'); return []; } const data = fs.readJsonSync(INTERFACES_FILE); // 为了避免日志过长,只打印前100个字符 const dataPreview = JSON.stringify(data).substring(0, 100) + (JSON.stringify(data).length > 100 ? '...' : ''); log(`读取到接口配置数据: ${dataPreview}`); // 处理不同的数据结构 let interfaces = []; if (data && Array.isArray(data)) { // 如果直接是数组 interfaces = data; } else if (data && data.interfaces && Array.isArray(data.interfaces)) { // 如果是 {interfaces: [...]} 格式 interfaces = data.interfaces; } else { log('未能识别接口数据结构'); return []; } log(`总共发现 ${interfaces.length} 个接口配置`); // 加载功能模块配置 const enabledFeatures = loadFeatures(); const enabledFeatureIds = enabledFeatures.map(feature => feature.id); // 过滤出启用状态的接口 // 兼容不同字段: active 或 enabled const enabledInterfaces = interfaces.filter(intf => { // 首先检查接口本身是否启用 const isInterfaceEnabled = intf.enabled === true || intf.active === true; // 如果接口未启用,直接排除 if (!isInterfaceEnabled) { return false; } // 如果接口有关联功能,检查功能是否启用 const featureId = intf.featureId; if (featureId) { return enabledFeatureIds.includes(featureId); } // 如果接口没有关联功能,只要接口本身启用就可以 return true; }); log(`找到 ${enabledInterfaces.length} 个启用的接口配置`); return enabledInterfaces; } catch (err) { log(`加载接口配置失败: ${err.message}`); return []; } }; // 匹配URL是否符合模式 const isUrlMatch = (url, pattern, proxyType, fullUrl) => { if (!pattern) return false; try { // 针对不同的代理类型采用不同的匹配策略 // 对于url_redirect类型,需要完全匹配fullPath才命中 if (proxyType === 'url_redirect') { // 精确匹配完整路径 return pattern === fullUrl; } // 对于redirect类型,只要url以pattern开头即可命中(前缀匹配) if (proxyType === 'redirect') { return fullUrl.indexOf(pattern) === 0; } // 以下是默认的匹配逻辑(用于response类型等) // 精确匹配 if (pattern === url) { return true; } // 通配符匹配 if (pattern.includes('*')) { // 需要转换为正则表达式,处理特殊字符 const regexPattern = pattern .replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&') // 转义特殊字符 .replace(/\*/g, '.*'); // 将 * 替换为 .* const regex = new RegExp('^' + regexPattern + '$'); return regex.test(url); } // 正则表达式匹配 if (pattern.startsWith('/')) { const regexStr = pattern.slice(1, -1); const regex = new RegExp(regexStr); return regex.test(url); } } catch (e) { log(`正则表达式匹配错误: ${e.message}`); } return false; }; // 验证目标URL是否有效 const isValidTargetUrl = (url) => { if (!url) return false; try { // 检查是否是合法的URL格式 new URL(url); return true; } catch (e) { log(`URL验证错误: ${e.message}`); return false; } }; // 验证重定向规则的有效性 const validateRedirectRule = (interface) => { if (!interface) return false; const { proxyType, targetUrl } = interface; // 对于redirect和url_redirect类型,必须有有效的目标URL if ((proxyType === 'redirect' || proxyType === 'url_redirect') && !isValidTargetUrl(targetUrl)) { log(`接口 ${interface.id || interface.name || '未知'} 的目标URL无效: ${targetUrl}`); return false; } return true; }; // 生成随机数工具函数 const generateRandomValue = (pattern) => { // 如果不是以@开头的模式,直接返回原值 if (!pattern || !pattern.startsWith('@')) { return pattern; } const formatPattern = pattern.substring(1); // 去掉@前缀 // 为每个x生成一个随机字符(字母或数字) return formatPattern.replace(/x/g, () => { const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; return chars.charAt(Math.floor(Math.random() * chars.length)); }); }; // 构建重定向规则 const buildRedirectRule = (fullUrl, targetUrl, customHeaders, pattern, isUrlRedirect = 0) => { if(!isUrlRedirect){ targetUrl = fullUrl.replace(pattern, targetUrl); }else{ targetUrl = `redirect://${targetUrl}`; } let rule = `${fullUrl} ${targetUrl}`; // 如果有自定义请求头,添加到规则中 if (customHeaders && Object.keys(customHeaders).length > 0) { // 构建headerReplace规则,格式: headerReplace://req.header-name:pattern1=replacement1&pattern2=replacement2 const headerRuleLines = Object.entries(customHeaders).map(([key, value]) => { // 处理随机值 const processedValue = generateRandomValue(value); return `${fullUrl} headerReplace://req.${key}:/(.*)/=${processedValue}`; }); if (headerRuleLines.length > 0) { // 将所有规则合并为一个多行规则字符串 rule = `${rule}\n${headerRuleLines.join('\n')}`; } } return rule; }; // Whistle 规则服务器实现 module.exports = (server, options) => { server.on('request', (req, res) => { const oReq = req.originalReq; // 提取请求相关信息 const fullUrl = oReq.url; log(`原始URL: ${fullUrl}`); // 特殊调试路径,用于查看配置文件内容 if (fullUrl.endsWith('/_debug/config')) { try { const configs = { interfaces: fs.existsSync(INTERFACES_FILE) ? fs.readJsonSync(INTERFACES_FILE) : null, features: fs.existsSync(FEATURES_FILE) ? fs.readJsonSync(FEATURES_FILE) : null }; res.end(JSON.stringify(configs, null, 2)); return; } catch (err) { log(`读取配置文件失败: ${err.message}`); res.end(`读取配置文件失败: ${err.message}`); return; } } // 调试路径,查看启用的接口 if (fullUrl.endsWith('/_debug/enabled-interfaces')) { try { const enabledInterfaces = loadInterfaces(); res.end(JSON.stringify({ count: enabledInterfaces.length, interfaces: enabledInterfaces }, null, 2)); return; } catch (err) { log(`获取启用接口失败: ${err.message}`); res.end(`获取启用接口失败: ${err.message}`); return; } } // 解析URL let parsedUrl; try { parsedUrl = url.parse(fullUrl); } catch (err) { log(`解析URL失败: ${err.message}`); // 解析失败时,不处理请求 res.end(''); return; } // 尝试获取 pathname 或 path,用于匹配 const method = oReq.method; const path = parsedUrl.pathname || ''; log(`请求方法: ${method}, 请求路径: ${path}`); // 加载启用的接口配置 const enabledInterfaces = loadInterfaces(); log(`已加载 ${enabledInterfaces.length} 个接口配置`); if (enabledInterfaces.length === 0) { log('没有找到启用的接口配置,跳过处理'); res.end(''); return; } // 检查是否有匹配的接口 const matchedInterface = enabledInterfaces.find(intf => { // 获取URL模式 const pattern = intf.urlPattern || ''; const proxyType = intf.proxyType || 'response'; if (!pattern) { log(`接口 ${intf.id || intf.name || '未知'} 缺少URL模式`); return false; } // 检查URL模式匹配,根据proxyType选择适当的匹配策略 const urlMatches = isUrlMatch(path, pattern, proxyType, fullUrl); if (!urlMatches) { // 不记录每个失败的匹配,以减少日志量 return false; } log(`URL模式匹配成功: ${fullUrl} 匹配 ${pattern} (${proxyType}模式)`); // 检查HTTP方法匹配 const methodField = intf.httpMethod || intf.method || ''; const methodValue = methodField.toUpperCase(); const methodMatches = !methodValue || methodValue === 'ALL' || methodValue === 'ANY' || methodValue === method; if (!methodMatches) { log(`方法不匹配: 请求方法 ${method}, 接口方法 ${methodValue}`); return false; } log(`方法匹配成功: ${method} 匹配 ${methodValue || 'ALL'}`); return true; }); // 如果找到匹配的接口,处理对应的规则 if (matchedInterface) { log(`找到匹配接口 "${matchedInterface.name || matchedInterface.id || '未知'}" 用于 ${method} ${path}`); // 处理不同的代理类型 const proxyType = matchedInterface.proxyType || 'response'; // 优先级判断:首先处理 response 类型 if (proxyType === 'response') { // 返回指向插件服务的规则 log(`接口 "${matchedInterface.name}" 使用模拟响应模式`); res.end(`${fullUrl} mock-plugins://`); return; } // 对于redirect和url_redirect类型,验证规则有效性 if ((proxyType === 'redirect' || proxyType === 'url_redirect') && !validateRedirectRule(matchedInterface)) { log(`接口 "${matchedInterface.name}" 的重定向规则无效,跳过处理`); res.end(''); return; } // 处理 redirect 类型:将请求的 url 中的源 url 部分替换为目标 url if (proxyType === 'redirect') { const targetUrl = matchedInterface.targetUrl.trim(); const pattern = matchedInterface.urlPattern.trim(); log(`接口 "${matchedInterface.name}" 使用重定向模式,模式: ${pattern}, 目标URL: ${targetUrl}`); // 确保目标URL合法 if (!isValidTargetUrl(targetUrl)) { log(`目标URL不合法: ${targetUrl},跳过处理`); res.end(''); return; } // 执行重定向:将fullUrl中匹配的pattern替换为targetUrl // 对于redirect类型,我们使用indexOf确认前缀匹配 if (fullUrl.indexOf(pattern) === 0) { // 获取自定义请求头 const customHeaders = matchedInterface.customHeaders || {}; // 构建重定向规则,包括自定义请求头 const redirectRule = buildRedirectRule(fullUrl, targetUrl, customHeaders, pattern, 0); // 返回规则 res.end(redirectRule); log(`重定向规则: ${redirectRule}`); return; } else { log(`URL ${fullUrl} 不以 ${pattern} 开头,跳过处理`); res.end(''); return; } } // 处理 url_redirect 类型:完全匹配 fullPath 并直接返回目标URL if (proxyType === 'url_redirect') { const targetUrl = matchedInterface.targetUrl.trim(); log(`接口 "${matchedInterface.name}" 使用URL重定向模式,目标URL: ${targetUrl}`); // 确保目标URL合法 if (!isValidTargetUrl(targetUrl)) { log(`目标URL不合法: ${targetUrl},跳过处理`); res.end(''); return; } // 对于url_redirect,我们需要完全匹配 if (fullUrl === matchedInterface.urlPattern) { // 获取自定义请求头 const customHeaders = matchedInterface.customHeaders || {}; // 构建重定向规则,包括自定义请求头 const redirectRule = buildRedirectRule(fullUrl, targetUrl, customHeaders, pattern, 1); // 返回规则 res.end(redirectRule); log(`URL重定向规则: ${redirectRule}`); return; } else { log(`URL ${fullUrl} 不完全匹配 ${matchedInterface.urlPattern},跳过处理`); res.end(''); return; } } // 默认情况下使用mock-plugins处理 log(`接口 "${matchedInterface.name}" 使用默认处理模式`); res.end(`${fullUrl} mock-plugins://`); } else { // 如果没有匹配的接口,返回空字符串,Whistle 会继续处理下一个规则 log(`未找到匹配接口,跳过处理 ${method} ${path}`); res.end(''); } }); };