whistle.mock-plugins 1.0.0
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/API.md +247 -0
- package/README.md +91 -0
- package/app/cgi-bin/features.js +285 -0
- package/app/cgi-bin/index.js +31 -0
- package/app/cgi-bin/interfaces.js +409 -0
- package/app/cgi-bin/logs.js +151 -0
- package/app/cgi-bin/test-interface.js +152 -0
- package/app/public/index.html +1 -0
- package/app/public/js/index.js +2 -0
- package/app/public/js/index.js.LICENSE.txt +150 -0
- package/app/util/index.js +24 -0
- package/index.html +52 -0
- package/initialize.js +32 -0
- package/lib/dataManager.js +500 -0
- package/lib/index.js +3 -0
- package/lib/models.js +64 -0
- package/lib/routes/features.js +213 -0
- package/lib/routes/interfaces.js +304 -0
- package/lib/routes/proxy.js +239 -0
- package/lib/ruleManager.js +551 -0
- package/lib/rules-server.js +267 -0
- package/lib/server.js +805 -0
- package/lib/storage.js +16 -0
- package/lib/ui-server.js +68 -0
- package/package.json +51 -0
- package/public/bundle.js +2 -0
- package/public/bundle.js.LICENSE.txt +38 -0
- package/public/index.html +1 -0
- package/public/plugin.mock-plugin.html +14 -0
- package/rules.txt +22 -0
- package/test/rulesServer.test.js +120 -0
- package/ui/src/App.css +173 -0
- package/ui/src/App.js +24 -0
- package/ui/src/components/AppLayout.js +149 -0
- package/ui/src/components/FeatureCard.js +53 -0
- package/ui/src/components/Layout.js +94 -0
- package/ui/src/components/MainNav.js +42 -0
- package/ui/src/components/PageContainer.js +16 -0
- package/ui/src/index.html +11 -0
- package/ui/src/index.js +43 -0
- package/ui/src/pages/FileProxy.js +235 -0
- package/ui/src/pages/FilesPage.js +292 -0
- package/ui/src/pages/Home.js +128 -0
- package/ui/src/pages/HomePage.js +195 -0
- package/ui/src/pages/InterfaceManagement.js +829 -0
- package/ui/src/pages/InterfacePage.js +342 -0
- package/ui/src/pages/MockData.js +472 -0
- package/ui/src/pages/Settings.js +192 -0
- package/ui/src/pages/SettingsPage.js +165 -0
- package/ui/src/pages/UrlRedirect.js +225 -0
- package/ui/src/pages/Welcome.js +155 -0
- package/ui/src/services/api.js +186 -0
- package/ui/src/styles/app-layout.css +337 -0
- package/ui/src/styles/feature-card.css +172 -0
- package/ui/src/styles/file-proxy.css +301 -0
- package/ui/src/styles/global.css +224 -0
- package/ui/src/styles/home.css +123 -0
- package/ui/src/styles/index.css +419 -0
- package/ui/src/styles/interface-management.css +694 -0
- package/ui/src/styles/layout.css +159 -0
- package/ui/src/styles/logs-page.css +427 -0
- package/ui/src/styles/main-nav.css +52 -0
- package/ui/src/styles/mock-data.css +850 -0
- package/ui/src/styles/page-container.css +11 -0
- package/ui/src/styles/settings.css +208 -0
- package/ui/src/styles/url-redirect.css +279 -0
- package/ui/src/styles/welcome.css +282 -0
- package/webpack.config.js +49 -0
package/lib/server.js
ADDED
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const Mock = require('mockjs');
|
|
5
|
+
const storage = require('./storage');
|
|
6
|
+
|
|
7
|
+
// 数据存储目录
|
|
8
|
+
const DATA_DIR = path.join(process.env.WHISTLE_PLUGIN_DATA_DIR || storage.DATA_DIR);
|
|
9
|
+
// 功能和接口配置文件
|
|
10
|
+
const FEATURES_FILE = path.join(DATA_DIR, 'features.json');
|
|
11
|
+
const INTERFACES_FILE = path.join(DATA_DIR, 'interfaces.json');
|
|
12
|
+
|
|
13
|
+
// 日志目录
|
|
14
|
+
const LOG_DIR = path.join(DATA_DIR, 'logs');
|
|
15
|
+
try {
|
|
16
|
+
fs.ensureDirSync(LOG_DIR);
|
|
17
|
+
console.log('Log directory created:', LOG_DIR);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
console.error('Failed to create log directory:', err.message);
|
|
20
|
+
}
|
|
21
|
+
const LOG_FILE = path.join(LOG_DIR, 'plugin.log');
|
|
22
|
+
|
|
23
|
+
// 记录日志
|
|
24
|
+
const logMessage = (message) => {
|
|
25
|
+
try {
|
|
26
|
+
// 确保日志目录存在
|
|
27
|
+
if (!fs.existsSync(LOG_DIR)) {
|
|
28
|
+
fs.ensureDirSync(LOG_DIR);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const timestamp = new Date().toISOString();
|
|
32
|
+
const logEntry = `[${timestamp}] ${message}\n`;
|
|
33
|
+
|
|
34
|
+
fs.appendFileSync(LOG_FILE, logEntry);
|
|
35
|
+
// 同时将日志输出到控制台,方便调试
|
|
36
|
+
console.log(`[mock-plugin] ${message}`);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error('Error writing to log file:', err.message);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
// 记录日志到 logs.json 文件
|
|
44
|
+
const addToJsonLog = (logData) => {
|
|
45
|
+
try {
|
|
46
|
+
const logsFile = path.join(DATA_DIR, 'logs.json');
|
|
47
|
+
|
|
48
|
+
// 确保日志文件存在
|
|
49
|
+
if (!fs.existsSync(logsFile)) {
|
|
50
|
+
fs.writeJsonSync(logsFile, { logs: [] }, { spaces: 2 });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 读取现有日志
|
|
54
|
+
let logsData;
|
|
55
|
+
try {
|
|
56
|
+
logsData = fs.readJsonSync(logsFile);
|
|
57
|
+
if (!logsData.logs) {
|
|
58
|
+
logsData = { logs: [] };
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error('读取日志文件错误:', err);
|
|
62
|
+
logsData = { logs: [] };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 标准化日志数据,确保同时包含type和eventType字段
|
|
66
|
+
const standardizedLogData = {
|
|
67
|
+
...logData,
|
|
68
|
+
// 确保type和eventType都存在,前端可能使用eventType进行过滤
|
|
69
|
+
eventType: logData.eventType || 'unknown',
|
|
70
|
+
type: logData.eventType || 'unknown', // 复制eventType到type字段
|
|
71
|
+
// 如果缺少必要字段,提供默认值,防止前端过滤出错
|
|
72
|
+
url: logData.url || '',
|
|
73
|
+
method: logData.method || '',
|
|
74
|
+
message: logData.message || '',
|
|
75
|
+
status: logData.status || '',
|
|
76
|
+
pattern: logData.pattern || ''
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// 添加时间戳和ID
|
|
80
|
+
const newLog = {
|
|
81
|
+
...standardizedLogData,
|
|
82
|
+
id: Date.now().toString(),
|
|
83
|
+
timestamp: new Date().toISOString()
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// 添加新日志
|
|
87
|
+
logsData.logs.unshift(newLog);
|
|
88
|
+
|
|
89
|
+
// 限制日志数量,最多保留10000条
|
|
90
|
+
if (logsData.logs.length > 10000) {
|
|
91
|
+
logsData.logs = logsData.logs.slice(0, 10000);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fs.writeJsonSync(logsFile, logsData, { spaces: 2 });
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.error('记录日志到JSON文件失败:', err);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// 确保数据目录存在
|
|
101
|
+
try {
|
|
102
|
+
fs.ensureDirSync(DATA_DIR);
|
|
103
|
+
logMessage('数据目录已确认: ' + DATA_DIR);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
console.error('创建数据目录失败:', err.message);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 确保配置文件存在
|
|
109
|
+
if (!fs.existsSync(FEATURES_FILE)) {
|
|
110
|
+
try {
|
|
111
|
+
fs.writeJsonSync(FEATURES_FILE, { features: [] }, { spaces: 2 });
|
|
112
|
+
logMessage('创建了功能配置文件: ' + FEATURES_FILE);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error('创建功能配置文件失败:', err.message);
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
// 验证文件结构
|
|
118
|
+
try {
|
|
119
|
+
const fileData = fs.readJsonSync(FEATURES_FILE);
|
|
120
|
+
if (!fileData || !fileData.features) {
|
|
121
|
+
// 如果文件存在但结构无效,重新初始化
|
|
122
|
+
fs.writeJsonSync(FEATURES_FILE, { features: [] }, { spaces: 2 });
|
|
123
|
+
logMessage('修复了损坏的features文件结构');
|
|
124
|
+
}
|
|
125
|
+
} catch (e) {
|
|
126
|
+
// JSON解析错误时重新初始化文件
|
|
127
|
+
try {
|
|
128
|
+
fs.writeJsonSync(FEATURES_FILE, { features: [] }, { spaces: 2 });
|
|
129
|
+
logMessage('修复了无法解析的features文件');
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.error('重置功能配置文件失败:', err.message);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!fs.existsSync(INTERFACES_FILE)) {
|
|
137
|
+
try {
|
|
138
|
+
fs.writeJsonSync(INTERFACES_FILE, { interfaces: [] }, { spaces: 2 });
|
|
139
|
+
logMessage('创建了接口配置文件: ' + INTERFACES_FILE);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.error('创建接口配置文件失败:', err.message);
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
// 验证文件结构
|
|
145
|
+
try {
|
|
146
|
+
const fileData = fs.readJsonSync(INTERFACES_FILE);
|
|
147
|
+
if (!fileData || !fileData.interfaces) {
|
|
148
|
+
// 如果文件存在但结构无效,重新初始化
|
|
149
|
+
fs.writeJsonSync(INTERFACES_FILE, { interfaces: [] }, { spaces: 2 });
|
|
150
|
+
logMessage('修复了损坏的interfaces文件结构');
|
|
151
|
+
}
|
|
152
|
+
} catch (e) {
|
|
153
|
+
// JSON解析错误时重新初始化文件
|
|
154
|
+
try {
|
|
155
|
+
fs.writeJsonSync(INTERFACES_FILE, { interfaces: [] }, { spaces: 2 });
|
|
156
|
+
logMessage('修复了无法解析的interfaces文件');
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.error('重置接口配置文件失败:', err.message);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// API前缀
|
|
164
|
+
const RULE_VALUE_HEADER = 'x-whistle-rule-value';
|
|
165
|
+
const MOCK_PREFIX = 'mock://';
|
|
166
|
+
|
|
167
|
+
// 创建 Express 应用
|
|
168
|
+
const app = express();
|
|
169
|
+
|
|
170
|
+
// 解析请求体
|
|
171
|
+
app.use(express.json({limit: '100mb'}));
|
|
172
|
+
app.use(express.urlencoded({ extended: true, limit: '100mb'}));
|
|
173
|
+
|
|
174
|
+
// 加载功能和接口配置
|
|
175
|
+
const loadConfigurations = () => {
|
|
176
|
+
try {
|
|
177
|
+
let featuresData = { features: [] };
|
|
178
|
+
let interfacesData = { interfaces: [] };
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
if (fs.existsSync(FEATURES_FILE)) {
|
|
182
|
+
featuresData = fs.readJsonSync(FEATURES_FILE);
|
|
183
|
+
// 确保featuresData和features字段都存在,并且features是数组
|
|
184
|
+
if (!featuresData || !featuresData.features || !Array.isArray(featuresData.features)) {
|
|
185
|
+
logMessage('功能配置数据结构不正确,重新初始化');
|
|
186
|
+
featuresData = { features: [] };
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
logMessage('功能配置文件不存在,使用空数组');
|
|
190
|
+
}
|
|
191
|
+
} catch (err) {
|
|
192
|
+
logMessage('加载功能配置失败: ' + err.message);
|
|
193
|
+
featuresData = { features: [] };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
if (fs.existsSync(INTERFACES_FILE)) {
|
|
198
|
+
interfacesData = fs.readJsonSync(INTERFACES_FILE);
|
|
199
|
+
// 确保interfacesData和interfaces字段都存在,并且interfaces是数组
|
|
200
|
+
if (!interfacesData || !interfacesData.interfaces || !Array.isArray(interfacesData.interfaces)) {
|
|
201
|
+
logMessage('接口配置数据结构不正确,重新初始化');
|
|
202
|
+
interfacesData = { interfaces: [] };
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
logMessage('接口配置文件不存在,使用空数组');
|
|
206
|
+
}
|
|
207
|
+
} catch (err) {
|
|
208
|
+
logMessage('加载接口配置失败: ' + err.message);
|
|
209
|
+
interfacesData = { interfaces: [] };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
features: featuresData.features || [],
|
|
214
|
+
interfaces: interfacesData.interfaces || []
|
|
215
|
+
};
|
|
216
|
+
} catch (err) {
|
|
217
|
+
logMessage('加载配置失败: ' + err.message);
|
|
218
|
+
return { features: [], interfaces: [] };
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// 匹配URL是否符合模式
|
|
223
|
+
const isUrlMatch = (url, pattern) => {
|
|
224
|
+
if (!pattern) return false;
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
// 精确匹配
|
|
228
|
+
if (pattern === url) {
|
|
229
|
+
logMessage(`URL精确匹配成功: ${url} === ${pattern}`);
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 通配符匹配
|
|
234
|
+
if (pattern.includes('*')) {
|
|
235
|
+
// 需要转换为正则表达式,处理特殊字符
|
|
236
|
+
const regexPattern = pattern
|
|
237
|
+
.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&') // 转义特殊字符
|
|
238
|
+
.replace(/\*/g, '.*'); // 将 * 替换为 .*
|
|
239
|
+
|
|
240
|
+
const regex = new RegExp('^' + regexPattern + '$');
|
|
241
|
+
const result = regex.test(url);
|
|
242
|
+
logMessage(`URL通配符匹配${result ? '成功' : '失败'}: ${url} ${result ? '匹配' : '不匹配'} 模式 ${pattern}`);
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 正则表达式匹配
|
|
247
|
+
if (pattern.startsWith('/') && pattern.endsWith('/')) {
|
|
248
|
+
const regexStr = pattern.slice(1, -1);
|
|
249
|
+
const regex = new RegExp(regexStr);
|
|
250
|
+
const result = regex.test(url);
|
|
251
|
+
logMessage(`URL正则匹配${result ? '成功' : '失败'}: ${url} ${result ? '匹配' : '不匹配'} 模式 ${pattern}`);
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
} catch (e) {
|
|
255
|
+
logMessage('正则表达式匹配错误: ' + e.message);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
logMessage(`URL不匹配任何模式: ${url} != ${pattern}`);
|
|
259
|
+
return false;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// 解析原始URL
|
|
263
|
+
const parseOriginalUrl = (req) => {
|
|
264
|
+
try {
|
|
265
|
+
logMessage(`开始解析原始URL,请求URL: ${req.url}`);
|
|
266
|
+
logMessage(`请求方法: ${req.method}`);
|
|
267
|
+
|
|
268
|
+
// 首先尝试从 originalReq 获取
|
|
269
|
+
if (req.originalReq) {
|
|
270
|
+
// 优先使用 realUrl,因为它是真实的 URL
|
|
271
|
+
if (req.originalReq.realUrl) {
|
|
272
|
+
logMessage(`从req.originalReq.realUrl获取原始URL: ${req.originalReq.realUrl}`);
|
|
273
|
+
return req.originalReq.realUrl;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 如果 realUrl 不存在,则使用 url
|
|
277
|
+
if (req.originalReq.url) {
|
|
278
|
+
logMessage(`从req.originalReq.url获取原始URL: ${req.originalReq.url}`);
|
|
279
|
+
return req.originalReq.url;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 作为备选,继续尝试从请求头获取
|
|
284
|
+
// 尝试从 x-whistle-real-url 头获取
|
|
285
|
+
const whistleRealUrl = req.headers['x-whistle-real-url'];
|
|
286
|
+
if (whistleRealUrl) {
|
|
287
|
+
logMessage(`从x-whistle-real-url头获取原始URL: ${whistleRealUrl}`);
|
|
288
|
+
return whistleRealUrl;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 尝试从 x-forwarded-url 头获取
|
|
292
|
+
const forwardedUrl = req.headers['x-forwarded-url'];
|
|
293
|
+
if (forwardedUrl) {
|
|
294
|
+
logMessage(`从x-forwarded-url头获取原始URL: ${forwardedUrl}`);
|
|
295
|
+
return forwardedUrl;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 从规则值中获取信息
|
|
299
|
+
const ruleValue = req.headers['x-whistle-rule-value'];
|
|
300
|
+
if (ruleValue) {
|
|
301
|
+
logMessage(`从规则值获取信息: ${ruleValue}`);
|
|
302
|
+
|
|
303
|
+
// 尝试从规则值中提取完整URL
|
|
304
|
+
try {
|
|
305
|
+
const match = /^(?:whistle\.mock-plugin:\/\/)?(.*)$/.exec(ruleValue);
|
|
306
|
+
if (match && match[1]) {
|
|
307
|
+
// 如果规则值包含URL,则直接使用
|
|
308
|
+
if (match[1].startsWith('http')) {
|
|
309
|
+
logMessage(`从规则值提取到完整URL: ${match[1]}`);
|
|
310
|
+
return match[1];
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
} catch (e) {
|
|
314
|
+
logMessage(`解析规则值出错: ${e.message}`);
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
logMessage(`请求头中没有x-whistle-rule-value`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 如果没有找到原始URL,使用请求URL
|
|
321
|
+
logMessage(`无法获取原始URL,使用请求URL: ${req.url}`);
|
|
322
|
+
return req.url;
|
|
323
|
+
} catch (err) {
|
|
324
|
+
logMessage(`解析原始URL失败: ${err.message}`);
|
|
325
|
+
return req.url;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// 获取所有请求头信息并记录
|
|
330
|
+
const logAllHeaders = (req) => {
|
|
331
|
+
try {
|
|
332
|
+
logMessage('===== 请求头信息 =====');
|
|
333
|
+
Object.keys(req.headers).forEach(headerName => {
|
|
334
|
+
logMessage(` ${headerName}: ${req.headers[headerName]}`);
|
|
335
|
+
});
|
|
336
|
+
logMessage('=====================');
|
|
337
|
+
} catch (err) {
|
|
338
|
+
logMessage(`记录请求头失败: ${err.message}`);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// 添加中间件记录所有请求
|
|
343
|
+
app.use((req, res, next) => {
|
|
344
|
+
try {
|
|
345
|
+
logMessage(`----- 新请求开始 -----`);
|
|
346
|
+
logMessage(`收到请求: ${req.method} ${req.url}`);
|
|
347
|
+
logAllHeaders(req);
|
|
348
|
+
next();
|
|
349
|
+
} catch (err) {
|
|
350
|
+
logMessage(`请求日志中间件错误: ${err.message}`);
|
|
351
|
+
next();
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// 处理请求
|
|
356
|
+
app.use(async (req, res, next) => {
|
|
357
|
+
try {
|
|
358
|
+
logMessage(`----- 新请求开始 -----`);
|
|
359
|
+
logMessage(`收到请求: ${req.method} ${req.url}`);
|
|
360
|
+
|
|
361
|
+
// 记录请求头
|
|
362
|
+
logAllHeaders(req);
|
|
363
|
+
|
|
364
|
+
// 获取规则值
|
|
365
|
+
const ruleValue = req.headers['x-whistle-rule-value'];
|
|
366
|
+
logMessage(`规则值: ${ruleValue || '无'}`);
|
|
367
|
+
|
|
368
|
+
// 使用规则管理器处理请求
|
|
369
|
+
let result;
|
|
370
|
+
try {
|
|
371
|
+
// 使用当前 require 的 ruleManager
|
|
372
|
+
const ruleManagerModule = require('./ruleManager');
|
|
373
|
+
// 传递next参数,确保未匹配的请求能够继续
|
|
374
|
+
result = await ruleManagerModule.handleRequest(req, res, ruleValue, next);
|
|
375
|
+
} catch (err) {
|
|
376
|
+
logMessage(`规则管理器处理请求失败: ${err.message}`);
|
|
377
|
+
result = { handled: false };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (result && result.handled) {
|
|
381
|
+
logMessage(`规则管理器成功处理了请求`);
|
|
382
|
+
return; // 请求已处理,无需继续
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// 如果规则管理器未处理,使用旧的处理方式
|
|
386
|
+
logMessage(`规则管理器未处理请求,使用旧的处理方式`);
|
|
387
|
+
handleLegacyRequest(req, res, next);
|
|
388
|
+
} catch (err) {
|
|
389
|
+
logMessage(`请求处理过程中发生错误: ${err.message}`);
|
|
390
|
+
console.error('请求处理错误:', err);
|
|
391
|
+
|
|
392
|
+
// 返回500错误
|
|
393
|
+
res.status(500).json({
|
|
394
|
+
code: 500,
|
|
395
|
+
message: '内部服务器错误: ' + err.message,
|
|
396
|
+
data: null
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// 旧的请求处理方式,保持向后兼容
|
|
402
|
+
const handleLegacyRequest = (req, res, next) => {
|
|
403
|
+
const http = require('http');
|
|
404
|
+
const https = require('https');
|
|
405
|
+
const { Readable } = require('stream');
|
|
406
|
+
|
|
407
|
+
// 获取原始完整URL的所有可能来源
|
|
408
|
+
let originalUrl = null;
|
|
409
|
+
|
|
410
|
+
// 首先尝试从 originalReq 获取 (Whistle规范推荐方式)
|
|
411
|
+
if (req.originalReq) {
|
|
412
|
+
if (req.originalReq.realUrl) {
|
|
413
|
+
originalUrl = req.originalReq.realUrl;
|
|
414
|
+
logMessage(`从req.originalReq.realUrl获取原始URL: ${originalUrl}`);
|
|
415
|
+
} else if (req.originalReq.url) {
|
|
416
|
+
originalUrl = req.originalReq.url;
|
|
417
|
+
logMessage(`从req.originalReq.url获取原始URL: ${originalUrl}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 如果 originalReq 没有提供有效URL,尝试从请求头获取
|
|
422
|
+
if (!originalUrl || !originalUrl.startsWith('http')) {
|
|
423
|
+
const whistleRealUrl = req.headers['x-whistle-real-url'];
|
|
424
|
+
if (whistleRealUrl && whistleRealUrl.startsWith('http')) {
|
|
425
|
+
originalUrl = whistleRealUrl;
|
|
426
|
+
logMessage(`从x-whistle-real-url头获取原始URL: ${originalUrl}`);
|
|
427
|
+
} else {
|
|
428
|
+
const forwardedUrl = req.headers['x-forwarded-url'];
|
|
429
|
+
if (forwardedUrl && forwardedUrl.startsWith('http')) {
|
|
430
|
+
originalUrl = forwardedUrl;
|
|
431
|
+
logMessage(`从x-forwarded-url头获取原始URL: ${originalUrl}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// 如果仍然没有找到原始URL,尝试从规则值获取
|
|
437
|
+
if (!originalUrl || !originalUrl.startsWith('http')) {
|
|
438
|
+
const ruleValue = req.headers['x-whistle-rule-value'];
|
|
439
|
+
if (ruleValue) {
|
|
440
|
+
try {
|
|
441
|
+
const match = /^(?:whistle\.mock-plugin:\/\/)?(.*)$/.exec(ruleValue);
|
|
442
|
+
if (match && match[1] && match[1].startsWith('http')) {
|
|
443
|
+
originalUrl = match[1];
|
|
444
|
+
logMessage(`从规则值提取到完整URL: ${originalUrl}`);
|
|
445
|
+
}
|
|
446
|
+
} catch (e) {
|
|
447
|
+
logMessage(`解析规则值出错: ${e.message}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// 如果没有找到有效的原始URL,则无法继续
|
|
453
|
+
if (!originalUrl || !originalUrl.startsWith('http')) {
|
|
454
|
+
logMessage(`没有找到有效的原始URL,无法转发请求`);
|
|
455
|
+
res.statusCode = 400;
|
|
456
|
+
res.end(JSON.stringify({
|
|
457
|
+
code: 400,
|
|
458
|
+
message: '无法确定原始请求URL',
|
|
459
|
+
data: null
|
|
460
|
+
}));
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
// 解析URL
|
|
466
|
+
const urlObj = new URL(originalUrl);
|
|
467
|
+
const isHttps = urlObj.protocol === 'https:';
|
|
468
|
+
|
|
469
|
+
// 准备请求选项
|
|
470
|
+
const options = {
|
|
471
|
+
protocol: urlObj.protocol,
|
|
472
|
+
hostname: urlObj.hostname,
|
|
473
|
+
port: urlObj.port || (isHttps ? 443 : 80),
|
|
474
|
+
path: urlObj.pathname + urlObj.search,
|
|
475
|
+
method: req.method,
|
|
476
|
+
headers: {...req.headers}
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
// 删除可能导致问题的请求头
|
|
480
|
+
delete options.headers.host;
|
|
481
|
+
delete options.headers['x-whistle-real-url'];
|
|
482
|
+
delete options.headers['x-whistle-rule-value'];
|
|
483
|
+
delete options.headers['x-forwarded-url'];
|
|
484
|
+
|
|
485
|
+
// 设置正确的Host头
|
|
486
|
+
options.headers.host = urlObj.host;
|
|
487
|
+
|
|
488
|
+
logMessage(`准备转发请求到: ${originalUrl}, 方法: ${req.method}`);
|
|
489
|
+
|
|
490
|
+
// 选择http或https模块
|
|
491
|
+
const httpModule = isHttps ? https : http;
|
|
492
|
+
|
|
493
|
+
// 创建请求并处理响应
|
|
494
|
+
const proxyReq = httpModule.request(options, (proxyRes) => {
|
|
495
|
+
logMessage(`收到来自 ${originalUrl} 的响应,状态码: ${proxyRes.statusCode}`);
|
|
496
|
+
|
|
497
|
+
// 复制响应头
|
|
498
|
+
Object.keys(proxyRes.headers).forEach(key => {
|
|
499
|
+
res.setHeader(key, proxyRes.headers[key]);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// 设置状态码
|
|
503
|
+
res.statusCode = proxyRes.statusCode;
|
|
504
|
+
|
|
505
|
+
// 传输响应体
|
|
506
|
+
proxyRes.pipe(res);
|
|
507
|
+
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// 错误处理
|
|
511
|
+
proxyReq.on('error', (error) => {
|
|
512
|
+
logMessage(`转发请求到 ${originalUrl} 时出错: ${error.message}`);
|
|
513
|
+
|
|
514
|
+
// 如果响应尚未发送,返回错误信息
|
|
515
|
+
if (!res.headersSent) {
|
|
516
|
+
res.statusCode = 502;
|
|
517
|
+
res.end(JSON.stringify({
|
|
518
|
+
code: 502,
|
|
519
|
+
message: `转发请求到原始地址失败: ${error.message}`,
|
|
520
|
+
data: null
|
|
521
|
+
}));
|
|
522
|
+
} else {
|
|
523
|
+
// 如果已经发送了一部分响应,尝试结束响应
|
|
524
|
+
try {
|
|
525
|
+
res.end();
|
|
526
|
+
} catch (e) {
|
|
527
|
+
logMessage(`尝试结束已经部分发送的响应时出错: ${e.message}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// 处理超时
|
|
533
|
+
proxyReq.setTimeout(30000, () => {
|
|
534
|
+
logMessage(`转发请求到 ${originalUrl} 超时`);
|
|
535
|
+
proxyReq.destroy(new Error('请求超时'));
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// 转发请求体
|
|
539
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
540
|
+
// 如果请求有体,则转发
|
|
541
|
+
if (req.readable) {
|
|
542
|
+
req.pipe(proxyReq);
|
|
543
|
+
} else {
|
|
544
|
+
// 如果请求体已被读取,尝试重建并发送
|
|
545
|
+
if (req.body) {
|
|
546
|
+
const bodyStr = typeof req.body === 'string' ? req.body : JSON.stringify(req.body);
|
|
547
|
+
proxyReq.write(bodyStr);
|
|
548
|
+
proxyReq.end();
|
|
549
|
+
} else {
|
|
550
|
+
proxyReq.end();
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
} else {
|
|
554
|
+
// 对于GET和HEAD请求,直接结束请求
|
|
555
|
+
proxyReq.end();
|
|
556
|
+
}
|
|
557
|
+
} catch (error) {
|
|
558
|
+
logMessage(`准备转发请求时发生错误: ${error.message}`);
|
|
559
|
+
res.statusCode = 500;
|
|
560
|
+
res.end(JSON.stringify({
|
|
561
|
+
code: 500,
|
|
562
|
+
message: `内部服务器错误: ${error.message}`,
|
|
563
|
+
data: null
|
|
564
|
+
}));
|
|
565
|
+
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
// 处理响应
|
|
570
|
+
const handleResponse = (interfaceItem, req, res) => {
|
|
571
|
+
logMessage(`匹配到接口: ${interfaceItem.name}, ID: ${interfaceItem.id}`);
|
|
572
|
+
logMessage(`URL匹配规则: ${interfaceItem.urlPattern}`);
|
|
573
|
+
|
|
574
|
+
// 记录请求详情
|
|
575
|
+
logMessage(`请求方法: ${req.method}`);
|
|
576
|
+
logMessage(`原始URL: ${req.url}`);
|
|
577
|
+
|
|
578
|
+
// 设置响应状态码
|
|
579
|
+
const statusCode = parseInt(interfaceItem.httpStatus, 10) || 200;
|
|
580
|
+
|
|
581
|
+
// 模拟延迟
|
|
582
|
+
const delay = interfaceItem.responseDelay || 0;
|
|
583
|
+
if (delay > 0) {
|
|
584
|
+
logMessage(`模拟延迟处理: ${delay}ms`);
|
|
585
|
+
setTimeout(() => processResponse(), delay);
|
|
586
|
+
} else {
|
|
587
|
+
processResponse();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function processResponse() {
|
|
591
|
+
// 根据代理类型处理
|
|
592
|
+
switch (interfaceItem.proxyType) {
|
|
593
|
+
case 'response':
|
|
594
|
+
try {
|
|
595
|
+
// 设置响应头
|
|
596
|
+
if (interfaceItem.contentType) {
|
|
597
|
+
res.setHeader('Content-Type', interfaceItem.contentType);
|
|
598
|
+
} else {
|
|
599
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// 添加调试头信息
|
|
603
|
+
res.setHeader('X-Mock-Plugin', 'whistle.mock-plugin');
|
|
604
|
+
res.setHeader('X-Mock-Interface', interfaceItem.name);
|
|
605
|
+
res.setHeader('X-Mock-Interface-Id', interfaceItem.id);
|
|
606
|
+
res.setHeader('X-Mock-Feature-Id', interfaceItem.featureId);
|
|
607
|
+
|
|
608
|
+
// 尝试解析JSON
|
|
609
|
+
let responseBody = interfaceItem.responseContent;
|
|
610
|
+
logMessage(`响应内容(前50字符): ${responseBody.substring(0, 50)}...`);
|
|
611
|
+
|
|
612
|
+
try {
|
|
613
|
+
// 检查内容类型,如果是JSON则应用Mock.js
|
|
614
|
+
if (interfaceItem.contentType && interfaceItem.contentType.includes('json')) {
|
|
615
|
+
// 如果是JSON字符串,先解析为对象
|
|
616
|
+
const jsonData = JSON.parse(responseBody);
|
|
617
|
+
logMessage(`成功解析为JSON对象`);
|
|
618
|
+
|
|
619
|
+
// 使用Mock.js处理模板
|
|
620
|
+
const mockedData = Mock.mock(jsonData);
|
|
621
|
+
logMessage(`Mock.js处理完成`);
|
|
622
|
+
|
|
623
|
+
// 返回处理后的JSON
|
|
624
|
+
res.status(statusCode).json(mockedData);
|
|
625
|
+
logMessage(`响应JSON数据(状态码: ${statusCode}): ${JSON.stringify(mockedData).substr(0, 200)}...`);
|
|
626
|
+
} else {
|
|
627
|
+
// 不是JSON,直接返回文本
|
|
628
|
+
res.status(statusCode).send(responseBody);
|
|
629
|
+
logMessage(`响应文本数据(状态码: ${statusCode}): ${responseBody.substr(0, 200)}...`);
|
|
630
|
+
}
|
|
631
|
+
} catch (e) {
|
|
632
|
+
// JSON解析出错,直接返回原始内容
|
|
633
|
+
logMessage(`JSON解析错误,作为文本返回: ${e.message}`);
|
|
634
|
+
res.status(statusCode).send(responseBody);
|
|
635
|
+
}
|
|
636
|
+
} catch (err) {
|
|
637
|
+
logMessage('处理响应内容错误: ' + err.message);
|
|
638
|
+
res.status(500).json({
|
|
639
|
+
code: 500,
|
|
640
|
+
message: '处理响应内容错误: ' + err.message,
|
|
641
|
+
data: null
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
break;
|
|
645
|
+
|
|
646
|
+
case 'url':
|
|
647
|
+
// URL重定向 - 实际场景中这需要代理到目标URL
|
|
648
|
+
logMessage(`URL重定向: ${req.url} -> ${interfaceItem.targetUrl}`);
|
|
649
|
+
res.status(statusCode).json({
|
|
650
|
+
code: 302,
|
|
651
|
+
message: '需要重定向到: ' + interfaceItem.targetUrl,
|
|
652
|
+
data: {
|
|
653
|
+
originalUrl: req.url,
|
|
654
|
+
targetUrl: interfaceItem.targetUrl
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
break;
|
|
658
|
+
|
|
659
|
+
case 'file':
|
|
660
|
+
try {
|
|
661
|
+
// 文件代理 - 读取文件内容并返回
|
|
662
|
+
const filePath = interfaceItem.filePath;
|
|
663
|
+
|
|
664
|
+
if (!filePath) {
|
|
665
|
+
logMessage('文件路径未指定');
|
|
666
|
+
return res.status(400).json({
|
|
667
|
+
code: 400,
|
|
668
|
+
message: '文件路径未指定',
|
|
669
|
+
data: null
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// 构建绝对路径 - 使用相对插件根目录的路径
|
|
674
|
+
const absolutePath = path.resolve(process.cwd(), filePath);
|
|
675
|
+
logMessage(`读取文件: ${absolutePath}`);
|
|
676
|
+
|
|
677
|
+
if (!fs.existsSync(absolutePath)) {
|
|
678
|
+
logMessage(`文件不存在: ${absolutePath}`);
|
|
679
|
+
return res.status(404).json({
|
|
680
|
+
code: 404,
|
|
681
|
+
message: '文件不存在: ' + filePath,
|
|
682
|
+
data: null
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const content = fs.readFileSync(absolutePath, 'utf8');
|
|
687
|
+
|
|
688
|
+
// 尝试解析JSON
|
|
689
|
+
try {
|
|
690
|
+
const jsonData = JSON.parse(content);
|
|
691
|
+
const mockedData = Mock.mock(jsonData);
|
|
692
|
+
// 设置响应头
|
|
693
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
694
|
+
res.setHeader('X-Mock-Plugin', 'whistle.mock-plugin');
|
|
695
|
+
res.setHeader('X-Mock-Interface', interfaceItem.name);
|
|
696
|
+
|
|
697
|
+
res.status(statusCode).json(mockedData);
|
|
698
|
+
logMessage(`响应文件内JSON数据(状态码: ${statusCode}): ${JSON.stringify(mockedData).substr(0, 200)}...`);
|
|
699
|
+
} catch (e) {
|
|
700
|
+
// 不是JSON,直接返回文件内容
|
|
701
|
+
res.status(statusCode).send(content);
|
|
702
|
+
logMessage(`响应文件内文本数据(状态码: ${statusCode}): ${content.substr(0, 200)}...`);
|
|
703
|
+
}
|
|
704
|
+
} catch (err) {
|
|
705
|
+
logMessage('读取文件错误: ' + err.message);
|
|
706
|
+
res.status(500).json({
|
|
707
|
+
code: 500,
|
|
708
|
+
message: '读取文件错误: ' + err.message,
|
|
709
|
+
data: null
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
break;
|
|
713
|
+
|
|
714
|
+
default:
|
|
715
|
+
logMessage(`不支持的代理类型: ${interfaceItem.proxyType}`);
|
|
716
|
+
res.status(400).json({
|
|
717
|
+
code: 400,
|
|
718
|
+
message: '不支持的代理类型: ' + interfaceItem.proxyType,
|
|
719
|
+
data: null
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
} // 结束 processResponse 函数
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
// 启动服务器
|
|
726
|
+
const server = app.listen(0);
|
|
727
|
+
|
|
728
|
+
// 导出启动服务器的函数
|
|
729
|
+
module.exports = function startServer(server, options) {
|
|
730
|
+
// 确保之前的日志存在
|
|
731
|
+
try {
|
|
732
|
+
logMessage('------------------------------');
|
|
733
|
+
logMessage('启动 whistle.mock-plugin 服务器...');
|
|
734
|
+
logMessage('数据目录: ' + DATA_DIR);
|
|
735
|
+
logMessage('日志文件: ' + LOG_FILE);
|
|
736
|
+
|
|
737
|
+
// 加载并初始化规则管理器
|
|
738
|
+
try {
|
|
739
|
+
debugger
|
|
740
|
+
const ruleManager = require('./ruleManager');
|
|
741
|
+
|
|
742
|
+
// 初始化数据管理器
|
|
743
|
+
const dataManager = require('./dataManager');
|
|
744
|
+
dataManager.init({
|
|
745
|
+
baseDir: DATA_DIR,
|
|
746
|
+
log: logMessage
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
ruleManager.init({
|
|
750
|
+
server: server,
|
|
751
|
+
config: {
|
|
752
|
+
baseDir: DATA_DIR
|
|
753
|
+
},
|
|
754
|
+
dataManager: dataManager,
|
|
755
|
+
log: logMessage // 传递日志函数
|
|
756
|
+
});
|
|
757
|
+
logMessage('规则管理器初始化完成');
|
|
758
|
+
} catch (err) {
|
|
759
|
+
logMessage('规则管理器初始化失败: ' + err.message);
|
|
760
|
+
console.error('规则管理器初始化失败:', err);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// 输出插件版本
|
|
764
|
+
try {
|
|
765
|
+
const packagePath = path.resolve(__dirname, '../package.json');
|
|
766
|
+
if (fs.existsSync(packagePath)) {
|
|
767
|
+
const packageJson = require(packagePath);
|
|
768
|
+
logMessage(`插件版本: ${packageJson.name}@${packageJson.version}`);
|
|
769
|
+
}
|
|
770
|
+
} catch (e) {
|
|
771
|
+
logMessage('获取插件版本失败: ' + e.message);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// 输出Whistle信息
|
|
775
|
+
if (options && options.storage) {
|
|
776
|
+
logMessage(`Whistle存储目录: ${JSON.stringify(options.storage)}`);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (options && options.type) {
|
|
780
|
+
logMessage(`Whistle插件类型: ${options.type}`);
|
|
781
|
+
}
|
|
782
|
+
} catch (e) {
|
|
783
|
+
console.error('记录启动日志失败:', e);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// 检查和初始化配置文件
|
|
787
|
+
if (!fs.existsSync(FEATURES_FILE) || !fs.existsSync(INTERFACES_FILE)) {
|
|
788
|
+
logMessage('初始化配置文件...');
|
|
789
|
+
if (!fs.existsSync(FEATURES_FILE)) {
|
|
790
|
+
fs.writeJsonSync(FEATURES_FILE, { features: [] }, { spaces: 2 });
|
|
791
|
+
logMessage('创建了features.json文件');
|
|
792
|
+
}
|
|
793
|
+
if (!fs.existsSync(INTERFACES_FILE)) {
|
|
794
|
+
fs.writeJsonSync(INTERFACES_FILE, { interfaces: [] }, { spaces: 2 });
|
|
795
|
+
logMessage('创建了interfaces.json文件');
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// 记录whistle传入的options
|
|
800
|
+
logMessage('Whistle插件选项: ' + JSON.stringify(options));
|
|
801
|
+
|
|
802
|
+
server.on('request', app);
|
|
803
|
+
|
|
804
|
+
logMessage('whistle.mock-plugin 服务器已启动');
|
|
805
|
+
};
|