From cc12e4e669bbccc9bc8e702f5ff26691cb3037ed Mon Sep 17 00:00:00 2001 From: avwo Date: Sun, 14 Sep 2025 23:10:09 +0800 Subject: [PATCH] refactor: refine code --- lib/config.js | 1 + lib/handlers/file-proxy.js | 2 +- lib/https/h2.js | 19 ++++---- lib/https/index.js | 6 +-- lib/inspectors/req.js | 10 ++-- lib/inspectors/res.js | 4 +- lib/plugins/index.js | 5 ++ lib/rules/rules.js | 2 +- lib/rules/storage.js | 1 + lib/rules/util.js | 34 ++++++++++++-- lib/tunnel.js | 4 +- lib/util/common.js | 49 ++++++++++++++++++++ lib/util/index.js | 94 +++++++++++++++++++++++++++++--------- 13 files changed, 182 insertions(+), 49 deletions(-) diff --git a/lib/config.js b/lib/config.js index d2ffb48d9..aca8d0c09 100644 --- a/lib/config.js +++ b/lib/config.js @@ -73,6 +73,7 @@ if (hasDnsOrder) { } } +config.appName = common.upperFirst(config.name); config.defaultDnsOrder = useVerbatim ? 1 : 2; config.uid = uid; config.rejectUnauthorized = false; diff --git a/lib/handlers/file-proxy.js b/lib/handlers/file-proxy.js index cf5104579..0f5ca9816 100644 --- a/lib/handlers/file-proxy.js +++ b/lib/handlers/file-proxy.js @@ -318,7 +318,7 @@ module.exports = function (req, res, next) { var type = buffer != null && svrRes && svrRes.headers && svrRes.headers['x-whistle-response-type']; type = type || mime.lookup(rule._suffix || path, defaultType); var headers = { - server: config.name, + server: config.appName, 'content-type': type + (util.isText(type) ? '; charset=utf-8' : '') }; diff --git a/lib/https/h2.js b/lib/https/h2.js index d1771f819..586a1088a 100644 --- a/lib/https/h2.js +++ b/lib/https/h2.js @@ -22,6 +22,7 @@ var CONCURRENT = 3; var CLOSED_ERR = new Error('closed'); var MSM = 1200; var PMCS = 3600; +var REQ_OPTS = { endStream: true }; function onClose(stream, callback) { stream.on('error', callback); @@ -267,16 +268,14 @@ function requestH2(client, req, res, callback) { headers[':method'] = options.method; headers[':authority'] = req.headers.host; try { - var h2Session = client.request(headers); - if (req.noReqBody) { - onClose(h2Session, function() { - if (!responsed) { - responsed = true; - h2Session.destroy(); - callback(); - } - }); - } + var h2Session = client.request(headers, req.noReqBody ? REQ_OPTS : undefined); + onClose(h2Session, function() { + if (!responsed) { + responsed = true; + h2Session.destroy(); + req.noReqBody && callback(); + } + }); var additionalHeaders; h2Session.once('headers', function(headers) { try { diff --git a/lib/https/index.js b/lib/https/index.js index b81521a40..c7b005c64 100644 --- a/lib/https/index.js +++ b/lib/https/index.js @@ -698,9 +698,9 @@ function resolveWebsocket(socket, wss) { var newUrl = params ? util.replaceUrlQueryString(fullUrl, params) : fullUrl; - var queryProps = util.parseDelQuery(socket); - newUrl = util.parsePathReplace(newUrl, urlReplace) || newUrl; - newUrl = util.deleteQuery(newUrl, queryProps, socket._delQueryString); + var dProps = util.parseDelQuery(socket); + newUrl = util.parsePathReplace(newUrl, urlReplace, dProps.paths) || newUrl; + newUrl = util.deleteQuery(newUrl, dProps.query, socket._delQueryString); if (newUrl !== fullUrl) { fullUrl = newUrl; socket._realUrl = newUrl; diff --git a/lib/inspectors/req.js b/lib/inspectors/req.js index ecb4b42a3..108551a02 100644 --- a/lib/inspectors/req.js +++ b/lib/inspectors/req.js @@ -545,19 +545,19 @@ module.exports = function (req, res, next) { data.bottom = reqAppend; } var delType = {}; - var queryProps = util.parseDelQuery(req, delType); + var dProps = util.parseDelQuery(req, delType); + var queryProps = dProps.query; + var pathProps = dProps.paths; handleReq(req, data, reqRules, delType); handleParams(req, params, urlParams); - if (req._urlParams || urlReplace || queryProps || req._delQueryString) { + if (req._urlParams || urlReplace || pathProps || queryProps || req._delQueryString) { var options = req.options; var isUrl = util.isUrl(options.href); var newUrl = isUrl ? options.href : req.fullUrl; if (req._urlParams) { newUrl = util.replaceUrlQueryString(newUrl, req._urlParams); } - if (urlReplace) { - newUrl = util.parsePathReplace(newUrl, urlReplace); - } + newUrl = util.parsePathReplace(newUrl, urlReplace, pathProps) || newUrl; newUrl = util.deleteQuery(newUrl, queryProps, req._delQueryString); if (newUrl !== options.href) { if (isUrl) { diff --git a/lib/inspectors/res.js b/lib/inspectors/res.js index 4998cfc83..37882089e 100644 --- a/lib/inspectors/res.js +++ b/lib/inspectors/res.js @@ -175,9 +175,9 @@ function checkH2(req, isHttps) { var d = req.disable; var e = req.enable; if (isHttps) { - if (d.h2 || d.httpsH2) { + if (d.h2 || d.http2 || d.httpsH2) { req.useH2 = false; - } else if (e.h2 || e.httpsH2) { + } else if (e.h2 || e.http2 || e.httpsH2) { req.useH2 = true; } } else { diff --git a/lib/plugins/index.js b/lib/plugins/index.js index 10fb81e1a..7a60b58e1 100644 --- a/lib/plugins/index.js +++ b/lib/plugins/index.js @@ -168,6 +168,11 @@ pluginMgr.getRegistryList = function() { return registryList; }; +pluginMgr.disableAllPlugins = function(disabled) { + properties.set('disabledAllPlugins', disabled); + pluginMgr.emit('updateRules', 'disableAllPlugins'); +}; + pluginMgr.getTunnelKeys = function () { return tunnelKeys; }; diff --git a/lib/rules/rules.js b/lib/rules/rules.js index 08c747095..e627a5f5a 100644 --- a/lib/rules/rules.js +++ b/lib/rules/rules.js @@ -51,7 +51,7 @@ var FILTER_RE = /^(?:excludeFilter|includeFilter):\/\/(.*)$/; var PROPS_FILTER_RE = /^(?:filter|excludeFilter|includeFilter|ignore):\/\/(m(?:ethod)?|i(?:p)?|h(?:eader)?|env|s(?:tatusCode)?|from|b(?:ody)?|clientIp|clientIP|clientPort|remoteAddress|remotePort|serverIp|serverIP|serverPort|chance|probability|re[qs](?:H(?:eaders?)?)?):(.+)$/; var PURE_FILTER_RE = - /^(?:excludeFilter|includeFilter):\/\/(statusCode|from|env|clientIp|clientIP|clientPort|remoteAddress|remotePort|serverIp|serverIP|serverPort|host|re[qs](?:H(?:eaders?)?)?)[.=](.+)$/; + /^(?:excludeFilter|includeFilter):\/\/(statusCode|from|env|clientIp|clientIP|clientPort|remoteAddress|remotePort|serverIp|serverIP|chance|probability|serverPort|host|re[qs](?:H(?:eaders?)?)?)[.=](.+)$/; var PATTERN_WILD_FILTER_RE = /^(?:filter|ignore):\/\/(!)?(\*+\/)/; var INLINE_RE = /^((?:filter|excludeFilter|includeFilter|ignore):\/\/)(\(.*\)|\<.*\>)$/; var WILD_FILTER_RE = /^(\*+\/)/; diff --git a/lib/rules/storage.js b/lib/rules/storage.js index 92788acfd..335bdb984 100644 --- a/lib/rules/storage.js +++ b/lib/rules/storage.js @@ -405,6 +405,7 @@ proto.moveToGroup = function(name, groupName, isTop) { filesOrder.splice(index, 0, name); } this._writeProperties(); + return true; }; proto.getGroupName = function(name) { diff --git a/lib/rules/util.js b/lib/rules/util.js index be71b5f15..bf36ff61a 100644 --- a/lib/rules/util.js +++ b/lib/rules/util.js @@ -319,6 +319,30 @@ function moveRulesTo(fromName, toName, clientId, group, toTop) { } exports.rules = { + getConfig: function() { + var list = [ + { + name: 'Default', + selected: !defaultRulesIsDisabled() + } + ]; + var selectedList = getSelectedRulesList(); + rulesStorage.getFileList().forEach(function (file) { + if (!util.isGroup(file.name)) { + list.push({ name: file.name, selected: selectedList.indexOf(file.name) !== -1 }); + } + }); + return { + pluginsDisabled: propertiesStorage.getProperty('disabledAllPlugins'), + disabled: !config.notAllowedDisableRules && propertiesStorage.getProperty('disabledAllRules'), + list: list + }; + }, + disableAllRules: function(disabled) { + propertiesStorage.setProperty('disabledAllRules', disabled); + parseRules(); + proxy.emit('rulesDataChange', 'disabledAllRules', disabled); + }, getRawRulesText: function() { return rules._rawRulesText || ''; }, @@ -361,7 +385,9 @@ exports.rules = { first && moveRulesTo(name, first.name, clientId, null, true); }, moveToGroup: function(name, groupName, isTop) { - return rulesStorage.moveToGroup(name, groupName, isTop); + if (rulesStorage.moveToGroup(name, groupName, isTop)) { + proxy.emit('rulesDataChange', 'moveToGroup', name, groupName, isTop); + } }, get: function (file) { return rulesStorage.readFile(file); @@ -443,7 +469,9 @@ exports.values = { } }, moveToGroup: function(name, groupName, isTop) { - return valuesStorage.moveToGroup(name, groupName, isTop); + if (valuesStorage.moveToGroup(name, groupName, isTop)) { + proxy.emit('valuesDataChange', 'moveToGroup', name, groupName, isTop); + } }, getFirstGroup: function() { return getFirstGroup(valuesStorage); @@ -477,7 +505,7 @@ exports.values = { }; setTimeout(function getWhistleVersion() { - util.getLatestVersion(config.registry, function (ver) { + util.getLatestVersion('https://registry.npmjs.org/whistle', function (ver) { ver && propertiesStorage.writeFile('latestVersion', ver); setTimeout(getWhistleVersion, INTERVAL); }); diff --git a/lib/tunnel.js b/lib/tunnel.js index 579fa94c9..e326b73da 100644 --- a/lib/tunnel.js +++ b/lib/tunnel.js @@ -723,7 +723,7 @@ function tunnelProxy(server, proxy, type) { if (res) { code = res.statusCode || 200; if (!res.headers['proxy-agent']) { - res.headers['proxy-agent'] = config.name; + res.headers['proxy-agent'] = config.appName; res.rawHeaders = res.rawHeaders || []; res.rawHeaders.push('proxy-agent', 'Proxy-Agent'); } @@ -732,7 +732,7 @@ function tunnelProxy(server, proxy, type) { res = { statusCode: code, headers: { - 'proxy-agent': config.name + 'proxy-agent': config.appName }, rawHeaders: ['proxy-agent', 'Proxy-Agent'] }; diff --git a/lib/util/common.js b/lib/util/common.js index 4b5ac10a6..b184508d4 100644 --- a/lib/util/common.js +++ b/lib/util/common.js @@ -1106,3 +1106,52 @@ exports.createHash = function (str) { shasum.update(str || ''); return shasum.digest('hex'); }; + +var VER_LEN = 3; + +function compareVer(n1, n2, index) { + n1 = parseInt(n1, 10) || 0; + n2 = parseInt(n2, 10) || 0; + if (n1 === n2) { + return 0; + } + return n1 > n2 ? VER_LEN - index : index - VER_LEN; +} + +exports.compareVersion = function(v1, v2) { + if (v1 === v2 || !v1 || typeof v1 !== 'string') { + return 0; + } + if (!v2 || typeof v2 !== 'string') { + return 3; + } + v1 = v1.split('.'); + v2 = v2.split('.'); + + for (var i = 0; i < 3; i++) { + var flag = compareVer(v1[i], v2[i], i); + if (flag) { + return Math.max(flag, 0); + } + } + v1 = v1[2]; + v2 = v2[2]; + if (!v1 || !v2) { + return 0; + } + var i1 = v1.indexOf('-'); + var i2 = v2.indexOf('-'); + var test1 = i1 === -1 ? '' : v1.substring(i1); + var test2 = i2 === -1 ? '' : v2.substring(i2); + if (test1 === test2) { + return 0; + } + if (!test1 || !test2) { + return test1 ? 0 : 1; + } + return test1 > test2 ? 1 : 0; +}; + +exports.upperFirst = function(str) { + return isString(str) ? str[0].toUpperCase() + str.substring(1) : ''; +}; diff --git a/lib/util/index.js b/lib/util/index.js index e921e6a32..bfc9214c3 100644 --- a/lib/util/index.js +++ b/lib/util/index.js @@ -293,7 +293,7 @@ exports.hasBody = hasBody; var ESTABLISHED_CTN = 'HTTP/1.1 200 Connection Established\r\nProxy-Agent: ' + - config.name + + config.appName + '\r\n\r\n'; exports.setEstablished = function (socket) { socket.write(ESTABLISHED_CTN); @@ -514,7 +514,7 @@ function getErrorStack(err) { } catch (e) {} stack = stack || err.message || err; var result = [ - 'From: ' + config.name + '@' + config.version, + 'From: ' + config.appName + '@' + config.version, 'Node: ' + process.version, 'Host: ' + hostname, 'Date: ' + formatDate(), @@ -949,8 +949,12 @@ function disableResStore(headers) { exports.disableResStore = disableResStore; -function parsePathReplace(urlPath, params) { - if (!params || !/^(?:ws|http)s?:/.test(urlPath)) { +function notNull(value) { + return value != null; +} + +function parsePathReplace(urlPath, params, delPaths) { + if ((!params && !delPaths) || !/^(?:ws|http)s?:/.test(urlPath)) { return; } var index = urlPath.indexOf('://'); @@ -958,24 +962,57 @@ function parsePathReplace(urlPath, params) { return; } index = urlPath.indexOf('/', index + 3) + 1; - if (index <= 0) { + if (!index) { return; } - - var root = urlPath.substring(0, index); - urlPath = urlPath.substring(index); - - Object.keys(params).forEach(function (pattern) { + var curPath = urlPath.substring(index); + params && Object.keys(params).forEach(function (pattern) { var value = params[pattern]; value = value == null ? '' : value + ''; if (isOriginalRegExp(pattern) && (pattern = toOriginalRegExp(pattern))) { - urlPath = urlPath.replace(pattern, value); + curPath = curPath.replace(pattern, value); } else if (pattern) { - urlPath = urlPath.split(pattern).join(value); + curPath = curPath.split(pattern).join(value); } }); - root += urlPath; - return root !== urlPath ? root : null; + if (curPath && delPaths) { + var query = ''; + var qIdx = curPath.indexOf('?'); + if (qIdx !== -1) { + query = curPath.substring(qIdx); + curPath = curPath.substring(0, qIdx); + } + if (curPath) { + if (delPaths.all) { + delete delPaths.all; + curPath = query; + } else { + curPath = curPath.split('/'); + var len = curPath.length; + var last = delPaths.last; + if (last) { + delete delPaths.last; + delPaths[len - 1] = 1; + } + Object.keys(delPaths).forEach(function (key) { + key = +key; + key = key < 0 ? len + key : key; + if (key >= 0 && key < len) { + curPath[key] = null; + } + }); + + curPath = curPath.filter(notNull); + if (last && curPath[curPath.length - 1]) { + curPath.push(''); + } + curPath = curPath.join('/'); + } + } + curPath += query; + } + var newUrl = urlPath.substring(0, index) + curPath; + return newUrl === urlPath ? null : newUrl; } exports.parsePathReplace = parsePathReplace; @@ -985,7 +1022,7 @@ function wrapResponse(res) { passThrough.statusCode = res.statusCode; passThrough.rawHeaderNames = res.rawHeaderNames; passThrough.headers = lowerCaseify(res.headers); - passThrough.headers['x-server'] = config.name; + passThrough.headers['x-server'] = config.appName; res.body != null && passThrough.push(Buffer.isBuffer(res.body) ? res.body : String(res.body)); passThrough.push(null); @@ -1096,6 +1133,14 @@ exports.parseHeaders = parseHeaders; var QUERY_PARAM_RE = /^[^\\/]+=/; +function tryParseMatcher(text, rule) { + var matcher = !text && removeProtocol(getMatcher(rule), true); + if (!matcher || matcher.indexOf('=') === -1) { + return; + } + return parseQuery(matcher, null, null, true); +} + exports.parseRuleJson = function(rules, callback, req) { if (!Array.isArray(rules)) { rules = [rules]; @@ -1230,7 +1275,7 @@ function readRuleList(rule, callback, isJson, charset, isHtml, req) { rule, isJson ? function (value) { - callback(_parseJSON(value, RESOLVE_KEY_RE.test(rule.matcher))); + callback(tryParseMatcher(value, rule) || _parseJSON(value, RESOLVE_KEY_RE.test(rule.matcher))); } : callback, false, @@ -1250,7 +1295,7 @@ function readRuleList(rule, callback, isJson, charset, isHtml, req) { var deepMerge = isDeep(result); result = result.map(function(text, i) { var item = rule.list[i]; - return _parseJSON(text, item && RESOLVE_KEY_RE.test(item.matcher)); + return tryParseMatcher(text, item) || _parseJSON(text, item && RESOLVE_KEY_RE.test(item.matcher)); }).filter(noop); if (result.length > 1) { result.reverse(); @@ -2783,9 +2828,11 @@ var RES_BODY_RE = /^res\.?B(?:ody?)?\.(.+)$/i; var COOKIE_RE = /^cookies?\.(.+)$/i; var QUERY_RE = /^(?:query|params|url\.?Params?)\.(.+)$/i; var QUERY_STRING_RE = /^(?:query|params|url\.?Params?)$/i; +var PATH_INDEX_RE = /^pathname(?:\.?(-?\d+|first|last))?$/i; exports.parseDelQuery = function(req, delType) { var query; + var paths; var deleteRule = req['delete']; if (deleteRule) { Object.keys(deleteRule).forEach(function (prop) { @@ -2794,6 +2841,10 @@ exports.parseDelQuery = function(req, delType) { query[RegExp.$1] = 1; } else if (QUERY_STRING_RE.test(prop)) { req._delQueryString = true; + } else if (PATH_INDEX_RE.test(prop)) { + paths = paths || {}; + prop = RegExp.$1; + paths[prop === 'first' ? '0' : prop || 'all'] = 1; } else if (delType) { if (prop === 'reqType' || prop === 'req.type') { delType.reqType = true; @@ -2803,7 +2854,7 @@ exports.parseDelQuery = function(req, delType) { } }); } - return query; + return { query: query, paths: paths }; }; exports.deleteQuery = function(url, props, clear) { @@ -4150,11 +4201,10 @@ exports.getLocalhostIP = function (err, req, domain, curIp) { } }; -function checkHideProp(e, d, name) { - name = name || ''; +function checkHideProp(e, d, name, filterHide) { var hide = 'hide' + name; var show = 'show' + name; - return (e[hide] || d[show]) && !e[show] && !d[hide]; + return (e[hide] || d[show] || filterHide) && !e[show] && !d[hide]; } exports.checkHideProp = checkHideProp; @@ -4165,5 +4215,5 @@ exports.isHide = function(req) { } var e = req.enable || ''; var d = req.disable || ''; - return checkHideProp(e, d) && (!req.fromComposer || checkHideProp(e, d, 'Composer')); + return req.fromComposer ? checkHideProp(e, d, 'Composer') : checkHideProp(e, d, '', req._filters && req._filters.hide); };