whistle.mock-plugins 1.0.3 → 1.0.5
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/interfaces.js +3 -0
- package/app/cgi-bin/logs.js +38 -29
- package/app/cgi-bin/test-interface.js +228 -35
- package/app/public/js/index.js +1 -1
- package/app/public/js/index.js.LICENSE.txt +2 -0
- package/lib/dataManager.js +6 -0
- package/lib/ruleManager.js +62 -30
- package/lib/rules-server.js +179 -5
- package/lib/server.js +74 -3
- package/package.json +1 -1
- package/ui/src/pages/InterfaceManagement.js +651 -145
|
@@ -176,6 +176,7 @@ module.exports = function(req, res) {
|
|
|
176
176
|
proxyType: interfaceData.proxyType,
|
|
177
177
|
responseContent: interfaceData.responseContent,
|
|
178
178
|
targetUrl: interfaceData.targetUrl,
|
|
179
|
+
customHeaders: interfaceData.customHeaders || {},
|
|
179
180
|
filePath: interfaceData.filePath,
|
|
180
181
|
httpStatus: interfaceData.httpStatus || 200,
|
|
181
182
|
responseDelay: interfaceData.responseDelay || 0,
|
|
@@ -200,6 +201,7 @@ module.exports = function(req, res) {
|
|
|
200
201
|
proxyType: interfaceData.proxyType || 'response',
|
|
201
202
|
responseContent: interfaceData.responseContent || '',
|
|
202
203
|
targetUrl: interfaceData.targetUrl || '',
|
|
204
|
+
customHeaders: interfaceData.customHeaders || {},
|
|
203
205
|
filePath: interfaceData.filePath || '',
|
|
204
206
|
httpStatus: interfaceData.httpStatus || 200,
|
|
205
207
|
responseDelay: interfaceData.responseDelay || 0,
|
|
@@ -316,6 +318,7 @@ module.exports = function(req, res) {
|
|
|
316
318
|
proxyType: interfaceData.proxyType || interfacesData.interfaces[index].proxyType,
|
|
317
319
|
responseContent: interfaceData.responseContent !== undefined ? interfaceData.responseContent : interfacesData.interfaces[index].responseContent,
|
|
318
320
|
targetUrl: interfaceData.targetUrl !== undefined ? interfaceData.targetUrl : interfacesData.interfaces[index].targetUrl,
|
|
321
|
+
customHeaders: interfaceData.customHeaders !== undefined ? interfaceData.customHeaders : interfacesData.interfaces[index].customHeaders || {},
|
|
319
322
|
filePath: interfaceData.filePath !== undefined ? interfaceData.filePath : interfacesData.interfaces[index].filePath,
|
|
320
323
|
httpStatus: interfaceData.httpStatus || interfacesData.interfaces[index].httpStatus,
|
|
321
324
|
responseDelay: interfaceData.responseDelay !== undefined ? interfaceData.responseDelay : interfacesData.interfaces[index].responseDelay,
|
package/app/cgi-bin/logs.js
CHANGED
|
@@ -15,6 +15,15 @@ module.exports = function(req, res) {
|
|
|
15
15
|
const page = parseInt(req.query.page) || 1;
|
|
16
16
|
const pageSize = parseInt(req.query.pageSize) || 20;
|
|
17
17
|
|
|
18
|
+
// 检查参数是否合法
|
|
19
|
+
if (isNaN(page) || page < 1 || isNaN(pageSize) || pageSize < 1) {
|
|
20
|
+
return res.status(400).json({
|
|
21
|
+
code: 400,
|
|
22
|
+
message: '无效的分页参数',
|
|
23
|
+
data: null
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
// 确保日志文件存在
|
|
19
28
|
if (!fs.existsSync(logsFile)) {
|
|
20
29
|
fs.writeJsonSync(logsFile, { logs: [] }, { spaces: 2 });
|
|
@@ -31,39 +40,33 @@ module.exports = function(req, res) {
|
|
|
31
40
|
});
|
|
32
41
|
}
|
|
33
42
|
|
|
34
|
-
// 读取日志
|
|
35
|
-
|
|
43
|
+
// 读取日志 - 使用流式处理大文件
|
|
44
|
+
let logsData;
|
|
45
|
+
try {
|
|
46
|
+
logsData = fs.readJsonSync(logsFile, { throws: false }) || { logs: [] };
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error('读取日志文件失败:', err);
|
|
49
|
+
// 文件损坏或格式错误时,重置日志
|
|
50
|
+
fs.writeJsonSync(logsFile, { logs: [] }, { spaces: 2 });
|
|
51
|
+
logsData = { logs: [] };
|
|
52
|
+
}
|
|
36
53
|
|
|
37
54
|
// 确保日志结构正确
|
|
38
55
|
if (!logsData || !logsData.logs || !Array.isArray(logsData.logs)) {
|
|
39
|
-
logsData
|
|
56
|
+
logsData = { logs: [] };
|
|
40
57
|
}
|
|
41
58
|
|
|
42
|
-
//
|
|
43
|
-
let logs = logsData.logs
|
|
44
|
-
if (!log) return null;
|
|
45
|
-
|
|
46
|
-
// 标准化事件类型字段,确保兼容前端过滤
|
|
47
|
-
// 将type转换为eventType确保前端能够正确过滤
|
|
48
|
-
return {
|
|
49
|
-
id: log.id || Date.now().toString(),
|
|
50
|
-
timestamp: log.timestamp || new Date().toISOString(),
|
|
51
|
-
eventType: log.eventType || log.type || 'unknown',
|
|
52
|
-
type: log.type || log.eventType || 'unknown',
|
|
53
|
-
method: log.method || '',
|
|
54
|
-
url: log.url || '',
|
|
55
|
-
status: log.status || '',
|
|
56
|
-
message: log.message || '',
|
|
57
|
-
pattern: log.pattern || '',
|
|
58
|
-
...log // 保留原有其他字段
|
|
59
|
-
};
|
|
60
|
-
}).filter(Boolean); // 过滤掉null
|
|
59
|
+
// 只对需要处理的日志数据进行处理,减少内存使用
|
|
60
|
+
let logs = logsData.logs;
|
|
61
61
|
|
|
62
62
|
// 根据类型过滤
|
|
63
63
|
if (type && type !== 'all') {
|
|
64
64
|
try {
|
|
65
65
|
logs = logs.filter(log =>
|
|
66
|
-
log && (
|
|
66
|
+
log && (
|
|
67
|
+
(log.eventType && log.eventType === type) ||
|
|
68
|
+
(log.type && log.type === type)
|
|
69
|
+
)
|
|
67
70
|
);
|
|
68
71
|
} catch (err) {
|
|
69
72
|
console.error('按类型过滤日志失败:', err);
|
|
@@ -74,14 +77,14 @@ module.exports = function(req, res) {
|
|
|
74
77
|
// 根据关键词过滤
|
|
75
78
|
if (keyword) {
|
|
76
79
|
try {
|
|
80
|
+
const lowercaseKeyword = keyword.toLowerCase();
|
|
77
81
|
logs = logs.filter(log => {
|
|
78
82
|
if (!log) return false;
|
|
79
83
|
|
|
80
84
|
// 检查关键字段是否包含关键词
|
|
81
|
-
const url =
|
|
82
|
-
const pattern =
|
|
83
|
-
const message =
|
|
84
|
-
const lowercaseKeyword = keyword.toLowerCase();
|
|
85
|
+
const url = typeof log.url === 'string' ? log.url.toLowerCase() : '';
|
|
86
|
+
const pattern = typeof log.pattern === 'string' ? log.pattern.toLowerCase() : '';
|
|
87
|
+
const message = typeof log.message === 'string' ? log.message.toLowerCase() : '';
|
|
85
88
|
|
|
86
89
|
return url.includes(lowercaseKeyword) ||
|
|
87
90
|
pattern.includes(lowercaseKeyword) ||
|
|
@@ -97,9 +100,15 @@ module.exports = function(req, res) {
|
|
|
97
100
|
const total = logs.length;
|
|
98
101
|
const totalPages = Math.ceil(total / pageSize);
|
|
99
102
|
|
|
100
|
-
// 分页
|
|
103
|
+
// 分页 - 只返回当前页的数据
|
|
101
104
|
const startIndex = (page - 1) * pageSize;
|
|
102
|
-
|
|
105
|
+
// 确保不会越界
|
|
106
|
+
const endIndex = Math.min(startIndex + pageSize, total);
|
|
107
|
+
const pagedLogs = logs.slice(startIndex, endIndex);
|
|
108
|
+
|
|
109
|
+
// 清除大型对象引用,帮助垃圾回收
|
|
110
|
+
logs = null;
|
|
111
|
+
logsData = null;
|
|
103
112
|
|
|
104
113
|
return res.json({
|
|
105
114
|
code: 0,
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
const fs = require('fs-extra');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const Mock = require('mockjs');
|
|
4
|
+
const url = require('url');
|
|
5
|
+
|
|
6
|
+
// 生成随机数工具函数(与rules-server.js中保持一致)
|
|
7
|
+
const generateRandomValue = (pattern) => {
|
|
8
|
+
// 如果不是以@开头的模式,直接返回原值
|
|
9
|
+
if (!pattern || !pattern.startsWith('@')) {
|
|
10
|
+
return pattern;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const formatPattern = pattern.substring(1); // 去掉@前缀
|
|
14
|
+
// 为每个x生成一个随机字符(字母或数字)
|
|
15
|
+
return formatPattern.replace(/x/g, () => {
|
|
16
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
17
|
+
return chars.charAt(Math.floor(Math.random() * chars.length));
|
|
18
|
+
});
|
|
19
|
+
};
|
|
4
20
|
|
|
5
21
|
module.exports = async function(req, res) {
|
|
6
22
|
const dataDir = this.dataDir;
|
|
@@ -89,46 +105,223 @@ module.exports = async function(req, res) {
|
|
|
89
105
|
|
|
90
106
|
const startTime = Date.now();
|
|
91
107
|
|
|
92
|
-
//
|
|
93
|
-
let
|
|
108
|
+
// 根据代理类型处理不同的响应
|
|
109
|
+
let responseData;
|
|
94
110
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
111
|
+
// 检查 URL 是否匹配模式
|
|
112
|
+
const isUrlMatchPattern = (url, pattern, proxyType) => {
|
|
113
|
+
if (!url || !pattern) return false;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
// 对于url_redirect类型,需要完全匹配
|
|
117
|
+
if (proxyType === 'url_redirect') {
|
|
118
|
+
return url === pattern;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 对于redirect类型,只要url以pattern开头即可命中(前缀匹配)
|
|
122
|
+
if (proxyType === 'redirect') {
|
|
123
|
+
return url.indexOf(pattern) === 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 默认的匹配逻辑(用于response类型等)
|
|
127
|
+
// 如果是正则表达式
|
|
128
|
+
if (pattern.startsWith('/') && pattern.endsWith('/')) {
|
|
129
|
+
const regex = new RegExp(pattern.slice(1, -1));
|
|
130
|
+
return regex.test(url);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 如果是通配符模式
|
|
134
|
+
if (pattern.includes('*')) {
|
|
135
|
+
const regexPattern = pattern
|
|
136
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
137
|
+
.replace(/\*/g, '.*');
|
|
138
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
139
|
+
return regex.test(url);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 精确匹配
|
|
143
|
+
return url === pattern;
|
|
144
|
+
} catch (e) {
|
|
145
|
+
console.error('URL匹配检查失败:', e);
|
|
146
|
+
return false;
|
|
104
147
|
}
|
|
105
|
-
}
|
|
106
|
-
// 如果Mock失败,使用原始内容
|
|
107
|
-
}
|
|
148
|
+
};
|
|
108
149
|
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
150
|
+
// 处理不同的代理类型
|
|
151
|
+
switch (targetInterface.proxyType) {
|
|
152
|
+
case 'response':
|
|
153
|
+
// 处理内容,支持 Mock.js 模板语法
|
|
154
|
+
let responseBody = targetInterface.responseContent;
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
// 如果响应内容是JSON,则尝试应用Mock.js模板
|
|
158
|
+
if (targetInterface.contentType && targetInterface.contentType.includes('json')) {
|
|
159
|
+
// 尝试解析JSON
|
|
160
|
+
const jsonTemplate = JSON.parse(responseBody);
|
|
161
|
+
// 使用Mock.js生成随机数据
|
|
162
|
+
const mockData = Mock.mock(jsonTemplate);
|
|
163
|
+
// 将结果转换回字符串
|
|
164
|
+
responseBody = JSON.stringify(mockData);
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
// 如果Mock失败,使用原始内容
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 模拟接口处理延迟
|
|
171
|
+
const delay = targetInterface.responseDelay || 0;
|
|
172
|
+
if (delay > 0) {
|
|
173
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const endTime = Date.now();
|
|
177
|
+
const duration = endTime - startTime;
|
|
178
|
+
|
|
179
|
+
// 模拟接口处理
|
|
180
|
+
responseData = {
|
|
181
|
+
statusCode: targetInterface.httpStatus || 200,
|
|
182
|
+
contentType: targetInterface.contentType || 'application/json; charset=utf-8',
|
|
183
|
+
responseBody: responseBody,
|
|
184
|
+
mockInfo: {
|
|
185
|
+
matchedPattern: targetInterface.urlPattern,
|
|
186
|
+
requestMethod: targetInterface.httpMethod,
|
|
187
|
+
delay: delay,
|
|
188
|
+
duration: duration,
|
|
189
|
+
timestamp: new Date().toISOString()
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
break;
|
|
193
|
+
|
|
194
|
+
case 'redirect':
|
|
195
|
+
// 处理重定向
|
|
196
|
+
const redirectTarget = targetInterface.targetUrl || '';
|
|
197
|
+
|
|
198
|
+
// 验证目标URL是否有效
|
|
199
|
+
if (!redirectTarget) {
|
|
200
|
+
return res.status(400).json({
|
|
201
|
+
code: 400,
|
|
202
|
+
message: '重定向目标URL不能为空',
|
|
203
|
+
data: null
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
// 验证URL格式
|
|
209
|
+
new URL(redirectTarget);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
return res.status(400).json({
|
|
212
|
+
code: 400,
|
|
213
|
+
message: '重定向目标URL格式无效,必须包含http://或https://',
|
|
214
|
+
data: null
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 验证URL匹配(前缀匹配)
|
|
219
|
+
if (url.indexOf(targetInterface.urlPattern) !== 0) {
|
|
220
|
+
return res.status(400).json({
|
|
221
|
+
code: 400,
|
|
222
|
+
message: `测试URL不匹配重定向规则,URL必须以 ${targetInterface.urlPattern} 开头`,
|
|
223
|
+
data: null
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 处理自定义请求头,包括随机值
|
|
228
|
+
const redirectHeaders = targetInterface.customHeaders || {};
|
|
229
|
+
const processedHeaders = {};
|
|
230
|
+
|
|
231
|
+
// 处理随机值,转换为实际值
|
|
232
|
+
Object.entries(redirectHeaders).forEach(([key, value]) => {
|
|
233
|
+
processedHeaders[key] = generateRandomValue(value);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const formattedRedirectHeaders = Object.entries(processedHeaders)
|
|
237
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
238
|
+
.join('\n');
|
|
239
|
+
|
|
240
|
+
responseData = {
|
|
241
|
+
statusCode: 302,
|
|
242
|
+
contentType: 'application/json; charset=utf-8',
|
|
243
|
+
proxyType: 'redirect',
|
|
244
|
+
targetUrl: redirectTarget,
|
|
245
|
+
customHeaders: processedHeaders,
|
|
246
|
+
formattedHeaders: formattedRedirectHeaders,
|
|
247
|
+
mockInfo: {
|
|
248
|
+
matchedPattern: targetInterface.urlPattern,
|
|
249
|
+
requestMethod: targetInterface.httpMethod,
|
|
250
|
+
timestamp: new Date().toISOString()
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
break;
|
|
254
|
+
|
|
255
|
+
case 'url_redirect':
|
|
256
|
+
// 处理URL重定向
|
|
257
|
+
const baseUrl = targetInterface.targetUrl || '';
|
|
258
|
+
|
|
259
|
+
// 验证基础URL是否有效
|
|
260
|
+
if (!baseUrl) {
|
|
261
|
+
return res.status(400).json({
|
|
262
|
+
code: 400,
|
|
263
|
+
message: 'URL重定向目标URL不能为空',
|
|
264
|
+
data: null
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
// 验证URL格式
|
|
270
|
+
new URL(baseUrl);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
return res.status(400).json({
|
|
273
|
+
code: 400,
|
|
274
|
+
message: 'URL重定向目标URL格式无效,必须包含http://或https://',
|
|
275
|
+
data: null
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 验证URL完全匹配
|
|
280
|
+
if (url !== targetInterface.urlPattern) {
|
|
281
|
+
return res.status(400).json({
|
|
282
|
+
code: 400,
|
|
283
|
+
message: `测试URL不匹配URL重定向规则,URL必须完全匹配 ${targetInterface.urlPattern}`,
|
|
284
|
+
data: null
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// 处理自定义请求头
|
|
289
|
+
const urlRedirectHeaders = targetInterface.customHeaders || {};
|
|
290
|
+
const processedUrlRedirectHeaders = {};
|
|
291
|
+
|
|
292
|
+
// 处理随机值,转换为实际值
|
|
293
|
+
Object.entries(urlRedirectHeaders).forEach(([key, value]) => {
|
|
294
|
+
processedUrlRedirectHeaders[key] = generateRandomValue(value);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const formattedUrlRedirectHeaders = Object.entries(processedUrlRedirectHeaders)
|
|
298
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
299
|
+
.join('\n');
|
|
300
|
+
|
|
301
|
+
responseData = {
|
|
302
|
+
statusCode: 302,
|
|
303
|
+
contentType: 'application/json; charset=utf-8',
|
|
304
|
+
proxyType: 'url_redirect',
|
|
305
|
+
targetUrl: baseUrl,
|
|
306
|
+
customHeaders: processedUrlRedirectHeaders,
|
|
307
|
+
formattedHeaders: formattedUrlRedirectHeaders,
|
|
308
|
+
mockInfo: {
|
|
309
|
+
matchedPattern: targetInterface.urlPattern,
|
|
310
|
+
requestMethod: targetInterface.httpMethod,
|
|
311
|
+
timestamp: new Date().toISOString()
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
break;
|
|
315
|
+
|
|
316
|
+
default:
|
|
317
|
+
// 不支持的代理类型
|
|
318
|
+
return res.status(400).json({
|
|
319
|
+
code: 400,
|
|
320
|
+
message: `不支持的代理类型: ${targetInterface.proxyType || 'undefined'}`,
|
|
321
|
+
data: null
|
|
322
|
+
});
|
|
113
323
|
}
|
|
114
324
|
|
|
115
|
-
const endTime = Date.now();
|
|
116
|
-
const duration = endTime - startTime;
|
|
117
|
-
|
|
118
|
-
// 模拟接口处理
|
|
119
|
-
const responseData = {
|
|
120
|
-
statusCode: targetInterface.httpStatus || 200,
|
|
121
|
-
contentType: targetInterface.contentType || 'application/json; charset=utf-8',
|
|
122
|
-
responseBody: responseBody,
|
|
123
|
-
mockInfo: {
|
|
124
|
-
matchedPattern: targetInterface.urlPattern,
|
|
125
|
-
requestMethod: targetInterface.httpMethod,
|
|
126
|
-
delay: delay,
|
|
127
|
-
duration: duration,
|
|
128
|
-
timestamp: new Date().toISOString()
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
|
|
132
325
|
return res.json({
|
|
133
326
|
code: 0,
|
|
134
327
|
message: '成功',
|