Files
whistle/lib/rules/rules.js
T
2026-06-14 13:36:19 +08:00

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;
};