whistle.script 1.1.0 → 1.2.3

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/README.md CHANGED
@@ -18,7 +18,7 @@ whistle.script为[whistle](https://github.com/avwo/whistle)的一个扩展脚本
18
18
 
19
19
  # 安装
20
20
 
21
- 1. 安装Node(>=6): [官网下载安装最新版本(LTS和Stable都可以)](https://nodejs.org/)
21
+ 1. 安装Node: [官网下载安装最新版本(LTS和Stable都可以)](https://nodejs.org/)
22
22
  2. 安装最新版的[whistle](https://github.com/avwo/whistle)。
23
23
 
24
24
  npm install -g whistle
@@ -28,9 +28,7 @@ whistle.script为[whistle](https://github.com/avwo/whistle)的一个扩展脚本
28
28
 
29
29
  3. 安装script插件:
30
30
 
31
- npm install -g whistle.script
32
- # Mac、Linux用户可能需要加sudo
33
- sudo npm install -g whistle.script
31
+ w2 i whistle.script
34
32
 
35
33
  # 使用
36
34
 
@@ -161,12 +159,12 @@ whistle.script为[whistle](https://github.com/avwo/whistle)的一个扩展脚本
161
159
  // 正常断开 WebSocket 连接
162
160
  socket.on('disconnect', (code, message, opts) => {
163
161
  console.log(code, 'client disconnect');
164
- svrSocket.disconnect(code, message, opts);
162
+ svrSocket.disconnect(code, opts);
165
163
  });
166
164
  // 正常断开 WebSocket 连接
167
165
  svrSocket.on('disconnect', (code, message, opts) => {
168
166
  console.log(code, 'server disconnect');
169
- socket.disconnect(code, message, opts);
167
+ socket.disconnect(code, opts);
170
168
  });
171
169
  // 获取客户端解析后的帧数据
172
170
  socket.on('message', (data, opts) => {
@@ -185,18 +183,82 @@ whistle.script为[whistle](https://github.com/avwo/whistle)的一个扩展脚本
185
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)
186
184
  3. 操作Tunnel请求
187
185
  ``` js
188
- exports.handleTunnel = async (req, connect) => {
189
- const res = await connect();
190
- req.pipe(res).pipe(req);
186
+ exports.handleTunnel = async (socket, connect) => {
187
+ const svrSocket = await connect();
188
+ socket.pipe(svrSocket).pipe(socket);
191
189
  };
192
190
  ```
193
191
  whistle规则配置同上
194
- # License
195
192
 
196
- [MIT](https://github.com/whistle-plugins/whistle.script/blob/master/LICENSE)
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
+ };
197
223
 
224
+ exports.handleWsReqRead = (req, res, options) => {
225
+ req.pipe(res);
226
+ };
198
227
 
228
+ exports.handleWsReqWrite = (req, res, options) => {
229
+ req.pipe(res);
230
+ };
199
231
 
232
+ exports.handleWsResRead = (req, res, options) => {
233
+ req.pipe(res);
234
+ };
200
235
 
236
+ exports.handleWsResWrite = (req, res, options) => {
237
+ req.pipe(res);
238
+ };
201
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)
202
264
 
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 CHANGED
@@ -1,7 +1,23 @@
1
- /* eslint-enable no-console */
1
+ const handlePipe = require('./pipe');
2
+
3
+ exports.auth = require('./auth');
4
+ exports.sniCallback = require('./sniCallback');
2
5
  exports.uiServer = require('./uiServer');
3
6
  exports.rulesServer = require('./rulesServer');
4
7
  exports.resRulesServer = require('./resRulesServer');
5
8
  exports.tunnelRulesServer = require('./tunnelRulesServer');
6
9
  exports.server = require('./server');
7
10
  exports.tunnelServer = require('./tunnelServer');
11
+
12
+ exports.reqRead = handlePipe('reqRead');
13
+ exports.reqWrite = handlePipe('reqWrite');
14
+ exports.resRead = handlePipe('resRead');
15
+ exports.resWrite = handlePipe('resWrite');
16
+ exports.wsReqRead = handlePipe('wsReqRead');
17
+ exports.wsReqWrite = handlePipe('wsReqWrite');
18
+ exports.wsResRead = handlePipe('wsResRead');
19
+ exports.wsResWrite = handlePipe('wsResWrite');
20
+ exports.tunnelReqRead = handlePipe('tunnelReqRead');
21
+ exports.tunnelReqWrite = handlePipe('tunnelReqWrite');
22
+ exports.tunnelResRead = handlePipe('tunnelResRead');
23
+ exports.tunnelResWrite = handlePipe('tunnelResWrite');
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.pipeValue || oReq.ruleValue;
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
+ };
@@ -5,6 +5,16 @@ const scripts = require('./scripts');
5
5
  module.exports = (server, options) => {
6
6
  const app = new Koa();
7
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
+ }
8
18
  util.setupContext(ctx, options);
9
19
  const { handleResponseRules } = scripts.getHandler(ctx);
10
20
  if (util.isFunction(handleResponseRules)) {
@@ -5,6 +5,17 @@ const scripts = require('./scripts');
5
5
  module.exports = (server, options) => {
6
6
  const app = new Koa();
7
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
+ }
8
19
  util.setupContext(ctx, options);
9
20
  const {
10
21
  handleWebSocketRules,
package/lib/server.js CHANGED
@@ -5,20 +5,15 @@ const util = require('./util');
5
5
 
6
6
  module.exports = (server, options) => {
7
7
  server.on('request', async (req, res) => {
8
- const fullUrl = req.originalReq.url;
9
- const ctx = {
10
- iconv,
11
- req,
12
- res,
13
- options,
14
- fullUrl,
15
- url: fullUrl,
16
- headers: req.headers,
17
- method: req.method,
18
- getStreamBuffer: util.getStreamBuffer,
19
- getCharset: util.getCharset,
20
- isText: util.isText,
21
- };
8
+ if (util.isRemote(req)) {
9
+ return req.passThrough();
10
+ }
11
+ const ctx = util.getContext(req, res);
12
+ ctx.getStreamBuffer = util.getStreamBuffer;
13
+ ctx.getCharset = util.getCharset;
14
+ ctx.isText = util.isText;
15
+ ctx.iconv = iconv;
16
+ ctx.options = options;
22
17
  const { handleRequest } = scripts.getHandler(ctx);
23
18
  if (!util.isFunction(handleRequest)) {
24
19
  return req.passThrough();
@@ -29,6 +24,8 @@ module.exports = (server, options) => {
29
24
  await handleRequest(ctx, req.request);
30
25
  } catch (err) {
31
26
  clearup();
27
+ req.emit('error', err);
28
+ console.error(err); // eslint-disable-line
32
29
  }
33
30
  });
34
31
  setupWsServer(server, options);
@@ -0,0 +1,24 @@
1
+ const scripts = require('./scripts');
2
+ const { getRemoteUrl, getContext, getFn, request, SNI_URL, isSni } = require('./util');
3
+
4
+ module.exports = async (req, options) => {
5
+ if (!isSni(req)) {
6
+ const oReq = req.originalReq;
7
+ oReq.ruleValue = oReq.sniValue || oReq.ruleValue;
8
+ const ctx = getContext(req);
9
+ const { sniCallback, SNICallback } = scripts.getHandler(ctx);
10
+ const sniCb = getFn(sniCallback, SNICallback);
11
+ return sniCb && sniCb(req, options);
12
+ }
13
+ const sniUrl = getRemoteUrl(req, SNI_URL);
14
+ if (sniUrl) {
15
+ const result = await request(sniUrl, req.headers);
16
+ if (result === false || result.cert === false) {
17
+ return false;
18
+ }
19
+ if (result === true || result.cert === true) {
20
+ return true;
21
+ }
22
+ return result;
23
+ }
24
+ };
@@ -5,6 +5,14 @@ const scripts = require('./scripts');
5
5
  module.exports = (server, options) => {
6
6
  const app = new Koa();
7
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
+ if (rulesUrl) {
12
+ ctx.body = await util.request(rulesUrl, req.headers);
13
+ }
14
+ return;
15
+ }
8
16
  util.setupContext(ctx, options);
9
17
  const { handleTunnelRules } = scripts.getHandler(ctx);
10
18
  if (util.isFunction(handleTunnelRules)) {
@@ -4,6 +4,9 @@ const scripts = require('./scripts');
4
4
 
5
5
  module.exports = (server, options) => {
6
6
  server.on('connect', async (req, socket) => {
7
+ if (util.isRemote(req)) {
8
+ return req.passThrough();
9
+ }
7
10
  const { url, headers } = req;
8
11
  socket.headers = headers;
9
12
  socket.options = options;
package/lib/util.js CHANGED
@@ -1,8 +1,23 @@
1
1
  const zlib = require('zlib');
2
2
  const { EventEmitter } = require('events');
3
+ const { parse: parseUrl } = require('url');
4
+ const http = require('http');
5
+ const https = require('https');
3
6
  const dataSource = require('./dataSource');
4
7
 
5
- exports.isFunction = fn => typeof fn === 'function';
8
+ exports.AUTH_URL = 'x-whistle-.script-auth-url';
9
+ exports.SNI_URL = 'x-whistle-.script-sni-url';
10
+ exports.REQ_RULES_URL = 'x-whistle-.script-req-rules-url';
11
+ exports.RES_RULES_URL = 'x-whistle-.script-res-rules-url';
12
+ exports.STATS_URL = 'x-whistle-.script-stats-url';
13
+ exports.DATA_URL = 'x-whistle-.script-data-url';
14
+ exports.noop = () => {};
15
+
16
+ const POLICY = 'x-whistle-.script-policy';
17
+ const isFunction = fn => typeof fn === 'function';
18
+ const URL_RE = /^https?:(?:\/\/|%3A%2F%2F)[\w.-]/;
19
+
20
+ exports.isFunction = isFunction;
6
21
  exports.noop = () => {};
7
22
  const getCharset = (headers) => {
8
23
  if (/charset=([^\s]+)/.test(headers['content-type'])) {
@@ -62,15 +77,23 @@ exports.setupContext = (ctx, options) => {
62
77
  ctx.fullUrl = ctx.req.originalReq.url;
63
78
  };
64
79
 
65
- exports.responseRules = (ctx) => {
66
- if (!ctx.body && (ctx.rules || ctx.values)) {
67
- ctx.body = {
80
+ const formateRules = (ctx) => {
81
+ if (ctx.rules || ctx.values) {
82
+ return {
68
83
  rules: Array.isArray(ctx.rules) ? ctx.rules.join('\n') : `${ctx.rules}`,
69
84
  values: ctx.values,
70
85
  };
71
86
  }
72
87
  };
73
88
 
89
+ exports.formateRules = formateRules;
90
+
91
+ exports.responseRules = (ctx) => {
92
+ if (!ctx.body) {
93
+ ctx.body = formateRules(ctx);
94
+ }
95
+ };
96
+
74
97
  exports.getDataSource = () => {
75
98
  const ds = new EventEmitter();
76
99
  const handleData = (type, args) => {
@@ -85,3 +108,102 @@ exports.getDataSource = () => {
85
108
  },
86
109
  };
87
110
  };
111
+
112
+ exports.getContext = (req, res) => {
113
+ const fullUrl = req.originalReq.url;
114
+ return {
115
+ req,
116
+ res,
117
+ fullUrl,
118
+ url: fullUrl,
119
+ headers: req.headers,
120
+ method: req.method,
121
+ };
122
+ };
123
+
124
+ exports.getFn = (f1, f2) => {
125
+ if (isFunction(f1)) {
126
+ return f1;
127
+ } if (isFunction(f2)) {
128
+ return f2;
129
+ }
130
+ };
131
+
132
+
133
+ const request = (url, headers, data) => {
134
+ if (!url) {
135
+ return;
136
+ }
137
+ const options = parseUrl(url);
138
+ options.headers = Object.assign({}, headers);
139
+ delete options.headers.host;
140
+ if (data) {
141
+ data = Buffer.from(JSON.stringify(data));
142
+ options.method = 'POST';
143
+ }
144
+ return new Promise((resolve, reject) => {
145
+ const httpModule = options.protocol === 'https:' ? https : http;
146
+ options.rejectUnauthorized = true;
147
+ const client = httpModule.request(options, (res) => {
148
+ res.on('error', handleError); // eslint-disable-line
149
+ let body;
150
+ res.on('data', (chunk) => {
151
+ body = body ? Buffer.concat([body, chunk]) : chunk;
152
+ });
153
+ res.on('end', () => {
154
+ clearTimeout(timer); // eslint-disable-line
155
+ if (body) {
156
+ try {
157
+ resolve(JSON.parse(body.toString()) || '');
158
+ } catch (e) {}
159
+ }
160
+ resolve('');
161
+ });
162
+ });
163
+ const handleError = (err) => {
164
+ clearTimeout(timer); // eslint-disable-line
165
+ client.destroy();
166
+ reject(err);
167
+ };
168
+ const timer = setTimeout(() => handleError(new Error('Timeout')), 12000);
169
+ client.on('error', handleError);
170
+ client.end(data);
171
+ });
172
+ };
173
+
174
+ exports.request = async (url, headers, data) => {
175
+ try {
176
+ return await request(url, headers, data);
177
+ } catch (e) {
178
+ if (!data) {
179
+ return request(url, headers, data);
180
+ }
181
+ }
182
+ };
183
+
184
+ const hasPolicy = ({ headers }, name) => {
185
+ const policy = headers[POLICY];
186
+ if (typeof policy === 'string') {
187
+ return policy.toLowerCase().indexOf(name) !== -1;
188
+ }
189
+ };
190
+
191
+ const isRemote = (req) => {
192
+ return hasPolicy(req, 'remote');
193
+ };
194
+
195
+ exports.isRemote = isRemote;
196
+
197
+ exports.isSni = (req) => {
198
+ return hasPolicy(req, 'sni');
199
+ };
200
+
201
+ exports.getRemoteUrl = (req, name) => {
202
+ let url = req.headers[name];
203
+ if (typeof url === 'string') {
204
+ url = decodeURIComponent(url);
205
+ if (URL_RE.test(url)) {
206
+ return url;
207
+ }
208
+ }
209
+ };
package/lib/wsServer.js CHANGED
@@ -1,16 +1,8 @@
1
1
  const crypto = require('crypto');
2
- const util = require('./util');
2
+ const iconv = require('iconv-lite');
3
+ const { getDataSource, getFn } = require('./util');
3
4
  const scripts = require('./scripts');
4
5
 
5
- const { getDataSource } = util;
6
-
7
- const getFn = (f1, f2) => {
8
- if (util.isFunction(f1)) {
9
- return f1;
10
- } if (util.isFunction(f2)) {
11
- return f2;
12
- }
13
- };
14
6
 
15
7
  module.exports = (server, options) => {
16
8
  const { getReceiver, getSender } = options.wsParser;
@@ -63,6 +55,7 @@ module.exports = (server, options) => {
63
55
  req.on('error', clearup);
64
56
  socket.dataSource = dataSource;
65
57
  socket.headers = headers;
58
+ socket.iconv = iconv;
66
59
  socket.url = socket.fullUrl = oReq.url; // eslint-disable-line
67
60
  const handleUpgrade = (res) => {
68
61
  if (replied) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "whistle.script",
3
3
  "description": "The plugin for the extension script for whistle",
4
- "version": "1.1.0",
4
+ "version": "1.2.3",
5
5
  "author": "avenwu <avenwu@vip.qq.com>",
6
6
  "contributors": [],
7
7
  "license": "MIT",
@@ -15,14 +15,17 @@
15
15
  "ssi"
16
16
  ],
17
17
  "registry": "https://github.com/whistle-plugins/whistle.script",
18
+ "whistleConfig": {
19
+ "pluginVars": true
20
+ },
18
21
  "repository": {
19
22
  "type": "git",
20
23
  "url": "https://github.com/whistle-plugins/whistle.script.git"
21
24
  },
22
25
  "scripts": {
23
26
  "dev": "webpack --config ./src/webpack.config.js -w",
24
- "lint": "eslint ./lib index.js",
25
- "lintfix": "eslint --fix ./lib index.js"
27
+ "lint": "eslint ./lib test index.js",
28
+ "lintfix": "eslint --fix ./lib test index.js"
26
29
  },
27
30
  "devDependencies": {
28
31
  "babel-core": "^6.7.6",
package/rules.txt ADDED
@@ -0,0 +1,2 @@
1
+ * whistle.script:// includeFilter://reqH.x-whistle-.script-policy=remote
2
+ * sniCallback://script includeFilter://reqH.x-whistle-.script-policy=sni