whistle.interceptors 0.0.3 → 0.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/.console.log +24 -2
- package/CHANGELOG.md +40 -0
- package/commitlint.config.js +1 -0
- package/dist/server.js +1 -1
- package/dist/uiServer/index.js +3 -1
- package/package.json +15 -3
- package/public/assets/index-B49r7c_G.css +1 -0
- package/public/assets/index-B6ysER9Q.js +85 -0
- package/public/index.html +2 -2
- package/src/server.ts +117 -71
- package/src/types/rule.ts +10 -4
- package/src/uiServer/constant.ts +8 -0
- package/src/uiServer/router.ts +51 -23
- package/public/assets/index-Bn3cFRn4.css +0 -1
- package/public/assets/index-yFos5qg5.js +0 -83
package/public/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>Vite + Svelte + TS</title>
|
|
8
|
-
<script type="module" crossorigin src="./assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
8
|
+
<script type="module" crossorigin src="./assets/index-B6ysER9Q.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="./assets/index-B49r7c_G.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="app"></div>
|
package/src/server.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LOCAL_PREFIX } from "./uiServer/constant"
|
|
1
|
+
import { LOCAL_PREFIX, PROXY_MODE } from "./uiServer/constant"
|
|
2
2
|
import { Rule } from "./types/rule"
|
|
3
3
|
|
|
4
4
|
function parseQuery(queryString: string): Record<string, string> {
|
|
@@ -13,7 +13,7 @@ function parseQuery(queryString: string): Record<string, string> {
|
|
|
13
13
|
params[decodeURIComponent(key)] = decodeURIComponent(value);
|
|
14
14
|
}
|
|
15
15
|
});
|
|
16
|
-
|
|
16
|
+
|
|
17
17
|
return params;
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -26,61 +26,140 @@ function getBody(req: Whistle.PluginServerRequest): Promise<Record<string, strin
|
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
function handleAndMode({conditions, payload, res}: {
|
|
29
|
+
function handleAndMode({conditions, payload, res, req, options, extra}: {
|
|
30
30
|
conditions: Rule['config']['conditions'],
|
|
31
31
|
payload: Record<string, string>,
|
|
32
|
-
res: Whistle.PluginServerResponse
|
|
32
|
+
res: Whistle.PluginServerResponse,
|
|
33
|
+
req: Whistle.PluginServerRequest,
|
|
34
|
+
options: Whistle.PluginOptions,
|
|
35
|
+
extra: {
|
|
36
|
+
origin: string
|
|
37
|
+
}
|
|
33
38
|
}) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
let allMatch = true;
|
|
40
|
+
let firstMatchingCondition: any = null;
|
|
41
|
+
let firstMatchingIndex = -1;
|
|
42
|
+
|
|
43
|
+
// 检查所有条件是否都匹配
|
|
44
|
+
for (let i = 0; i < conditions.length; i++) {
|
|
45
|
+
const condition = conditions[i];
|
|
46
|
+
if (!condition.enabled) continue;
|
|
47
|
+
|
|
48
|
+
// 检查所有key-value对是否都匹配
|
|
49
|
+
const isMatch = condition.pairs.every(pair =>
|
|
50
|
+
pair.key && pair.value && payload[pair.key] === pair.value
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (!isMatch) {
|
|
54
|
+
allMatch = false;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (firstMatchingCondition === null) {
|
|
59
|
+
firstMatchingCondition = condition;
|
|
60
|
+
firstMatchingIndex = i;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
38
63
|
|
|
39
|
-
if (!
|
|
64
|
+
if (!allMatch) {
|
|
40
65
|
return true
|
|
41
66
|
}
|
|
42
67
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
68
|
+
if (firstMatchingCondition?.proxyMode === PROXY_MODE.NETWORK) {
|
|
69
|
+
req.getSession(session => {
|
|
70
|
+
// 以condition为维度保存结果
|
|
71
|
+
const conditionId = `${firstMatchingCondition.ruleId}_${firstMatchingIndex}`;
|
|
72
|
+
// @ts-ignore
|
|
73
|
+
options.localStorage.setProperty(`${LOCAL_PREFIX}_${conditionId}`, session.res.body)
|
|
74
|
+
})
|
|
75
|
+
return true
|
|
76
|
+
} else {
|
|
77
|
+
res.setHeader('whistle-plugin', 'whistle.interceptors');
|
|
78
|
+
res.setHeader('Content-Type', 'application/json; charset=UTF-8');
|
|
79
|
+
res.setHeader('Access-Control-Allow-Origin', extra.origin);
|
|
80
|
+
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
81
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With')
|
|
82
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST,GET,OPTIONS,PUT,DELETE,HEAD')
|
|
83
|
+
res.end(firstMatchingCondition?.response || conditions[0].response)
|
|
84
|
+
}
|
|
46
85
|
|
|
47
86
|
}
|
|
48
87
|
|
|
49
|
-
function handleOrMode({conditions, payload, res}: {
|
|
88
|
+
function handleOrMode({conditions, payload, res, req, options, extra}: {
|
|
50
89
|
conditions: Rule['config']['conditions'],
|
|
51
90
|
payload: Record<string, string>,
|
|
52
|
-
res: Whistle.PluginServerResponse
|
|
91
|
+
res: Whistle.PluginServerResponse,
|
|
92
|
+
req: Whistle.PluginServerRequest,
|
|
93
|
+
options:Whistle.PluginOptions,
|
|
94
|
+
extra: {
|
|
95
|
+
origin: string
|
|
96
|
+
}
|
|
53
97
|
}) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
98
|
+
let matchingCondition: any = null;
|
|
99
|
+
let matchingIndex = -1;
|
|
100
|
+
|
|
101
|
+
// 查找匹配的条件及其索引
|
|
102
|
+
for (let i = 0; i < conditions.length; i++) {
|
|
103
|
+
const condition = conditions[i];
|
|
104
|
+
if (!condition.enabled) continue;
|
|
105
|
+
|
|
106
|
+
// 检查所有key-value对是否都匹配
|
|
107
|
+
const isMatch = condition.pairs.every(pair =>
|
|
108
|
+
pair.key && pair.value && payload[pair.key] === pair.value
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (isMatch) {
|
|
112
|
+
matchingCondition = condition;
|
|
113
|
+
matchingIndex = i;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
58
118
|
if (!matchingCondition) {
|
|
59
119
|
return true
|
|
60
120
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
121
|
+
|
|
122
|
+
if (matchingCondition.proxyMode === PROXY_MODE.NETWORK) {
|
|
123
|
+
req.getSession(session => {
|
|
124
|
+
// 以condition为维度保存结果
|
|
125
|
+
const conditionId = `${matchingCondition.ruleId}_${matchingIndex}`;
|
|
126
|
+
// @ts-ignore
|
|
127
|
+
options.localStorage.setProperty(`${LOCAL_PREFIX}_${conditionId}`, session.res.body)
|
|
128
|
+
})
|
|
129
|
+
return true
|
|
130
|
+
} else {
|
|
131
|
+
res.setHeader('whistle-plugin', 'whistle.interceptors');
|
|
132
|
+
res.setHeader('Content-Type', 'application/json; charset=UTF-8');
|
|
133
|
+
res.setHeader('Access-Control-Allow-Origin', extra.origin);
|
|
134
|
+
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
135
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With')
|
|
136
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST,GET,OPTIONS,PUT,DELETE,HEAD')
|
|
137
|
+
res.end(matchingCondition.response);
|
|
138
|
+
}
|
|
66
139
|
}
|
|
67
140
|
|
|
68
|
-
function handleMatchMode({matchType, conditions, payload, res, req}: {
|
|
141
|
+
function handleMatchMode({matchType, conditions, payload, res, req, options}: {
|
|
69
142
|
matchType: 'and' | 'or',
|
|
70
143
|
conditions: Rule['config']['conditions'],
|
|
71
144
|
payload: Record<string, string>,
|
|
72
145
|
res: Whistle.PluginServerResponse,
|
|
73
146
|
req: Whistle.PluginServerRequest
|
|
147
|
+
options: Whistle.PluginOptions
|
|
74
148
|
}) {
|
|
75
149
|
const map = {
|
|
76
150
|
'and': handleAndMode,
|
|
77
151
|
'or': handleOrMode
|
|
78
152
|
}
|
|
79
|
-
|
|
153
|
+
|
|
80
154
|
const noMatch = map[matchType]({
|
|
81
155
|
conditions,
|
|
82
156
|
payload,
|
|
83
|
-
res
|
|
157
|
+
res,
|
|
158
|
+
req,
|
|
159
|
+
options,
|
|
160
|
+
extra: {
|
|
161
|
+
origin: req.headers.origin
|
|
162
|
+
}
|
|
84
163
|
})
|
|
85
164
|
|
|
86
165
|
if (noMatch) {
|
|
@@ -90,6 +169,7 @@ function handleMatchMode({matchType, conditions, payload, res, req}: {
|
|
|
90
169
|
|
|
91
170
|
|
|
92
171
|
export default (server: Whistle.PluginServer, options: Whistle.PluginOptions) => {
|
|
172
|
+
|
|
93
173
|
server.on('request', async (req: Whistle.PluginServerRequest, res: Whistle.PluginServerResponse) => {
|
|
94
174
|
|
|
95
175
|
try {
|
|
@@ -97,70 +177,36 @@ export default (server: Whistle.PluginServer, options: Whistle.PluginOptions) =>
|
|
|
97
177
|
const id = req.originalReq.ruleValue
|
|
98
178
|
const rules: Rule[] = JSON.parse(options.storage.getProperty(LOCAL_PREFIX)) || []
|
|
99
179
|
const targetRule = rules.filter((rule: Rule) => rule.id === id)[0]
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (req.method !== method) {
|
|
180
|
+
|
|
181
|
+
if (!targetRule) {
|
|
103
182
|
req.passThrough();
|
|
104
183
|
return
|
|
105
184
|
}
|
|
106
|
-
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
const { conditions } = targetRule.config
|
|
188
|
+
|
|
107
189
|
let payLoad: Record<string, string>
|
|
108
|
-
if (method === 'POST') {
|
|
190
|
+
if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') {
|
|
109
191
|
payLoad = await getBody(req)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (method === 'GET') {
|
|
192
|
+
} else {
|
|
113
193
|
// @ts-ignore
|
|
114
194
|
payLoad = parseQuery(options.parseUrl(req.fullUrl).query)
|
|
115
195
|
}
|
|
196
|
+
|
|
197
|
+
const matchType = 'or'
|
|
116
198
|
|
|
117
199
|
handleMatchMode({
|
|
118
200
|
matchType,
|
|
119
201
|
conditions,
|
|
120
202
|
res,
|
|
121
203
|
req,
|
|
204
|
+
options,
|
|
122
205
|
payload: payLoad
|
|
123
206
|
})
|
|
124
207
|
} catch (error) {
|
|
125
208
|
req.passThrough();
|
|
126
209
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// if (matchType === "or") {
|
|
130
|
-
// const matchingCondition = conditions.find(
|
|
131
|
-
// ({ key, value }) => payLoad[key] === value
|
|
132
|
-
// );
|
|
133
|
-
// if (matchingCondition) {
|
|
134
|
-
// res.setHeader('whistle-plugin', 'whistle.interceptors');
|
|
135
|
-
// res.setHeader('Content-Type', 'application/json; charset=UTF-8');
|
|
136
|
-
// res.end(matchingCondition.response);
|
|
137
|
-
// return;
|
|
138
|
-
// }
|
|
139
|
-
// }
|
|
140
|
-
// if (matchType === 'and') {
|
|
141
|
-
// const isMatch = conditions.every((condition) => {
|
|
142
|
-
// const { key, value } = condition
|
|
143
|
-
// return payLoad[key] && payLoad[key] === value
|
|
144
|
-
// })
|
|
145
|
-
// if (isMatch) {
|
|
146
|
-
// res.setHeader('whistle-plugin', 'whistle.interceptors');
|
|
147
|
-
// res.setHeader('Content-Type', 'application/json; charset=UTF-8');
|
|
148
|
-
// res.end(conditions[0].response)
|
|
149
|
-
// return
|
|
150
|
-
// }
|
|
151
|
-
// }
|
|
152
|
-
|
|
153
210
|
});
|
|
154
211
|
|
|
155
|
-
// handle websocket request
|
|
156
|
-
server.on('upgrade', (req: Whistle.PluginServerRequest, socket: Whistle.PluginServerSocket) => {
|
|
157
|
-
// do something
|
|
158
|
-
req.passThrough();
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
// handle tunnel request
|
|
162
|
-
server.on('connect', (req: Whistle.PluginServerRequest, socket: Whistle.PluginServerSocket) => {
|
|
163
|
-
// do something
|
|
164
|
-
req.passThrough();
|
|
165
|
-
});
|
|
166
212
|
};
|
package/src/types/rule.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
+
export interface KeyValuePair {
|
|
2
|
+
key: string;
|
|
3
|
+
value: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
1
6
|
export interface Rule {
|
|
2
7
|
id: string;
|
|
3
8
|
name: string;
|
|
4
9
|
config: {
|
|
5
|
-
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
6
|
-
matchType: 'and' | 'or';
|
|
7
10
|
conditions: {
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
ruleId: string;
|
|
12
|
+
pairs: KeyValuePair[];
|
|
10
13
|
response: string;
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
remark?: string;
|
|
16
|
+
proxyMode?: 'network' | 'mock';
|
|
11
17
|
}[];
|
|
12
18
|
};
|
|
13
19
|
}
|
package/src/uiServer/constant.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
|
|
2
2
|
export const LOCAL_PREFIX = 'whistle.interceptors'
|
|
3
3
|
|
|
4
|
+
// export const LOCAL
|
|
5
|
+
|
|
6
|
+
export const PROXY_MODE = {
|
|
7
|
+
NETWORK: 'network',
|
|
8
|
+
MOCK: 'mock',
|
|
9
|
+
}
|
|
10
|
+
|
|
4
11
|
export const apis = {
|
|
5
12
|
get: '/collections/query',
|
|
6
13
|
add: '/collections/add',
|
|
7
14
|
delete: '/collections/delete',
|
|
15
|
+
sse: '/collections/sse',
|
|
8
16
|
}
|
|
9
17
|
|
|
10
18
|
|
package/src/uiServer/router.ts
CHANGED
|
@@ -1,40 +1,68 @@
|
|
|
1
|
-
import Router from
|
|
2
|
-
import { apis, LOCAL_PREFIX } from
|
|
3
|
-
|
|
4
|
-
type RouterContext = {
|
|
5
|
-
storage: Whistle.Storage
|
|
6
|
-
} & Router.IRouterContext
|
|
1
|
+
import Router from "koa-router";
|
|
2
|
+
import { apis, LOCAL_PREFIX } from "./constant";
|
|
7
3
|
|
|
4
|
+
type RouterContext = {
|
|
5
|
+
storage: Whistle.Storage;
|
|
6
|
+
} & Router.IRouterContext;
|
|
8
7
|
|
|
9
8
|
export default (router: Router) => {
|
|
10
|
-
|
|
11
9
|
router.get(apis.get, (ctx: RouterContext) => {
|
|
12
10
|
try {
|
|
13
11
|
const data = ctx.storage.getProperty(LOCAL_PREFIX);
|
|
14
|
-
console.log('get data', data)
|
|
15
12
|
ctx.body = {
|
|
16
|
-
result:
|
|
17
|
-
data: JSON.parse(data)
|
|
18
|
-
}
|
|
13
|
+
result: "ok",
|
|
14
|
+
data: data ? JSON.parse(data) : [],
|
|
15
|
+
};
|
|
19
16
|
} catch (error) {
|
|
20
17
|
ctx.body = {
|
|
21
|
-
result:
|
|
22
|
-
data:
|
|
23
|
-
}
|
|
18
|
+
result: "error",
|
|
19
|
+
data: "get rules error " + error,
|
|
20
|
+
};
|
|
24
21
|
}
|
|
25
22
|
});
|
|
26
|
-
|
|
27
|
-
router.post(apis.add, (ctx
|
|
28
|
-
console.log(
|
|
29
|
-
ctx.storage.setProperty(LOCAL_PREFIX, JSON.stringify(ctx.request.body))
|
|
23
|
+
|
|
24
|
+
router.post(apis.add, (ctx: RouterContext) => {
|
|
25
|
+
console.log("ssss", ctx.request.body, typeof ctx.request.body);
|
|
26
|
+
ctx.storage.setProperty(LOCAL_PREFIX, JSON.stringify(ctx.request.body));
|
|
30
27
|
ctx.body = {
|
|
31
|
-
result:
|
|
32
|
-
data: null
|
|
33
|
-
}
|
|
28
|
+
result: "ok",
|
|
29
|
+
data: null,
|
|
30
|
+
};
|
|
34
31
|
});
|
|
35
32
|
|
|
36
33
|
router.delete(apis.delete, (ctx) => {
|
|
37
|
-
console.log(
|
|
38
|
-
ctx.body =
|
|
34
|
+
console.log("ssss", ctx);
|
|
35
|
+
ctx.body = "ok";
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
router.get(apis.sse, (ctx: RouterContext) => {
|
|
39
|
+
|
|
40
|
+
const storage_prefix = ctx.query.storage_prefix as string
|
|
41
|
+
|
|
42
|
+
ctx.set("Content-Type", "text/event-stream");
|
|
43
|
+
ctx.set("Cache-Control", "no-cache");
|
|
44
|
+
ctx.set("Connection", "keep-alive");
|
|
45
|
+
|
|
46
|
+
ctx.status = 200;
|
|
47
|
+
ctx.respond = false; // 对于 Koa 框架
|
|
48
|
+
ctx.res.flushHeaders();
|
|
49
|
+
|
|
50
|
+
// 发送数据
|
|
51
|
+
const sendEvent = (data: any) => {
|
|
52
|
+
// console.log('[info: 49]:', '向客户端发送消息', data)
|
|
53
|
+
ctx.res.write(`data: ${data}\n\n`);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// 模拟实时数据发送
|
|
57
|
+
const interval = setInterval(() => {
|
|
58
|
+
// console.log('[info: 55]:', '模拟实时数据发送')
|
|
59
|
+
sendEvent(ctx.storage.getProperty(storage_prefix));
|
|
60
|
+
}, 1000);
|
|
61
|
+
|
|
62
|
+
// 当客户端关闭连接时清除定时器
|
|
63
|
+
ctx.req.on("close", () => {
|
|
64
|
+
clearInterval(interval);
|
|
65
|
+
ctx.storage.removeProperty(storage_prefix);
|
|
66
|
+
});
|
|
39
67
|
});
|
|
40
68
|
};
|