whistle.mock-plugins 1.0.6-beta.2 → 1.0.6-beta.21
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 +35 -0
- package/app/public/js/index.js +1 -1
- package/app/public/js/index.js.LICENSE.txt +8 -2
- package/lib/models.js +12 -1
- package/lib/routes/interfaces.js +274 -56
- package/lib/ruleManager.js +41 -16
- package/lib/rules-server.js +473 -210
- package/lib/server.js +0 -28
- package/package.json +1 -1
- package/ui/src/components/interface-management/InterfaceTestModal.js +304 -0
- package/ui/src/components/interface-management/PreviewModal.js +50 -0
- package/ui/src/components/interface-management/README.md +143 -0
- package/ui/src/components/interface-management/ResponseContentEditor.js +406 -0
- package/ui/src/components/interface-management/constants.js +36 -0
- package/ui/src/components/interface-management/index.js +6 -0
- package/ui/src/components/interface-management/utils.js +103 -0
- package/ui/src/pages/InterfaceManagement.js +295 -700
package/lib/rules-server.js
CHANGED
|
@@ -1,284 +1,547 @@
|
|
|
1
|
-
// 引入必要的模块
|
|
2
1
|
const fs = require('fs-extra');
|
|
3
2
|
const path = require('path');
|
|
4
3
|
const url = require('url');
|
|
4
|
+
const storage = require('./storage');
|
|
5
5
|
|
|
6
6
|
// 数据存储目录
|
|
7
|
-
const DATA_DIR =
|
|
8
|
-
|
|
7
|
+
const DATA_DIR = path.join(process.env.WHISTLE_PLUGIN_DATA_DIR || storage.DATA_DIR);
|
|
8
|
+
// 接口配置文件
|
|
9
9
|
const INTERFACES_FILE = path.join(DATA_DIR, 'interfaces.json');
|
|
10
|
+
// 添加功能模块配置文件
|
|
11
|
+
const FEATURES_FILE = path.join(DATA_DIR, 'features.json');
|
|
10
12
|
|
|
11
|
-
//
|
|
13
|
+
// 配置项
|
|
12
14
|
const CONFIG = {
|
|
15
|
+
// 日志级别:0=NONE, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG
|
|
16
|
+
LOG_LEVEL: process.env.MOCK_PLUGIN_LOG_LEVEL || 3,
|
|
13
17
|
// 缓存有效期(毫秒),默认 60 秒
|
|
14
|
-
CACHE_INTERVAL: process.env.MOCK_PLUGIN_CACHE_INTERVAL || 60000
|
|
18
|
+
CACHE_INTERVAL: process.env.MOCK_PLUGIN_CACHE_INTERVAL || 60000,
|
|
19
|
+
// 是否在控制台输出详细日志
|
|
20
|
+
VERBOSE_CONSOLE: process.env.MOCK_PLUGIN_VERBOSE === 'true'
|
|
15
21
|
};
|
|
16
22
|
|
|
17
23
|
// 缓存控制变量
|
|
18
24
|
let enabledInterfacesCache = null; // 仅缓存启用状态的接口
|
|
19
25
|
let cacheTime = 0;
|
|
26
|
+
let cacheVersion = 1;
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// 正则表达式缓存
|
|
29
|
+
const regexCache = new Map();
|
|
30
|
+
|
|
31
|
+
// 记录日志
|
|
32
|
+
const log = (message, level = 3) => {
|
|
33
|
+
// 只记录小于等于当前日志级别的日志
|
|
34
|
+
if (level <= CONFIG.LOG_LEVEL) {
|
|
35
|
+
const prefix = level === 1 ? '[ERROR] ' :
|
|
36
|
+
level === 2 ? '[WARN] ' :
|
|
37
|
+
level === 4 ? '[DEBUG] ' : '';
|
|
38
|
+
|
|
39
|
+
console.log(`[mock-plugin:rulesServer] ${prefix}${message}`);
|
|
30
40
|
}
|
|
31
|
-
|
|
32
|
-
const formatPattern = pattern.substring(1); // 去掉@前缀
|
|
33
|
-
// 使用单次正则替换优化性能
|
|
34
|
-
return formatPattern.replace(/x/g, () => {
|
|
35
|
-
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
36
|
-
return chars.charAt(Math.floor(Math.random() * chars.length));
|
|
37
|
-
});
|
|
38
41
|
};
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
// 错误日志
|
|
44
|
+
const logError = (message) => log(message, 1);
|
|
45
|
+
|
|
46
|
+
// 警告日志
|
|
47
|
+
const logWarn = (message) => log(message, 2);
|
|
48
|
+
|
|
49
|
+
// 信息日志
|
|
50
|
+
const logInfo = (message) => log(message, 3);
|
|
51
|
+
|
|
52
|
+
// 调试日志
|
|
53
|
+
const logDebug = (message) => log(message, 4);
|
|
54
|
+
|
|
55
|
+
// 获取文件最后修改时间
|
|
56
|
+
const getFileModTime = (filePath) => {
|
|
48
57
|
try {
|
|
49
|
-
|
|
50
|
-
return
|
|
58
|
+
const stats = fs.statSync(filePath);
|
|
59
|
+
return stats.mtimeMs;
|
|
51
60
|
} catch (err) {
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// 加载所有启用的接口(整合了功能模块和接口的加载逻辑)
|
|
66
|
+
const loadEnabledInterfaces = () => {
|
|
67
|
+
try {
|
|
68
|
+
const currentTime = Date.now();
|
|
69
|
+
const interfacesFileModTime = getFileModTime(INTERFACES_FILE);
|
|
70
|
+
const featuresFileModTime = getFileModTime(FEATURES_FILE);
|
|
71
|
+
|
|
72
|
+
// 取两个文件的最后修改时间的最大值
|
|
73
|
+
const lastModTime = Math.max(interfacesFileModTime, featuresFileModTime);
|
|
74
|
+
|
|
75
|
+
// 如果缓存有效且文件未被修改,直接返回缓存
|
|
76
|
+
if (enabledInterfacesCache &&
|
|
77
|
+
currentTime - cacheTime < CONFIG.CACHE_INTERVAL &&
|
|
78
|
+
lastModTime <= cacheTime) {
|
|
79
|
+
logDebug('使用已启用接口的缓存');
|
|
80
|
+
return enabledInterfacesCache;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
logDebug('缓存无效或文件已修改,重新加载接口');
|
|
84
|
+
|
|
85
|
+
// 1. 读取并解析所有功能模块
|
|
86
|
+
let enabledFeatureIds = [];
|
|
87
|
+
if (fs.existsSync(FEATURES_FILE)) {
|
|
88
|
+
try {
|
|
89
|
+
const featuresData = fs.readJsonSync(FEATURES_FILE);
|
|
90
|
+
let features = [];
|
|
91
|
+
|
|
92
|
+
if (featuresData && Array.isArray(featuresData)) {
|
|
93
|
+
features = featuresData;
|
|
94
|
+
} else if (featuresData && featuresData.features && Array.isArray(featuresData.features)) {
|
|
95
|
+
features = featuresData.features;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 过滤出启用状态的功能ID
|
|
99
|
+
enabledFeatureIds = features
|
|
100
|
+
.filter(feature => feature.active === true)
|
|
101
|
+
.map(feature => feature.id);
|
|
102
|
+
|
|
103
|
+
logDebug(`找到 ${enabledFeatureIds.length} 个启用的功能模块`);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
logError(`读取功能模块失败: ${err.message}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 2. 读取并解析所有接口
|
|
110
|
+
let enabledInterfaces = [];
|
|
111
|
+
if (fs.existsSync(INTERFACES_FILE)) {
|
|
112
|
+
try {
|
|
113
|
+
const interfacesData = fs.readJsonSync(INTERFACES_FILE);
|
|
114
|
+
let interfaces = [];
|
|
115
|
+
|
|
116
|
+
if (interfacesData && Array.isArray(interfacesData)) {
|
|
117
|
+
interfaces = interfacesData;
|
|
118
|
+
} else if (interfacesData && interfacesData.interfaces && Array.isArray(interfacesData.interfaces)) {
|
|
119
|
+
interfaces = interfacesData.interfaces;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 过滤出启用状态的接口
|
|
123
|
+
enabledInterfaces = interfaces.filter(intf => {
|
|
124
|
+
// 首先检查接口本身是否启用
|
|
125
|
+
const isInterfaceEnabled = intf.active !== false; // 默认为true
|
|
126
|
+
|
|
127
|
+
if (!isInterfaceEnabled) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 如果接口有关联功能,检查功能是否启用
|
|
132
|
+
if (intf.featureId) {
|
|
133
|
+
return enabledFeatureIds.includes(intf.featureId);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 如果接口没有关联功能,只要接口本身启用就可以
|
|
137
|
+
return true;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
logInfo(`找到 ${enabledInterfaces.length} 个启用的接口`);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
logError(`读取接口配置失败: ${err.message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 更新缓存
|
|
147
|
+
enabledInterfacesCache = enabledInterfaces;
|
|
148
|
+
cacheTime = currentTime;
|
|
149
|
+
|
|
150
|
+
return enabledInterfaces;
|
|
151
|
+
} catch (err) {
|
|
152
|
+
logError(`加载启用接口失败: ${err.message}`);
|
|
153
|
+
return enabledInterfacesCache || [];
|
|
54
154
|
}
|
|
55
155
|
};
|
|
56
156
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
* @param {string} pattern 匹配模式
|
|
61
|
-
* @param {string} proxyType 代理类型
|
|
62
|
-
* @returns {boolean} 是否匹配
|
|
63
|
-
*/
|
|
64
|
-
const isUrlMatch = (url, pattern, proxyType) => {
|
|
65
|
-
if (!url || !pattern) return false;
|
|
157
|
+
// 匹配URL是否符合模式
|
|
158
|
+
const isUrlMatch = (url, pattern, proxyType, fullUrl) => {
|
|
159
|
+
if (!pattern) return false;
|
|
66
160
|
|
|
67
161
|
try {
|
|
68
|
-
//
|
|
162
|
+
// 针对不同的代理类型采用不同的匹配策略
|
|
163
|
+
|
|
164
|
+
// 对于url_redirect类型,需要完全匹配fullPath才命中
|
|
69
165
|
if (proxyType === 'url_redirect') {
|
|
70
|
-
|
|
166
|
+
// 精确匹配完整路径,这是最快的比较方式
|
|
167
|
+
return pattern === fullUrl;
|
|
71
168
|
}
|
|
72
169
|
|
|
73
170
|
// 对于redirect类型,只要url以pattern开头即可命中(前缀匹配)
|
|
74
171
|
if (proxyType === 'redirect') {
|
|
75
|
-
|
|
172
|
+
// 使用字符串原生的startsWith方法更高效
|
|
173
|
+
return fullUrl.startsWith(pattern);
|
|
76
174
|
}
|
|
77
175
|
|
|
78
|
-
//
|
|
79
|
-
|
|
176
|
+
// 以下是默认的匹配逻辑(用于response类型等)
|
|
177
|
+
|
|
178
|
+
// 精确匹配 - 最高效的匹配方式
|
|
80
179
|
if (pattern === url) {
|
|
81
180
|
return true;
|
|
82
181
|
}
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
if (pattern.
|
|
86
|
-
|
|
182
|
+
|
|
183
|
+
// 通配符匹配
|
|
184
|
+
if (pattern.includes('*')) {
|
|
185
|
+
// 使用缓存的正则表达式
|
|
186
|
+
const cacheKey = `wildcard:${pattern}`;
|
|
187
|
+
let regex = regexCache.get(cacheKey);
|
|
188
|
+
|
|
189
|
+
if (!regex) {
|
|
190
|
+
// 需要转换为正则表达式,处理特殊字符
|
|
191
|
+
const regexPattern = pattern
|
|
192
|
+
.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&') // 转义特殊字符
|
|
193
|
+
.replace(/\*/g, '.*'); // 将 * 替换为 .*
|
|
194
|
+
|
|
195
|
+
regex = new RegExp('^' + regexPattern + '$');
|
|
196
|
+
regexCache.set(cacheKey, regex);
|
|
197
|
+
}
|
|
198
|
+
|
|
87
199
|
return regex.test(url);
|
|
88
200
|
}
|
|
89
|
-
|
|
90
|
-
//
|
|
91
|
-
if (pattern.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
201
|
+
|
|
202
|
+
// 正则表达式匹配
|
|
203
|
+
if (pattern.startsWith('/') && pattern.length > 2 && pattern.endsWith('/')) {
|
|
204
|
+
// 使用缓存的正则表达式
|
|
205
|
+
const cacheKey = `regex:${pattern}`;
|
|
206
|
+
let regex = regexCache.get(cacheKey);
|
|
207
|
+
|
|
208
|
+
if (!regex) {
|
|
209
|
+
const regexStr = pattern.slice(1, -1);
|
|
210
|
+
regex = new RegExp(regexStr);
|
|
211
|
+
regexCache.set(cacheKey, regex);
|
|
212
|
+
}
|
|
213
|
+
|
|
96
214
|
return regex.test(url);
|
|
97
215
|
}
|
|
98
|
-
|
|
99
|
-
return false;
|
|
100
216
|
} catch (e) {
|
|
101
|
-
|
|
102
|
-
return false;
|
|
217
|
+
log(`正则表达式匹配错误: ${e.message}`);
|
|
103
218
|
}
|
|
219
|
+
|
|
220
|
+
return false;
|
|
104
221
|
};
|
|
105
222
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const loadEnabledInterfaces = async () => {
|
|
223
|
+
// 验证目标URL是否有效
|
|
224
|
+
const isValidTargetUrl = (url) => {
|
|
225
|
+
if (!url) return false;
|
|
226
|
+
|
|
111
227
|
try {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// 从数据管理器获取启用的接口
|
|
121
|
-
const dataManager = require('./dataManager');
|
|
122
|
-
const enabledInterfaces = await dataManager.getEnabledInterfaces();
|
|
123
|
-
|
|
124
|
-
// 更新缓存
|
|
125
|
-
enabledInterfacesCache = enabledInterfaces;
|
|
126
|
-
cacheTime = currentTime;
|
|
127
|
-
|
|
128
|
-
return enabledInterfaces;
|
|
129
|
-
} catch (err) {
|
|
130
|
-
console.error('[loadEnabledInterfaces] 加载启用接口失败:', err);
|
|
131
|
-
return enabledInterfacesCache || [];
|
|
228
|
+
// 检查是否是合法的URL格式
|
|
229
|
+
new URL(url);
|
|
230
|
+
return true;
|
|
231
|
+
} catch (e) {
|
|
232
|
+
log(`URL验证错误: ${e.message}`);
|
|
233
|
+
return false;
|
|
132
234
|
}
|
|
133
235
|
};
|
|
134
236
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
|
|
237
|
+
// 验证重定向规则的有效性
|
|
238
|
+
const validateRedirectRule = (interface) => {
|
|
239
|
+
if (!interface) return false;
|
|
240
|
+
|
|
241
|
+
const { proxyType, targetUrl } = interface;
|
|
242
|
+
|
|
243
|
+
// 对于redirect和url_redirect类型,必须有有效的目标URL
|
|
244
|
+
if ((proxyType === 'redirect' || proxyType === 'url_redirect') && !isValidTargetUrl(targetUrl)) {
|
|
245
|
+
log(`接口 ${interface.id || interface.name || '未知'} 的目标URL无效: ${targetUrl}`);
|
|
246
|
+
return false;
|
|
144
247
|
}
|
|
145
248
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// 没有找到匹配的接口
|
|
158
|
-
return null;
|
|
159
|
-
} catch (err) {
|
|
160
|
-
console.error('[findMatchingInterface] 查找匹配接口出错:', err);
|
|
161
|
-
return null;
|
|
249
|
+
return true;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// 生成随机数工具函数
|
|
253
|
+
const generateRandomValue = (pattern) => {
|
|
254
|
+
// 如果不是以@开头的模式,直接返回原值
|
|
255
|
+
if (!pattern || !pattern.startsWith('@')) {
|
|
256
|
+
return pattern;
|
|
162
257
|
}
|
|
258
|
+
|
|
259
|
+
const formatPattern = pattern.substring(1); // 去掉@前缀
|
|
260
|
+
// 使用单次正则替换代替每个字符的遍历
|
|
261
|
+
return formatPattern.replace(/x/g, () => {
|
|
262
|
+
// 使用一个固定的字符集,避免生成不兼容的字符
|
|
263
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
264
|
+
return chars.charAt(Math.floor(Math.random() * chars.length));
|
|
265
|
+
});
|
|
163
266
|
};
|
|
164
267
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
268
|
+
// 构建重定向规则
|
|
269
|
+
const buildRedirectRule = (fullUrl, targetUrl, customHeaders, pattern, isUrlRedirect = 0) => {
|
|
270
|
+
// 构建目标URL
|
|
271
|
+
let finalTargetUrl;
|
|
272
|
+
if(!isUrlRedirect){
|
|
273
|
+
finalTargetUrl = fullUrl.replace(pattern, targetUrl);
|
|
274
|
+
}else{
|
|
275
|
+
finalTargetUrl = `redirect://${targetUrl}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 定义结果数组,避免多次字符串拼接
|
|
279
|
+
const rules = [`${fullUrl} ${finalTargetUrl}`];
|
|
176
280
|
|
|
177
|
-
//
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
281
|
+
// 如果有自定义请求头,添加到规则中
|
|
282
|
+
if (customHeaders && Object.keys(customHeaders).length > 0) {
|
|
283
|
+
// 为每个请求头创建一个规则行
|
|
284
|
+
Object.entries(customHeaders).forEach(([key, value]) => {
|
|
285
|
+
// 处理随机值
|
|
286
|
+
const processedValue = generateRandomValue(value);
|
|
287
|
+
|
|
288
|
+
// 添加到规则数组
|
|
289
|
+
rules.push(`${fullUrl} headerReplace://req.${key}:/(.*)/=${processedValue}`);
|
|
290
|
+
});
|
|
181
291
|
}
|
|
182
292
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
console.log(`[getWhistleRules] 无效的URL重定向目标: ${urlRedirectTarget}`);
|
|
209
|
-
return '';
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// 自定义请求头
|
|
213
|
-
const urlRedirectHeaders = options.customHeaders || {};
|
|
214
|
-
|
|
215
|
-
// 对于url_redirect,使用redirect://前缀
|
|
216
|
-
return `${fullUrl} redirect://${urlRedirectTarget}\n${fullUrl} resCors://enable`;
|
|
217
|
-
|
|
218
|
-
default:
|
|
219
|
-
console.log(`[getWhistleRules] 不支持的代理类型: ${proxyType}`);
|
|
220
|
-
return '';
|
|
221
|
-
}
|
|
222
|
-
} catch (err) {
|
|
223
|
-
console.error('[getWhistleRules] 生成规则时出错:', err);
|
|
224
|
-
return '';
|
|
293
|
+
// 最后才将所有规则合并为一个字符串,减少字符串操作
|
|
294
|
+
return rules.join('\n');
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// 缓存管理接口 - 简化版本
|
|
298
|
+
const cacheManager = {
|
|
299
|
+
// 清除缓存
|
|
300
|
+
clearCache: () => {
|
|
301
|
+
logInfo('主动清除接口缓存');
|
|
302
|
+
enabledInterfacesCache = null;
|
|
303
|
+
cacheTime = 0;
|
|
304
|
+
cacheVersion++;
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
// 获取当前缓存状态
|
|
308
|
+
getStatus: () => {
|
|
309
|
+
return {
|
|
310
|
+
enabled: {
|
|
311
|
+
cached: !!enabledInterfacesCache,
|
|
312
|
+
cacheTime: cacheTime,
|
|
313
|
+
itemCount: enabledInterfacesCache ? enabledInterfacesCache.length : 0
|
|
314
|
+
},
|
|
315
|
+
cacheVersion,
|
|
316
|
+
configInterval: CONFIG.CACHE_INTERVAL
|
|
317
|
+
};
|
|
225
318
|
}
|
|
226
319
|
};
|
|
227
320
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
const { req, res } = ctx;
|
|
321
|
+
// Whistle 规则服务器实现
|
|
322
|
+
module.exports = (server, options) => {
|
|
323
|
+
// 优化:缓存常用路径匹配
|
|
324
|
+
const debugConfigPath = '/_debug/config';
|
|
325
|
+
const debugEnabledInterfacesPath = '/_debug/enabled-interfaces';
|
|
326
|
+
const clearCachePath = '/_flush_cache';
|
|
235
327
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const fullUrl = req.url;
|
|
328
|
+
server.on('request', (req, res) => {
|
|
329
|
+
const oReq = req.originalReq;
|
|
239
330
|
|
|
240
|
-
//
|
|
241
|
-
|
|
331
|
+
// 提取请求相关信息
|
|
332
|
+
const fullUrl = oReq.url;
|
|
333
|
+
logDebug(`原始URL: ${fullUrl}`);
|
|
242
334
|
|
|
243
|
-
//
|
|
244
|
-
|
|
335
|
+
// 处理缓存刷新请求
|
|
336
|
+
if (fullUrl.startsWith(clearCachePath)) {
|
|
337
|
+
cacheManager.clearCache();
|
|
338
|
+
|
|
339
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
340
|
+
res.end(JSON.stringify({
|
|
341
|
+
success: true,
|
|
342
|
+
message: '缓存已刷新',
|
|
343
|
+
cacheStatus: cacheManager.getStatus()
|
|
344
|
+
}));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
245
347
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
res.
|
|
348
|
+
// 特殊调试路径,查看缓存状态
|
|
349
|
+
if (fullUrl.endsWith('/_cache_status')) {
|
|
350
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
351
|
+
res.end(JSON.stringify(cacheManager.getStatus()));
|
|
249
352
|
return;
|
|
250
353
|
}
|
|
251
354
|
|
|
252
|
-
//
|
|
253
|
-
|
|
355
|
+
// 特殊调试路径,使用更高效的匹配
|
|
356
|
+
if (fullUrl.endsWith(debugConfigPath)) {
|
|
357
|
+
try {
|
|
358
|
+
const configs = {
|
|
359
|
+
interfaces: fs.existsSync(INTERFACES_FILE) ? fs.readJsonSync(INTERFACES_FILE) : null,
|
|
360
|
+
features: fs.existsSync(FEATURES_FILE) ? fs.readJsonSync(FEATURES_FILE) : null
|
|
361
|
+
};
|
|
362
|
+
res.end(JSON.stringify(configs, null, 2));
|
|
363
|
+
return;
|
|
364
|
+
} catch (err) {
|
|
365
|
+
logError(`读取配置文件失败: ${err.message}`);
|
|
366
|
+
res.end(`读取配置文件失败: ${err.message}`);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 调试路径,查看启用的接口
|
|
372
|
+
if (fullUrl.endsWith(debugEnabledInterfacesPath)) {
|
|
373
|
+
try {
|
|
374
|
+
const enabledInterfaces = loadEnabledInterfaces();
|
|
375
|
+
res.end(JSON.stringify({
|
|
376
|
+
count: enabledInterfaces.length,
|
|
377
|
+
interfaces: enabledInterfaces
|
|
378
|
+
}, null, 2));
|
|
379
|
+
return;
|
|
380
|
+
} catch (err) {
|
|
381
|
+
logError(`获取启用接口失败: ${err.message}`);
|
|
382
|
+
res.end(`获取启用接口失败: ${err.message}`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
254
386
|
|
|
255
|
-
|
|
256
|
-
|
|
387
|
+
// 解析URL
|
|
388
|
+
let parsedUrl;
|
|
389
|
+
try {
|
|
390
|
+
parsedUrl = url.parse(fullUrl);
|
|
391
|
+
} catch (err) {
|
|
392
|
+
logError(`解析URL失败: ${err.message}`);
|
|
393
|
+
// 解析失败时,不处理请求
|
|
257
394
|
res.end('');
|
|
258
395
|
return;
|
|
259
396
|
}
|
|
260
397
|
|
|
261
|
-
//
|
|
262
|
-
const
|
|
398
|
+
// 尝试获取 pathname 或 path,用于匹配
|
|
399
|
+
const method = oReq.method;
|
|
400
|
+
const path = parsedUrl.pathname || '';
|
|
263
401
|
|
|
264
|
-
|
|
265
|
-
const rule = getWhistleRules(fullUrl, pattern, proxyType, {
|
|
266
|
-
targetUrl,
|
|
267
|
-
customHeaders
|
|
268
|
-
});
|
|
402
|
+
logDebug(`请求方法: ${method}, 请求路径: ${path}`);
|
|
269
403
|
|
|
270
|
-
//
|
|
271
|
-
|
|
404
|
+
// 加载启用的接口配置(使用缓存优化)
|
|
405
|
+
const enabledInterfaces = loadEnabledInterfaces();
|
|
272
406
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
407
|
+
if (enabledInterfaces.length === 0) {
|
|
408
|
+
logDebug('没有找到启用的接口配置,跳过处理');
|
|
409
|
+
res.end('');
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// 优化:使用 find 查找匹配的接口,只遍历一次数组
|
|
414
|
+
const matchedInterface = enabledInterfaces.find(intf => {
|
|
415
|
+
// 获取URL模式
|
|
416
|
+
const pattern = intf.urlPattern || '';
|
|
417
|
+
const proxyType = intf.proxyType || 'response';
|
|
418
|
+
|
|
419
|
+
if (!pattern) {
|
|
420
|
+
logDebug(`接口 ${intf.id || intf.name || '未知'} 缺少URL模式`);
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// 检查URL模式匹配,根据proxyType选择适当的匹配策略
|
|
425
|
+
const urlMatches = isUrlMatch(path, pattern, proxyType, fullUrl);
|
|
426
|
+
|
|
427
|
+
if (!urlMatches) {
|
|
428
|
+
// 不记录每个失败的匹配,以减少日志量
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
logDebug(`URL模式匹配成功: ${fullUrl} 匹配 ${pattern} (${proxyType}模式)`);
|
|
433
|
+
|
|
434
|
+
// 检查HTTP方法匹配
|
|
435
|
+
const methodField = intf.httpMethod || intf.method || '';
|
|
436
|
+
const methodValue = methodField.toUpperCase();
|
|
437
|
+
const methodMatches = !methodValue ||
|
|
438
|
+
methodValue === 'ALL' ||
|
|
439
|
+
methodValue === 'ANY' ||
|
|
440
|
+
methodValue === method;
|
|
441
|
+
|
|
442
|
+
if (!methodMatches) {
|
|
443
|
+
logDebug(`方法不匹配: 请求方法 ${method}, 接口方法 ${methodValue}`);
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
logDebug(`方法匹配成功: ${method} 匹配 ${methodValue || 'ALL'}`);
|
|
448
|
+
|
|
449
|
+
return true;
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// 如果找到匹配的接口,处理对应的规则
|
|
453
|
+
if (matchedInterface) {
|
|
454
|
+
logInfo(`找到匹配接口 "${matchedInterface.name || matchedInterface.id || '未知'}" 用于 ${method} ${path}`);
|
|
455
|
+
|
|
456
|
+
// 处理不同的代理类型
|
|
457
|
+
const proxyType = matchedInterface.proxyType || 'response';
|
|
458
|
+
|
|
459
|
+
// 优先级判断:首先处理 response 类型
|
|
460
|
+
if (proxyType === 'response') {
|
|
461
|
+
// 返回指向插件服务的规则
|
|
462
|
+
logDebug(`接口 "${matchedInterface.name}" 使用模拟响应模式`);
|
|
463
|
+
res.end(`${fullUrl} mock-plugins://`);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// 对于redirect和url_redirect类型,验证规则有效性
|
|
468
|
+
if ((proxyType === 'redirect' || proxyType === 'url_redirect') && !validateRedirectRule(matchedInterface)) {
|
|
469
|
+
logWarn(`接口 "${matchedInterface.name}" 的重定向规则无效,跳过处理`);
|
|
470
|
+
res.end('');
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// 处理 redirect 类型:将请求的 url 中的源 url 部分替换为目标 url
|
|
475
|
+
if (proxyType === 'redirect') {
|
|
476
|
+
const targetUrl = matchedInterface.targetUrl.trim();
|
|
477
|
+
const pattern = matchedInterface.urlPattern.trim();
|
|
478
|
+
logDebug(`接口 "${matchedInterface.name}" 使用重定向模式,模式: ${pattern}, 目标URL: ${targetUrl}`);
|
|
479
|
+
|
|
480
|
+
// 确保目标URL合法
|
|
481
|
+
if (!isValidTargetUrl(targetUrl)) {
|
|
482
|
+
logWarn(`目标URL不合法: ${targetUrl},跳过处理`);
|
|
483
|
+
res.end('');
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// 执行重定向:将fullUrl中匹配的pattern替换为targetUrl
|
|
488
|
+
// 使用startsWith优化匹配效率
|
|
489
|
+
if (fullUrl.startsWith(pattern)) {
|
|
490
|
+
// 获取自定义请求头
|
|
491
|
+
const customHeaders = matchedInterface.customHeaders || {};
|
|
492
|
+
|
|
493
|
+
// 构建重定向规则,包括自定义请求头
|
|
494
|
+
const redirectRule = buildRedirectRule(fullUrl, targetUrl, customHeaders, pattern, 0);
|
|
495
|
+
|
|
496
|
+
// 返回规则
|
|
497
|
+
res.end(redirectRule);
|
|
498
|
+
logDebug(`重定向规则: ${redirectRule}`);
|
|
499
|
+
return;
|
|
500
|
+
} else {
|
|
501
|
+
logDebug(`URL ${fullUrl} 不以 ${pattern} 开头,跳过处理`);
|
|
502
|
+
res.end('');
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// 处理 url_redirect 类型:完全匹配 fullPath 并直接返回目标URL
|
|
508
|
+
if (proxyType === 'url_redirect') {
|
|
509
|
+
const targetUrl = matchedInterface.targetUrl.trim();
|
|
510
|
+
logDebug(`接口 "${matchedInterface.name}" 使用URL重定向模式,目标URL: ${targetUrl}`);
|
|
511
|
+
|
|
512
|
+
// 确保目标URL合法
|
|
513
|
+
if (!isValidTargetUrl(targetUrl)) {
|
|
514
|
+
logWarn(`目标URL不合法: ${targetUrl},跳过处理`);
|
|
515
|
+
res.end('');
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// 对于url_redirect,我们需要完全匹配
|
|
520
|
+
if (fullUrl === matchedInterface.urlPattern) {
|
|
521
|
+
// 获取自定义请求头
|
|
522
|
+
const customHeaders = matchedInterface.customHeaders || {};
|
|
523
|
+
|
|
524
|
+
// 构建重定向规则,包括自定义请求头
|
|
525
|
+
const redirectRule = buildRedirectRule(fullUrl, targetUrl, customHeaders, pattern, 1);
|
|
526
|
+
|
|
527
|
+
// 返回规则
|
|
528
|
+
res.end(redirectRule);
|
|
529
|
+
logDebug(`URL重定向规则: ${redirectRule}`);
|
|
530
|
+
return;
|
|
531
|
+
} else {
|
|
532
|
+
logDebug(`URL ${fullUrl} 不完全匹配 ${matchedInterface.urlPattern},跳过处理`);
|
|
533
|
+
res.end('');
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// 默认情况下使用mock-plugins处理
|
|
539
|
+
logDebug(`接口 "${matchedInterface.name}" 使用默认处理模式`);
|
|
540
|
+
res.end(`${fullUrl} mock-plugins://`);
|
|
541
|
+
} else {
|
|
542
|
+
// 如果没有匹配的接口,返回空字符串,Whistle 会继续处理下一个规则
|
|
543
|
+
logDebug(`未找到匹配接口,跳过处理 ${method} ${path}`);
|
|
544
|
+
res.end('');
|
|
545
|
+
}
|
|
546
|
+
});
|
|
284
547
|
};
|