whistle 2.9.74 → 2.9.76-beta
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/bin/status.js +5 -7
- package/biz/webui/htdocs/index.html +1 -1
- package/biz/webui/htdocs/js/index.js +9 -9
- package/biz/webui/lib/index.js +27 -8
- package/lib/handlers/file-proxy.js +40 -0
- package/lib/inspectors/res.js +2 -2
- package/lib/rules/protocols.js +6 -1
- package/lib/util/index.js +43 -52
- package/lib/util/replace-pattern-transform.js +8 -1
- package/lib/util/replace-string-transform.js +9 -2
- package/package.json +4 -6
package/biz/webui/lib/index.js
CHANGED
|
@@ -6,7 +6,6 @@ var https = require('https');
|
|
|
6
6
|
var parseReqUrl = require('parseurl');
|
|
7
7
|
var bodyParser = require('body-parser');
|
|
8
8
|
var crypto = require('crypto');
|
|
9
|
-
var cookie = require('cookie');
|
|
10
9
|
var fs = require('fs');
|
|
11
10
|
var zlib = require('zlib');
|
|
12
11
|
var extend = require('extend');
|
|
@@ -69,6 +68,26 @@ function shasum(str) {
|
|
|
69
68
|
return shasum.digest('hex');
|
|
70
69
|
}
|
|
71
70
|
|
|
71
|
+
function parseCookie(str) {
|
|
72
|
+
var result = {};
|
|
73
|
+
str && str.split(';').forEach(function(pair) {
|
|
74
|
+
var index = pair.indexOf('=');
|
|
75
|
+
if (index === -1) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
var key = pair.substring(0, index).trim();
|
|
79
|
+
var val = pair.substring(index + 1).trim();
|
|
80
|
+
if (result[key] == null) {
|
|
81
|
+
try {
|
|
82
|
+
result[key] = decodeURIComponent(val);
|
|
83
|
+
} catch (e) {
|
|
84
|
+
result[key] = val;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
72
91
|
function getLoginKey (req, res, auth) {
|
|
73
92
|
var ip = util.getClientIp(req);
|
|
74
93
|
var password = auth.password;
|
|
@@ -112,7 +131,7 @@ function verifyLogin(req, res, auth) {
|
|
|
112
131
|
return true;
|
|
113
132
|
}
|
|
114
133
|
var authKey = auth.authKey;
|
|
115
|
-
var cookies =
|
|
134
|
+
var cookies = parseCookie(req.headers.cookie);
|
|
116
135
|
var lkey = cookies[authKey];
|
|
117
136
|
var correctKey = getLoginKey(req, res, auth);
|
|
118
137
|
if (correctKey === lkey) {
|
|
@@ -125,12 +144,12 @@ function verifyLogin(req, res, auth) {
|
|
|
125
144
|
queryAuth.pass = queryAuth.pass && shasum(queryAuth.pass);
|
|
126
145
|
}
|
|
127
146
|
if (equalAuth(headerAuth, auth) || equalAuth(queryAuth, auth)) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
147
|
+
res.setHeader('Set-Cookie', [
|
|
148
|
+
authKey + '=' + util.encodeURIComponent(correctKey),
|
|
149
|
+
'Max-Age=' + MAX_AGE,
|
|
150
|
+
'Path=/',
|
|
151
|
+
'Expires=' + new Date(Date.now() + (MAX_AGE * 1000)).toUTCString()
|
|
152
|
+
].join('; '));
|
|
134
153
|
return true;
|
|
135
154
|
}
|
|
136
155
|
}
|
|
@@ -17,6 +17,15 @@ var VAR_RE = /\{\S+\}/;
|
|
|
17
17
|
var QUERY_VAR_RE = /\$?(?:\{\{([\w$-]+)\}\}|\{([\w$-]+)\})/g;
|
|
18
18
|
var UTF8_OPTIONS = { encoding: 'utf8' };
|
|
19
19
|
var ENABLE_OPTIONS = { enable: true };
|
|
20
|
+
var SLASH_RE = /\\+/g;
|
|
21
|
+
var QUOTE_RE = /"/g;
|
|
22
|
+
var JS_RE = /^js:/i;
|
|
23
|
+
var HTML_RE = /^html:/i;
|
|
24
|
+
var BLANK_RE = /\s/g;
|
|
25
|
+
|
|
26
|
+
function urlToStr(url) {
|
|
27
|
+
return url.replace(SLASH_RE, '').replace(QUOTE_RE, '\\$&').replace(BLANK_RE, ' ');
|
|
28
|
+
}
|
|
20
29
|
|
|
21
30
|
function isRawFileProtocol(protocol) {
|
|
22
31
|
return RAW_FILE_RE.test(protocol);
|
|
@@ -176,6 +185,34 @@ function sendResponse(rule, res, reader, req) {
|
|
|
176
185
|
res.response(reader);
|
|
177
186
|
}
|
|
178
187
|
|
|
188
|
+
function handleLocHref(req, rule, handleRes) {
|
|
189
|
+
var body = (util.getMatcherValue(rule) || '').trim();
|
|
190
|
+
var isJs = JS_RE.test(body);
|
|
191
|
+
var isHtml = HTML_RE.test(body);
|
|
192
|
+
if (isJs) {
|
|
193
|
+
body = body.substring(3);
|
|
194
|
+
} else if (isHtml) {
|
|
195
|
+
body = body.substring(5);
|
|
196
|
+
} else {
|
|
197
|
+
isJs = req.headers['sec-fetch-dest'] === 'script';
|
|
198
|
+
}
|
|
199
|
+
var type = isJs ? 'application/javascript' : 'text/html';
|
|
200
|
+
if (body) {
|
|
201
|
+
body = 'window.location.href = "' + urlToStr(body) + '";';
|
|
202
|
+
if (!isJs) {
|
|
203
|
+
body = '<script>' + body + '</script>';
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
handleRes({
|
|
208
|
+
statusCode: 200,
|
|
209
|
+
body: body,
|
|
210
|
+
headers: {
|
|
211
|
+
'content-type': type + '; charset=utf-8'
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
179
216
|
module.exports = function (req, res, next) {
|
|
180
217
|
var options = req.options;
|
|
181
218
|
var config = this.config;
|
|
@@ -185,6 +222,9 @@ module.exports = function (req, res, next) {
|
|
|
185
222
|
}
|
|
186
223
|
var rules = req.rules;
|
|
187
224
|
var rule = rules.rule;
|
|
225
|
+
if (protoMgr.isLocHref(protocol)) {
|
|
226
|
+
return handleLocHref(req, rule, render);
|
|
227
|
+
}
|
|
188
228
|
if (isAutoCors(req, rule) && req.method === 'OPTIONS') {
|
|
189
229
|
var optsRes = util.wrapResponse({ statusCode: 200 });
|
|
190
230
|
optsRes.realUrl = rule.matcher;
|
package/lib/inspectors/res.js
CHANGED
|
@@ -134,9 +134,9 @@ function handleReplace(res, replacement) {
|
|
|
134
134
|
util.isOriginalRegExp(pattern) &&
|
|
135
135
|
(pattern = util.toOriginalRegExp(pattern))
|
|
136
136
|
) {
|
|
137
|
-
res.addTextTransform(new ReplacePatternTransform(pattern, value));
|
|
137
|
+
res.addTextTransform(new ReplacePatternTransform(pattern, value, util.isSSE(res)));
|
|
138
138
|
} else if (pattern) {
|
|
139
|
-
res.addTextTransform(new ReplaceStringTransform(pattern, value));
|
|
139
|
+
res.addTextTransform(new ReplaceStringTransform(pattern, value, util.isSSE(res)));
|
|
140
140
|
}
|
|
141
141
|
});
|
|
142
142
|
}
|
package/lib/rules/protocols.js
CHANGED
|
@@ -74,6 +74,7 @@ var protocols = [
|
|
|
74
74
|
];
|
|
75
75
|
|
|
76
76
|
var RULE_RE = /^(?:|x|xs)(?:file|rawfile|dust|tpl|jsonp):/;
|
|
77
|
+
var LOC_RE = /^locationHref:/;
|
|
77
78
|
var PROXY_RE =
|
|
78
79
|
/^x?(?:socks|proxy|https?-proxy|internal-proxy|internal-https?-proxy|https2http-proxy|http2https-proxy)$/;
|
|
79
80
|
var pureResProtocols = [
|
|
@@ -275,11 +276,15 @@ function isWebsocketProtocol(protocol) {
|
|
|
275
276
|
exports.isWebsocketProtocol = isWebsocketProtocol;
|
|
276
277
|
|
|
277
278
|
function isFileProxy(protocol) {
|
|
278
|
-
return RULE_RE.test(protocol);
|
|
279
|
+
return RULE_RE.test(protocol) || LOC_RE.test(protocol);
|
|
279
280
|
}
|
|
280
281
|
|
|
281
282
|
exports.isFileProxy = isFileProxy;
|
|
282
283
|
|
|
284
|
+
exports.isLocHref = function(protocol) {
|
|
285
|
+
return LOC_RE.test(protocol);
|
|
286
|
+
};
|
|
287
|
+
|
|
283
288
|
function contains(name) {
|
|
284
289
|
if (
|
|
285
290
|
protocols.indexOf(name) != -1 ||
|
package/lib/util/index.js
CHANGED
|
@@ -15,7 +15,6 @@ var iconv = require('iconv-lite');
|
|
|
15
15
|
var zlib = require('zlib');
|
|
16
16
|
var dns = require('dns');
|
|
17
17
|
var PipeStream = require('pipestream');
|
|
18
|
-
var Q = require('q');
|
|
19
18
|
var Buffer = require('safe-buffer').Buffer;
|
|
20
19
|
var protoMgr = require('../rules/protocols');
|
|
21
20
|
var protocols = protoMgr.protocols;
|
|
@@ -438,6 +437,12 @@ function stat(file, callback, force) {
|
|
|
438
437
|
});
|
|
439
438
|
}
|
|
440
439
|
|
|
440
|
+
function spreadPromise(promise, callback) {
|
|
441
|
+
promise.then(function(list) {
|
|
442
|
+
callback.apply(null, list);
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
441
446
|
function getFileWriter(file, callback, force) {
|
|
442
447
|
if (!file) {
|
|
443
448
|
return callback();
|
|
@@ -476,19 +481,19 @@ function getFileWriters(files, callback, force) {
|
|
|
476
481
|
files = [files];
|
|
477
482
|
}
|
|
478
483
|
|
|
479
|
-
|
|
484
|
+
spreadPromise(Promise.all(
|
|
480
485
|
files.map(function (file) {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
486
|
+
return new Promise(function(resolve) {
|
|
487
|
+
getFileWriter(
|
|
488
|
+
file,
|
|
489
|
+
function (writer) {
|
|
490
|
+
resolve(writer);
|
|
491
|
+
},
|
|
492
|
+
force
|
|
493
|
+
);
|
|
494
|
+
});
|
|
490
495
|
})
|
|
491
|
-
)
|
|
496
|
+
), callback);
|
|
492
497
|
}
|
|
493
498
|
|
|
494
499
|
exports.getFileWriters = getFileWriters;
|
|
@@ -1248,22 +1253,13 @@ exports.parseRuleJson = function(rules, callback, req) {
|
|
|
1248
1253
|
if (!Array.isArray(rules)) {
|
|
1249
1254
|
rules = [rules];
|
|
1250
1255
|
}
|
|
1251
|
-
|
|
1256
|
+
spreadPromise(Promise.all(
|
|
1252
1257
|
rules.map(function (rule) {
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
function (data) {
|
|
1257
|
-
defer.resolve(data);
|
|
1258
|
-
},
|
|
1259
|
-
true,
|
|
1260
|
-
null,
|
|
1261
|
-
null,
|
|
1262
|
-
req
|
|
1263
|
-
);
|
|
1264
|
-
return defer.promise;
|
|
1258
|
+
return new Promise(function(resolve) {
|
|
1259
|
+
readRuleList(rule, resolve, true, null, null, req);
|
|
1260
|
+
});
|
|
1265
1261
|
})
|
|
1266
|
-
)
|
|
1262
|
+
), callback);
|
|
1267
1263
|
};
|
|
1268
1264
|
|
|
1269
1265
|
function getTempFilePath(filePath, rule) {
|
|
@@ -1487,23 +1483,13 @@ exports.getRuleValue = function(rules, callback, noBody, charset, isHtml, req) {
|
|
|
1487
1483
|
if (!Array.isArray(rules)) {
|
|
1488
1484
|
rules = [rules];
|
|
1489
1485
|
}
|
|
1490
|
-
|
|
1491
|
-
Q.all(
|
|
1486
|
+
spreadPromise(Promise.all(
|
|
1492
1487
|
rules.map(function (rule) {
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
function (data) {
|
|
1497
|
-
defer.resolve(data);
|
|
1498
|
-
},
|
|
1499
|
-
false,
|
|
1500
|
-
charset,
|
|
1501
|
-
isHtml,
|
|
1502
|
-
req
|
|
1503
|
-
);
|
|
1504
|
-
return defer.promise;
|
|
1488
|
+
return new Promise(function(resolve) {
|
|
1489
|
+
readRuleList(rule, resolve, false, charset, isHtml, req);
|
|
1490
|
+
});
|
|
1505
1491
|
})
|
|
1506
|
-
)
|
|
1492
|
+
), callback);
|
|
1507
1493
|
};
|
|
1508
1494
|
|
|
1509
1495
|
function decodePath(path) {
|
|
@@ -3681,18 +3667,17 @@ exports.getBoundIp = function (host, cb) {
|
|
|
3681
3667
|
}
|
|
3682
3668
|
var boundIpDefer = boundIpDeferMap[host];
|
|
3683
3669
|
if (boundIpDefer) {
|
|
3684
|
-
return boundIpDefer.
|
|
3685
|
-
}
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
}
|
|
3694
|
-
defer.resolve(ip);
|
|
3670
|
+
return boundIpDefer.then(cb);
|
|
3671
|
+
}
|
|
3672
|
+
boundIpDeferMap[host] = boundIpDefer = new Promise(function(resolve) {
|
|
3673
|
+
dns.lookup(host, function (err, ip) {
|
|
3674
|
+
if (err) {
|
|
3675
|
+
throw err;
|
|
3676
|
+
}
|
|
3677
|
+
resolve(ip);
|
|
3678
|
+
});
|
|
3695
3679
|
});
|
|
3680
|
+
boundIpDefer.then(cb);
|
|
3696
3681
|
};
|
|
3697
3682
|
|
|
3698
3683
|
function getPluginConfig(conf, name) {
|
|
@@ -4122,3 +4107,9 @@ exports.needAbortRes = function(req) {
|
|
|
4122
4107
|
}
|
|
4123
4108
|
return req.isTunnel && disable.tunnel;
|
|
4124
4109
|
};
|
|
4110
|
+
|
|
4111
|
+
var SSE_RE = /^\s*text\/event-stream\s*;?/i;
|
|
4112
|
+
|
|
4113
|
+
exports.isSSE = function(res) {
|
|
4114
|
+
return SSE_RE.test(res.headers['content-type']);
|
|
4115
|
+
};
|
|
@@ -7,11 +7,12 @@ var SUB_MATCH_RE = /(^|\\{0,2})?(\$\$?(b)?[&\d])/g;
|
|
|
7
7
|
var ALL_RE = /^\/\.[*+]\/g?i?g?$/;
|
|
8
8
|
var MAX_SUB_MATCH_LEN = 512;
|
|
9
9
|
|
|
10
|
-
function ReplacePatternTransform(pattern, value) {
|
|
10
|
+
function ReplacePatternTransform(pattern, value, isSSE) {
|
|
11
11
|
Transform.call(this);
|
|
12
12
|
this._pattern = pattern;
|
|
13
13
|
this._replaceAll = ALL_RE.test(pattern);
|
|
14
14
|
this._value = value == null ? '' : value + '';
|
|
15
|
+
this._isSSE = isSSE;
|
|
15
16
|
this._rest = '';
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -39,6 +40,12 @@ proto._transform = function (chunk, _, callback) {
|
|
|
39
40
|
return replacePattern(value, arguments);
|
|
40
41
|
});
|
|
41
42
|
index = Math.max(index, chunk.length - LENGTH);
|
|
43
|
+
if (this._isSSE) {
|
|
44
|
+
var endIndex = chunk.lastIndexOf('\n\n');
|
|
45
|
+
if (endIndex !== -1) {
|
|
46
|
+
index = Math.max(endIndex + 2, index);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
42
49
|
this._rest = chunk.substring(index);
|
|
43
50
|
chunk = result.substring(0, result.length - this._rest.length);
|
|
44
51
|
} else if (this._rest) {
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
var Transform = require('pipestream').Transform;
|
|
2
2
|
var util = require('util');
|
|
3
3
|
|
|
4
|
-
function ReplaceStringTransform(str, value) {
|
|
4
|
+
function ReplaceStringTransform(str, value, isSSE) {
|
|
5
5
|
Transform.call(this);
|
|
6
6
|
this._str = str;
|
|
7
7
|
this._length = this._str.length;
|
|
8
8
|
this._value = value == null ? '' : value + '';
|
|
9
|
+
this._isSSE = isSSE;
|
|
9
10
|
this._rest = '';
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
util.inherits(ReplaceStringTransform, Transform);
|
|
13
14
|
|
|
14
15
|
var proto = ReplaceStringTransform.prototype;
|
|
15
|
-
proto._transform = function (chunk,
|
|
16
|
+
proto._transform = function (chunk, _, callback) {
|
|
16
17
|
if (chunk != null) {
|
|
17
18
|
chunk = this._rest + chunk;
|
|
18
19
|
var minIndex = chunk.length + 1 - this._length;
|
|
@@ -23,6 +24,12 @@ proto._transform = function (chunk, encoding, callback) {
|
|
|
23
24
|
} else {
|
|
24
25
|
index = minIndex;
|
|
25
26
|
}
|
|
27
|
+
if (this._isSSE) {
|
|
28
|
+
var endIndex = chunk.lastIndexOf('\n\n');
|
|
29
|
+
if (endIndex !== -1) {
|
|
30
|
+
index = Math.max(endIndex + 2, index);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
26
33
|
this._rest = chunk.substring(index);
|
|
27
34
|
chunk = chunk.substring(0, index);
|
|
28
35
|
} else {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whistle",
|
|
3
3
|
"description": "HTTP, HTTP2, HTTPS, Websocket debugging proxy",
|
|
4
|
-
"version": "2.9.
|
|
4
|
+
"version": "2.9.76-beta",
|
|
5
5
|
"dataDirname": ".whistle",
|
|
6
6
|
"localUIHost": "local.whistlejs.com",
|
|
7
7
|
"port": 8899,
|
|
@@ -37,11 +37,10 @@
|
|
|
37
37
|
"async-limiter": "2.0.0",
|
|
38
38
|
"body-parser": "^1.19.0",
|
|
39
39
|
"colors": "1.1.2",
|
|
40
|
-
"cookie": "^0.3.1",
|
|
41
40
|
"express": "^4.19.2",
|
|
42
41
|
"extend": "^3.0.2",
|
|
43
42
|
"fs-extra2": "^1.0.0",
|
|
44
|
-
"hagent": "^0.9.
|
|
43
|
+
"hagent": "^0.9.2",
|
|
45
44
|
"hparser": "^0.4.0",
|
|
46
45
|
"iconv-lite": "^0.4.24",
|
|
47
46
|
"json5": "^2.2.3",
|
|
@@ -51,11 +50,10 @@
|
|
|
51
50
|
"node-forge": "^1.3.1",
|
|
52
51
|
"node-pac": "^0.5.0",
|
|
53
52
|
"parseurl": "^1.3.1",
|
|
54
|
-
"pfork": "^0.6.
|
|
53
|
+
"pfork": "^0.6.2",
|
|
55
54
|
"pipestream": "^0.7.3",
|
|
56
|
-
"q": "1.4.1",
|
|
57
55
|
"safe-buffer": "^5.1.2",
|
|
58
|
-
"set-global-proxy": "
|
|
56
|
+
"set-global-proxy": "0.2.0-beta",
|
|
59
57
|
"sni": "1.0.0",
|
|
60
58
|
"sockx": "^0.2.1",
|
|
61
59
|
"starting": "^8.0.2",
|