whistle.mock-plugins 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/cgi-bin/logs.js +38 -29
- package/app/cgi-bin/test-interface.js +182 -35
- package/app/public/js/index.js +1 -1
- package/lib/dataManager.js +6 -0
- package/lib/ruleManager.js +62 -30
- package/lib/rules-server.js +126 -5
- package/lib/server.js +74 -3
- package/package.json +1 -1
- package/ui/src/pages/InterfaceManagement.js +430 -136
package/lib/dataManager.js
CHANGED
|
@@ -71,6 +71,12 @@ const dataManager = {
|
|
|
71
71
|
*/
|
|
72
72
|
logRequest(logData) {
|
|
73
73
|
try {
|
|
74
|
+
// 首先检查参数是否有效,避免不必要的文件操作
|
|
75
|
+
if (!logData || typeof logData !== 'object') {
|
|
76
|
+
this.log(`记录请求日志失败: 无效的日志数据`);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
74
80
|
// 确保日志文件存在
|
|
75
81
|
if (!fs.existsSync(this.logsFile)) {
|
|
76
82
|
fs.writeJsonSync(this.logsFile, { logs: [] }, { spaces: 2 });
|
package/lib/ruleManager.js
CHANGED
|
@@ -105,8 +105,19 @@ const ruleManager = {
|
|
|
105
105
|
return null;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
//
|
|
108
|
+
// 防止空路径
|
|
109
|
+
if (!requestPath) {
|
|
110
|
+
this.log(`[规则处理器] 错误: 请求路径为空`);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 先尝试完全匹配,效率更高
|
|
109
115
|
let matchedInterface = interfaces.find(intf => {
|
|
116
|
+
// 检查接口对象是否有效
|
|
117
|
+
if (!intf || !intf.urlPattern) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
110
121
|
// 检查URL和方法是否匹配
|
|
111
122
|
const urlMatches = intf.urlPattern === requestPath;
|
|
112
123
|
|
|
@@ -130,38 +141,59 @@ const ruleManager = {
|
|
|
130
141
|
// 如果没有完全匹配,尝试正则表达式匹配
|
|
131
142
|
this.log(`[规则处理器] 未找到完全匹配,尝试正则表达式匹配...`);
|
|
132
143
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const regex = new RegExp(pattern);
|
|
138
|
-
|
|
139
|
-
// 获取方法值,兼容不同的字段名
|
|
140
|
-
const methodField = intf.httpMethod || intf.method;
|
|
141
|
-
const methodValue = methodField && methodField.toUpperCase();
|
|
142
|
-
|
|
143
|
-
// 只记录尝试匹配的模式,不记录详细过程
|
|
144
|
-
this.log(`[规则处理器] 尝试正则匹配,测试 ${interfaces.length} 个模式`);
|
|
145
|
-
|
|
146
|
-
// 检查URL和方法是否匹配
|
|
147
|
-
const urlMatches = regex.test(requestPath);
|
|
148
|
-
const methodMatches = !methodValue ||
|
|
149
|
-
methodValue === 'ALL' ||
|
|
150
|
-
methodValue === 'ANY' ||
|
|
151
|
-
methodValue === method;
|
|
152
|
-
|
|
153
|
-
// 移除中间匹配过程的日志
|
|
154
|
-
return urlMatches && methodMatches;
|
|
155
|
-
} catch (err) {
|
|
156
|
-
this.log(`[规则处理器] 无效的URL模式: ${intf.urlPattern}, 错误: ${err.message}`);
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
159
|
-
});
|
|
144
|
+
// 使用正则表达式缓存优化性能
|
|
145
|
+
if (!this.regexCache) {
|
|
146
|
+
this.regexCache = new Map();
|
|
147
|
+
}
|
|
160
148
|
|
|
149
|
+
try {
|
|
150
|
+
matchedInterface = interfaces.find(intf => {
|
|
151
|
+
try {
|
|
152
|
+
// 验证接口对象
|
|
153
|
+
if (!intf || !intf.urlPattern) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 从缓存获取或创建正则表达式
|
|
158
|
+
let regex;
|
|
159
|
+
if (this.regexCache.has(intf.urlPattern)) {
|
|
160
|
+
regex = this.regexCache.get(intf.urlPattern);
|
|
161
|
+
} else {
|
|
162
|
+
// 将通配符转换为正则表达式
|
|
163
|
+
const pattern = this.convertPatternToRegex(intf.urlPattern);
|
|
164
|
+
regex = new RegExp(pattern);
|
|
165
|
+
// 缓存正则表达式,避免重复创建
|
|
166
|
+
this.regexCache.set(intf.urlPattern, regex);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 获取方法值,兼容不同的字段名
|
|
170
|
+
const methodField = intf.httpMethod || intf.method;
|
|
171
|
+
const methodValue = methodField && methodField.toUpperCase();
|
|
172
|
+
|
|
173
|
+
// 检查URL和方法是否匹配
|
|
174
|
+
const urlMatches = regex.test(requestPath);
|
|
175
|
+
const methodMatches = !methodValue ||
|
|
176
|
+
methodValue === 'ALL' ||
|
|
177
|
+
methodValue === 'ANY' ||
|
|
178
|
+
methodValue === method;
|
|
179
|
+
|
|
180
|
+
return urlMatches && methodMatches;
|
|
181
|
+
} catch (err) {
|
|
182
|
+
// 记录特定接口的错误,但继续处理其他接口
|
|
183
|
+
this.log(`[规则处理器] 匹配接口 ${intf?.name || 'unknown'} 时出错: ${err.message}`);
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
} catch (err) {
|
|
188
|
+
// 记录整体匹配过程的错误
|
|
189
|
+
this.log(`[规则处理器] 正则匹配过程出错: ${err.message}`);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
161
193
|
if (matchedInterface) {
|
|
162
|
-
this.log(`[规则处理器]
|
|
194
|
+
this.log(`[规则处理器] 找到正则匹配的接口: ${matchedInterface.name}`);
|
|
163
195
|
} else {
|
|
164
|
-
this.log(`[规则处理器]
|
|
196
|
+
this.log(`[规则处理器] 未找到匹配的接口`);
|
|
165
197
|
}
|
|
166
198
|
|
|
167
199
|
return matchedInterface;
|
package/lib/rules-server.js
CHANGED
|
@@ -115,10 +115,25 @@ const loadInterfaces = () => {
|
|
|
115
115
|
};
|
|
116
116
|
|
|
117
117
|
// 匹配URL是否符合模式
|
|
118
|
-
const isUrlMatch = (url, pattern) => {
|
|
118
|
+
const isUrlMatch = (url, pattern, proxyType, fullUrl) => {
|
|
119
119
|
if (!pattern) return false;
|
|
120
120
|
|
|
121
121
|
try {
|
|
122
|
+
// 针对不同的代理类型采用不同的匹配策略
|
|
123
|
+
|
|
124
|
+
// 对于url_redirect类型,需要完全匹配fullPath才命中
|
|
125
|
+
if (proxyType === 'url_redirect') {
|
|
126
|
+
// 精确匹配完整路径
|
|
127
|
+
return pattern === fullUrl;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 对于redirect类型,只要url以pattern开头即可命中(前缀匹配)
|
|
131
|
+
if (proxyType === 'redirect') {
|
|
132
|
+
return fullUrl.indexOf(pattern) === 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 以下是默认的匹配逻辑(用于response类型等)
|
|
136
|
+
|
|
122
137
|
// 精确匹配
|
|
123
138
|
if (pattern === url) {
|
|
124
139
|
return true;
|
|
@@ -148,6 +163,35 @@ const isUrlMatch = (url, pattern) => {
|
|
|
148
163
|
return false;
|
|
149
164
|
};
|
|
150
165
|
|
|
166
|
+
// 验证目标URL是否有效
|
|
167
|
+
const isValidTargetUrl = (url) => {
|
|
168
|
+
if (!url) return false;
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
// 检查是否是合法的URL格式
|
|
172
|
+
new URL(url);
|
|
173
|
+
return true;
|
|
174
|
+
} catch (e) {
|
|
175
|
+
log(`URL验证错误: ${e.message}`);
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// 验证重定向规则的有效性
|
|
181
|
+
const validateRedirectRule = (interface) => {
|
|
182
|
+
if (!interface) return false;
|
|
183
|
+
|
|
184
|
+
const { proxyType, targetUrl } = interface;
|
|
185
|
+
|
|
186
|
+
// 对于redirect和url_redirect类型,必须有有效的目标URL
|
|
187
|
+
if ((proxyType === 'redirect' || proxyType === 'url_redirect') && !isValidTargetUrl(targetUrl)) {
|
|
188
|
+
log(`接口 ${interface.id || interface.name || '未知'} 的目标URL无效: ${targetUrl}`);
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return true;
|
|
193
|
+
};
|
|
194
|
+
|
|
151
195
|
// Whistle 规则服务器实现
|
|
152
196
|
module.exports = (server, options) => {
|
|
153
197
|
server.on('request', (req, res) => {
|
|
@@ -220,21 +264,22 @@ module.exports = (server, options) => {
|
|
|
220
264
|
const matchedInterface = enabledInterfaces.find(intf => {
|
|
221
265
|
// 获取URL模式
|
|
222
266
|
const pattern = intf.urlPattern || '';
|
|
267
|
+
const proxyType = intf.proxyType || 'response';
|
|
223
268
|
|
|
224
269
|
if (!pattern) {
|
|
225
270
|
log(`接口 ${intf.id || intf.name || '未知'} 缺少URL模式`);
|
|
226
271
|
return false;
|
|
227
272
|
}
|
|
228
273
|
|
|
229
|
-
// 检查URL
|
|
230
|
-
const urlMatches = isUrlMatch(path, pattern);
|
|
274
|
+
// 检查URL模式匹配,根据proxyType选择适当的匹配策略
|
|
275
|
+
const urlMatches = isUrlMatch(path, pattern, proxyType, fullUrl);
|
|
231
276
|
|
|
232
277
|
if (!urlMatches) {
|
|
233
278
|
// 不记录每个失败的匹配,以减少日志量
|
|
234
279
|
return false;
|
|
235
280
|
}
|
|
236
281
|
|
|
237
|
-
log(`URL模式匹配成功: ${
|
|
282
|
+
log(`URL模式匹配成功: ${fullUrl} 匹配 ${pattern} (${proxyType}模式)`);
|
|
238
283
|
|
|
239
284
|
// 检查HTTP方法匹配
|
|
240
285
|
const methodField = intf.httpMethod || intf.method || '';
|
|
@@ -254,9 +299,85 @@ module.exports = (server, options) => {
|
|
|
254
299
|
return true;
|
|
255
300
|
});
|
|
256
301
|
|
|
257
|
-
//
|
|
302
|
+
// 如果找到匹配的接口,处理对应的规则
|
|
258
303
|
if (matchedInterface) {
|
|
259
304
|
log(`找到匹配接口 "${matchedInterface.name || matchedInterface.id || '未知'}" 用于 ${method} ${path}`);
|
|
305
|
+
|
|
306
|
+
// 处理不同的代理类型
|
|
307
|
+
const proxyType = matchedInterface.proxyType || 'response';
|
|
308
|
+
|
|
309
|
+
// 优先级判断:首先处理 response 类型
|
|
310
|
+
if (proxyType === 'response') {
|
|
311
|
+
// 返回指向插件服务的规则
|
|
312
|
+
log(`接口 "${matchedInterface.name}" 使用模拟响应模式`);
|
|
313
|
+
res.end(`${fullUrl} mock-plugins://`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 对于redirect和url_redirect类型,验证规则有效性
|
|
318
|
+
if ((proxyType === 'redirect' || proxyType === 'url_redirect') && !validateRedirectRule(matchedInterface)) {
|
|
319
|
+
log(`接口 "${matchedInterface.name}" 的重定向规则无效,跳过处理`);
|
|
320
|
+
res.end('');
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// 处理 redirect 类型:将请求的 url 中的源 url 部分替换为目标 url
|
|
325
|
+
if (proxyType === 'redirect') {
|
|
326
|
+
const targetUrl = matchedInterface.targetUrl.trim();
|
|
327
|
+
const pattern = matchedInterface.urlPattern.trim();
|
|
328
|
+
log(`接口 "${matchedInterface.name}" 使用重定向模式,模式: ${pattern}, 目标URL: ${targetUrl}`);
|
|
329
|
+
|
|
330
|
+
// 确保目标URL合法
|
|
331
|
+
if (!isValidTargetUrl(targetUrl)) {
|
|
332
|
+
log(`目标URL不合法: ${targetUrl},跳过处理`);
|
|
333
|
+
res.end('');
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// 执行重定向:将fullUrl中匹配的pattern替换为targetUrl
|
|
338
|
+
// 对于redirect类型,我们使用indexOf确认前缀匹配
|
|
339
|
+
if (fullUrl.indexOf(pattern) === 0) {
|
|
340
|
+
// 构建新的目标URL
|
|
341
|
+
const redirectUrl = fullUrl.replace(pattern, targetUrl);
|
|
342
|
+
|
|
343
|
+
// 按照 whistle 规则格式返回重定向规则
|
|
344
|
+
res.end(`${fullUrl} ${redirectUrl}`);
|
|
345
|
+
log(`重定向规则: ${fullUrl} ${redirectUrl}`);
|
|
346
|
+
return;
|
|
347
|
+
} else {
|
|
348
|
+
log(`URL ${fullUrl} 不以 ${pattern} 开头,跳过处理`);
|
|
349
|
+
res.end('');
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// 处理 url_redirect 类型:完全匹配 fullPath 并直接返回目标URL
|
|
355
|
+
if (proxyType === 'url_redirect') {
|
|
356
|
+
const targetUrl = matchedInterface.targetUrl.trim();
|
|
357
|
+
log(`接口 "${matchedInterface.name}" 使用URL重定向模式,目标URL: ${targetUrl}`);
|
|
358
|
+
|
|
359
|
+
// 确保目标URL合法
|
|
360
|
+
if (!isValidTargetUrl(targetUrl)) {
|
|
361
|
+
log(`目标URL不合法: ${targetUrl},跳过处理`);
|
|
362
|
+
res.end('');
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 对于url_redirect,我们需要完全匹配
|
|
367
|
+
if (fullUrl === matchedInterface.urlPattern) {
|
|
368
|
+
// 直接返回whistle规则
|
|
369
|
+
res.end(`${fullUrl} redirect://${targetUrl}`);
|
|
370
|
+
log(`URL重定向规则: ${fullUrl} redirect://${targetUrl}`);
|
|
371
|
+
return;
|
|
372
|
+
} else {
|
|
373
|
+
log(`URL ${fullUrl} 不完全匹配 ${matchedInterface.urlPattern},跳过处理`);
|
|
374
|
+
res.end('');
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 默认情况下使用mock-plugins处理
|
|
380
|
+
log(`接口 "${matchedInterface.name}" 使用默认处理模式`);
|
|
260
381
|
res.end(`${fullUrl} mock-plugins://`);
|
|
261
382
|
} else {
|
|
262
383
|
// 如果没有匹配的接口,返回空字符串,Whistle 会继续处理下一个规则
|
package/lib/server.js
CHANGED
|
@@ -43,6 +43,12 @@ const logMessage = (message) => {
|
|
|
43
43
|
// 记录日志到 logs.json 文件
|
|
44
44
|
const addToJsonLog = (logData) => {
|
|
45
45
|
try {
|
|
46
|
+
// 验证参数有效性
|
|
47
|
+
if (!logData || typeof logData !== 'object') {
|
|
48
|
+
console.error('无效的日志数据');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
const logsFile = path.join(DATA_DIR, 'logs.json');
|
|
47
53
|
|
|
48
54
|
// 确保日志文件存在
|
|
@@ -86,9 +92,9 @@ const addToJsonLog = (logData) => {
|
|
|
86
92
|
// 添加新日志
|
|
87
93
|
logsData.logs.unshift(newLog);
|
|
88
94
|
|
|
89
|
-
// 限制日志数量,最多保留
|
|
90
|
-
if (logsData.logs.length >
|
|
91
|
-
logsData.logs = logsData.logs.slice(0,
|
|
95
|
+
// 限制日志数量,最多保留5000条 (减少内存使用)
|
|
96
|
+
if (logsData.logs.length > 5000) {
|
|
97
|
+
logsData.logs = logsData.logs.slice(0, 5000);
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
fs.writeJsonSync(logsFile, logsData, { spaces: 2 });
|
|
@@ -643,6 +649,71 @@ const handleResponse = (interfaceItem, req, res) => {
|
|
|
643
649
|
}
|
|
644
650
|
break;
|
|
645
651
|
|
|
652
|
+
case 'redirect':
|
|
653
|
+
// 处理redirect - 完全重定向
|
|
654
|
+
const redirectUrl = interfaceItem.targetUrl || '';
|
|
655
|
+
logMessage(`重定向处理: ${req.url} -> ${redirectUrl}`);
|
|
656
|
+
// 这里不需要实际执行重定向,因为规则服务器已经返回了whistle规则
|
|
657
|
+
// 仅记录日志,并返回信息性消息
|
|
658
|
+
res.status(200).json({
|
|
659
|
+
code: 302,
|
|
660
|
+
message: '请求已被重定向到: ' + redirectUrl,
|
|
661
|
+
data: {
|
|
662
|
+
originalUrl: req.url,
|
|
663
|
+
targetUrl: redirectUrl,
|
|
664
|
+
mode: 'redirect'
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// 记录重定向日志
|
|
669
|
+
addToJsonLog({
|
|
670
|
+
eventType: 'redirect',
|
|
671
|
+
url: req.url,
|
|
672
|
+
method: req.method,
|
|
673
|
+
targetUrl: redirectUrl,
|
|
674
|
+
message: `请求被重定向: ${req.method} ${req.url} -> ${redirectUrl}`,
|
|
675
|
+
pattern: interfaceItem.urlPattern
|
|
676
|
+
});
|
|
677
|
+
break;
|
|
678
|
+
|
|
679
|
+
case 'url_redirect':
|
|
680
|
+
// 处理url_redirect - URL重定向,保留查询参数
|
|
681
|
+
const baseUrl = interfaceItem.targetUrl || '';
|
|
682
|
+
// 提取原始URL的查询参数,实际重定向在规则服务器中处理
|
|
683
|
+
const reqUrl = req.url || '';
|
|
684
|
+
let finalRedirectUrl = baseUrl;
|
|
685
|
+
|
|
686
|
+
if (req.url.includes('?')) {
|
|
687
|
+
const queryStr = req.url.split('?')[1];
|
|
688
|
+
finalRedirectUrl = baseUrl.includes('?')
|
|
689
|
+
? `${baseUrl}&${queryStr}`
|
|
690
|
+
: `${baseUrl}?${queryStr}`;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
logMessage(`URL重定向处理: ${req.url} -> ${finalRedirectUrl}`);
|
|
694
|
+
|
|
695
|
+
// 返回信息性消息
|
|
696
|
+
res.status(200).json({
|
|
697
|
+
code: 302,
|
|
698
|
+
message: '请求已被URL重定向到: ' + finalRedirectUrl,
|
|
699
|
+
data: {
|
|
700
|
+
originalUrl: req.url,
|
|
701
|
+
targetUrl: finalRedirectUrl,
|
|
702
|
+
mode: 'url_redirect'
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
// 记录URL重定向日志
|
|
707
|
+
addToJsonLog({
|
|
708
|
+
eventType: 'url_redirect',
|
|
709
|
+
url: req.url,
|
|
710
|
+
method: req.method,
|
|
711
|
+
targetUrl: finalRedirectUrl,
|
|
712
|
+
message: `请求被URL重定向: ${req.method} ${req.url} -> ${finalRedirectUrl}`,
|
|
713
|
+
pattern: interfaceItem.urlPattern
|
|
714
|
+
});
|
|
715
|
+
break;
|
|
716
|
+
|
|
646
717
|
case 'url':
|
|
647
718
|
// URL重定向 - 实际场景中这需要代理到目标URL
|
|
648
719
|
logMessage(`URL重定向: ${req.url} -> ${interfaceItem.targetUrl}`);
|