whistle.script 1.2.1
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/.editorconfig +18 -0
- package/.eslintrc +1 -0
- package/LICENSE +21 -0
- package/README.md +264 -0
- package/index.js +1 -0
- package/initialize.js +26 -0
- package/lib/auth.js +39 -0
- package/lib/dataSource.js +6 -0
- package/lib/index.js +22 -0
- package/lib/logger.js +56 -0
- package/lib/pipe.js +32 -0
- package/lib/resRulesServer.js +26 -0
- package/lib/rulesServer.js +41 -0
- package/lib/scripts.js +110 -0
- package/lib/server.js +32 -0
- package/lib/tunnelRulesServer.js +24 -0
- package/lib/tunnelServer.js +37 -0
- package/lib/uiServer/cgi-bin/create.js +9 -0
- package/lib/uiServer/cgi-bin/delete.js +9 -0
- package/lib/uiServer/cgi-bin/emitData.js +14 -0
- package/lib/uiServer/cgi-bin/init.js +10 -0
- package/lib/uiServer/cgi-bin/log.js +4 -0
- package/lib/uiServer/cgi-bin/rename.js +10 -0
- package/lib/uiServer/index.js +29 -0
- package/lib/uiServer/router.js +15 -0
- package/lib/util.js +192 -0
- package/lib/wsServer.js +107 -0
- package/package.json +55 -0
- package/public/index.html +12 -0
- package/public/index.js +75 -0
- package/rules.txt +1 -0
package/.editorconfig
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# http://editorconfig.org
|
|
2
|
+
root = true
|
|
3
|
+
|
|
4
|
+
[*]
|
|
5
|
+
charset = utf-8
|
|
6
|
+
end_of_line = lf
|
|
7
|
+
indent_size = 2
|
|
8
|
+
indent_style = space
|
|
9
|
+
insert_final_newline = true
|
|
10
|
+
max_line_length = 80
|
|
11
|
+
trim_trailing_whitespace = true
|
|
12
|
+
|
|
13
|
+
[*.md]
|
|
14
|
+
max_line_length = 0
|
|
15
|
+
trim_trailing_whitespace = false
|
|
16
|
+
|
|
17
|
+
[COMMIT_EDITMSG]
|
|
18
|
+
max_line_length = 0
|
package/.eslintrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "extends": "eslint-config-imweb" }
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 avenwu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# whistle.script
|
|
2
|
+
whistle.script为[whistle](https://github.com/avwo/whistle)的一个扩展脚本插件,可以直接在界面上引用全局安装的Node模块及Node的内容模块编写脚本操作请求及其响应,所有正常Node程序可以实现的功能,都可以通过该插件实现,包括:
|
|
3
|
+
|
|
4
|
+
1. HTTP[s]:
|
|
5
|
+
- 动态设置[whistle规则](https://avwo.github.io/whistle/rules/)
|
|
6
|
+
- 拦截请求响应
|
|
7
|
+
- 控制请求响应速度
|
|
8
|
+
- 修改请求url、请求方法、请求头、请求内容
|
|
9
|
+
- 修改响应状态码、响应头、响应内容
|
|
10
|
+
- 在插件界面的Console上显示脚本程序 `console.xxx` 的内容,如果可以打印响应的内容或调试信息等
|
|
11
|
+
2. WebSocket:
|
|
12
|
+
- 动态设置[whistle规则](https://avwo.github.io/whistle/rules/)
|
|
13
|
+
- 拦截请求响应
|
|
14
|
+
- 修改发送或收到的数据
|
|
15
|
+
- 直接向WebSocket客户端或服务端发送数据
|
|
16
|
+
- 在插件界面的Console上显示脚本程序 `console.xxx` 的内容,如果可以打印发送和接收到的数据或调试信息等,从而通过该插件可以直接查看WebSocket的数据
|
|
17
|
+
3. Tunnel: 基本功能同WebSocket,可以用来直接操作Socket请求,如Protobuf协议的请求等
|
|
18
|
+
|
|
19
|
+
# 安装
|
|
20
|
+
|
|
21
|
+
1. 安装Node: [官网下载安装最新版本(LTS和Stable都可以)](https://nodejs.org/)
|
|
22
|
+
2. 安装最新版的[whistle](https://github.com/avwo/whistle)。
|
|
23
|
+
|
|
24
|
+
npm install -g whistle
|
|
25
|
+
|
|
26
|
+
# Mac、Linux用户可能需要加sudo
|
|
27
|
+
sudo npm install -g whistle
|
|
28
|
+
|
|
29
|
+
3. 安装script插件:
|
|
30
|
+
|
|
31
|
+
w2 i whistle.script
|
|
32
|
+
|
|
33
|
+
# 使用
|
|
34
|
+
|
|
35
|
+
打开script插件的界面,创建一个名字为 `test` 的脚本:
|
|
36
|
+
|
|
37
|
+
1. 可以通过 `Plugins->Home->script`打开或右键并选择 `在新标签页中打开`
|
|
38
|
+
2. 直接访问 [http://local.whistlejs.com/plugin.script](http://local.whistlejs.com/plugin.script/)
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
#### 设置规则
|
|
43
|
+
|
|
44
|
+
1. 设置HTTP或HTTPs请求的[whistle规则](https://avwo.github.io/whistle/rules/)(操作HTTPs需要[开启HTTPs拦截](https://avwo.github.io/whistle/webui/https.html))
|
|
45
|
+
|
|
46
|
+
在界面中的`test` 脚本输入(也可以在其它编辑器编辑后再copy进来):
|
|
47
|
+
|
|
48
|
+
exports.handleRequestRules = (ctx) => {
|
|
49
|
+
// ctx.fullUrl 可以获取请求url
|
|
50
|
+
// ctx.headers 可以获取请求头
|
|
51
|
+
// ctx.options 里面包含一些特殊的请求头字段,分别可以获取一些额外信息,如请求方法、设置的规则等
|
|
52
|
+
ctx.rules = ['www.qq.com file://{test.html}'];
|
|
53
|
+
ctx.values = { 'test.html': 'Hello world.' };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
Note: 如果里面包含一些异步方法可以采用 async 函数,即:`exports.handleRequestRules = async () => {}`
|
|
57
|
+
|
|
58
|
+
在whistle的Rules配置界面上输入规则:
|
|
59
|
+
|
|
60
|
+
whistle.script://test www.ifeng.com www.qq.com www.baidu.com echo.websocket.org
|
|
61
|
+
|
|
62
|
+
分别访问[http://www.ifeng.com](http://www.ifeng.com)和[http://www.qq.com](http://www.qq.com),前者可以正常访问,后者输出 `Hello world.`。
|
|
63
|
+
|
|
64
|
+
具体效果见图:[demo1](https://user-images.githubusercontent.com/11450939/126302225-2598772c-d6a3-45e3-97d6-685fbed1ba37.gif)
|
|
65
|
+
|
|
66
|
+
如果需要通过配置给脚本传递一些额外参数,可以如下配置(注意中间不能有空格):
|
|
67
|
+
|
|
68
|
+
whistle.script://test(a,b,c) www.ifeng.com www.qq.com www.baidu.com echo.websocket.org
|
|
69
|
+
|
|
70
|
+
可以在脚本中通过 `process.args` 获取:
|
|
71
|
+
|
|
72
|
+
exports.handleRequestRules = (ctx) => {
|
|
73
|
+
console.log(process.args); // output: ["a", "b", "c"]
|
|
74
|
+
ctx.rules = ['www.qq.com file://{test.html}'];
|
|
75
|
+
ctx.values = { 'test.html': 'Hello world.' };
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
2. 设置WebSocket请求的规则(需要[开启HTTPs拦截](https://avwo.github.io/whistle/webui/https.html)):
|
|
79
|
+
|
|
80
|
+
exports.handleWebSocketRules = (ctx) => {
|
|
81
|
+
// ctx.fullUrl 可以获取请求url
|
|
82
|
+
// ctx.headers 可以获取请求头
|
|
83
|
+
// ctx.options 里面包含一些特殊的请求头字段,分别可以获取一些额外信息,如请求方法、设置的规则等
|
|
84
|
+
this.rules = '127.0.0.1 echo.websocket.org';
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
接下来的操作同上。
|
|
88
|
+
|
|
89
|
+
3. 设置Tunnel请求的规则(要测试可以暂时[关闭HTTPs拦截](https://avwo.github.io/whistle/webui/https.html)):
|
|
90
|
+
|
|
91
|
+
exports.handleTunnel = (ctx) => {
|
|
92
|
+
// ctx.fullUrl 可以获取请求url
|
|
93
|
+
// ctx.headers 可以获取请求头
|
|
94
|
+
// ctx.options 里面包含一些特殊的请求头字段,分别可以获取一些额外信息,如请求方法、设置的规则等
|
|
95
|
+
this.rules = '127.0.0.1 www.baidu.com';
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
接下来的操作同上。
|
|
100
|
+
|
|
101
|
+
#### 操作请求
|
|
102
|
+
|
|
103
|
+
1. 操作HTTP或HTTPs请求(操作HTTPs需要[开启HTTPs拦截](https://avwo.github.io/whistle/webui/https.html))
|
|
104
|
+
``` js
|
|
105
|
+
exports.handleRequest = async (ctx, request) => {
|
|
106
|
+
// ctx.fullUrl 可以获取请求url
|
|
107
|
+
// ctx.headers 可以获取请求头
|
|
108
|
+
// ctx.options 里面包含一些特殊的请求头字段,分别可以获取一些额外信息,如请设置的规则等
|
|
109
|
+
// ctx.method 获取和设置请求方法
|
|
110
|
+
// ctx.req
|
|
111
|
+
// ctx.res
|
|
112
|
+
const { req, res } = ctx;
|
|
113
|
+
const client = request((svrRes) => {
|
|
114
|
+
res.writeHead(svrRes.statusCode, svrRes.headers);
|
|
115
|
+
svrRes.pipe(res);
|
|
116
|
+
// try {
|
|
117
|
+
// const body = await ctx.getStreamBuffer(svrRes);
|
|
118
|
+
// delete svrRes.headers['content-encoding'];
|
|
119
|
+
// res.writeHead(svrRes.statusCode, svrRes.headers);
|
|
120
|
+
// res.end(body);
|
|
121
|
+
// } catch (err) {}
|
|
122
|
+
});
|
|
123
|
+
req.pipe(client);
|
|
124
|
+
};
|
|
125
|
+
```
|
|
126
|
+
在whistle的Rules配置界面上输入规则:
|
|
127
|
+
``` txt
|
|
128
|
+
# 这里不能用whistle.script,否则请求不会转发到handleRequest
|
|
129
|
+
# whistle.script只会执行handleXxxRules
|
|
130
|
+
# 你也可以通过在handleXxxRules里面设置 script://test(a,b,c),实现转发
|
|
131
|
+
script://test www.ifeng.com www.qq.com www.baidu.com echo.websocket.org
|
|
132
|
+
```
|
|
133
|
+
分别访问[http://www.ifeng.com](http://www.ifeng.com)和[http://www.qq.com](http://www.qq.com),可以在script的界面中的Consle看到打印出来的请求的url、响应状态吗和头部。
|
|
134
|
+
|
|
135
|
+
具体效果见图:[demo2](https://user-images.githubusercontent.com/11450939/126302210-e3aa0b56-9001-4e03-83c8-8986d8f544ff.gif)
|
|
136
|
+
|
|
137
|
+
需要在配置中带上参数,可以参考上面的规则设置
|
|
138
|
+
2. 操作WebSocket请求(需要[开启HTTPs拦截](https://avwo.github.io/whistle/webui/https.html))
|
|
139
|
+
``` js
|
|
140
|
+
exports.handleWebSocket = async (socket, connect) => {
|
|
141
|
+
// 与服务器建立连接
|
|
142
|
+
const svrSocket = await connect();
|
|
143
|
+
// 客户端 pong 服务端
|
|
144
|
+
socket.on('pong', (data) => {
|
|
145
|
+
svrSocket.pong(data);
|
|
146
|
+
});
|
|
147
|
+
// 客户端 ping 服务pong 端
|
|
148
|
+
socket.on('ping', (data) => {
|
|
149
|
+
svrSocket.ping(data);
|
|
150
|
+
});
|
|
151
|
+
// 服务端 ping 客户端
|
|
152
|
+
svrSocket.on('ping', (data) => {
|
|
153
|
+
socket.ping(data);
|
|
154
|
+
});
|
|
155
|
+
// 服务端 pong 客户端
|
|
156
|
+
svrSocket.on('pong', (data) => {
|
|
157
|
+
socket.pong(data);
|
|
158
|
+
});
|
|
159
|
+
// 正常断开 WebSocket 连接
|
|
160
|
+
socket.on('disconnect', (code, message, opts) => {
|
|
161
|
+
console.log(code, 'client disconnect');
|
|
162
|
+
svrSocket.disconnect(code, opts);
|
|
163
|
+
});
|
|
164
|
+
// 正常断开 WebSocket 连接
|
|
165
|
+
svrSocket.on('disconnect', (code, message, opts) => {
|
|
166
|
+
console.log(code, 'server disconnect');
|
|
167
|
+
socket.disconnect(code, opts);
|
|
168
|
+
});
|
|
169
|
+
// 获取客户端解析后的帧数据
|
|
170
|
+
socket.on('message', (data, opts) => {
|
|
171
|
+
console.log(data, 'client data');
|
|
172
|
+
svrSocket.send(data, opts);
|
|
173
|
+
});
|
|
174
|
+
// 获取服务端解析后的帧数据
|
|
175
|
+
svrSocket.on('message', (data, opts) => {
|
|
176
|
+
console.log(data, 'server data');
|
|
177
|
+
socket.send(data, opts);
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
whistle规则配置同上,访问[https://www.websocket.org/echo.html](https://www.websocket.org/echo.html),点击下面的connect按钮及send按钮,可以如下效果:[demo3](https://user-images.githubusercontent.com/11450939/126302243-26c8b4af-851c-4b00-87b9-3286e9e67251.gif)
|
|
184
|
+
3. 操作Tunnel请求
|
|
185
|
+
``` js
|
|
186
|
+
exports.handleTunnel = async (socket, connect) => {
|
|
187
|
+
const svrSocket = await connect();
|
|
188
|
+
socket.pipe(svrSocket).pipe(socket);
|
|
189
|
+
};
|
|
190
|
+
```
|
|
191
|
+
whistle规则配置同上
|
|
192
|
+
|
|
193
|
+
4. 鉴权
|
|
194
|
+
插件 `v1.2.0` 版本开始支持自定义鉴权方法(要求 Whistle 版本 >= `v2.7.16`):
|
|
195
|
+
``` js
|
|
196
|
+
exports.auth = async (req, options) => {
|
|
197
|
+
// 给请求添加自定义头,必须与 `x-whistle-` 开头
|
|
198
|
+
// 这样可以在插件的其他 hook 里面获取到该请求头(除了 http 请求的 reqRead 钩子)
|
|
199
|
+
req.setHeader('x-whistle-test', '1111111111');
|
|
200
|
+
// return false; // 直接返回 403
|
|
201
|
+
};
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
5. pipe
|
|
205
|
+
插件 `v1.2.1` 版本开始支持自定义 pipe 方法:
|
|
206
|
+
``` js
|
|
207
|
+
|
|
208
|
+
exports.handleReqRead = (req, res, options) => {
|
|
209
|
+
req.pipe(res);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
exports.handleReqWrite = (req, res, options) => {
|
|
213
|
+
req.pipe(res);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
exports.handleResRead = (req, res, options) => {
|
|
217
|
+
req.pipe(res);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
exports.handleResWrite = (req, res, options) => {
|
|
221
|
+
req.pipe(res);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
exports.handleWsReqRead = (req, res, options) => {
|
|
225
|
+
req.pipe(res);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
exports.handleWsReqWrite = (req, res, options) => {
|
|
229
|
+
req.pipe(res);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
exports.handleWsResRead = (req, res, options) => {
|
|
233
|
+
req.pipe(res);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
exports.handleWsResWrite = (req, res, options) => {
|
|
237
|
+
req.pipe(res);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
exports.handleTunnelReqRead = (req, res, options) => {
|
|
241
|
+
req.pipe(res);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
exports.handleTunnelReqWrite = (req, res, options) => {
|
|
245
|
+
req.pipe(res);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
exports.handleTunnelResRead = (req, res, options) => {
|
|
249
|
+
req.pipe(res);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
exports.handleTunnelResWrite = (req, res, options) => {
|
|
253
|
+
req.pipe(res);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
# 如何引入第三方模块
|
|
259
|
+
使用绝对路径引入,如假设你的模块安装路径为 `/Users/test/node_modules/xxx`,则可以在脚本里面通过 `require('/Users/test/node_modules/xxx')` 引入。
|
|
260
|
+
|
|
261
|
+
# License
|
|
262
|
+
|
|
263
|
+
[MIT](https://github.com/whistle-plugins/whistle.script/blob/master/LICENSE)
|
|
264
|
+
|
package/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./lib');
|
package/initialize.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
module.exports = ({ debugMode }) => {
|
|
4
|
+
const logger = require('./lib/logger');
|
|
5
|
+
const LEVELS = ['fatal', 'error', 'warn', 'info', 'debug', 'trace'];
|
|
6
|
+
|
|
7
|
+
/* eslint-disable no-console */
|
|
8
|
+
const originalLog = console.log;
|
|
9
|
+
console.log = (...args) => {
|
|
10
|
+
logger.log(args, 'log');
|
|
11
|
+
if (debugMode) {
|
|
12
|
+
originalLog.apply(console, args);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
LEVELS.forEach((level) => {
|
|
17
|
+
const originalFn = console[level];
|
|
18
|
+
console[level] = (...args) => {
|
|
19
|
+
logger.log(args, level);
|
|
20
|
+
if (debugMode) {
|
|
21
|
+
originalFn.apply(console, args);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
};
|
package/lib/auth.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const scripts = require('./scripts');
|
|
2
|
+
const { getRemoteUrl, getContext, getFn, noop, request, AUTH_URL, STATS_URL, DATA_URL, isRemote } = require('./util');
|
|
3
|
+
|
|
4
|
+
module.exports = async (req, options) => {
|
|
5
|
+
if (!isRemote(req)) {
|
|
6
|
+
const ctx = getContext(req);
|
|
7
|
+
const { auth, verify } = scripts.getHandler(ctx);
|
|
8
|
+
const check = getFn(auth, verify);
|
|
9
|
+
return check && check(req, options);
|
|
10
|
+
}
|
|
11
|
+
const authUrl = getRemoteUrl(req, AUTH_URL);
|
|
12
|
+
if (authUrl) {
|
|
13
|
+
const { auth, headers } = await request(authUrl, req.headers);
|
|
14
|
+
if (auth === false) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (headers) {
|
|
18
|
+
Object.keys(headers).forEach((key) => {
|
|
19
|
+
req.set(key, headers[key]);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const statsUrl = getRemoteUrl(req, STATS_URL);
|
|
24
|
+
const dataUrl = getRemoteUrl(req, DATA_URL);
|
|
25
|
+
if (statsUrl) {
|
|
26
|
+
req.getReqSession((session) => {
|
|
27
|
+
if (session) {
|
|
28
|
+
request(statsUrl, req.headers, session).then(noop);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
if (dataUrl) {
|
|
33
|
+
req.getSession((session) => {
|
|
34
|
+
if (session) {
|
|
35
|
+
request(dataUrl, req.headers, session).then(noop);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const handlePipe = require('./pipe');
|
|
2
|
+
|
|
3
|
+
exports.auth = require('./auth');
|
|
4
|
+
exports.uiServer = require('./uiServer');
|
|
5
|
+
exports.rulesServer = require('./rulesServer');
|
|
6
|
+
exports.resRulesServer = require('./resRulesServer');
|
|
7
|
+
exports.tunnelRulesServer = require('./tunnelRulesServer');
|
|
8
|
+
exports.server = require('./server');
|
|
9
|
+
exports.tunnelServer = require('./tunnelServer');
|
|
10
|
+
|
|
11
|
+
exports.reqRead = handlePipe('reqRead');
|
|
12
|
+
exports.reqWrite = handlePipe('reqWrite');
|
|
13
|
+
exports.resRead = handlePipe('resRead');
|
|
14
|
+
exports.resWrite = handlePipe('resWrite');
|
|
15
|
+
exports.wsReqRead = handlePipe('wsReqRead');
|
|
16
|
+
exports.wsReqWrite = handlePipe('wsReqWrite');
|
|
17
|
+
exports.wsResRead = handlePipe('wsResRead');
|
|
18
|
+
exports.wsResWrite = handlePipe('wsResWrite');
|
|
19
|
+
exports.tunnelReqRead = handlePipe('tunnelReqRead');
|
|
20
|
+
exports.tunnelReqWrite = handlePipe('tunnelReqWrite');
|
|
21
|
+
exports.tunnelResRead = handlePipe('tunnelResRead');
|
|
22
|
+
exports.tunnelResWrite = handlePipe('tunnelResWrite');
|
package/lib/logger.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const util = require('util');
|
|
2
|
+
|
|
3
|
+
const MAX_COUNT = 666;
|
|
4
|
+
const MIN_COUNT = 600;
|
|
5
|
+
const COUNT = 60;
|
|
6
|
+
let logs = [];
|
|
7
|
+
let index = 0;
|
|
8
|
+
|
|
9
|
+
const leftPad = (num) => {
|
|
10
|
+
if (num > 99) {
|
|
11
|
+
return num;
|
|
12
|
+
}
|
|
13
|
+
if (num > 9) {
|
|
14
|
+
return `0${num}`;
|
|
15
|
+
}
|
|
16
|
+
return `00${num}`;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const getId = () => {
|
|
20
|
+
if (index > 999) {
|
|
21
|
+
index = 0;
|
|
22
|
+
}
|
|
23
|
+
return `${Date.now()}-${leftPad(index++)}`;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const log = (args, level) => {
|
|
27
|
+
const msg = [];
|
|
28
|
+
for (let i = 0, len = args.length; i < len; i++) {
|
|
29
|
+
const arg = args[i];
|
|
30
|
+
if (arg instanceof Error) {
|
|
31
|
+
msg.push(arg.stack || arg);
|
|
32
|
+
} else {
|
|
33
|
+
msg.push(arg);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
logs.push({
|
|
37
|
+
level,
|
|
38
|
+
id: getId(),
|
|
39
|
+
msg: util.format.apply(null, msg),
|
|
40
|
+
});
|
|
41
|
+
const len = logs.length;
|
|
42
|
+
if (len > MAX_COUNT) {
|
|
43
|
+
logs = logs.slice(0, len - MIN_COUNT);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
exports.log = log;
|
|
48
|
+
exports.getLogs = (id) => {
|
|
49
|
+
id = id || String(Date.now() - 5000);
|
|
50
|
+
for (let i = 0, len = logs.length; i < len; i++) {
|
|
51
|
+
if (logs[i].id > id) {
|
|
52
|
+
return logs.slice(i, i + COUNT);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return [];
|
|
56
|
+
};
|
package/lib/pipe.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const iconv = require('iconv-lite');
|
|
2
|
+
const scripts = require('./scripts');
|
|
3
|
+
const util = require('./util');
|
|
4
|
+
|
|
5
|
+
const getHandlerName = (name) => {
|
|
6
|
+
return `handle${name[0].toUpperCase()}${name.substring(1)}`;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
module.exports = (name) => {
|
|
10
|
+
const eventName = name[0] === 'r' ? 'request' : 'connect';
|
|
11
|
+
return (server, options) => {
|
|
12
|
+
options = Object.assign({
|
|
13
|
+
getCharset: util.getCharset,
|
|
14
|
+
isText: util.isText,
|
|
15
|
+
iconv,
|
|
16
|
+
}, options);
|
|
17
|
+
server.on(eventName, async (req, res) => {
|
|
18
|
+
const oReq = req.originalReq;
|
|
19
|
+
oReq.ruleValue = oReq.ruleValue || oReq.pipeValue;
|
|
20
|
+
const handleRequest = scripts.getHandler({ req })[getHandlerName(name)];
|
|
21
|
+
if (!util.isFunction(handleRequest)) {
|
|
22
|
+
return req.pipe(res);
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
await handleRequest(req, res, options);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
req.emit('error', err);
|
|
28
|
+
console.error(err); // eslint-disable-line
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const Koa = require('koa');
|
|
2
|
+
const util = require('./util');
|
|
3
|
+
const scripts = require('./scripts');
|
|
4
|
+
|
|
5
|
+
module.exports = (server, options) => {
|
|
6
|
+
const app = new Koa();
|
|
7
|
+
app.use(async (ctx) => {
|
|
8
|
+
const { req } = ctx;
|
|
9
|
+
const rulesUrl = req.sessionStorage.get(util.RES_RULES_URL);
|
|
10
|
+
if (rulesUrl != null) {
|
|
11
|
+
req.sessionStorage.remove(util.RES_RULES_URL);
|
|
12
|
+
if (rulesUrl) {
|
|
13
|
+
const result = await util.request(rulesUrl, req.headers);
|
|
14
|
+
ctx.body = util.formateRules(result);
|
|
15
|
+
}
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
util.setupContext(ctx, options);
|
|
19
|
+
const { handleResponseRules } = scripts.getHandler(ctx);
|
|
20
|
+
if (util.isFunction(handleResponseRules)) {
|
|
21
|
+
await handleResponseRules(ctx);
|
|
22
|
+
util.responseRules(ctx);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
server.on('request', app.callback());
|
|
26
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const Koa = require('koa');
|
|
2
|
+
const util = require('./util');
|
|
3
|
+
const scripts = require('./scripts');
|
|
4
|
+
|
|
5
|
+
module.exports = (server, options) => {
|
|
6
|
+
const app = new Koa();
|
|
7
|
+
app.use(async (ctx) => {
|
|
8
|
+
const { req } = ctx;
|
|
9
|
+
if (util.isRemote(req)) {
|
|
10
|
+
const rulesUrl = util.getRemoteUrl(req, util.REQ_RULES_URL);
|
|
11
|
+
const resRulesUrl = util.getRemoteUrl(req, util.RES_RULES_URL);
|
|
12
|
+
req.sessionStorage.set(util.RES_RULES_URL, resRulesUrl || '');
|
|
13
|
+
if (rulesUrl) {
|
|
14
|
+
const result = await util.request(rulesUrl, req.headers);
|
|
15
|
+
ctx.body = util.formateRules(result);
|
|
16
|
+
}
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
util.setupContext(ctx, options);
|
|
20
|
+
const {
|
|
21
|
+
handleWebSocketRules,
|
|
22
|
+
handleWebsocketRules,
|
|
23
|
+
handleRequestRules,
|
|
24
|
+
} = scripts.getHandler(ctx);
|
|
25
|
+
let handleReqRules;
|
|
26
|
+
if (/^ws/.test(ctx.fullUrl)) {
|
|
27
|
+
if (util.isFunction(handleWebSocketRules)) {
|
|
28
|
+
handleReqRules = handleWebSocketRules;
|
|
29
|
+
} else if (util.isFunction(handleWebsocketRules)) {
|
|
30
|
+
handleReqRules = handleWebsocketRules;
|
|
31
|
+
}
|
|
32
|
+
} else if (util.isFunction(handleRequestRules)) {
|
|
33
|
+
handleReqRules = handleRequestRules;
|
|
34
|
+
}
|
|
35
|
+
if (handleReqRules) {
|
|
36
|
+
await handleReqRules(ctx);
|
|
37
|
+
util.responseRules(ctx);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
server.on('request', app.callback());
|
|
41
|
+
};
|
package/lib/scripts.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const vm = require('vm');
|
|
2
|
+
const iconv = require('iconv-lite');
|
|
3
|
+
|
|
4
|
+
const GLOBAL_VARS = [
|
|
5
|
+
'process',
|
|
6
|
+
'Buffer',
|
|
7
|
+
'clearImmediate',
|
|
8
|
+
'clearInterval',
|
|
9
|
+
'clearTimeout',
|
|
10
|
+
'setImmediate',
|
|
11
|
+
'setInterval',
|
|
12
|
+
'setTimeout',
|
|
13
|
+
'console',
|
|
14
|
+
'module',
|
|
15
|
+
'require',
|
|
16
|
+
];
|
|
17
|
+
/* eslint-disable no-console */
|
|
18
|
+
const TIMEOUT = 600;
|
|
19
|
+
const RULE_VALUE_RE = /^([\w\-.]+)(?:\((.+)\))?.*$/;
|
|
20
|
+
const scripts = {};
|
|
21
|
+
const VM_OPTIONS = {
|
|
22
|
+
displayErrors: false,
|
|
23
|
+
timeout: TIMEOUT,
|
|
24
|
+
};
|
|
25
|
+
let CONTEXT = vm.createContext();
|
|
26
|
+
|
|
27
|
+
setInterval(() => {
|
|
28
|
+
CONTEXT = vm.createContext();
|
|
29
|
+
}, 30000);
|
|
30
|
+
|
|
31
|
+
const getScript = (code, name) => {
|
|
32
|
+
code = code && code.trim();
|
|
33
|
+
if (!code) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
code = `const d = require('domain').create();
|
|
37
|
+
d.on('error', console.error);
|
|
38
|
+
d.run(() => {
|
|
39
|
+
${code}
|
|
40
|
+
});`;
|
|
41
|
+
try {
|
|
42
|
+
return new vm.Script(`(function(){\n${code}\n})()`, { filename: name, timeout: TIMEOUT });
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error(err);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const execScript = (script, value) => {
|
|
49
|
+
if (!script) {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
GLOBAL_VARS.forEach((key) => {
|
|
53
|
+
CONTEXT[key] = global[key];
|
|
54
|
+
});
|
|
55
|
+
CONTEXT.require = require;
|
|
56
|
+
CONTEXT.iconv = iconv;
|
|
57
|
+
CONTEXT.process = {
|
|
58
|
+
args: value ? value.split(',') : [],
|
|
59
|
+
};
|
|
60
|
+
CONTEXT.exports = {};
|
|
61
|
+
CONTEXT.module = { exports: CONTEXT.exports };
|
|
62
|
+
try {
|
|
63
|
+
script.runInContext(CONTEXT, VM_OPTIONS);
|
|
64
|
+
return CONTEXT.module.exports || {};
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error(err);
|
|
67
|
+
} finally {
|
|
68
|
+
Object.keys(CONTEXT).forEach((key) => {
|
|
69
|
+
if (GLOBAL_VARS.indexOf(key) === -1) {
|
|
70
|
+
delete CONTEXT[key];
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return {};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const parseRuleValue = (ctx) => {
|
|
78
|
+
const { ruleValue } = ctx.req.originalReq;
|
|
79
|
+
if (!RULE_VALUE_RE.test(ruleValue)) {
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
name: RegExp.$1,
|
|
84
|
+
value: RegExp.$2,
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
exports.get = (name) => {
|
|
89
|
+
return scripts[name];
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
exports.getHandler = (ctx) => {
|
|
93
|
+
const { name, value } = parseRuleValue(ctx);
|
|
94
|
+
return execScript(scripts[name], value);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const set = (name, text) => {
|
|
98
|
+
scripts[name] = typeof text === 'string' ? getScript(text) : text;
|
|
99
|
+
};
|
|
100
|
+
exports.set = set;
|
|
101
|
+
|
|
102
|
+
exports.remove = (name) => {
|
|
103
|
+
delete scripts[name];
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
exports.load = (storage) => {
|
|
107
|
+
storage.getFileList().forEach(({ name, data }) => {
|
|
108
|
+
set(name, data);
|
|
109
|
+
});
|
|
110
|
+
};
|