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/lib/server.js ADDED
@@ -0,0 +1,32 @@
1
+ const iconv = require('iconv-lite');
2
+ const scripts = require('./scripts');
3
+ const setupWsServer = require('./wsServer');
4
+ const util = require('./util');
5
+
6
+ module.exports = (server, options) => {
7
+ server.on('request', async (req, res) => {
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;
17
+ const { handleRequest } = scripts.getHandler(ctx);
18
+ if (!util.isFunction(handleRequest)) {
19
+ return req.passThrough();
20
+ }
21
+ const { dataSource, clearup } = util.getDataSource();
22
+ ctx.dataSource = dataSource;
23
+ try {
24
+ await handleRequest(ctx, req.request);
25
+ } catch (err) {
26
+ clearup();
27
+ req.emit('error', err);
28
+ console.error(err); // eslint-disable-line
29
+ }
30
+ });
31
+ setupWsServer(server, options);
32
+ };
@@ -0,0 +1,24 @@
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
+ if (rulesUrl) {
12
+ ctx.body = await util.request(rulesUrl, req.headers);
13
+ }
14
+ return;
15
+ }
16
+ util.setupContext(ctx, options);
17
+ const { handleTunnelRules } = scripts.getHandler(ctx);
18
+ if (util.isFunction(handleTunnelRules)) {
19
+ await handleTunnelRules(ctx);
20
+ util.responseRules(ctx);
21
+ }
22
+ });
23
+ server.on('request', app.callback());
24
+ };
@@ -0,0 +1,37 @@
1
+ const util = require('./util');
2
+ const scripts = require('./scripts');
3
+ /* eslint-disable no-empty */
4
+
5
+ module.exports = (server, options) => {
6
+ server.on('connect', async (req, socket) => {
7
+ if (util.isRemote(req)) {
8
+ return req.passThrough();
9
+ }
10
+ const { url, headers } = req;
11
+ socket.headers = headers;
12
+ socket.options = options;
13
+ socket.req = req;
14
+ socket.originalReq = req.originalReq;
15
+ socket.url = socket.fullUrl = req.originalReq.url; // eslint-disable-line
16
+ const { handleTunnel } = scripts.getHandler(socket);
17
+ if (!util.isFunction(handleTunnel)) {
18
+ return req.passThrough();
19
+ }
20
+ const { dataSource, clearup } = util.getDataSource();
21
+ socket.dataSource = dataSource;
22
+ socket.url = url;
23
+ socket.on('error', clearup);
24
+ socket.on('close', clearup);
25
+ const connect = (opts) => {
26
+ return new Promise((resolve, reject) => {
27
+ const client = req.connect(opts, resolve) || req;
28
+ client.on('error', reject);
29
+ });
30
+ };
31
+ try {
32
+ await handleTunnel(socket, connect);
33
+ } catch (err) {
34
+ socket.emit('error', err);
35
+ }
36
+ });
37
+ };
@@ -0,0 +1,9 @@
1
+
2
+ module.exports = (ctx) => {
3
+ const { name, value } = ctx.request.body;
4
+ if (name && typeof name === 'string') {
5
+ ctx.storage.writeFile(name, value);
6
+ ctx.scripts.set(name, value);
7
+ }
8
+ ctx.body = { ec: 0 };
9
+ };
@@ -0,0 +1,9 @@
1
+
2
+ module.exports = (ctx) => {
3
+ const { name } = ctx.request.body;
4
+ if (name && typeof name === 'string') {
5
+ ctx.storage.removeFile(name);
6
+ ctx.scripts.remove(name);
7
+ }
8
+ ctx.body = { ec: 0 };
9
+ };
@@ -0,0 +1,14 @@
1
+ /* eslint-disable no-empty */
2
+ module.exports = (ctx) => {
3
+ let { type, args } = ctx.request.body;
4
+ if (args && type && typeof type === 'string') {
5
+ try {
6
+ args = JSON.parse(args);
7
+ if (Array.isArray(args)) {
8
+ const { dataSource } = ctx;
9
+ dataSource.emit('data', type, args);
10
+ }
11
+ } catch (e) {}
12
+ }
13
+ ctx.body = { ec: 0 };
14
+ };
@@ -0,0 +1,10 @@
1
+
2
+ module.exports = (ctx) => {
3
+ const { storage } = ctx;
4
+ /* eslint-disable prefer-arrow-callback */
5
+ ctx.body = {
6
+ list: storage.getFileList().map((item) => {
7
+ return { name: item.name, value: item.data };
8
+ }),
9
+ };
10
+ };
@@ -0,0 +1,4 @@
1
+
2
+ module.exports = (ctx) => {
3
+ ctx.body = ctx.getLogs(ctx.request.query.id);
4
+ };
@@ -0,0 +1,10 @@
1
+
2
+ module.exports = (ctx) => {
3
+ const { name, newName } = ctx.request.body;
4
+ if (name && newName) {
5
+ ctx.storage.renameFile(name, newName);
6
+ ctx.scripts.set(newName, ctx.scripts.get(name));
7
+ ctx.scripts.remove(name);
8
+ }
9
+ ctx.body = { ec: 0 };
10
+ };
@@ -0,0 +1,29 @@
1
+ const Koa = require('koa');
2
+ const onerror = require('koa-onerror');
3
+ const bodyParser = require('koa-bodyparser');
4
+ const serve = require('koa-static');
5
+ const path = require('path');
6
+ const router = require('koa-router')();
7
+ const setupRouter = require('./router');
8
+ const logger = require('../logger');
9
+ const scripts = require('../scripts');
10
+ const dataSource = require('../dataSource');
11
+
12
+ module.exports = function (server, options) {
13
+ scripts.load(options.storage);
14
+ const app = new Koa();
15
+ onerror(app);
16
+ app.use(async (ctx, next) => {
17
+ ctx.storage = options.storage;
18
+ ctx.getLogs = logger.getLogs;
19
+ ctx.scripts = scripts;
20
+ ctx.dataSource = dataSource;
21
+ await next();
22
+ });
23
+ app.use(bodyParser());
24
+ setupRouter(router);
25
+ app.use(router.routes());
26
+ app.use(router.allowedMethods());
27
+ app.use(serve(path.join(__dirname, '../../public')));
28
+ server.on('request', app.callback());
29
+ };
@@ -0,0 +1,15 @@
1
+ const init = require('./cgi-bin/init');
2
+ const log = require('./cgi-bin/log');
3
+ const create = require('./cgi-bin/create');
4
+ const rename = require('./cgi-bin/rename');
5
+ const del = require('./cgi-bin/delete');
6
+ const emitData = require('./cgi-bin/emitData');
7
+
8
+ module.exports = (router) => {
9
+ router.get('/cgi-bin/init', init);
10
+ router.get('/cgi-bin/log', log);
11
+ router.post('/cgi-bin/create', create);
12
+ router.post('/cgi-bin/rename', rename);
13
+ router.post('/cgi-bin/delete', del);
14
+ router.post('/cgi-bin/emitData', emitData);
15
+ };
package/lib/util.js ADDED
@@ -0,0 +1,192 @@
1
+ const zlib = require('zlib');
2
+ const { EventEmitter } = require('events');
3
+ const { parse: parseUrl } = require('url');
4
+ const http = require('http');
5
+ const dataSource = require('./dataSource');
6
+
7
+ exports.AUTH_URL = 'x-whistle-.script-auth-url';
8
+ exports.REQ_RULES_URL = 'x-whistle-.script-req-rules-url';
9
+ exports.RES_RULES_URL = 'x-whistle-.script-res-rules-url';
10
+ exports.STATS_URL = 'x-whistle-.script-stats-url';
11
+ exports.DATA_URL = 'x-whistle-.script-data-url';
12
+ exports.noop = () => {};
13
+
14
+ const POLICY = 'x-whistle-.script-policy';
15
+ const isFunction = fn => typeof fn === 'function';
16
+ const URL_RE = /^http:(?:\/\/|%3A%2F%2F)[\w.-]/;
17
+
18
+ exports.isFunction = isFunction;
19
+ exports.noop = () => {};
20
+ const getCharset = (headers) => {
21
+ if (/charset=([^\s]+)/.test(headers['content-type'])) {
22
+ return RegExp.$1;
23
+ }
24
+ return 'utf8';
25
+ };
26
+ exports.getCharset = getCharset;
27
+ exports.isText = (headers) => {
28
+ const type = headers['content-type'];
29
+ return !type || /javascript|css|html|json|xml|application\/x-www-form-urlencoded|text\//i.test(type);
30
+ };
31
+
32
+ const unzipBody = (headers, body, callback) => {
33
+ let unzip;
34
+ let encoding = headers['content-encoding'];
35
+ if (body && typeof encoding === 'string') {
36
+ encoding = encoding.trim().toLowerCase();
37
+ if (encoding === 'gzip') {
38
+ unzip = zlib.gunzip.bind(zlib);
39
+ } else if (encoding === 'deflate') {
40
+ unzip = zlib.inflate.bind(zlib);
41
+ }
42
+ }
43
+ if (!unzip) {
44
+ return callback(null, body);
45
+ }
46
+ unzip(body, (err, data) => {
47
+ if (err) {
48
+ return zlib.inflateRaw(body, callback);
49
+ }
50
+ callback(null, data);
51
+ });
52
+ };
53
+
54
+ exports.getStreamBuffer = (stream) => {
55
+ return new Promise((resolve, reject) => {
56
+ let buffer;
57
+ stream.on('data', (data) => {
58
+ buffer = buffer ? Buffer.concat([buffer, data]) : data;
59
+ });
60
+ stream.on('end', () => {
61
+ unzipBody(stream.headers, buffer, (err, data) => {
62
+ if (err) {
63
+ reject(err);
64
+ } else {
65
+ resolve(data || null);
66
+ }
67
+ });
68
+ });
69
+ stream.on('error', reject);
70
+ });
71
+ };
72
+
73
+ exports.setupContext = (ctx, options) => {
74
+ ctx.options = options;
75
+ ctx.fullUrl = ctx.req.originalReq.url;
76
+ };
77
+
78
+ const formateRules = (ctx) => {
79
+ if (ctx.rules || ctx.values) {
80
+ return {
81
+ rules: Array.isArray(ctx.rules) ? ctx.rules.join('\n') : `${ctx.rules}`,
82
+ values: ctx.values,
83
+ };
84
+ }
85
+ };
86
+
87
+ exports.formateRules = formateRules;
88
+
89
+ exports.responseRules = (ctx) => {
90
+ if (!ctx.body) {
91
+ ctx.body = formateRules(ctx);
92
+ }
93
+ };
94
+
95
+ exports.getDataSource = () => {
96
+ const ds = new EventEmitter();
97
+ const handleData = (type, args) => {
98
+ ds.emit(type, ...args);
99
+ };
100
+ dataSource.on('data', handleData);
101
+ return {
102
+ dataSource: ds,
103
+ clearup: () => {
104
+ dataSource.removeListener('data', handleData);
105
+ ds.removeAllListeners();
106
+ },
107
+ };
108
+ };
109
+
110
+ exports.getContext = (req, res) => {
111
+ const fullUrl = req.originalReq.url;
112
+ return {
113
+ req,
114
+ res,
115
+ fullUrl,
116
+ url: fullUrl,
117
+ headers: req.headers,
118
+ method: req.method,
119
+ };
120
+ };
121
+
122
+ exports.getFn = (f1, f2) => {
123
+ if (isFunction(f1)) {
124
+ return f1;
125
+ } if (isFunction(f2)) {
126
+ return f2;
127
+ }
128
+ };
129
+
130
+
131
+ const request = (url, headers, data) => {
132
+ if (!url) {
133
+ return;
134
+ }
135
+ const options = parseUrl(url);
136
+ options.headers = Object.assign({}, headers);
137
+ delete options.headers.host;
138
+ if (data) {
139
+ data = Buffer.from(JSON.stringify(data));
140
+ options.method = 'POST';
141
+ }
142
+ return new Promise((resolve, reject) => {
143
+ const client = http.request(options, (res) => {
144
+ res.on('error', handleError); // eslint-disable-line
145
+ let body;
146
+ res.on('data', (chunk) => {
147
+ body = body ? Buffer.concat([body, chunk]) : chunk;
148
+ });
149
+ res.on('end', () => {
150
+ clearTimeout(timer); // eslint-disable-line
151
+ if (body) {
152
+ try {
153
+ resolve(JSON.parse(body.toString()) || '');
154
+ } catch (e) {}
155
+ }
156
+ resolve('');
157
+ });
158
+ });
159
+ const handleError = (err) => {
160
+ clearTimeout(timer); // eslint-disable-line
161
+ client.destroy();
162
+ reject(err);
163
+ };
164
+ const timer = setTimeout(() => handleError(new Error('Timeout')), 12000);
165
+ client.on('error', handleError);
166
+ client.end(data);
167
+ });
168
+ };
169
+
170
+ exports.request = async (url, headers, data) => {
171
+ try {
172
+ return await request(url, headers, data);
173
+ } catch (e) {
174
+ if (!data) {
175
+ return request(url, headers, data);
176
+ }
177
+ }
178
+ };
179
+
180
+ const isRemote = ({ headers }) => (headers[POLICY] === 'remote');
181
+
182
+ exports.isRemote = isRemote;
183
+
184
+ exports.getRemoteUrl = (req, name) => {
185
+ let url = req.headers[name];
186
+ if (typeof url === 'string') {
187
+ url = decodeURIComponent(url);
188
+ if (URL_RE.test(url)) {
189
+ return url;
190
+ }
191
+ }
192
+ };
@@ -0,0 +1,107 @@
1
+ const crypto = require('crypto');
2
+ const iconv = require('iconv-lite');
3
+ const { getDataSource, getFn } = require('./util');
4
+ const scripts = require('./scripts');
5
+
6
+
7
+ module.exports = (server, options) => {
8
+ const { getReceiver, getSender } = options.wsParser;
9
+
10
+ const wrap = (socket, receiver, sender) => {
11
+ socket.on('data', receiver.add.bind(receiver));
12
+ receiver.onData = (data, opts) => socket.emit('message', data, opts);
13
+ receiver.onping = (data) => socket.emit('ping', data);
14
+ receiver.onpong = (data) => socket.emit('pong', data);
15
+ receiver.onclose = (code, message, opts) => socket.emit('disconnect', code, message, opts);
16
+ receiver.onerror = (reason, code) => {
17
+ const err = new Error(reason);
18
+ err.code = code;
19
+ socket.emit('error', err);
20
+ };
21
+ socket.send = sender.send.bind(sender);
22
+ socket.ping = sender.ping.bind(sender);
23
+ socket.pong = sender.pong.bind(sender);
24
+ socket.disconnect = sender.close.bind(sender);
25
+ };
26
+
27
+ const wrapServerSocket = (socket) => {
28
+ const receiver = getReceiver(socket, true);
29
+ const sender = getSender(socket);
30
+ wrap(socket, receiver, sender);
31
+ };
32
+
33
+ const wrapClientSocket = (socket) => {
34
+ const receiver = getReceiver(socket);
35
+ const sender = getSender(socket, true);
36
+ wrap(socket, receiver, sender);
37
+ };
38
+
39
+ server.on('upgrade', async (req, socket) => {
40
+ const oReq = req.originalReq;
41
+ socket.options = options;
42
+ socket.req = req;
43
+ socket.originalReq = oReq;
44
+ const {
45
+ handleWebsocket,
46
+ handleWebSocket,
47
+ } = scripts.getHandler(socket);
48
+ const handleRequest = getFn(handleWebSocket, handleWebsocket);
49
+ if (!handleRequest) {
50
+ return req.passThrough();
51
+ }
52
+ const { dataSource, clearup } = getDataSource();
53
+ const { headers } = req;
54
+ let replied;
55
+ req.on('error', clearup);
56
+ socket.dataSource = dataSource;
57
+ socket.headers = headers;
58
+ socket.iconv = iconv;
59
+ socket.url = socket.fullUrl = oReq.url; // eslint-disable-line
60
+ const handleUpgrade = (res) => {
61
+ if (replied) {
62
+ return;
63
+ }
64
+ replied = true;
65
+ let key = `${headers['sec-websocket-key']}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`;
66
+ key = crypto.createHash('sha1').update(key, 'binary').digest('base64');
67
+ if (res) {
68
+ if (res.statusCode == 101) { // eslint-disable-line
69
+ res.headers['sec-websocket-accept'] = key;
70
+ }
71
+ } else {
72
+ const protocol = (headers['sec-websocket-protocol'] || '').split(/, */)[0];
73
+ res = {
74
+ statusCode: 101,
75
+ headers: {
76
+ 'Sec-WebSocket-Accept': key,
77
+ Upgrade: 'websocket',
78
+ Connection: 'Upgrade',
79
+ },
80
+ };
81
+ if (protocol) {
82
+ res.headers['Sec-WebSocket-Protocol'] = protocol;
83
+ }
84
+ }
85
+ req.writeHead(res.statusCode, res.statusMessage, res.headers);
86
+ };
87
+ const connect = opts => new Promise((resolve, reject) => {
88
+ const client = req.request(opts, (svrRes) => {
89
+ handleUpgrade(svrRes);
90
+ wrapClientSocket(svrRes);
91
+ resolve(svrRes);
92
+ }, true) || req;
93
+ client.on('error', reject);
94
+ });
95
+ try {
96
+ wrapServerSocket(socket);
97
+ await handleRequest(socket, connect);
98
+ handleUpgrade();
99
+ } catch (err) {
100
+ clearup();
101
+ // 这么写才能在 Network 的 body 里面显示内容
102
+ const body = String(err.stack || '');
103
+ const length = Buffer.byteLength(body);
104
+ socket.write(`HTTP/1.1 502 Bad Gateway\r\nServer: whistle.script\r\nContent-length: ${length}\r\n\r\n${body}`);
105
+ }
106
+ });
107
+ };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "whistle.script",
3
+ "description": "The plugin for the extension script for whistle",
4
+ "version": "1.2.1",
5
+ "author": "avenwu <avenwu@vip.qq.com>",
6
+ "contributors": [],
7
+ "license": "MIT",
8
+ "bugs": {
9
+ "url": "https://github.com/whistle-plugins/whistle.script/issues"
10
+ },
11
+ "homepage": "https://github.com/whistle-plugins/whistle.script",
12
+ "keywords": [
13
+ "whistle",
14
+ "proxy",
15
+ "ssi"
16
+ ],
17
+ "registry": "https://github.com/whistle-plugins/whistle.script",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/whistle-plugins/whistle.script.git"
21
+ },
22
+ "scripts": {
23
+ "dev": "webpack --config ./src/webpack.config.js -w",
24
+ "lint": "eslint ./lib test index.js",
25
+ "lintfix": "eslint --fix ./lib test index.js"
26
+ },
27
+ "devDependencies": {
28
+ "babel-core": "^6.7.6",
29
+ "babel-eslint": "^8.2.6",
30
+ "babel-loader": "^6.2.4",
31
+ "babel-preset-react": "^6.5.0",
32
+ "bootstrap": "^3.3.6",
33
+ "clipboard": "^1.5.8",
34
+ "codemirror": "^5.11.0",
35
+ "css-loader": "^0.28.11",
36
+ "eslint": "^5.4.0",
37
+ "eslint-config-imweb": "^0.2.11",
38
+ "file-loader": "^0.8.5",
39
+ "jquery": "^3.3.1",
40
+ "jsx-loader": "^0.13.2",
41
+ "react": "^15.4.2",
42
+ "react-dom": "^15.4.2",
43
+ "style-loader": "^0.23.1",
44
+ "url-loader": "0.5.6",
45
+ "webpack": "^1.14.0"
46
+ },
47
+ "dependencies": {
48
+ "iconv-lite": "^0.4.24",
49
+ "koa": "^2.13.1",
50
+ "koa-bodyparser": "^4.3.0",
51
+ "koa-onerror": "^4.1.0",
52
+ "koa-router": "^10.0.0",
53
+ "koa-static": "^5.0.0"
54
+ }
55
+ }
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="google" value="notranslate">
6
+ <title>Whistle Script</title>
7
+ </head>
8
+ <body>
9
+ <div id="main" class="main"></div>
10
+ <script src="index.js?v=0.4.0"></script>
11
+ </body>
12
+ </html>