whistle
Version:
HTTP, HTTP2, HTTPS, Websocket debugging proxy
1,566 lines (1,466 loc) • 46.7 kB
JavaScript
var path = require('path');
var p = require('pfork');
var http = require('http');
var LRU = require('lru-cache');
var extend = require('extend');
var EventEmitter = require('events').EventEmitter;
var pluginMgr = new EventEmitter();
var colors = require('colors/safe');
var util = require('../util');
var logger = require('../util/logger');
var pluginUtil = require('./util');
var config = require('../config');
var getPluginsSync = require('./get-plugins-sync');
var getPlugin = require('./get-plugins');
var rulesMgr = require('../rules');
var properties = require('../rules/util').properties;
var httpMgr = require('../util/http-mgr');
var protocols = require('../rules/protocols');
var common = require('../util/common');
var mp = require('./module-paths');
var createSharedStorage = require('../plugins/shared-storage');
var Rules = rulesMgr.Rules;
var getPluginName = pluginUtil.getPluginName;
var encodeURIComponent = util.encodeURIComponent;
var RULES_TPL_RE = /^[^\n\r\S]*(``)[^\n\r\S]*((?:_?var(\.[^\s=]+)?|whistle|rule)\.tpl)[^\n\r\S]*[\r\n]([\s\S]+?)[\r\n][^\n\r\S]*\1\s*$/gm;
var REMOTE_RULES_RE =
/^\s*@(`?)([\w.-]+(?:[\\/][^\s#]*)?|(?:https?:\/\/|[a-z]:[\\/]|~?\/)[^\s#]+|\$(?:whistle\.)?[a-z\d_-]+[/:][^\s#]+)\s*?\1(?:#.*)?$/gim;
var PLUGIN_MAIN = path.join(__dirname, './load-plugin');
var PIPE_PLUGIN_RE =
/^pipe:\/\/(?:whistle\.|plugin\.)?([a-z\d_\-]+)(?:\(([\s\S]*)\))?$/;
var JSON_RE = /^\{[\s\S]+\}$/;
var ETAG_HEADER = 'x-whistle-etag';
var MAX_AGE_HEADER = 'x-whistle-max-age';
var STATUS_ERR = new Error('Non 200');
var INTERVAL = 6000;
var CHECK_INTERVAL = 1000 * 60 * 60 * 2;
var MAX_CERT_SIZE = 72 * 1024;
var portsField = Symbol('ports');
var notLoadPlugins = config.networkMode || config.rulesOnlyMode;
var allPlugins = notLoadPlugins ? {} : getPluginsSync();
var authPlugins = [];
var tunnelKeys = [];
var LOCALHOST = '127.0.0.1';
var MAX_RULES_LENGTH = 1024 * 256;
var rulesCache = new LRU({ max: 36 });
var PLUGIN_HOOKS = config.PLUGIN_HOOKS;
var PLUGIN_HOOK_NAME_HEADER = config.PLUGIN_HOOK_NAME_HEADER;
var conf = {};
var EXCLUDE_CONF_KEYS = {
uid: 1,
WEBUI_HEAD: 1,
COMPOSER_CLIENT_ID_HEADER: 1,
DISABLE_RULES_HEADER: 1,
WHISTLE_POLICY_HEADER: 1,
CLIENT_IP_HEADER: 1,
HTTPS_FIELD: 1,
CLIENT_PORT_HEADER: 1,
CLIENT_ID_HEADER: 1,
FROM_COM_HEADER: 1,
ALPN_PROTOCOL_HEADER: 1
};
var EXCLUDE_NAMES = {
password: 1,
shadowRules: 1,
rules: 1,
values: 1
};
var debugMode = config.debugMode;
var proxy;
var REGISTRY_LIST = path.join(common.getDefaultWhistlePath(), '.registry.list');
var MAX_REG_COUNT = 100;
var storage = createSharedStorage(REGISTRY_LIST);
var registryList = filterRegList(common.readJsonSync(REGISTRY_LIST));
function filterRegList(l) {
if (!Array.isArray(l)) {
l = [];
}
return l.filter(function(url) {
return common.getRegistry(url);
}).slice(0, MAX_REG_COUNT);
}
function readRegList() {
common.readJson(REGISTRY_LIST, function(e, list) {
if (!e) {
registryList = filterRegList(list);
}
});
}
if (notLoadPlugins) {
getPlugin = function (cb) {
cb({});
};
}
pluginMgr.addRegistry = function(registry) {
registry = common.getRegistry(registry);
if (registry && registryList.indexOf(registry) === -1) {
registryList.push(registry);
storage.setAll(registryList);
}
};
/*eslint no-console: "off"*/
Object.keys(config).forEach(function (name) {
if (EXCLUDE_NAMES[name]) {
return;
}
var value = config[name];
if (name === 'passwordHash') {
conf.password = value;
}
if (name === 'globalData') {
conf.globalData = value;
} else if (!EXCLUDE_CONF_KEYS[name]) {
var type = typeof value;
if (type == 'string' || type == 'number' || type === 'boolean') {
conf[name] = value;
}
}
});
conf.client = config.client;
conf.PLUGIN_HOOKS = config.PLUGIN_HOOKS;
conf.uiHostList = config.uiHostList;
var pluginHostMap = config.pluginHostMap;
var pluginHosts = (conf.pluginHosts = {});
if (pluginHostMap) {
Object.keys(pluginHostMap).forEach(function (host) {
var name = pluginHostMap[host];
var list = (pluginHosts[name] = pluginHosts[name] || []);
list.push(host);
});
}
pluginMgr.REGISTRY_LIST = REGISTRY_LIST;
pluginMgr.getPluginsPath = mp.getPaths;
pluginMgr.getRegistryList = function() {
return registryList;
};
pluginMgr.disableAllPlugins = function(disabled) {
properties.set('disabledAllPlugins', disabled);
pluginMgr.emit('updateRules', 'disableAllPlugins');
};
pluginMgr.getTunnelKeys = function () {
return tunnelKeys;
};
var MAX_REMOTE_RULES_COUNT = 3;
pluginMgr.on('updateRules', function (byParse) {
rulesMgr.clearAppend();
authPlugins = [];
tunnelKeys = [];
var ruleTpls;
Object.keys(allPlugins)
.sort(function (a, b) {
var p1 = allPlugins[a];
var p2 = allPlugins[b];
return (
util.compare(p1.priority, p2.priority) ||
util.compare(p2.mtime, p1.mtime) ||
(a > b ? 1 : 0)
);
})
.forEach(function (name) {
if (pluginIsDisabled(name.slice(0, -1))) {
return;
}
var plugin = allPlugins[name];
var rules = plugin.rules;
if (plugin.enableAuthUI) {
authPlugins.push(plugin);
}
if (plugin.tunnelKey) {
plugin.tunnelKey.forEach(function (key) {
if (tunnelKeys.indexOf(key) === -1) {
tunnelKeys.push(key);
}
});
}
if (rules) {
var root = plugin.path;
var index = 0;
rules = rules.replace(REMOTE_RULES_RE, function (_, apo, rulesUrl) {
if (index >= MAX_REMOTE_RULES_COUNT) {
return '';
}
++index;
return util.getRemoteRules(apo, rulesUrl, root);
});
rules = rules.replace(RULES_TPL_RE, function (_, __, key, subVar, value) {
ruleTpls = ruleTpls || {};
var simpleName = name.slice(0, -1);
var isPrivate = key[0] === '_';
if (isPrivate || key[0] === 'v') {
key = (isPrivate ? '_' : '') + '%' + simpleName + (subVar || '');
} else if (key === 'whistle.tpl') {
key = 'whistle.' + simpleName;
} else {
key = simpleName;
}
if (!ruleTpls[key]) {
ruleTpls[key] = value;
}
return '';
});
rulesMgr.append(rules, plugin.path, true);
}
});
pluginMgr.ruleTpls = ruleTpls;
if (byParse !== true) {
httpMgr.triggerChange();
proxy.emit('pluginsChange');
}
});
pluginMgr.updateRules = function() {
pluginMgr.emit('updateRules');
};
pluginMgr.loadCert = function (req, plugin, callback) {
loadPlugin(plugin, function (err, ports) {
if (err || !ports || !ports.sniPort) {
return callback();
}
if (!req.useSNI || req.isHttpsServer) {
req._sniType = req.isHttpsServer ? '1' : '0';
}
var options = getOptions(req);
options.maxLength = MAX_CERT_SIZE;
options.headers[PLUGIN_HOOK_NAME_HEADER] = PLUGIN_HOOKS.SNI;
options.port = ports.sniPort;
addPluginVars(req, options.headers, {
plugin: plugin,
value: ''
});
requestPlugin(options, function (err, body, res) {
if (err || res.statusCode !== 200) {
return callback(err || STATUS_ERR);
}
if (!body) {
return callback();
}
if (body === 'false') {
return callback(false);
}
if (body === 'true') {
return callback(true);
}
try {
body = JSON.parse(body);
if (
!body ||
!body.name ||
!util.isString(body.key) ||
!util.isString(body.cert)
) {
return callback();
}
callback({
key: body.key,
cert: body.cert,
name: body.name,
mtime: body.mtime > 0 ? body.mtime : undefined
});
} catch (e) {
callback(e);
}
});
});
};
pluginMgr.on('update', function (result) {
Object.keys(result).forEach(function (name) {
pluginMgr.stopPlugin(result[name]);
});
readRegList();
});
pluginMgr.on('uninstall', function (result) {
Object.keys(result).forEach(function (name) {
pluginMgr.stopPlugin(result[name]);
});
});
function formatMTime(mtime) {
return '[' + (new Date(mtime).toLocaleTimeString()) + ']';
}
function showVerbose(oldData, newData) {
if (!debugMode) {
return;
}
var uninstallData, installData, updateData;
Object.keys(oldData).forEach(function (name) {
var oldItem = oldData[name];
var newItem = newData[name];
if (!newItem) {
uninstallData = uninstallData || {};
uninstallData[name] = oldItem;
} else if (newItem.path != oldItem.path || newItem.mtime != oldItem.mtime) {
updateData = updateData || {};
updateData[name] = newItem;
}
});
Object.keys(newData).forEach(function (name) {
if (!oldData[name]) {
installData = installData || {};
installData[name] = newData[name];
}
});
uninstallData &&
Object.keys(uninstallData).forEach(function (name) {
console.log(
colors.red(
formatMTime(uninstallData[name].mtime) +
' UNINSTALL ' + name.slice(0, -1)
)
);
});
installData &&
Object.keys(installData).forEach(function (name) {
console.log(
colors.green(
formatMTime(installData[name].mtime) +
' INSTALL ' + name.slice(0, -1)
)
);
});
updateData &&
Object.keys(updateData).forEach(function (name) {
console.log(
colors.yellow(
formatMTime(updateData[name].mtime) +
' UPDATE ' + name.slice(0, -1)
)
);
});
}
function readReqRules(dir, callback) {
pluginUtil.readFile(path.join(path.join(dir, '_rules.txt')), function (err, rulesText) {
if (err) {
pluginUtil.readFile(
path.join(path.join(dir, 'reqRules.txt')),
function (_, rulesText) {
callback(util.trim(rulesText));
}
);
return;
}
callback(util.trim(rulesText));
});
}
function readPackages(obj, callback) {
var _plugins = {};
var count = 0;
var callbackHandler = function () {
if (--count <= 0) {
callback(_plugins);
}
};
Object.keys(obj).forEach(function (name) {
var pkg = allPlugins[name];
var newPkg = obj[name];
if (!pkg || pkg.path != newPkg.path || pkg.mtime != newPkg.mtime) {
++count;
common.readJson(path.join(newPkg.path, 'package.json'), function (_, result) {
if (result && result.version && pluginUtil.isPluginName(result.name)) {
var conf = result.whistleConfig || '';
var tabs = util.getInspectorTabs(conf);
var hintList = util.getHintList(conf);
var simpleName = name.slice(0, -1);
newPkg.enableAuthUI = !!conf.enableAuthUI;
newPkg.noOpt = !!(conf.noOption || conf.notOption || conf.disableOption|| conf.disabledOption);
newPkg.updateUrl = util.getUpdateUrl(conf);
newPkg.inheritAuth = !!conf.inheritAuth;
newPkg.tunnelKey = util.getTunnelKey(conf);
newPkg.version = result.version;
newPkg.staticDir = util.getStaticDir(conf);
newPkg.priority =
parseInt(conf.priority, 10) ||
parseInt(result.pluginPriority, 10) ||
0;
newPkg.rulesUrl = util.getCgiUrl(conf.rulesUrl);
newPkg.valuesUrl = util.getCgiUrl(conf.valuesUrl);
newPkg.installUrl = util.getCgiUrl(conf.installUrl);
newPkg.favicon = util.getCgiUrl(conf.favicon);
newPkg.installRegistry = util.getInstallRegistry(conf.installRegistry);
newPkg.networkColumn = util.getNetworkColumn(conf);
newPkg.networkMenus = util.getPluginMenu(
conf.networkMenus || conf.networkMenu,
simpleName
);
newPkg.rulesMenus = util.getPluginMenu(
conf.rulesMenus || conf.rulesMenu,
simpleName
);
newPkg.valuesMenus = util.getPluginMenu(
conf.valuesMenus || conf.valuesMenu,
simpleName
);
newPkg.pluginsMenus = util.getPluginMenu(
conf.pluginsMenus || conf.pluginsMenu,
simpleName
);
newPkg.reqTab = util.getCustomTab(tabs.req, simpleName);
newPkg.resTab = util.getCustomTab(tabs.res, simpleName);
newPkg.tab = util.getCustomTab(tabs, simpleName);
newPkg.toolTab = util.getCustomTab(conf.toolsTab || conf.toolTab, simpleName);
newPkg.comTab = util.getCustomTab(conf.composerTab, simpleName);
newPkg[util.PLUGIN_MENU_CONFIG] = util.getPluginMenuConfig(conf);
newPkg[util.PLUGIN_INSPECTOR_CONFIG] =
util.getPluginInspectorConfig(conf);
newPkg.hintUrl = hintList ? undefined : util.getCgiUrl(conf.hintUrl);
newPkg.hintList = hintList;
newPkg.pluginVars = util.getPluginVarsConf(conf);
newPkg.hideShortProtocol = !!conf.hideShortProtocol;
newPkg.hideLongProtocol = !!conf.hideLongProtocol;
newPkg.homepage = pluginUtil.getHomePageFromPackage(result) || pluginUtil.getHomePageFromPackage(conf);
newPkg.description = result.description;
newPkg.moduleName = result.name;
newPkg.pluginHomepage = pluginUtil.getPluginHomepage(result) || pluginUtil.getPluginHomepage(conf);
newPkg.openInPlugins = conf.openInPlugins ? 1 : undefined;
newPkg.openInModal = util.getPluginModal(conf);
newPkg.openExternal = conf.openExternal ? 1 : undefined;
newPkg.registry = util.getRegistry(result);
newPkg.latest = pkg && pkg.latest;
_plugins[name] = newPkg;
pluginUtil.readFile(
path.join(path.join(newPkg.path, 'rules.txt')),
function (err, rulesText) {
newPkg.rules = util.renderPluginRules(
util.trim(rulesText),
result,
simpleName
);
readReqRules(newPkg.path, function (rulesText) {
newPkg._rules = util.renderPluginRules(
util.trim(rulesText),
result,
simpleName
);
pluginUtil.readFile(
path.join(path.join(newPkg.path, '_values.txt')),
function (err, rulesText) {
newPkg[util.PLUGIN_VALUES] = pluginUtil.parseValues(util.renderPluginRules(rulesText, result, simpleName), simpleName);
pluginUtil.readFile(
path.join(path.join(newPkg.path, 'resRules.txt')),
function (err, rulesText) {
newPkg.resRules = util.renderPluginRules(
util.trim(rulesText),
result,
simpleName
);
pluginUtil.readWorker(newPkg.path, conf, function(hash) {
newPkg.webWorker = hash;
callbackHandler();
}, simpleName);
}
);
}
);
});
}
);
} else {
callbackHandler();
}
});
} else {
_plugins[name] = pkg;
}
});
if (count <= 0) {
callback(_plugins);
}
}
function checkUpdate(pluginNames) {
pluginNames = pluginNames || Object.keys(allPlugins);
var name = pluginNames.shift();
var plugin;
while (name) {
if ((plugin = allPlugins[name]) && !plugin.isProj) {
break;
}
name = pluginNames.shift();
}
if (name) {
util.getLatestVersion(plugin, function (ver) {
if (ver && plugin.version !== ver) {
plugin.latest = ver;
}
checkUpdate(pluginNames);
});
} else {
setTimeout(checkUpdate, CHECK_INTERVAL);
}
}
setTimeout(checkUpdate, 5000);
var updating;
var updateTimer;
var delayTimer;
function refreshPlugins() {
updating = true;
delayTimer && clearTimeout(delayTimer);
updateTimer && clearTimeout(updateTimer);
delayTimer = null;
updateTimer = null;
getPlugin(function (result) {
readPackages(result, function (_plugins) {
var updatePlugins, uninstallPlugins;
var pluginNames = Object.keys(allPlugins);
pluginNames.forEach(function (name) {
var plugin = allPlugins[name];
var newPlugin = _plugins[name];
if (!newPlugin) {
uninstallPlugins = uninstallPlugins || {};
uninstallPlugins[name] = plugin;
} else if (
newPlugin.path != plugin.path ||
newPlugin.mtime != plugin.mtime
) {
updatePlugins = updatePlugins || {};
updatePlugins[name] = newPlugin;
}
});
showVerbose(allPlugins, _plugins);
allPlugins = _plugins;
if (
uninstallPlugins ||
updatePlugins ||
Object.keys(_plugins).length !== pluginNames.length
) {
uninstallPlugins && pluginMgr.emit('uninstall', uninstallPlugins);
updatePlugins && pluginMgr.emit('update', updatePlugins);
pluginMgr.emit('updateRules');
pluginUtil.resetWorkers(allPlugins);
}
updating = false;
update();
});
});
}
function update() {
if (!config.inspectMode && !updating) {
updateTimer = setTimeout(refreshPlugins, INTERVAL);
}
}
update();
pluginMgr.refreshPlugins = function() {
delayTimer = delayTimer || setTimeout(refreshPlugins, 200);
};
function addRealUrl(req) {
var realUrl = req._realUrl;
if (!realUrl) {
var href = req.options && req.options.href;
realUrl = util.isUrl(href) ? href : null;
}
if (realUrl && realUrl != req.fullUrl) {
req._finalUrl = realUrl;
}
var rule = req.rules && req.rules.rule;
if (rule) {
if (rule.url !== rule.matcher) {
req._relativeUrl = rule.url.substring(rule.matcher.length);
}
req._ruleUrl = rule.url;
}
}
function getPluginVars(vars, name) {
var value = vars && vars[name];
if (value) {
try {
value = JSON.stringify(value);
return Buffer.from(value).toString('base64');
} catch (e) {}
}
}
function addPluginVars(req, headers, rule) {
if (rule) {
var plugin = rule.plugin;
var name;
var value;
if (plugin) {
name = getPluginName(plugin.moduleName);
value = rule.value;
} else {
name = rule.matcher.split(':', 1)[0];
value = util.getMatcherValue(rule);
}
req._ruleValue = value;
if (rule.rawPattern) {
req._rawPattern = (rule.isRegExp ? 1 : 0) + ',' + rule.rawPattern;
}
if (rule.url) {
var extraUrl = rule.url;
if (value) {
extraUrl = rule.url.substring(value.length);
}
req._extraUrl = extraUrl;
}
req._globalPluginVarsValue = getPluginVars(req._globalPluginVars, name);
req._pluginVarsValue = getPluginVars(req._pluginVars, name);
}
addRealUrl(req);
common.setSessionInfo(req, headers);
}
pluginMgr.addSessionInfo = function(req) {
var headers = req.headers;
var rules = req.rules;
addPluginVars(req, headers, rules && rules.rule);
};
function loadPlugin(plugin, callback) {
if (!plugin) {
return callback(null, '');
}
var ports = plugin[portsField];
if (ports) {
return callback(null, ports);
}
util.getBoundIp(config.host, function (host) {
conf.host = host || LOCALHOST;
var moduleName = plugin.moduleName;
var name = moduleName.substring(moduleName.indexOf('/') + 1);
var isInline = config.inspectMode || process.env.PFORK_MODE === 'inline';
p.fork(
{
data: config.getPluginData(moduleName),
_inspect: config.inspectMode,
name: moduleName,
script: PLUGIN_MAIN,
value: plugin.path,
isDev: plugin.isDev,
version: plugin.version,
staticDir: plugin.staticDir,
MAX_AGE_HEADER: MAX_AGE_HEADER,
ETAG_HEADER: ETAG_HEADER,
debugMode: debugMode,
config: isInline ? extend(true, {}, conf) : conf // 防止 inline 时,子进程删除 conf
},
function (err, ports, child, first) {
callback(err, ports);
if (!first) {
return;
}
if (err) {
proxy.emit('pluginLoadError', err, name, moduleName);
logger.error(err);
var mode = process.env.PFORK_MODE;
if (debugMode || mode === 'inline' || mode === 'bind') {
console.log(err);
}
} else {
proxy.emit('pluginLoad', child, name, moduleName);
plugin[portsField] = ports;
child.on('close', function () {
delete plugin[portsField];
});
}
}
);
});
}
pluginMgr.loadPlugin = loadPlugin;
pluginMgr.loadPluginByName = function (name, callback) {
loadPlugin(getActivePluginByName(name), callback);
};
pluginMgr.stopPlugin = function (plugin) {
p.kill(
{
script: PLUGIN_MAIN,
value: plugin.path
},
10000
);
};
pluginMgr.getPlugins = function () {
return allPlugins;
};
function pluginIsDisabled(name) {
if (config.notAllowedDisablePlugins) {
return false;
}
if (properties.get('disabledAllPlugins')) {
return true;
}
var disabledPlugins = properties.get('disabledPlugins') || {};
return disabledPlugins[name];
}
function _getPlugin(protocol) {
return protocol && pluginIsDisabled(protocol.slice(0, -1)) ? null : allPlugins[protocol];
}
pluginMgr.isDisabled = pluginIsDisabled;
pluginMgr.getPlugin = _getPlugin;
function getActivePluginByName(name) {
return pluginIsDisabled(name) ? null : allPlugins[name + ':'];
}
rulesMgr.getPlugin = getActivePluginByName;
function getPluginByName(name) {
return name && allPlugins[name + ':'];
}
pluginMgr.getPluginByName = getPluginByName;
pluginMgr.getModifiablePlugin = function(name) {
name = getPluginByName(name);
return name && !name.isProj && !name.notUn ? name : null;
};
function getPluginByRuleUrl(ruleUrl) {
if (!ruleUrl || typeof ruleUrl != 'string') {
return;
}
var index = ruleUrl.indexOf(':');
if (index == -1) {
return null;
}
var protocol = ruleUrl.substring(0, index + 1);
return pluginIsDisabled(protocol.slice(0, -1)) ? null : allPlugins[protocol];
}
pluginMgr.getPluginByRuleUrl = getPluginByRuleUrl;
function _loadPlugins(plugins, callback) {
var rest = plugins.length;
var results = [];
var execCallback = function () {
--rest === 0 && callback(results);
};
plugins.forEach(function (plugin, i) {
loadPlugin(plugin, function (err, ports) {
plugin.ports = ports;
results[i] = ports || null;
execCallback();
});
});
}
function loadPlugins(plugins, callback) {
plugins = plugins.map(function (plugin) {
return plugin.plugin;
});
_loadPlugins(plugins, callback);
}
pluginMgr.loadAuthPlugins = function (req, callback) {
if (config.disableAuthUI || !authPlugins.length) {
return callback();
}
req._isUIRequest = true;
_loadPlugins(authPlugins, function (ports) {
ports = ports.map(function (port, i) {
return {
authPort: port && port.authPort,
plugin: authPlugins[i]
};
});
var rest = ports.length;
if (!rest) {
return callback();
}
var options = getOptions(req);
authReq(true, ports, req, options, function (forbidden) {
if (forbidden) {
if (req._redirectUrl || req._authHtmlUrl) {
return callback(req._redirectUrl, null, req._authHtmlUrl);
}
var status = req._authStatus ? (req._showLoginBox ? 401 : 403) : 502;
return callback(status, forbidden);
}
return callback();
});
});
};
function parseRulesList(req, results, isResRules) {
var values = {};
results = results.filter(emptyFilter);
results.reverse().forEach(function (item) {
extend(values, item.values);
});
var pluginRulesMgr = new Rules(values);
pluginRulesMgr.parse(results);
if (isResRules) {
req.curUrl = req.fullUrl;
util.mergeRules(req, pluginRulesMgr.resolveRules(req), true);
}
return pluginRulesMgr;
}
function getPluginReqOpts(item, req, options, port) {
var opts = extend({}, options);
opts.headers = extend({}, options.headers);
opts.port = port;
addPluginVars(req, opts.headers, item);
return opts;
}
function authReq(isReq, ports, req, options, callback) {
if (!isReq) {
return callback();
}
var rest = ports.length;
var forbidden;
var execCallback = function () {
if (--rest === 0) {
callback(forbidden);
}
};
ports.forEach(function (item) {
if (!item.authPort) {
return execCallback();
}
options.headers[PLUGIN_HOOK_NAME_HEADER] = PLUGIN_HOOKS.AUTH;
var opts = getPluginReqOpts(item, req, options, item.authPort);
opts.maxLength = MAX_RULES_LENGTH;
requestPlugin(opts, function (err, body, res) {
var headers = res && res.headers;
if (err || body) {
if (!forbidden) {
if (debugMode) {
forbidden = err ? err.message || 'Error' : body;
} else {
forbidden = err ? 'Error' : body;
}
var file = item.plugin && getPluginName(item.plugin.moduleName);
req._statusFile = file;
if (headers) {
var authHtmlUrl = headers[common.AUTH_URL];
req._authStatus = headers[common.AUTH_STATUS];
if (authHtmlUrl) {
try {
authHtmlUrl = decodeURIComponent(authHtmlUrl);
req._authHtmlUrl = util.isUrl(authHtmlUrl) ? authHtmlUrl : 'file://' + authHtmlUrl;
req._htmlFile = file;
} catch (e) {}
} else if (headers.location) {
req._redirectUrl = headers.location;
req._redirectFile = file;
} else if (headers[config.SHOW_LOGIN_BOX]) {
req._showLoginBox = true;
}
}
}
err && logger.error(err);
}
if (!forbidden && headers) {
Object.keys(headers).forEach(function (key) {
if (
key.indexOf('x-whistle-') === 0 ||
key === 'proxy-authorization'
) {
var value = headers[key];
if (key === config.WHISTLE_POLICY_HEADER &&
value === 'enableCaptureByAuth') {
req._forceCapture = true;
} else if (key === config.CLIENT_ID_HEADER) {
req._customClientId = value;
}
req.headers[key] = value;
options.headers[key] = value;
}
});
}
execCallback();
});
});
}
function getSrcName(file) {
return file ? util.getPluginFile(file) : 'Plugin Auth';
}
function getRulesFromPlugins(type, req, res, callback) {
var plugins = req.whistlePlugins;
loadPlugins(plugins, function (ports) {
ports = ports.map(function (port, i) {
var plugin = plugins[i];
return {
port: port && port[type + 'Port'],
authPort: port && port.authPort,
plugin: plugin.plugin,
isRegExp: plugin.isRegExp,
rawPattern: plugin.rawPattern,
value: plugin.value,
url: plugin.url
};
});
var rest = ports.length;
if (!rest) {
return callback();
}
var results = [];
var options = getOptions(req, res, type);
var isResRules = type == 'resRules';
var enableAuth =
!isResRules &&
req.justAuth !== false &&
(!req.isPluginReq || req._isProxyReq) &&
(!req.fromTunnel || !util.isAuthCapture(req));
authReq(enableAuth, ports, req, options, function (forbidden) {
if (forbidden) {
var noTunnel = !req.isTunnel;
req._authForbidden = true;
var file = getSrcName(req._statusFile);
var mgr = new Rules(util.toPrivateValues({ msg: forbidden }, file));
if (noTunnel && req._authHtmlUrl) {
mgr._file = getSrcName(req._htmlFile);
mgr.parse(
'* ignore://!method|!file|!http|!https method://get ' +
req._authHtmlUrl
);
} else if (noTunnel && req._redirectUrl) {
mgr._file = getSrcName(req._redirectFile);
mgr.parse('* ignore://!redirect redirect://' + req._redirectUrl);
} else {
var status = req._authStatus ? (req._showLoginBox ? (noTunnel ? 401 : 407) : 403) : 502;
mgr._file = file;
mgr.parse(
'* ignore://!statusCode|!resBody|!resType|!resCharset status://' +
status +
' resBody://{msg} resType://html resCharset://utf8'
);
}
return callback(mgr);
}
if (req.justAuth) {
return callback();
}
var hookName = isResRules ? 'RES_RULES' : type === 'tunnelRules' ? 'TUNNEL_RULES' : 'REQ_RULES';
options.headers[PLUGIN_HOOK_NAME_HEADER] = PLUGIN_HOOKS[hookName];
var execCallback = function () {
if (--rest <= 0) {
if (req.resScriptRules) {
results = results.concat(req.resScriptRules);
}
callback(parseRulesList(req, results, isResRules));
}
};
ports.forEach(function (item, i) {
var plugin = item.plugin;
if (!item.port) {
var rulesText = isResRules ? plugin.resRules : plugin._rules;
if (rulesText) {
results[i] = {
text: rulesText,
values: plugin[util.PLUGIN_VALUES],
root: plugin.path
};
}
return execCallback();
}
var opts = getPluginReqOpts(item, req, options, item.port);
var cacheKey = plugin.moduleName + '\n' + type;
var data = rulesCache.get(cacheKey);
var updateMaxAge = function (obj, age) {
if (age >= 0) {
obj.maxAge = age;
obj.now = Date.now();
}
};
var handleRules = function (err, body, values, raw, res) {
if (err === false && data) {
body = data.body;
values = data.values;
raw = data.raw;
updateMaxAge(data, res && res.headers[MAX_AGE_HEADER]);
} else {
values = util.toPrivateValues(values, getSrcName(getPluginName(plugin.moduleName)));
if (res) {
var etag = res.headers[ETAG_HEADER];
var maxAge = res.headers[MAX_AGE_HEADER];
var newData;
if (maxAge >= 0) {
newData = newData || {};
updateMaxAge(newData, maxAge);
}
if (etag) {
newData = newData || {};
newData.etag = etag;
}
if (newData) {
newData.body = body;
newData.values = values;
newData.raw = raw;
rulesCache.set(cacheKey, newData);
} else {
rulesCache.del(cacheKey);
}
}
}
var pendingCallbacks = data && data.pendingCallbacks;
if (pendingCallbacks) {
delete data.pendingCallbacks;
pendingCallbacks.forEach(function (cb) {
cb(err, body, values, raw);
});
}
body = body || '';
if (isResRules) {
body += plugin.resRules ? '\n' + plugin.resRules : '';
} else {
body += plugin._rules ? '\n' + plugin._rules : '';
}
if (body || values) {
var pluginVals = plugin[util.PLUGIN_VALUES];
if (values && pluginVals) {
var vals = extend({}, pluginVals);
values = extend(vals, values);
} else {
values = values || pluginVals;
}
results[i] = {
text: body,
values: values,
root: plugin.path
};
}
execCallback();
};
delete opts.headers[ETAG_HEADER];
if (data) {
if (Date.now() - data.now <= data.maxAge) {
return handleRules(false);
}
if (data.etag) {
opts.headers[ETAG_HEADER] = data.etag;
}
if (data.maxAge >= 0) {
if (data.pendingCallbacks) {
data.pendingCallbacks.push(handleRules);
return;
}
data.pendingCallbacks = [];
}
}
opts.ignoreExceedError = true;
opts.maxLength = MAX_RULES_LENGTH;
requestRules(opts, handleRules);
});
});
});
}
function removeInvalidHeaders(headers) {
delete headers.upgrade;
delete headers.connection;
delete headers['content-length'];
}
function getOptions(req, res, type) {
var fullUrl = req.fullUrl || util.getFullUrl(req);
var options = util.parseUrl(fullUrl);
var isResRules = res && type === 'resRules';
var headers = extend({}, isResRules ? res.headers : req.headers);
if (isResRules) {
headers.host = req.headers.host;
req.hostIp = req.hostIp || LOCALHOST;
req._statusCode = res.statusCode;
if (req.headers.cookie) {
headers.cookie = req.headers.cookie;
} else {
delete headers.cookie;
}
}
options.headers = headers;
removeInvalidHeaders(headers);
options.protocol = 'http:';
options.host = LOCALHOST;
options.hostname = null;
options.agent = false;
return options;
}
function requestPlugin(options, callback, retryCount) {
retryCount = retryCount || 0;
util.request(options, function (err, body, res) {
if (err && retryCount < 5) {
return requestPlugin(options, callback, ++retryCount);
}
if (res && res.statusCode == 304) {
return callback(false, null, res);
}
callback(null, body && body.trim(), res);
});
}
function requestRules(options, callback) {
requestPlugin(options, function (err, body, res) {
if (err === false) {
return callback(false, null, null, null, res);
}
var rules = body;
var values = null;
var data = !err && rulesToJson(body);
if (data) {
rules = data.text;
values = data.values;
}
callback(err, rules, values, body, res);
});
}
function rulesToJson(body) {
if (body && JSON_RE.test(body)) {
try {
body = JSON.parse(body);
return {
root: typeof body.root === 'string' ? body.root : null,
text: typeof body.rules === 'string' ? body.rules : '',
values: body.values
};
} catch (e) {}
}
}
function emptyFilter(val) {
return !!val;
}
function getRulesMgr(type, req, res, callback) {
var plugins = req.whistlePlugins;
if (!plugins) {
return callback();
}
getRulesFromPlugins(type, req, res, callback);
}
function getPluginRulesCallback(req, callback) {
return function (pluginRules) {
req.pluginRules = pluginRules;
callback(pluginRules);
};
}
function resolvePipePlugin(req, callback) {
if (req._pipePlugin == null) {
var pipe;
var hRules = req.headerRulesMgr;
if (config.multiEnv) {
pipe = (hRules && hRules.resolvePipe(req)) || rulesMgr.resolvePipe(req);
} else {
pipe = rulesMgr.resolvePipe(req) || (hRules && hRules.resolvePipe(req));
}
var plugin;
req._pipeRule = pipe;
if (pipe && PIPE_PLUGIN_RE.test(pipe.matcher)) {
req._pipeValue = RegExp.$2;
plugin = _getPlugin(RegExp.$1 + ':');
}
req._pipePlugin = plugin || '';
}
loadPlugin(req._pipePlugin, function (_, ports) {
req._pipePluginPorts = ports || '';
callback(ports);
});
}
pluginMgr.resolvePipePlugin = resolvePipePlugin;
function getPipe(type, hookName) {
var isRes = type.toLowerCase().indexOf('res') !== -1;
hookName = PLUGIN_HOOKS[hookName];
return function (req, res, callback) {
if (!isRes) {
callback = res;
res = null;
}
resolvePipePlugin(req, function (ports) {
var port = ports && ports[type + 'Port'];
if (!port || req._hasClosed || req._hasError) {
return callback();
}
var options = getOptions(req, res, isRes && 'resRules');
var rule = req.rules && req.rules.rule;
var item;
if (req._pipePlugin) {
rule = rule || rulesMgr.resolveRule(req);
item = { plugin: req._pipePlugin };
if (rule) {
var name = getPluginName(req._pipePlugin.moduleName) + '://';
if (rule.matcher.indexOf(name) === 0)
item.value = util.getMatcherValue(rule);
}
}
addPluginVars(req, options.headers, item);
options.headers[PLUGIN_HOOK_NAME_HEADER] = hookName;
options.headers[common.ACK_HEADER] = 1;
options.proxyHost = LOCALHOST;
options.proxyPort = port;
if (req._websocketExtensions !== null) {
if (req._websocketExtensions) {
req.headers['sec-websocket-extensions'] = req._websocketExtensions;
} else {
delete req.headers['sec-websocket-extensions'];
}
}
var client;
var done;
var handleConnect = function (socket, _res) {
if (!done) {
done = true;
if (req._hasError) {
return socket.destroy();
}
callback(socket);
}
};
var destroy = function () {
if (client) {
client.destroy();
client.socket && client.socket.destroy();
client = null;
handleConnect();
}
};
var handleError = function (err) {
if (client) {
destroy();
if (err) {
debugMode && console.log(req._pipeRule.matcher, type);
(res || req).emit('error', err);
}
}
};
client = config.connect(options, handleConnect);
client.on('error', function () {
if (client) {
destroy();
client = !req._hasError && config.connect(options, handleConnect);
client && util.onSocketEnd(client, handleError);
}
});
req.once('_closed', function() {
handleConnect();
destroy();
});
});
};
}
pluginMgr.getReqReadPipe = getPipe('reqRead', 'REQ_READ');
pluginMgr.getReqWritePipe = getPipe('reqWrite', 'REQ_WRITE');
pluginMgr.getResReadPipe = getPipe('resRead', 'RES_READ');
pluginMgr.getResWritePipe = getPipe('resWrite', 'RES_WRITE');
var getWsReqReadPipe = getPipe('wsReqRead', 'WS_REQ_READ');
var getWsReqWritePipe = getPipe('wsReqWrite', 'WS_REQ_WRITE');
var getWsResReadPipe = getPipe('wsResRead', 'WS_RES_READ');
var getWsResWritePipe = getPipe('wsResWrite', 'WS_RES_WRITE');
var getTunnelReqReadPipe = getPipe('tunnelReqRead', 'TUNNEL_REQ_READ');
var getTunnelReqWritePipe = getPipe('tunnelReqWrite', 'TUNNEL_REQ_WRITE');
var getTunnelResReadPipe = getPipe('tunnelResRead', 'TUNNEL_RES_READ');
var getTunnelResWritePipe = getPipe('tunnelResWrite', 'TUNNEL_RES_WRITE');
pluginMgr.getWsPipe = function (req, res, callback) {
req._websocketExtensions = res.headers['sec-websocket-extensions'] || '';
getWsReqReadPipe(req, function (reqRead) {
getWsReqWritePipe(req, function (reqWrite) {
getWsResReadPipe(req, res, function (resReadStream) {
getWsResWritePipe(req, res, function (resWriteStream) {
callback(reqRead, reqWrite, resReadStream, resWriteStream);
});
});
});
});
};
pluginMgr.getTunnelPipe = function (req, res, callback) {
getTunnelReqReadPipe(req, function (reqRead) {
getTunnelReqWritePipe(req, function (reqWrite) {
getTunnelResReadPipe(req, res, function (resRead) {
getTunnelResWritePipe(req, res, function (resWrite) {
callback(reqRead, reqWrite, resRead, resWrite);
});
});
});
});
};
pluginMgr.getRules = function (req, callback) {
getRulesMgr('rules', req, null, getPluginRulesCallback(req, callback));
};
pluginMgr.getResRules = function (req, res, callback) {
req.curUrl = req.fullUrl;
if (!req.resHeaders && res) {
req.resHeaders = res.headers;
}
var resRules = rulesMgr.resolveResRules(req, true);
var pRules = req.pluginRules && req.pluginRules.resolveResRules(req, true);
var fRules = req.rulesFileMgr && req.rulesFileMgr.resolveResRules(req, true);
var hRules =
req.headerRulesMgr && req.headerRulesMgr.resolveResRules(req, true);
fRules && util.mergeRules(req, fRules, true);
config.multiEnv && util.mergeRules(req, resRules, true);
hRules && util.mergeRules(req, hRules, true);
pRules && util.mergeRules(req, pRules, true);
!config.multiEnv && util.mergeRules(req, resRules, true);
var resScriptRules;
var resHeaderRules = res.headers[config.RES_RULES_HEAD];
if (resHeaderRules) {
try {
resHeaderRules = rulesToJson(decodeURIComponent(resHeaderRules));
if (resHeaderRules) {
resScriptRules = resScriptRules || [];
resScriptRules.push(resHeaderRules);
}
} catch (e) {}
}
delete res.headers[config.RES_RULES_HEAD];
rulesMgr.resolveResRulesFile(req, res, function (result) {
if (result) {
resScriptRules = resScriptRules || [];
resScriptRules.push(result);
}
req.resScriptRules = resScriptRules;
getRulesMgr('resRules', req, res, function (pluginRulesMgr) {
if (!pluginRulesMgr && resScriptRules) {
pluginRulesMgr = parseRulesList(req, resScriptRules, true);
}
req.resScriptRules = resScriptRules = null;
callback(pluginRulesMgr);
});
});
};
pluginMgr.getTunnelRules = function (req, callback) {
getRulesMgr('tunnelRules', req, null, getPluginRulesCallback(req, callback));
};
function postStats(req, res) {
var plugins = req.whistlePlugins;
var type = res ? '_postResStats' : '_postReqStats';
if (!plugins || req.isPluginReq || req[type]) {
return;
}
req[type] = true;
loadPlugins(plugins, function (ports) {
ports = ports
.map(function (port, i) {
var plugin = plugins[i];
var statsPort = port && (res ? port.resStatsPort : port.statsPort);
if (!statsPort) {
return;
}
return {
plugin: plugin.plugin,
port: statsPort,
value: plugin.value,
url: plugin.url
};
})
.filter(emptyFilter);
if (!ports.length) {
return;
}
var options = getOptions(req, res, 'resRules');
options.headers[PLUGIN_HOOK_NAME_HEADER] = PLUGIN_HOOKS[res ? 'RES_STATS' : 'REQ_STATS'];
ports.forEach(function (item) {
var opts = getPluginReqOpts(item, req, options, item.port);
var request = http.request(opts, function (response) {
response.on('error', util.noop);
response.on('data', util.noop);
});
request.on('error', util.noop);
request.end();
});
});
}
pluginMgr.postStats = postStats;
var PLUGIN_RULE_RE = /^([a-z\d_\-]+)(?:\(([\s\S]*)\))?$/;
var PLUGIN_RULE_RE2 = /^(?:\w+\.)?([a-z\d_\-]+)(?:\:\/\/([\s\S]*))?$/;
var PLUGIN_RE = /^plugin:\/\//;
function getPluginByPluginRule(pluginRule) {
if (!pluginRule) {
return;
}
var value = pluginRule.matcher;
if (PLUGIN_RE.test(value)) {
value = util.getMatcherValue(pluginRule);
} else {
value = util.rule.getMatcher(pluginRule);
}
if (PLUGIN_RULE_RE.test(value) || PLUGIN_RULE_RE2.test(value)) {
value = RegExp.$2;
var plugin = _getPlugin(RegExp.$1 + ':');
if (!plugin) {
return;
}
var ruleUrl = util.getUrlValue(pluginRule);
return (
plugin && {
plugin: plugin,
value: value,
url: ruleUrl == value ? undefined : ruleUrl
}
);
}
}
function resolveWhistlePlugins(req) {
var rules = req.rules;
var plugins = [];
var plugin = (req.pluginMgr = getPluginByRuleUrl(
util.rule.getUrl(rules.rule)
));
if (plugin) {
rules._pluginRule = rules.rule;
var ruleValue = util.getMatcherValue(rules.rule);
var ruleUrl = util.getUrlValue(rules.rule);
plugins.push({
plugin: plugin,
value: ruleValue,
isRegExp: rules.rule.isRegExp,
rawPattern: rules.rule.rawPattern,
url: ruleUrl == ruleValue ? undefined : ruleUrl
});
}
if (rules.plugin) {
var _plugins = [plugin];
rules.plugin.list.forEach(function (rule) {
var info = getPluginByPluginRule(rule);
if (info && _plugins.indexOf(info.plugin) == -1) {
info.isRegExp = rule.isRegExp;
info.rawPattern = rule.rawPattern;
_plugins.push(info.plugin);
plugins.push(info);
}
});
}
if (plugins.length) {
req.whistlePlugins = plugins;
}
return plugin;
}
pluginMgr.resolveWhistlePlugins = resolveWhistlePlugins;
pluginMgr.updatePluginRules = function (name) {
name &&
httpMgr.forceUpdate(name + '/' );
};
pluginMgr.setProxy = function (p) {
proxy = p;
};
httpMgr.setPluginMgr(pluginMgr);
var PLUGIN_KEY_RE =/^\$(?:whistle\.)?([a-z\d_-]+)[/:]([\S\s]+)$/;
var MAX_VALUE_LEN = 1024 * 1024 * 16;
var MAX_URL_VAL_LEN = 1024 * 256;
function requestValue(options, callback, isBin) {
options.needRawData = isBin;
var handleCallback = function(err, body, res) {
var code = res && res.statusCode;
if (code != 200) {
body = '';
if (!err) {
err = new Error('Error: response ' + code);
err.code = code || 500;
}
}
err && logger.error(err);
callback(body, err, res);
};
util.request(extend({}, options), function(err, body, res) {
if (err) {
return util.request(options, handleCallback);
}
handleCallback(err, body, res);
});
return options;
}
pluginMgr.resolveKey = function(url, rule, req) {
if (util.isUrl(url)) {
return {
url: url,
isInternalReq: req && req._isInternalReq,
originalKey: url,
maxLength: MAX_URL_VAL_LEN
};
}
if (!PLUGIN_KEY_RE.test(url)) {
return;
}
var name = RegExp.$1;
var key = RegExp.$2;
if (!getActivePluginByName(name)) {
return;
}
var headers = extend({}, req && req.headers, config.pluginHeaders);
if (req) {
req._ruleProtocol = protocols.getRuleProto(rule);
common.setSessionInfo(req, headers);
removeInvalidHeaders(headers);
}
return {
originalKey: url,
pluginName: name,
maxLength: MAX_VALUE_LEN,
url: name + '/api/key/value?key=' + encodeURIComponent(key),
headers: headers
};
};
pluginMgr.requestText = function(options, callback) {
return requestValue(options, callback);
};
pluginMgr.requestBin =function(options, callback) {
return requestValue(options, callback, true);
};
util.setPluginMgr(pluginMgr);
module.exports = pluginMgr;
Rules.setPluginMgr(pluginMgr);