mirror of
https://github.com/avwo/whistle.git
synced 2026-10-18 14:41:12 +08:00
2476 lines
67 KiB
JavaScript
2476 lines
67 KiB
JavaScript
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 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(/\s+/g, ' ');
|
|
}
|
|
|
|
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];
|
|
var patternList = matchers.filter(function (p) {
|
|
if (isPattern(p) || isHost(p) || !util.hasProtocol(p)) {
|
|
return true;
|
|
}
|
|
opList.push(p);
|
|
});
|
|
opList.forEach(function (matcher) {
|
|
patternList.forEach(function (pattern) {
|
|
parseRule(rulesMgr, pattern, matcher, raw, root, result, file);
|
|
});
|
|
});
|
|
} else {
|
|
//supports: pattern operator-uri1 operator-uri2 ... operator-uriN
|
|
matchers.forEach(function (matcher) {
|
|
parseRule(rulesMgr, pattern, matcher, raw, root, result, file);
|
|
});
|
|
}
|
|
enabledList.push([rawLine, file]);
|
|
});
|
|
regUrlCache = {};
|
|
hostCache = {};
|
|
}
|
|
|
|
function isExactPattern(pattern) {
|
|
return EXACT_RE.test(pattern);
|
|
}
|
|
|
|
function isNegativePattern(pattern) {
|
|
return NON_RE.test(pattern);
|
|
}
|
|
|
|
function getFilterResult(result, filter) {
|
|
return result == null ? false : filter.not ? !result : result;
|
|
}
|
|
function matchFilter(url, filter, req) {
|
|
var result;
|
|
if (filter.pattern) {
|
|
result = filter.pattern.test(url);
|
|
return getFilterResult(result, filter);
|
|
}
|
|
if (!req) {
|
|
return false;
|
|
}
|
|
var filterProp = function (value, expectVal, pattern) {
|
|
if (value == null) {
|
|
return expectVal || pattern;
|
|
}
|
|
if (expectVal) {
|
|
result = value == expectVal;
|
|
return true;
|
|
}
|
|
if (pattern) {
|
|
result = pattern.test(value);
|
|
return true;
|
|
}
|
|
};
|
|
if (filter.from) {
|
|
if (filter.from === 'tunnel') {
|
|
return filter.not ? !req.fromTunnel : req.fromTunnel;
|
|
}
|
|
if (filter.from === 'composer') {
|
|
return filter.not ? !req.fromComposer : req.fromComposer;
|
|
}
|
|
if (filter.from === 'internalPath') {
|
|
return filter.not ? !req.fromInternalPath : req.fromInternalPath;
|
|
}
|
|
if (filter.from === 'sni') {
|
|
return filter.not ? !req.useSNI : req.useSNI;
|
|
}
|
|
if (filter.from === 'httpserver') {
|
|
return filter.not ? !req.fromHttpServer : req.fromHttpServer;
|
|
}
|
|
if (filter.from === 'httpsserver') {
|
|
return filter.not ? !req.fromHttpsServer : req.fromHttpsServer;
|
|
}
|
|
if (filter.from === 'httpsport') {
|
|
return filter.not ? !req.isHttpsServer : req.isHttpsServer;
|
|
}
|
|
return false;
|
|
}
|
|
var chance = filter.chance || filter.probability;
|
|
if (chance) {
|
|
var key = chance.key;
|
|
var keyLen = key && key.length - 1;
|
|
if (keyLen > 0 && key[keyLen] === '%') {
|
|
key = key.substring(0, keyLen) / 100;
|
|
}
|
|
return getFilterResult(Math.random() < key, filter);
|
|
}
|
|
if (filterProp(req.method, filter.method, filter.mPattern)) {
|
|
return getFilterResult(result, filter);
|
|
}
|
|
if (filterProp(req.statusCode, filter.statusCode, filter.sPattern)) {
|
|
return getFilterResult(result, filter);
|
|
}
|
|
if (filterProp(req.clientIp, filter.ip, filter.iPattern)) {
|
|
return getFilterResult(result, filter);
|
|
}
|
|
if (filterProp(req.hostIp, filter.ip, filter.iPattern)) {
|
|
return getFilterResult(result, filter);
|
|
}
|
|
if (filterProp(req.clientIp, filter.clientIp, filter.clientPattern)) {
|
|
return getFilterResult(result, filter);
|
|
}
|
|
if (filterProp(req.hostIp, filter.serverIp, filter.serverPattern)) {
|
|
return getFilterResult(result, filter);
|
|
}
|
|
if (filterProp(req.clientPort, filter.clientPort, filter.cpPattern)) {
|
|
return getFilterResult(result, filter);
|
|
}
|
|
if (filterProp(req.serverPort, filter.serverPort, filter.spPattern)) {
|
|
return getFilterResult(result, filter);
|
|
}
|
|
|
|
if (filterProp(req._remoteAddr, filter.remoteAddress, filter.addrPattern)) {
|
|
return getFilterResult(result, filter);
|
|
}
|
|
|
|
if (filterProp(req._remotePort, filter.remotePort, filter.portPattern)) {
|
|
return getFilterResult(result, filter);
|
|
}
|
|
|
|
var reqBody = req._reqBody;
|
|
if (filter.bodyPattern || filter.body) {
|
|
if (typeof reqBody !== 'string') {
|
|
return false;
|
|
}
|
|
if (filter.bodyPattern) {
|
|
if (result = filter.bodyPattern.exec(reqBody)) {
|
|
req._bodySubVals = [reqBody];
|
|
for (var i = 1, len = result.length; i < len; i++) {
|
|
req._bodySubVals[i] = (result[i] || '');
|
|
}
|
|
}
|
|
} else {
|
|
reqBody = reqBody.toLowerCase();
|
|
result =
|
|
reqBody.indexOf(filter.body.value) !== -1 ||
|
|
reqBody.indexOf(filter.body.orgVal) !== -1;
|
|
}
|
|
|
|
return filter.not ? !result : result;
|
|
}
|
|
|
|
var filterHeader = function (headers, filterVal, resHeaders) {
|
|
if (!filterVal || !headers) {
|
|
return;
|
|
}
|
|
var value = headers[filterVal.key];
|
|
if (value == null) {
|
|
value = resHeaders && resHeaders[filterVal.key];
|
|
if (value == null) {
|
|
result = false;
|
|
return true;
|
|
}
|
|
}
|
|
if (!value) {
|
|
result = !filterVal.hPattern && !filterVal.value;
|
|
return true;
|
|
}
|
|
value = String(value);
|
|
if (filterVal.hPattern) {
|
|
result = filterVal.hPattern.test(value);
|
|
} else {
|
|
value = value.toLowerCase();
|
|
result =
|
|
value.indexOf(filterVal.value) !== -1 ||
|
|
value.indexOf(filterVal.orgVal) !== -1;
|
|
}
|
|
return true;
|
|
};
|
|
if (filterHeader(req.headers, filter.header, req.resHeaders)) {
|
|
return getFilterResult(result, filter);
|
|
}
|
|
if (filterHeader(req.headers, filter.reqHeader)) {
|
|
return getFilterResult(result, filter);
|
|
}
|
|
if (filterHeader(req.resHeaders, filter.resHeader)) {
|
|
return getFilterResult(result, filter);
|
|
}
|
|
if (filterHeader(env, filter.env)) {
|
|
return getFilterResult(result, filter);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function matchExcludeFilters(url, rule, req) {
|
|
var filters = rule.filters;
|
|
if (!filters) {
|
|
return;
|
|
}
|
|
filters.curFilter = null;
|
|
var hasIncludeFilter;
|
|
var include, exclude;
|
|
for (var i = 0, len = filters.length; i < len; i++) {
|
|
var filter = filters[i];
|
|
hasIncludeFilter = hasIncludeFilter || filter.isInclude;
|
|
if (
|
|
(filter.isInclude ? !include : !exclude) &&
|
|
matchFilter(url, filter, req)
|
|
) {
|
|
if (filter.isInclude) {
|
|
include = true;
|
|
filters.curFilter = filter.raw;
|
|
} else {
|
|
exclude = true;
|
|
}
|
|
}
|
|
}
|
|
return hasIncludeFilter ? !include || exclude : exclude;
|
|
}
|
|
|
|
function Rules(values) {
|
|
this._rules = protoMgr.getRules();
|
|
this._globalPluginVars = {};
|
|
this._sniCallback = [];
|
|
this._orgValues = this._values = values || {};
|
|
}
|
|
|
|
var proto = Rules.prototype;
|
|
|
|
function resolveInlineValuesFn(item, rulesMgr) {
|
|
var file = item.file || getPluginName(item.root) || rulesMgr._file;
|
|
item.file = file;
|
|
item.text = util.resolveInlineValues(item.text, inlineValues, file);
|
|
return item;
|
|
}
|
|
|
|
function trimInlineValues(item, rulesMgr) {
|
|
var text = item.text;
|
|
var isArr = Array.isArray(text);
|
|
var first = isArr && item.text[0];
|
|
if (first && first.resolved) {
|
|
return isArr ? text : [item];
|
|
}
|
|
return isArr ? text.map(function(i) {
|
|
return resolveInlineValuesFn(i, rulesMgr);
|
|
}) : [resolveInlineValuesFn(item, rulesMgr)];
|
|
}
|
|
|
|
proto._parse = function (list, append) {
|
|
var self = this;
|
|
if (!append) {
|
|
protoMgr.resetRules(self._rules);
|
|
self._globalPluginVars = {};
|
|
self._sniCallback = [];
|
|
}
|
|
trimInlineValues(list, self).forEach(function (item) {
|
|
item && parseText(self, item.text, item.root, item.file);
|
|
});
|
|
};
|
|
|
|
proto.parse = function (text, root, _inlineValues) {
|
|
var item = {
|
|
first: true,
|
|
text: text,
|
|
root: root
|
|
};
|
|
this._enabledList = [];
|
|
this._inlineValues = _inlineValues;
|
|
this.disabled = !arguments.length;
|
|
if (this._rawText) {
|
|
if (this._rawText[0].first) {
|
|
this._rawText.shift();
|
|
}
|
|
this._rawText.unshift(item);
|
|
} else {
|
|
this._rawText = [item];
|
|
}
|
|
inlineValues = extend({}, _inlineValues);
|
|
this._parse(item);
|
|
if (!this.disabled) {
|
|
for (var i = 1, len = this._rawText.length; i < len; i++) {
|
|
item = this._rawText[i];
|
|
this._parse(item, true);
|
|
}
|
|
}
|
|
this._values = extend({}, this._orgValues, inlineValues);
|
|
inlineValues = {};
|
|
this._mflag = getMFlag();
|
|
};
|
|
|
|
proto.clearAppend = function () {
|
|
this._enabledList = [];
|
|
if (this._rawText && this._rawText[0].first) {
|
|
var item = this._rawText[0];
|
|
inlineValues = extend({}, this._inlineValues);
|
|
!this.disabled && this._parse(item);
|
|
this._rawText = [item];
|
|
this._values = extend({}, this._orgValues, inlineValues);
|
|
inlineValues = {};
|
|
} else {
|
|
this._rawText = null;
|
|
}
|
|
this._mflag = getMFlag();
|
|
};
|
|
|
|
proto.append = function (text, root, batchUpdate) {
|
|
var item = {
|
|
text: text,
|
|
root: root
|
|
};
|
|
if (this._rawText) {
|
|
this._rawText.push(item);
|
|
!this.disabled && !batchUpdate && this._parse(item, true);
|
|
extend(this._values, inlineValues);
|
|
inlineValues = {};
|
|
} else {
|
|
this._rawText = [item];
|
|
}
|
|
this._mflag = getMFlag();
|
|
};
|
|
|
|
proto.resolveHost = function (
|
|
req,
|
|
callback,
|
|
pluginRulesMgr,
|
|
rulesFileMgr,
|
|
headerRulesMgr
|
|
) {
|
|
if (!req.curUrl) {
|
|
return callback();
|
|
}
|
|
var host = this.getHost(req, pluginRulesMgr, rulesFileMgr, headerRulesMgr);
|
|
if (host) {
|
|
return callback(
|
|
null,
|
|
util.removeProtocol(host.matcher, true),
|
|
host.port,
|
|
host
|
|
);
|
|
}
|
|
if (!req._enableProxyHost && req.rules) {
|
|
delete req.rules.host;
|
|
}
|
|
this.lookupHost(req, callback);
|
|
};
|
|
|
|
proto.lookupHost = function (req, callback) {
|
|
req.curUrl = util.formatUrl(util.setProtocol(req.curUrl));
|
|
var options = parseUrl(req.curUrl);
|
|
lookup(
|
|
options.hostname,
|
|
callback,
|
|
allowDnsCache && !this.resolveDisable(req).dnsCache
|
|
);
|
|
};
|
|
|
|
var ignoreHost = function (req, rulesMgr, filter) {
|
|
if (!rulesMgr) {
|
|
return false;
|
|
}
|
|
var ignore = rulesMgr.resolveFilter(req);
|
|
extend(filter, ignore);
|
|
return (
|
|
ignore.host ||
|
|
ignore.hosts ||
|
|
ignore['ignore|host'] ||
|
|
ignore['ignore|hosts']
|
|
);
|
|
};
|
|
|
|
function getHostFromList(list, req) {
|
|
var host;
|
|
for (var i = 0, len = list.length; i < len; i++) {
|
|
var mgr = list[i];
|
|
var _host = mgr && mgr.getRule(req, mgr._rules.host, mgr._values);
|
|
if (_host) {
|
|
if (util.isImportant(_host)) {
|
|
return _host;
|
|
}
|
|
host = host || _host;
|
|
}
|
|
}
|
|
return host;
|
|
}
|
|
|
|
proto.getHost = function (req, pluginRulesMgr, rulesFileMgr, headerRulesMgr) {
|
|
var curUrl = util.formatUrl(util.setProtocol(req.curUrl));
|
|
req.curUrl = curUrl;
|
|
var filter = {};
|
|
var filterHost =
|
|
ignoreHost(req, this, filter) ||
|
|
ignoreHost(req, pluginRulesMgr, filter) ||
|
|
ignoreHost(req, rulesFileMgr, filter) ||
|
|
ignoreHost(req, headerRulesMgr, filter);
|
|
if (filterHost) {
|
|
return;
|
|
}
|
|
var list;
|
|
if (config.multiEnv) {
|
|
list = [pluginRulesMgr, headerRulesMgr, this, rulesFileMgr];
|
|
} else {
|
|
list = [pluginRulesMgr, this, rulesFileMgr, headerRulesMgr];
|
|
}
|
|
var host = getHostFromList(list, req);
|
|
if (!host || util.exactIgnore(filter, host)) {
|
|
return;
|
|
}
|
|
var matcher = util.rule.getMatcher(host);
|
|
if (matcher && PORT_RE.test(matcher)) {
|
|
host.matcher = RegExp.$1 + (RegExp.$2 || RegExp.$3);
|
|
host.port = host.port || RegExp.$4;
|
|
}
|
|
return host;
|
|
};
|
|
|
|
proto.resolveFilter = function (req) {
|
|
var filter = resolveProps.call(this, req, this._rules.filter, this._values);
|
|
var ignore = resolveProps.call(this, req, this._rules.ignore, this._values, true);
|
|
util.resolveFilter(ignore, filter);
|
|
delete filter.filter;
|
|
delete filter.ignore;
|
|
delete filter['ignore|filter'];
|
|
delete filter['ignore|ignore'];
|
|
return filter;
|
|
};
|
|
proto.resolveDisable = function (req) {
|
|
return resolveProps.call(this, req, this._rules.disable, this._values);
|
|
};
|
|
var pluginProtocols = [
|
|
'enable',
|
|
'disable',
|
|
'plugin',
|
|
'rule',
|
|
'reqHeaders',
|
|
'auth',
|
|
'referer',
|
|
'forwardedFor'
|
|
];
|
|
var proxyProtocols = [
|
|
'enable',
|
|
'disable',
|
|
'plugin',
|
|
'reqHeaders',
|
|
'auth',
|
|
'referer',
|
|
'forwardedFor'
|
|
];
|
|
|
|
function resolveRules(req, isReq, isRes) {
|
|
if (req.isInternalUrl) {
|
|
return {};
|
|
}
|
|
var self = this;
|
|
var rule;
|
|
var rules = self._rules;
|
|
var _rules = {};
|
|
var vals = self._values;
|
|
var filter = self.resolveFilter(req);
|
|
var protos;
|
|
if (req.isPluginReq) {
|
|
protos = req._isProxyReq ? proxyProtocols : pluginProtocols;
|
|
} else if (isRes) {
|
|
protos = pureResProtocols;
|
|
} else if (isReq) {
|
|
protos = req._resolvedG ? reqProtosWithoutG : reqProtocols;
|
|
} else {
|
|
protos = req._resolvedG ? protocolsWithoutG : protocols;
|
|
}
|
|
req._inlineValues = vals;
|
|
protos.forEach(function (name) {
|
|
if (
|
|
name !== 'pipe' &&
|
|
(name === 'proxy' ||
|
|
name === 'rule' ||
|
|
name === 'plugin' ||
|
|
!filter[name]) &&
|
|
(rule = self.getRule(req, rules[name], vals, null, null, null, isReq))
|
|
) {
|
|
_rules[name] = rule;
|
|
}
|
|
});
|
|
|
|
multiMatchs.forEach(function (name) {
|
|
rule = _rules[name];
|
|
if (rule) {
|
|
rule.list = self.getRuleList(req, rules[name], vals);
|
|
util.filterRepeatPlugin(rule);
|
|
if (name === 'rulesFile' || name === 'resScript') {
|
|
var hasScript,
|
|
scriptIndex = -1;
|
|
rule.list = rule.list.filter(function (item, i) {
|
|
if (item.isRules) {
|
|
return true;
|
|
}
|
|
if (hasScript) {
|
|
return false;
|
|
}
|
|
scriptIndex = i;
|
|
hasScript = true;
|
|
return true;
|
|
});
|
|
rule.scriptIndex = scriptIndex;
|
|
rule.isRawList = true;
|
|
}
|
|
}
|
|
});
|
|
util.ignoreRules(_rules, filter);
|
|
return _rules;
|
|
}
|
|
|
|
proto.getRule = function(req, list, vals, index, isFilter, host, isReq) {
|
|
if (!this._isGlobal || !req._disabledProxyRules) {
|
|
return getRule(req, list, vals, index, isFilter, host, isReq);
|
|
}
|
|
};
|
|
|
|
proto.getRuleList = function(req, list, vals, isEnableProxy) {
|
|
if (!this._isGlobal || !req._disabledProxyRules) {
|
|
return resolveRuleList(req, list, vals, null, false, isEnableProxy).map(function (rule) {
|
|
return resolveValue(rule, vals, req);
|
|
});
|
|
}
|
|
return [];
|
|
};
|
|
|
|
proto.resolveRules = function (req) {
|
|
return resolveRules.call(this, req);
|
|
};
|
|
|
|
proto.resolveReqRules = function (req) {
|
|
return resolveRules.call(this, req, true);
|
|
};
|
|
|
|
proto.resolveResRules = function (req) {
|
|
return resolveRules.call(this, req, false, true);
|
|
};
|
|
|
|
proto.resolveEnable = function (req) {
|
|
return resolveProps.call(this, req, this._rules.enable, this._values);
|
|
};
|
|
|
|
function mergeRule(rules, list, name) {
|
|
if (list.length) {
|
|
if (rules[name]) {
|
|
var curList = rules[name].list;
|
|
var flags = curList.map(function (item) {
|
|
return item.raw;
|
|
});
|
|
list.forEach(function (item) {
|
|
if (flags.indexOf(item.raw) === -1) {
|
|
curList.push(item);
|
|
}
|
|
});
|
|
} else {
|
|
rules[name] = extend({ list: list }, list[0]);
|
|
}
|
|
}
|
|
}
|
|
|
|
proto.resolveProxyProps = function (req) {
|
|
if (req.curUrl === req.fullUrl) {
|
|
return;
|
|
}
|
|
var enable = this.getRuleList(req, this._rules.enable, this._values, true);
|
|
var disable = this.getRuleList(req, this._rules.disable, this._values, true);
|
|
if (!enable.length && !disable.length) {
|
|
return;
|
|
}
|
|
mergeRule(req.rules, enable, 'enable');
|
|
mergeRule(req.rules, disable, 'disable');
|
|
enable = util.parseRuleProps(enable);
|
|
disable = util.parseRuleProps(disable);
|
|
if (disable.clientId || disable.clientID || disable.clientid) {
|
|
req.disable.clientId = true;
|
|
}
|
|
if (enable.clientId || enable.clientID || enable.clientid) {
|
|
req.enable.clientId = true;
|
|
}
|
|
if (enable.multiClient) {
|
|
req.enable.multiClient = true;
|
|
}
|
|
if (enable.singleClient) {
|
|
req.enable.singleClient = true;
|
|
}
|
|
if (disable.multiClient) {
|
|
req.disable.multiClient = true;
|
|
}
|
|
return {
|
|
enable: enable,
|
|
disable: disable
|
|
};
|
|
};
|
|
|
|
function resolveSingleRule(req, protocol, multi) {
|
|
req.curUrl = req.curUrl || req.fullUrl;
|
|
var filter = this.resolveFilter(req);
|
|
if (util.isIgnored(filter, protocol)) {
|
|
return;
|
|
}
|
|
var list = protocol === 'sniCallback' ? this._sniCallback : this._rules[protocol];
|
|
if (multi) {
|
|
list = this.getRuleList(req, list, this._values).filter(function(rule) {
|
|
return !util.exactIgnore(filter, rule);
|
|
});
|
|
return list.length ? list : null;
|
|
}
|
|
var rule = this.getRule(req, list, this._values);
|
|
return rule && !util.exactIgnore(filter, rule) ? rule : null;
|
|
}
|
|
|
|
proto.resolvePipe = function (req) {
|
|
return req.isPluginReq ? null : resolveSingleRule.call(this, req, 'pipe');
|
|
};
|
|
|
|
proto.getGRuleList = function (req) {
|
|
return resolveSingleRule.call(this, req, 'G', true);
|
|
};
|
|
|
|
proto.resolvePac = function (req) {
|
|
return resolveSingleRule.call(this, req, 'pac');
|
|
};
|
|
|
|
proto.resolveRule = function (req) {
|
|
return resolveSingleRule.call(this, req, 'rule');
|
|
};
|
|
|
|
var WHISTLE_INTERNAL_HOST =
|
|
/^reqHeaders:\/\/whistleInternalHost=([a-z\d.-]+(?::\d{1,5})?)$/;
|
|
proto.resolveInternalHost = function (req) {
|
|
var list = resolveSingleRule.call(this, req, 'reqHeaders', true);
|
|
if (list) {
|
|
for (var i = 0, len = list.length; i < len; i++) {
|
|
var item = list[i];
|
|
if (WHISTLE_INTERNAL_HOST.test(item.matcher)) {
|
|
return RegExp.$1;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
proto.hasReqScript = function (req) {
|
|
return req.isPluginReq
|
|
? null
|
|
: this.getRule(req, this._rules.rulesFile, this._values);
|
|
};
|
|
|
|
proto.resolveProxy = function (req, host) {
|
|
var proxy = this.getRule(req, this._rules.proxy, this._values, null, null, host);
|
|
var matcher = proxy && proxy.matcher;
|
|
var name = matcher && matcher.substring(0, matcher.indexOf(':'));
|
|
var filter = this.resolveFilter(req);
|
|
if (!proxy) {
|
|
return proxy;
|
|
}
|
|
if (util.exactIgnore(filter, proxy)) {
|
|
return;
|
|
}
|
|
if (filter['ignore:' + name]) {
|
|
return proxy;
|
|
}
|
|
if (util.isIgnored(filter, 'proxy')) {
|
|
return;
|
|
}
|
|
if (matcher[0] === 'x' && util.isIgnored(filter, 'xproxy')) {
|
|
return;
|
|
}
|
|
if (matcher.indexOf(name + '://') === 0 && util.isIgnored(filter, name)) {
|
|
return;
|
|
}
|
|
return proxy;
|
|
};
|
|
|
|
proto.resolveSNICallback = function (req) {
|
|
return resolveSingleRule.call(this, req, 'sniCallback');
|
|
};
|
|
|
|
proto.resolveLocalRule = function (req) {
|
|
return this.getRule(req, this._rules._localRule);
|
|
};
|
|
proto.resolveClientCert = function (req) {
|
|
return this.getRule(req, this._rules._clientCerts);
|
|
};
|
|
proto.resolveBodyFilter = function (req) {
|
|
return req.isPluginReq
|
|
? null
|
|
: this.getRule(
|
|
req,
|
|
req._bodyFilters || this._rules._bodyFilters,
|
|
null,
|
|
null,
|
|
true
|
|
);
|
|
};
|
|
|
|
Rules.disableDnsCache = function () {
|
|
allowDnsCache = false;
|
|
};
|
|
|
|
module.exports = Rules;
|
|
|
|
Rules.setPluginMgr = function(p) {
|
|
pluginMgr = p;
|
|
};
|