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/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-yFos5qg5.js"></script>
9
- <link rel="stylesheet" crossorigin href="./assets/index-Bn3cFRn4.css">
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
- const isMatch = conditions.every((condition) => {
35
- const { key, value } = condition
36
- return payload[key] && payload[key] === value
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 (!isMatch) {
64
+ if (!allMatch) {
40
65
  return true
41
66
  }
42
67
 
43
- res.setHeader('whistle-plugin', 'whistle.interceptors');
44
- res.setHeader('Content-Type', 'application/json; charset=UTF-8');
45
- res.end(conditions[0].response)
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
- const matchingCondition = conditions.find(
55
- ({ key, value }) => payload[key] === value
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
- res.setHeader('whistle-plugin', 'whistle.interceptors');
63
- res.setHeader('Content-Type', 'application/json; charset=UTF-8');
64
- res.end(matchingCondition.response);
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
- const { matchType, method, conditions } = targetRule.config
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
- key: string;
9
- value: string;
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
  }
@@ -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
 
@@ -1,40 +1,68 @@
1
- import Router from 'koa-router';
2
- import { apis, LOCAL_PREFIX } from './constant';
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: 'ok',
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: 'error',
22
- data: 'get rules error ' + error
23
- }
18
+ result: "error",
19
+ data: "get rules error " + error,
20
+ };
24
21
  }
25
22
  });
26
-
27
- router.post(apis.add, (ctx : RouterContext) => {
28
- console.log('ssss', ctx.request.body, typeof ctx.request.body)
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: 'ok',
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('ssss', ctx)
38
- ctx.body = 'ok'
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
  };