UNPKG

whistle

Version:

HTTP, HTTP2, HTTPS, Websocket debugging proxy

1,781 lines (1,709 loc) 68.5 kB
var parseUrl = require('../util/parse-url-safe'); var net = require('net'); var extend = require('extend'); var crypto = require('crypto'); var util = require('../util'); var rulesUtil = require('./util'); var lookup = require('./dns'); var protoMgr = require('./protocols'); var config = require('../config'); var rules = rulesUtil.rules; var values = rulesUtil.values; var env = process.env || {}; var allowDnsCache = true; var SUB_MATCH_RE = /\$[&\d]/; var BODY_MATCH_RE = /\$b[&\d]/; var SPACE_RE = /\s+/g; var EXACT_RE = /^\$/; var NON_RE = /^!/; var HAS_SPACE_RE = /\s/; var MULTI_TO_ONE_RE = /^\s*line`\s*[\r\n]([\s\S]*?)[\r\n]\s*`\s*?$/gm; var WEB_PROTOCOL_RE = /^(?:https?|wss?|tunnel):\/\//; var PORT_RE = /^(x?hosts?:\/\/)(?:([\w.-]*)|\[([:\da-f.]+)\])(?::(\d+))?$/i; var PLUGIN_RE = /^(?:plugin|whistle)\.([a-z\d_\-]+:\/\/[\s\S]*)/; var PLUGIN_TPL_RE = /(^|\s)?(%[a-z\d_-]+(?:\.[^\s=]+)?=|(?:whistle\.)?[a-z\d_-]+:\/\/)([^\s]*)/g; var ROOT_PLUGIN_RE = /[\\/]whistle\.([a-z\d_-]+)$/; var TPL_KEY_RE = /\{\{\s*%?ruleValue\s*\}\}/g; var END_RE = /\$$/; var protocols = protoMgr.protocols; var protocolsWithoutG = protoMgr.protocolsWithoutG; var reqProtocols = protoMgr.reqProtocols; var reqProtosWithoutG = protoMgr.reqProtosWithoutG; var pureResProtocols = protoMgr.pureResProtocols; var multiMatchs = protoMgr.multiMatchs; var aliasProtocols = protoMgr.aliasProtocols; var FILE_RE = /^(?:[a-z]:(?:\\|\/[^/])|\/[^/])/i; var PROXY_RE = /^x?(socks|proxy|https?-proxy|internal-proxy|internal-https?-proxy|https2http-proxy|http2https-proxy):\/\//; var VAR_RE = /\${([^{}]+)}/g; var NO_SCHEMA_RE = /^\/\/[^/]/; var WILDCARD_RE = /^(\$?((?:[a-z*]+):\/\/)?([^/?]*))/; var RULE_KEY_RE = /^\$\{(\S+)\}$/; var VALUE_KEY_RE = /^\{(\S+)\}$/; var LINE_END_RE = /\n|\r\n|\r/; var LOCAL_RULE_RE = /^https?:\/\/local\.(?:whistlejs\.com|wproxy\.org)(:realPort)?(?:\/|\?|$)/; var PATH_RE = /^<.*>$/; var VALUE_RE = /^\(.*\)$/; var REG_URL_RE = /^((?:[a-z*]+:)?\/\/)?([^/?]*)/; var LIKE_REG_URL_RE = /^(?:(?:(?:https?|wss?|tunnel):)?\/\/)?\*+\/[^?*]*\*/; var LIKE_REG_URL_RE2 = /^(?:(?:(?:https?|wss?|tunnel):)?\/\/)?\.[^./?]+\.[^/?]+\/[^?*]*\*/; var DOT_DOMAIN_RE = /^\.[^./?]+\.[^/?]/; var REG_URL_SYMBOL_RE = /^(\^+)/; var PATTERN_FILTER_RE = /^(?:filter|ignore):\/\/(.+)\/(i)?$/; var LINE_PROPS_RE = /^lineProps:\/\/(.*)$/; 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|chance|probability|serverPort|host|re[qs](?:H(?:eaders?)?)?)[.=](.+)$/; var PATTERN_WILD_FILTER_RE = /^(?:filter|ignore):\/\/(!)?(\*+\/)/; var INLINE_RE = /^((?:filter|excludeFilter|includeFilter|ignore):\/\/)(\(.*\)|\<.*\>)$/; var CHROME_PATH_RE = /^file:\/\/\/[A-Z]:\//; var AT_RE = /^@/; var WILD_FILTER_RE = /^(\*+\/)/; var regUrlCache = {}; var hostCache = {}; var NON_STAR_RE = /[^*]/; var DOMAIN_STAR_RE = /([*~]+)(\\.)?/g; var STAR_RE = /\*+/g; var PORT_PATTERN_RE = /^!?:\d{1,5}$/; var COMMENT_RE = /#[^\r\n]*/g; var TPL_RE = /^((?:[\w.-]+:)?\/\/)?(`.*`)$/; // url: protocol, host, port, hostname, search, query, pathname, path, href, query.key // req|res: ip, method, statusCode, headers?.key, cookies?.key var PLUGIN_NAME_RE = /^[a-z\d_\-]+(?:\.g?(?:all)?var(?:\$|\d*))?(?:\.replace\(.+\))?$/i; var VAR_INDEX_RE = /^([a-z\d_\-]+)\.(g)?(all)?var(\$|\d*)/i; var TPL_VAR_RE = /(\$)?\$\{(\{)?(id|reqId|whistle|env|now|random(?:Int\(\d{1,15}(?:-\d{1,15})?\))?|randomUUID|host|port|realPort|realHost|realUrl|version|url|hostname|query|search|queryString|searchString|path|pathname|clientId|localClientId|ip|clientIp|clientPort|remoteAddress|remotePort|serverIp|serverPort|method|status(?:Code)|reqCookies?|resCookies?|re[qs]H(?:eaders?)?)(?:\.([^{}]+))?\}(\})?/gi; var RANDOM_RE = /randomInt\((\d{1,15})(?:-(\d{1,15}))?\)/i; var REPLACE_PATTERN_RE = /(^|\.)replace\((.+)\)$/i; var SEP_RE = /^[?/]/; var COMMA1_RE = /\\,/g; var COMMA2_RE = /\\\\,/g; var G_CR_RE = /\r/g; var G_LF_RE = /\n/g; var SUFFIX_RE = /^\\\.[\w-]+$/; var DOT_PATTERN_RE = /^\.[\w-]+(?:[?$]|$)/; var inlineValues = {}; var pluginMgr; var ENABLE_PROXY_RE = /\bproxy(?:Host|First|Tunnel)|clientId|multiClient|singleClient\b/i; var PLUGIN_VAR_RE = /^%([a-z\d_\-]+)([=.])([^\s]*)/; var EXACT_IGNORE_RE = /^ignore:\/\/(pattern|matcher|operator|operation)[=:](.+)$/; var EXACT_SKIP_RE = /^(pattern|matcher|operator|operation)[=:](.+)$/; var FILE_PROTO_RE = /^x?((raw)?file|tpl|jsonp|dust):\/\//; var NO_PROTO_RE = /[^\w!*|.-]/; var SKIP_RE = /^skip:\/\//; var SUB_VAR_RE = /\$\{RegExp\.\$([&\d])\}/g; var TOOL_RE = /^(?:log|weinre):\/\//; var mIndex = 0; var mNow = Date.now(); var IS_JSON = Symbol('isJson'); function getMFlag() { if (mIndex === Number.MAX_SAFE_INTEGER) { mIndex = 0; mNow = Date.now(); } return mNow + '-' + (mIndex++); } function removeComment(text) { return text.replace(COMMENT_RE, '').trim(); } function domainToRegExp(all, star, dot) { var len = star.length; var result = len > 1 ? '([^/?]*)' : '([^/?.]*)'; if (dot) { result += '\\.'; if (len > 2) { result = '(?:' + result + ')?'; } } return result; } function pathToRegExp(all) { var len = all.length; if (len > 2) { return '(.*)'; } return len > 1 ? '([^?]*)' : '([^?/]*)'; } function queryToRegExp(all) { return all.length > 1 ? '(.*)' : '([^&]*)'; } function isRegUrl(url, isCheck) { var result = regUrlCache[url]; if (result) { return isCheck ? result : extend({}, result); } var oriUrl = url; var not = isNegativePattern(url); if (not) { url = url.substring(1); } if (DOT_PATTERN_RE.test(url)) { url = '^' + url; } var hasStartSymbol = REG_URL_SYMBOL_RE.test(url); var hasEndSymbol, ignoreCase, startWithDot; if (hasStartSymbol) { ignoreCase = RegExp.$1.length; url = url.substring(ignoreCase); hasEndSymbol = END_RE.test(url); if (hasEndSymbol) { url = url.slice(0, -1); } ignoreCase = ignoreCase === 1; } else { startWithDot = LIKE_REG_URL_RE2.test(url); if (startWithDot || LIKE_REG_URL_RE.test(url)) { ignoreCase = hasStartSymbol = true; } } if (!hasStartSymbol || !REG_URL_RE.test(url)) { return false; } var protocol = RegExp.$1 || ''; var domain = RegExp.$2; var pathname = url.substring(protocol.length + domain.length); var query = ''; var index = pathname.indexOf('?'); if (index !== -1) { query = pathname.substring(index); pathname = pathname.substring(0, index); } if (!protocol || protocol === '//') { protocol = '[a-z]+://'; } else { protocol = util.escapeRegExp(protocol).replace(/\*+/, '([a-z:]*)'); } if (startWithDot) { domain = domain.substring(1); } domain = util.escapeRegExp(domain); if (domain.length > 2 && !NON_STAR_RE.test(domain)) { domain = '([^?]*)'; } else if (domain) { domain = domain.replace(DOMAIN_STAR_RE, domainToRegExp); } else { domain = '[^/?]*'; } if (startWithDot) { domain = '(?:[^/?.]*.)?' + domain; } if (pathname) { pathname = util.escapeRegExp(pathname).replace(STAR_RE, pathToRegExp); } else if (hasStartSymbol && SUFFIX_RE.test(domain)) { pathname = '/[^?]+' + domain + (hasEndSymbol || query ? '' : '(?:\\??.*)$'); domain = '[^/?]+'; } else if (query || hasEndSymbol) { pathname = '/'; } query = pathname + (query ? util.escapeRegExp(query).replace(STAR_RE, queryToRegExp) : ''); var pattern = '^' + protocol + domain + query + (hasEndSymbol ? '$' : ''); try { result = regUrlCache[oriUrl] = { not: not, pattern: new RegExp(pattern, ignoreCase ? 'i' : '') }; } catch (e) {} return result; } function formatShorthand(url) { if (NO_SCHEMA_RE.test(url)) { return url; } if (url === 'includeFilter://safeHtml') { return 'lineProps://safeHtml'; } if (url === 'includeFilter://strictHtml') { return 'lineProps://strictHtml'; } if ( url === '{}' || VALUE_KEY_RE.test(url) || PATH_RE.test(url) || VALUE_RE.test(url) ) { return 'file://' + url; } if (url === '/' || (FILE_RE.test(url) && !util.isRegExp(url))) { return 'file://' + url; } // compact Chrome if (CHROME_PATH_RE.test(url)) { return 'file://' + url.substring(8); } if (AT_RE.test(url)) { if (url.indexOf('@://') === -1) { url = '@://' + url.substring(1); } return url.replace('@', 'G'); } if (PLUGIN_VAR_RE.test(url)) { return url.replace('%', 'P://'); } return url; } function getKey(url) { if (url.indexOf('{') == 0) { var index = url.lastIndexOf('}'); return index > 1 && url.substring(1, index); } return false; } function getValue(url, start, end, lineProps) { if (url.indexOf(start || '(') == 0) { var len = url.length - 1; if (url[len] === (end || ')')) { return url.substring(1, len); } } if (lineProps) { if (lineProps[IS_JSON] == null) { lineProps[IS_JSON] = util.isJson(url); } if (lineProps[IS_JSON]) { return url; } } return false; } function getFiles(path) { return FILE_PROTO_RE.test(path) ? util.removeProtocol(path, true).split('|') : null; } function setProtocol(target, source) { if (util.hasProtocol(target)) { return target; } var protocol = util.getProtocol(source); if (protocol == null) { return target; } return protocol + (NO_SCHEMA_RE.test(target) ? '' : '//') + target; } function isPathSeparator(ch) { return ch == '/' || ch == '\\' || ch == '?'; } /** * query1: xxxx, xxxx?, xxx?xxxx * query2: ?xxx, xxx?xxxx * @param query1 * @param query2 * @returns */ function joinQuery(query1, query2) { if (!query1 || !query2) { return query1 || query2; } query2 = query2.substring(1); var firstLen = query1.length; var secondLen = query2.length; var sep = firstLen < 2 || !secondLen || query1[firstLen - 1] === '&' || query2[0] === '&' ? '' : '&'; return query1 + sep + query2; } function joinUrl(a, b) { if (!a || !b) { return a + b; } var firstIndex = a.indexOf('?'); var secondIndex = b.indexOf('?'); var firstQuery = ''; var secondQuery = ''; if (firstIndex != -1) { firstQuery = a.substring(firstIndex); a = a.substring(0, firstIndex); } if (secondIndex != -1) { secondQuery = b.substring(secondIndex); b = b.substring(0, secondIndex); } if (b) { var lastIndex = a.length - 1; var startWithSep = isPathSeparator(b[0]); if (isPathSeparator(a[lastIndex])) { a = startWithSep ? a.substring(0, lastIndex) + b : a + b; } else { a = a + (startWithSep ? '' : '/') + b; } } var query = joinQuery(firstQuery, secondQuery); return WEB_PROTOCOL_RE.test(a) ? util.formatUrl(a + query) : a + query; } function toLine(_, line) { return line.replace(SPACE_RE, ' '); } function mergeLines(text) { return removeComment(text).replace(MULTI_TO_ONE_RE, toLine); } function parsePluginTpl(lines) { var ruleTpls = pluginMgr && pluginMgr.ruleTpls; if (!ruleTpls) { return lines; } var result = []; lines.forEach(function(line) { line = removeComment(line); var isPrivate = HAS_SPACE_RE.test(line); line = line.replace(PLUGIN_TPL_RE, function(all, sep, key, value) { var isVar = key.indexOf('=') !== -1; if (isVar && isPrivate) { key = '_' + key; } var tpl = ruleTpls[key.slice(0, isVar ? -1 : -3)]; if (tpl == null) { return all; } return (sep || '') + mergeLines(tpl).replace(TPL_KEY_RE, function(key) { return key.indexOf('%') === -1 ? value : util.encodeURIComponent(value); }); }); result.push.apply(result, mergeLines(line).split(LINE_END_RE)); }); return result; } function getLines(text, root) { if (!text || !(text = text.trim())) { return []; } text = mergeLines(text); var ruleKeys = {}; var valueKeys = {}; var lines = text.split(LINE_END_RE); var result = []; lines.forEach(function (line) { line = line.trim(); if (!line) { return; } var ruleList; var isRuleKey = RULE_KEY_RE.test(line); if (isRuleKey || VALUE_KEY_RE.test(line)) { if (root) { var key = RegExp.$1; line = ''; if (isRuleKey) { if (!ruleKeys[key]) { ruleKeys[key] = 1; line = rules.get(key); } } else if (!valueKeys[key]) { valueKeys[key] = 1; line = values.get(key); } ruleList = line && mergeLines(line).split(LINE_END_RE); } } else { ruleList = [line]; } if (ruleList) { ruleList = parsePluginTpl(ruleList); result.push.apply(result, ruleList); } }); return result; } function resolvePropValue(obj, key) { return (key && obj && obj[key.toLowerCase()]) || ''; } function resolveUrlVar(req, key, escape) { var url = req.fullUrl || req.curUrl; if (!key) { return url; } var options = req.__options || req.options; if (!options || options.href !== url) { options = req.__options = util.parseUrl(url); req.__query = req.__query$ = ''; } if (key.indexOf('query.') !== 0 || !options.query) { if (key === 'actualPort' || key === 'realPort') { return options['port'] || (options.protocol === 'https:' || options.protocol === 'wss:' ? '443' : '80'); } return options[key] || ''; } var queryKey = '__query' + (escape ? '$' : ''); var query = req[queryKey]; if (!query) { query = req[queryKey] = util.parseQuery(options.query, null, null, escape); } return util.getQueryValue(query[key.substring(6)]); } function resolveReqCookiesVar(req, key, escape) { var cookie = req.headers.cookie || ''; if (!cookie || !key) { return cookie; } var cookies = req.__cookies; if (!cookies || req.__rawCookies !== cookie) { req.__rawCookies = cookie; cookies = req.__cookies = util.parseQuery(cookie, '; ', null, escape); } return util.getQueryValue(cookies[key]); } function resolveResCookiesVar(req, key) { var resHeaders = req.resHeaders; var cookie = resHeaders && resHeaders['set-cookie']; var isArray = Array.isArray(cookie); if (!isArray && cookie) { isArray = true; cookie = [String(cookie)]; } if (!isArray) { return cookie || ''; } var rawCookie = cookie.join(', '); if (!key || !rawCookie) { return rawCookie; } var cookies = req.__resCookies; if (!cookies || req.__rawResCookies !== rawCookie) { req.__rawResCookies = cookie.join(); cookies = req.__resCookies = {}; cookie.forEach(function (c) { c = util.parseQuery(c, '; ', null, escape); Object.keys(c).forEach(function (key) { var item = {}; switch (key.toLowerCase()) { case 'domain': item.domain = c[key]; break; case 'path': item.path = c[key]; break; case 'expires': item.expires = c[key]; break; case 'max-age': item.maxAge = item['max-age'] = item['Max-Age'] = c[key]; break; case 'httponly': item.httpOnly = true; break; case 'secure': item.secure = true; break; case 'samesite': item.samesite = item.sameSite = item.SameSite = c[key]; break; case 'partitioned': item.partitioned = true; break; default: if (!cookies[key]) { item.value = c[key]; cookies[key] = item; } } }); }); } var index = key.indexOf('.'); var name; if (index !== -1) { name = key.substring(index + 1); key = key.substring(0, index); } cookie = cookies[key]; if (!cookie) { return ''; } return (name ? cookie[name] : cookie.value) || ''; } function resolveServerIpVar(req, key) { if (!req.resHeaders) { return ''; } return req.hostIp || '127.0.0.1'; } function resolveResHeadersVar(req, key) { return resolvePropValue(req.resHeaders, key); } function getPluginVar(vars, index) { if (!vars) { return ''; } if (vars && index === '$') { index = vars.length - 1; } return (vars && vars[index || 0]) || ''; } function resolveRuleValue(req, key) { var curRules = key && req.rules; if (curRules) { if (VAR_INDEX_RE.test(key)) { var shortName = RegExp.$1; var isGlobal = RegExp.$2; var isAll = RegExp.$3; var index = RegExp.$4 || 0; var gVars = req._globalPluginVars && req._globalPluginVars[shortName]; var vars = req._pluginVars && req._pluginVars[shortName]; if (isAll) { if (vars && gVars) { vars = isGlobal ? gVars.concat(vars) : vars.concat(gVars); } return getPluginVar(vars || gVars, index); } return getPluginVar(isGlobal ? gVars : vars, index); } var plugin = curRules.plugin; var matcher; key = key + '://'; if (plugin) { var list = Array.isArray(plugin.list) ? plugin.list : [plugin]; var name = 'whistle.' + key; for (var i = 0, len = list.length; i < len; i++) { matcher = list[i].matcher; if (!matcher.indexOf(name)) { return matcher.substring(name.length); } } } matcher = curRules.rule && curRules.rule.matcher; if (matcher && !matcher.indexOf(key)) { return matcher.substring(key.length); } } return ''; } function resolveVarValue(req, escape, name, key) { var lname = name.toLowerCase(); if (RANDOM_RE.test(lname)) { var min = 0; var max = 0; if (RegExp.$2) { min = parseInt(RegExp.$1, 10); max = parseInt(RegExp.$2, 10); if (max < min) { var temp = max; max = min; min = temp; } max = max - min; } else if (RegExp.$1) { max = parseInt(RegExp.$1, 10); } return max && (Math.floor(Math.random() * (max + 1)) + min); } switch (lname) { case 'now': return Date.now(); case 'random': return Math.random(); case 'randomuuid': return crypto.randomUUID ? crypto.randomUUID() : ''; case 'id': case 'reqid': return req.reqId || ''; case 'whistle': return resolveRuleValue(req, key); case 'path': case 'pathname': case 'search': return key ? '' : resolveUrlVar(req, lname, escape); case 'querystring': case 'searchstring': return key ? '' : resolveUrlVar(req, 'search', escape) || '?'; case 'query': key = key ? 'query.' + key : 'query'; return resolveUrlVar(req, key, escape); case 'url': return resolveUrlVar(req, key, escape); case 'port': return config.port; case 'host': return config.host || ''; case 'realport': return config.realPort || config.port; case 'realhost': return config.realHost || config.host || ''; case 'realurl': return req.realUrl && (req.realUrl !== req.fullUrl) ? req.realUrl : ''; case 'version': return config.version; case 'reqcookie': case 'reqcookies': return resolveReqCookiesVar(req, key, escape); case 'rescookie': case 'rescookies': return resolveResCookiesVar(req, key, escape); case 'method': return req.method; case 'ip': case 'clientip': return req.clientIp; case 'clientid': return req._origClientId || util.getClientId(req.headers); case 'clientport': return req.clientPort || ''; case 'localclientid': return config.clientId; case 'statuscode': case 'status': return req.statusCode || ''; case 'serverip': return resolveServerIpVar(req, key); case 'serverport': return req.serverPort || ''; case 'reqh': case 'reqheader': case 'reqheaders': return resolvePropValue(req.headers, key); case 'hostname': return util.hostname(); case 'remoteaddress': return req._remoteAddr || ''; case 'remoteport': return req._remotePort || '0'; case 'env': return (key && env[key]) || ''; default: return resolveResHeadersVar(req, key); } } function resetComma(str) { return str && str.replace(G_CR_RE, ',').replace(G_LF_RE, '\\,'); } function resolveTplVar(value, req) { return value.replace(TPL_VAR_RE, function (all, escape, lb, name, key, rb) { if ( (lb && !rb) || (name === 'whistle' && (!key || !PLUGIN_NAME_RE.test(key))) ) { return all; } var pattern, regPattern; var replacement = ''; if (REPLACE_PATTERN_RE.test(key)) { pattern = RegExp.$2; var dot = RegExp.$1 || ''; key = key.substring(0, key.length - 9 - dot.length - pattern.length); if (pattern.indexOf(',') !== -1) { pattern = pattern.replace(COMMA2_RE, '\n').replace(COMMA1_RE, '\r'); var index = pattern.indexOf(','); if (index !== -1) { replacement = resetComma(pattern.substring(index + 1)); pattern = pattern.substring(0, index); } pattern = resetComma(pattern); } regPattern = util.toOriginalRegExp(pattern); } var val = resolveVarValue(req, escape, name, key); if (typeof val !== 'string') { val = val == null ? '' : val + ''; } if (!val || !pattern) { val = pattern ? val : val || replacement; } else if (!regPattern || !SUB_MATCH_RE.test(replacement)) { val = val.replace(regPattern || pattern, replacement); } else { val = val.replace(regPattern, function () { return util.replacePattern(replacement, arguments); }); } if (val && lb) { val = util.encodeURIComponent(val); } return val + (!lb && rb ? '}' : ''); }); } Rules.resolveTplVar = resolveTplVar; function renderTpl(rule, req) { var matcher = rule.matcher; if (rule.isTpl === false) { return matcher; } rule.isTpl = false; return matcher.replace(TPL_RE, function (_, proto, value) { rule.isTpl = true; return (proto || '') + resolveTplVar(value.slice(1, -1), req); }); } function resolveVar(rule, vals, req) { var matcher = renderTpl(rule, req); return matcher.replace(VAR_RE, function (all, key) { key = getValueFor(key, vals, rule.file); if (typeof key === 'string') { return rule.isTpl && key ? resolveTplVar(key, req) : key; } return all; }); } function getValueFor(key, vals, file) { if (!key) { return; } var key1 = util.getInlineKey(key, file); var val = vals ? vals[key1] : undefined; if (val !== undefined) { val = vals[key1] = val && typeof val == 'object' ? JSON.stringify(val) : val; } else { val = values.get(key); } return val; } function getRule(req, list, vals, index, isFilter, host, isReq) { var rule = resolveRuleList(req, list, vals, index || 0, isFilter, null, host, isReq); resolveValue(rule, vals, req); return rule; } function resolveValue(rule, vals, req) { if (!rule) { return; } var matcher = rule.matcher; var index = matcher.indexOf('://') + 3; var protocol = matcher.substring(0, index); var regExp = rule.regExp; delete rule.regExp; matcher = matcher.substring(index); var key = getKey(matcher); if (key) { rule.key = key; } var value = getValueFor(key, vals, rule.file); if (value == null) { value = getValue(matcher, null, null, rule.lineProps); regExp = null; } if (value !== false) { var val = setProtocol(protocol + value, req.curUrl); if (rule.isTpl && regExp) { val = val.replace(SUB_VAR_RE, function(_, index) { index = index === '&' ? 0 : index; return regExp[index] || ''; }); } if (rule.isTpl) { val = resolveTplVar(val, req); } if (protocol === 'style://') { rule.value = val.substring(0, 128); } else { Object.defineProperty(rule, 'value', { value: val }); } } else if (!key && (value = getValue(matcher, '<', '>')) !== false) { rule.path = setProtocol(protocol + value, req.curUrl); rule.files = getFiles(rule.path); } return rule; } function getRelativePath(pattern, url, matcher) { var index = url.indexOf('?'); if (index === -1 || pattern.indexOf('?') !== -1) { return ''; } if (matcher.indexOf('?') === -1) { return url.substring(index); } url = url.substring(index + 1); return (url && '&') + url; } function removeFilters(rule) { var filters = rule.filters; if (filters) { if (filters.curFilter) { rule.filter = filters.curFilter; } delete rule.filters; } } function replaceSubMatcher(url, regExp, req) { var vals = req._bodySubVals; req._bodySubVals = undefined; if ((!regExp || !SUB_MATCH_RE.test(url)) && (!vals || !BODY_MATCH_RE.test(url))) { return url; } return util.replacePattern(url, regExp, vals); } var PROTOS = { http: 1, https: 1, tunnel: 1, ws: 1, wss: 1 }; function removePort(url) { var index = url.indexOf('://'); if (index === -1) { return url; } var protocol = url.substring(0, index); if (!PROTOS[protocol]) { return url; } index += 3; var urlPath = ''; var end = url.indexOf('/', index); if (end !== -1) { urlPath = url.substring(end); url = url.substring(0, end); } end = url.indexOf(':', index); if (end !== -1) { url = url.substring(0, end); } return url + urlPath; } function checkInternal(req, rule) { var props = rule.lineProps; if (req._isInternalReq) { return !props.internal && !props.internalOnly; } return props.internalOnly; } function resolveRuleList(req, list, vals, index, isFilter, isEnableProxy, host, isReq) { var curUrl = util.formatUrl(req.curUrl); var notHttp = list.isRuleProto && curUrl[0] !== 'h'; //支持域名匹配 var domainUrl = removePort(curUrl); var hasIndex = typeof index === 'number'; index = hasIndex ? index : -1; var results = []; var url = util.getPureUrl(curUrl); var _domainUrl = util.getPureUrl(domainUrl); var rule, matchedUrl, files, matcher, result, origMatcher, filePath; var setMatcher = function(matcher) { result.matcher = matcher; var _matcher = rule ? rule.matcher : undefined; if (_matcher !== matcher) { result._matcher = _matcher; } }; var getPathRule = function () { result = extend( { files: files, url: joinUrl(matcher, filePath) }, rule ); if (files && filePath) { result.files = files.map(function (file) { return joinUrl(file, filePath); }); result.rawFiles = files; } setMatcher(origMatcher); removeFilters(result); if (hasIndex) { return result; } results.push(result); }; var getExactRule = function (relPath, regObj) { origMatcher = resolveVar(rule, vals, req); origMatcher = replaceSubMatcher(origMatcher, regObj, req); matcher = setProtocol(origMatcher, curUrl); result = extend( { files: getFiles(matcher), url: matcher + relPath }, rule ); setMatcher(origMatcher); removeFilters(result); if (hasIndex) { return result; } results.push(result); }; var checkFilter = function () { req._bodySubVals = undefined; if (notHttp && protoMgr.isFileProxy(rule.matcher)) { return false; } if (isReq && TOOL_RE.test(rule.matcher)) { return true; } return (isFilter || !matchExcludeFilters(curUrl, rule, req)) && (host == null || util.checkProxyHost(rule, host)); }; for (var i = 0; (rule = list[i]); i++) { if ((isEnableProxy && !ENABLE_PROXY_RE.test(rule.matcher)) || checkInternal(req, rule) || (req._skipProps && (util.exactIgnore(req._skipProps, rule) || util.checkSkip(req._skipProps, rule, curUrl)))) { continue; } var pattern = rule.isRegExp ? rule.pattern : setProtocol(rule.pattern, curUrl); var not = rule.not; var matchedRes; if (rule.isRegExp) { matchedRes = pattern.test(curUrl); matchedRes = not ? !matchedRes : matchedRes; var regExp; if (matchedRes) { regExp = {}; if (!not) { for (var j = 1; j < 10; j++) { regExp[j] = RegExp['$' + j]; } } } if (matchedRes && checkFilter() && --index < 0) { regExp['0'] = curUrl; matcher = resolveVar(rule, vals, req); // 支持 $x 包含 `|` 的情形 matcher = setProtocol(replaceSubMatcher(matcher, regExp, req), curUrl); files = getFiles(matcher); result = extend({ url: matcher, files: files }, rule); setMatcher(matcher); result.regExp = regExp; removeFilters(result); if (hasIndex) { return result; } results.push(result); } } else if (rule.wildcard) { var wildcard = rule.wildcard; var matched = wildcard.preMatch.exec(curUrl); if (matched && checkFilter()) { var regObj = {}; for (var k = 0; k < 9; k++) { regObj[k] = matched[k + 1] || ''; } filePath = curUrl.substring(regObj[0].length); var wPath = wildcard.path; if (wildcard.isExact) { if ( (filePath === wPath || util.getPureUrl(filePath) === wPath) && --index < 0 ) { if ( (result = getExactRule( getRelativePath(wPath, filePath, rule.matcher), regObj )) ) { return result; } } } else if (filePath.indexOf(wPath) === 0) { var wpLen = wPath.length; filePath = filePath.substring(wpLen); if ( (wildcard.hasQuery || !filePath || wPath[wpLen - 1] === '/' || SEP_RE.test(filePath)) && --index < 0 ) { origMatcher = resolveVar(rule, vals, req); origMatcher = replaceSubMatcher(origMatcher, regObj, req); matcher = setProtocol(origMatcher, curUrl); files = getFiles(matcher); if (wildcard.hasQuery && filePath) { filePath = '?' + filePath; } if ((result = getPathRule())) { return result; } } } } } else if (rule.isExact) { matchedRes = pattern === url || pattern === curUrl; if ((not ? !matchedRes : matchedRes) && checkFilter() && --index < 0) { if ( (result = getExactRule( getRelativePath(pattern, curUrl, rule.matcher) )) ) { return result; } } } else if ( ((matchedUrl = curUrl.indexOf(pattern) === 0) || (rule.isDomain && domainUrl.indexOf(pattern) === 0)) && checkFilter() ) { var len = pattern.length; origMatcher = resolveVar(rule, vals, req); origMatcher = replaceSubMatcher(origMatcher, null, req); matcher = setProtocol(origMatcher, curUrl); files = getFiles(matcher); var hasQuery = pattern.indexOf('?') !== -1; if ( (hasQuery || (matchedUrl ? pattern == url || isPathSeparator(url[len]) : pattern == _domainUrl || isPathSeparator(_domainUrl[len])) || isPathSeparator(pattern[len - 1])) && --index < 0 ) { filePath = (matchedUrl ? curUrl : domainUrl).substring(len); if (hasQuery) { if (filePath) { filePath = '?' + filePath; } } else if (rule.isDomain && rule.lineProps.originUrl) { filePath = '/'; } if ((result = getPathRule())) { return result; } } } } return hasIndex ? null : results; } function resolveProps(req, rules, vals, isIgnore) { var list = this.getRuleList(req, rules, vals); var result = {}; if (isIgnore) { list = list.filter(function(rule) { var matcher = rule.matcher; if (SKIP_RE.test(matcher)) { matcher = matcher.slice(7); if (!matcher) { return false; } if (EXACT_SKIP_RE.test(matcher) || NO_PROTO_RE.test(matcher)) { var prop ='ignore|' + (RegExp.$1 === 'pattern' ? 'pattern' : 'matcher') + '=' + (RegExp.$2 || matcher); req._skipProps = req._skipProps || {}; result[prop] = true; req._skipProps[prop] = true; return false; } matcher.split('|').forEach(function(name) { if (name) { req._skipProps = req._skipProps || {}; req._skipProps[name] = true; } }); } else if (EXACT_IGNORE_RE.test(matcher)) { result['ignore|' + (RegExp.$1 === 'pattern' ? 'pattern' : 'matcher') + '=' + RegExp.$2] = true; return false; } return true; }); if (!list.length) { return result; } } return util.parseRuleProps(list, result); } function parseWildcard(pattern, not) { if (!WILDCARD_RE.test(pattern)) { return; } var preMatch = RegExp.$1; var protocol = RegExp.$2; var domain = RegExp.$3; var startWithDot = DOT_DOMAIN_RE.test(domain); if ( !startWithDot && protocol.indexOf('*') === -1 && domain.indexOf('*') === -1 && domain.indexOf('~') === -1 ) { return; } if (not) { return false; } var restPath = pattern.substring(preMatch.length); var path = restPath || '/'; var isExact = preMatch.indexOf('$') === 0; if (isExact) { preMatch = preMatch.substring(1); } var index = path.indexOf('?'); var hasQuery = index !== -1; if (hasQuery && index === 0) { path = '/' + path; } var dLen = domain.length; var allowMatchPath = dLen > 2 && !NON_STAR_RE.test(domain); if (allowMatchPath) { preMatch = '[^?]*'; } else { if ( !startWithDot && (domain === '*' || domain === '~') && path.charAt(0) === '/' ) { preMatch += '*'; } preMatch = util .escapeRegExp(preMatch) .replace(DOMAIN_STAR_RE, domainToRegExp); if (dLen && domain[dLen - 1] !== '*' && domain.indexOf(':') === -1) { preMatch += '(?::\\d+)?'; } if (startWithDot) { preMatch = preMatch.replace('\\.', '(?:[^/?.]*\\.)?'); } } if (!protocol) { preMatch = '[a-z]+://' + preMatch; } else if (protocol === '//') { preMatch = '[a-z]+:' + preMatch; } preMatch = '^(' + preMatch + (restPath ? '' : '[^/?]*') + ')' + (allowMatchPath ? util.escapeRegExp(path, true) : ''); return { preMatch: new RegExp(preMatch), path: path, hasQuery: hasQuery, isExact: isExact }; } function parseRule(rulesMgr, pattern, matcher, raw, root, options, file) { if (isNegativePattern(matcher)) { return; } var regUrl = regUrlCache[pattern]; var rawPattern = pattern; var rawMatcher = matcher; var noSchema; var isRegExp, not, port, protocol, isExact; if (regUrl) { not = regUrl.not; isRegExp = true; pattern = regUrl.pattern; } else { not = isNegativePattern(pattern); // 位置不能变 var isPortPattern = PORT_PATTERN_RE.test(pattern); if (not) { pattern = pattern.substring(1); } if (NO_SCHEMA_RE.test(pattern)) { noSchema = true; pattern = pattern.substring(2); } if (!pattern) { return; } if (isPortPattern) { isRegExp = true; pattern = new RegExp('^[\\w]+://[^/?]+' + pattern + '/'); } if ( !isRegExp && (isRegExp = util.isRegExp(pattern)) && !(pattern = util.toRegExp(pattern)) ) { return; } if (!isRegExp) { var wildcard = parseWildcard(pattern, not); if (wildcard === false) { return; } if (!wildcard && isExactPattern(pattern)) { isExact = true; pattern = pattern.slice(1); } else if (not) { return; } } } var proxyName, isRules, isSpec; if (isHost(matcher)) { matcher = 'host://' + matcher; protocol = 'host'; } else if (matcher[0] === '/') { if (matcher[1] === '/') { protocol = 'rule'; } else { matcher = 'file://' + matcher; protocol = 'file'; } } else if (PLUGIN_RE.test(matcher)) { protocol = 'plugin'; } else if (PROXY_RE.test(matcher)) { proxyName = RegExp.$1; protocol = 'proxy'; } else { var index = matcher.indexOf('://'); var origProto; if (index !== -1) { origProto = matcher.substring(0, index); protocol = aliasProtocols[origProto]; } if (!protocol) { protocol = origProto; if (matcher === 'host://') { matcher = 'host://127.0.0.1'; } } else if (protocol && (origProto === 'reqRules' || origProto === 'resRules')) { isRules = true; } var isStatus = protocol === 'statusCode'; if (isStatus || protocol === 'redirect') { isSpec = isStatus ? 1 : 2; } } var rules = rulesMgr._rules; var list = protocol === 'sniCallback' ? rulesMgr._sniCallback : rules[protocol]; var useRealPort; if (!list) { protocol = 'rule'; list = LOCAL_RULE_RE.test(matcher) ? rules._localRule : rules.rule; useRealPort = RegExp.$1; } else if (!matcher.indexOf('G://clientCert://')) { list = rules._clientCerts; } else if (protocol == 'host') { var protoIndex = matcher.indexOf(':') + 3; var realProto = matcher.substring(0, protoIndex); var opts = isHost(matcher.substring(protoIndex)); if (opts) { matcher = realProto + opts.host; port = opts.port; } } var rule = { not: not, file: file, isRules: isRules, isSpec: isSpec, name: protocol, root: root, wildcard: wildcard, isRegExp: isRegExp, isExact: isExact, protocol: isRegExp ? null : util.getProtocol(pattern), pattern: isRegExp ? pattern : util.formatUrl(pattern), matcher: matcher, port: port, raw: raw, isDomain: !isRegExp && !not && (noSchema ? pattern : util.removeProtocol(rawPattern, true)).replace(/\?.*$/, '').indexOf( '/' ) == -1, rawPattern: rawPattern, rawMatcher: rawMatcher, filters: options.filters, lineProps: options.lineProps, hostFilter: options.hostFilter }; if (options.rawProps.length) { rule.rawProps = options.rawProps; } if (protocol === 'log' || protocol === 'weinre') { rule.isTpl = false; } if (useRealPort) { rule.realPort = config.realPort; rule.matcher = rule.matcher.replace( 'realPort', config.realPort || config.port ); } if (proxyName) { switch (proxyName) { case 'socks': rule.isSocks = true; break; case 'https-proxy': rule.isHttps = true; break; case 'internal-http-proxy': case 'https2http-proxy': case 'internal-proxy': rule.isInternal = true; break; case 'internal-https-proxy': rule.isInternal = true; rule.isHttps = true; break; case 'http2https-proxy': rule.isHttp2https = true; break; } } if (options.hasBodyFilter) { rules._bodyFilters.push(rule); } if (util.isImportant(options)) { for (var i = 0, len = list.length; i < len; i++) { if (!util.isImportant(list[i])) { return list.splice(i, 0, rule); } } } list.push(rule); } function isPattern(item) { return ( PORT_PATTERN_RE.test(item) || isExactPattern(item) || isRegUrl(item, true) || // 缓存 regUrl NO_SCHEMA_RE.test(item) || isNegativePattern(item) || WEB_PROTOCOL_RE.test(item) || util.isRegExp(item) ); } var IP_WITH_PORT_RE = /^\[([:\da-f.]+)\](?::(\d+))?$/i; var IPV4_RE = /^(?:::(?:ffff:)?)?(\d+\.[\d.]+)(?:\:(\d+))?$/; function parseHost(item) { var port; if (IP_WITH_PORT_RE.test(item)) { item = RegExp.$1; port = RegExp.$2; } if (IPV4_RE.test(item)) { port = port || RegExp.$2; item = RegExp.$1; if (!net.isIP(item)) { return false; } } else if (!net.isIP(item)) { return false; } return { host: item, port: port }; } function isHost(item) { var result = hostCache[item]; if (result == null) { result = parseHost(item); } hostCache[item] = result; return result; } function indexOfPattern(list) { var ipIndex = -1; for (var i = 0, len = list.length; i < len; i++) { var item = list[i]; if (isPattern(item)) { return i; } if (!util.hasProtocol(item)) { if (!isHost(item)) { return i; } else if (ipIndex === -1) { ipIndex = i; } } } return ipIndex; } function resolveFilterPattern(matcher) { var not, isInclude, filter, caseIns, wildcard; if (PATTERN_FILTER_RE.test(matcher)) { filter = RegExp.$1; caseIns = RegExp.$2; not = filter[0] === '!'; if (not) { filter = filter.substring(1); } if (filter[0] === '/') { filter = filter.substring(1); } return filter ? { not: not, filter: filter, caseIns: caseIns } : false; } else if (FILTER_RE.test(matcher)) { filter = RegExp.$1; if (!filter || filter === '!') { return false; } isInclude = matcher[0] === 'i'; if (filter[0] === '!') { not = !not; filter = filter.substring(1); } if (util.isRegExp(filter)) { filter = RegExp.$1; caseIns = RegExp.$2; return { not: not, isInclude: isInclude, filter: filter, caseIns: caseIns }; } if (filter[0] === '/' && filter[1] !== '/') { wildcard = '/'; } else if (WILD_FILTER_RE.test(filter)) { wildcard = RegExp.$1; } } else if (PATTERN_WILD_FILTER_RE.test(matcher)) { not = RegExp.$1 || ''; wildcard = RegExp.$2; } else { return; } if (wildcard) { matcher = filter || matcher.substring(matcher.indexOf('://') + 3 + not.length); var path = util.escapeRegExp(matcher.substring(wildcard.length)); if (path.indexOf('*') !== -1) { path = path.replace(STAR_RE, pathToRegExp); } else if (path && path[path.length - 1] !== '/') { path += '(?:[/?]|$)'; } return { not: not, isInclude: isInclude, filter: '^[a-z]+://' + (wildcard.length > 3 ? '[^?]' : '[^/?]') + '+/' + path }; } var result = isRegUrl('^' + filter); if (result) { result.not = not; result.isInclude = isInclude; return result; } } function resolveMatchFilter(list) { var matchers = []; var lineProps = {}; var rawProps = []; var filters, hasBodyFilter, hostFilter; list.forEach(function (matcher) { var rawMatcher = matcher; if (INLINE_RE.test(matcher)) { matcher = RegExp.$1 + RegExp.$2.slice(1, -1); } if (LINE_PROPS_RE.test(matcher)) { rawProps.push(rawMatcher); extend(lineProps, util.parseLineProps(matcher)); return; } var filter, not, isInclude, orgVal; if (PROPS_FILTER_RE.test(matcher) || PURE_FILTER_RE.test(matcher)) { var raw = RegExp['$&']; var propName = RegExp.$1; var value = RegExp.$2; var isHostFilter = propName === 'host'; isInclude = matcher[1] === 'n'; if (value[0] === '!') { not = !not; value = value.substring(1); } var pattern; var isIp = propName === 'i' || propName === 'ip'; var isClientPort, isServerPort, isClientIp, isServerIp; if (!isIp) { isClientPort = propName === 'clientPort'; if (!isClientPort) { isServerPort = propName === 'serverPort'; if (!isServerPort) { isClientIp = (propName === 'clientIp' || propName === 'clientIP'); isServerIp = !isClientIp && (propName === 'serverIp' || propName === 'serverIP'); } } } if (isClientPort || isServerPort) { pattern = util.toRegExp(value); if (isClientPort) { propName = pattern ? 'cpPattern' : 'clientPort'; } else { propName = pattern ? 'spPattern' : 'serverPort'; } value = pattern || value.toLowerCase(); } else if (isIp || isClientIp || isServerIp) { pattern = util.toRegExp(value); if (!pattern && !net.isIP(value)) { return; } if (isIp) { propName = pattern ? 'iPattern' : 'ip'; } else if (isClientIp) { propName = pattern ? 'clientPattern' : 'clientIp'; } else if (isServerIp) { propName = pattern ? 'serverPattern' : 'serverIp'; } value = pattern || value; } else if (propName[0] === 'm') { pattern = util.toRegExp(value, true); propName = pattern ? 'mPattern' : 'method'; value = pattern || value.toUpperCase(); } else if (propName === 'from') { pattern = null; propName = 'from'; value = value.toLowerCase(); } else if (propName === 's' || propName === 'statusCode') { pattern = util.toRegExp(value); propName = pattern ? 'sPattern' : 'statusCode'; value = pattern || value.toLowerCase(); } else if (propName === 'b' || propName === 'body') { hasBodyFilter = true; pattern = util.toRegExp(value); if (pattern) { propName = 'bodyPattern'; value = pattern; } else { propName = 'body'; value = { orgVal: util.encodeURIComponent(value).toLowerCase(), value: value.toLowerCase() }; } } else if (propName === 'remoteAddress') { pattern = util.toRegExp(value); if (pattern) { propName = 'addrPattern'; } value = pattern || value.toLowerCase(); } else if (propName === 'remotePort') { pattern = util.toRegExp(value); if (pattern) { propName = 'portPattern'; } value = pattern || value.toLowerCase(); } else if (isHostFilter) { pattern = util.toRegExp(value); if (pattern) { propName = 'hostPattern'; } value = pattern || value.toLowerCase(); } else { var isHeader = propName !== 'env' && propName !== 'chance' && propName !== 'probability'; var index = value.indexOf('='); if (isHeader && index === -1) { index = value.indexOf(':'); } var key = index === -1 ? value : value.substring(0, index); if (isHeader) { key = key.toLowerCase(); } var lastIndex = key.length - 1; if (key[lastIndex] === '!') { key = key.substring(0, lastIndex); if (!key) { return; } not = !not; } orgVal = index === -1 ? '' : value.substring(index + 1); value = { key: key }; if ((pattern = util.toRegExp(orgVal))) { value.hPattern = pattern; } else { value.orgVal = orgVal = orgVal.toLowerCase(); value.value = util.encodeURIComponent(orgVal); } if (isHeader) { switch (propName[2]) { case 'q': propName = 'reqHeader'; break; case 's': propName = 'resHeader'; break; case 'v': propName = 'env'; break; default: propName = 'header'; } } } filter = { not: not, isInclude: isInclude }; filter[propName] = value; filter.raw = raw; if (isHostFilter) { hostFilter = hostFilter || []; hostFilter.push(filter); } else { filters = filters || []; filters.push(filter); } return; } var result = resolveFilterPattern(matcher); if (result === false) { return; } else if (!result) { matchers.push(rawMatcher); return; } if (result.pattern) { filters = filters || []; result.raw = rawMatcher; return filters.push(result); } filter = '/' + result.filter + '/' + (result.caseIns ? 'i' : ''); if ((filter = util.toRegExp(filter))) { filters = filters || []; filters.push({ raw: rawMatcher, pattern: filter, not: result.not, isInclude: result.isInclude }); } }); return { rawProps: rawProps, hasBodyFilter: hasBodyFilter, matchers: matchers, hostFilter: hostFilter, filters: filters, lineProps: lineProps }; } function getPluginName(root) { return root && ROOT_PLUGIN_RE.test(root) ? util.getPluginFile(RegExp.$1) : undefined; } function parseText(rulesMgr, text, root, file) { var pluginVars = rulesMgr._globalPluginVars; var enabledList = rulesMgr._enabledList || []; rulesMgr._enabledList = enabledList; getLines(text, root).forEach(function (line) { var raw = line; var rawLine = line; line = removeComment(line); line = line && line.split(/\s+/); var len = line && line.length; if (len === 1 && PLUGIN_VAR_RE.test(line[0])) { var name = RegExp.$1; var value = RegExp.$3; if (value) { var vars = pluginVars[name]; if (!vars) { vars = [value]; pluginVars[name] = vars; } else if (vars.indexOf(value) === -1) { vars.push(value); } } enabledList.push([rawLine, file]); return; } if (!len || len < 2) { return; } line = line.map(formatShorthand); var patternIndex = indexOfPattern(line); if (patternIndex === -1) { return; } var pattern = line[0]; var result = resolveMatchFilter(line.slice(1)); var matchers = result.matchers; if (patternIndex > 0) { //supports: operator-uri1 operator-uriX pattern1 pattern2 ... patternN var opList = [pattern