mirror of
https://github.com/avwo/whistle.git
synced 2026-10-28 22:59:50 +08:00
1049 lines
32 KiB
JavaScript
1049 lines
32 KiB
JavaScript
var path = require('path');
|
|
var p = require('pfork');
|
|
var fs = require('fs');
|
|
var fse = require('fs-extra2');
|
|
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 RulesMgr = require('../rules/rules');
|
|
var properties = require('../rules/util').properties;
|
|
var httpMgr = require('../util/http-mgr');
|
|
|
|
var encodeURIComponent = util.encodeURIComponent;
|
|
var REMOTE_RULES_RE = /^\s*@(`?)(whistle\.[a-z\d_\-]+(?:\/[^\s#]*)?|(?:https?:\/\/|[a-z]:[\\/]|~?\/)[^\s#]+)\s*\1(?:#.*)?$/im;
|
|
var PLUGIN_MAIN = path.join(__dirname, './load-plugin');
|
|
var PIPE_PLUGIN_RE = /^pipe:\/\/(?:whistle\.|plugin\.)?([a-z\d_\-]+)(?:\(([\s\S]*)\))?$/;
|
|
var REQ_FROM_HEADER = config.WHISTLE_REQ_FROM_HEADER;
|
|
var RULE_VALUE_HEADER = 'x-whistle-rule-value';
|
|
var RULE_URL_HEADER = 'x-whistle-rule-url';
|
|
var ETAG_HEADER = 'x-whistle-etag';
|
|
var MAX_AGE_HEADER = 'x-whistle-max-age';
|
|
var FULL_URL_HEADER = 'x-whistle-full-url';
|
|
var REAL_URL_HEADER = 'x-whistle-real-url';
|
|
var RELATIVE_URL_HEADER = 'x-whistle-relative-url';
|
|
var REQ_ID_HEADER = 'x-whistle-req-id';
|
|
var PIPE_VALUE_HEADER = 'x-whistle-pipe-value';
|
|
var CUSTOM_PARSER_HEADER = 'x-whistle-frame-parser';
|
|
var STATUS_CODE_HEADER = 'x-whistle-status-code';
|
|
var PLUGIN_REQUEST_HEADER = 'x-whistle-plugin-request';
|
|
var LOCAL_HOST_HEADER = 'x-whistle-local-host';
|
|
var PROXY_VALUE_HEADER = 'x-whistle-proxy-value';
|
|
var PAC_VALUE_HEADER = 'x-whistle-pac-value';
|
|
var METHOD_HEADER = 'x-whistle-method';
|
|
var CLIENT_PORT_HEAD = config.CLIENT_PORT_HEAD;
|
|
var HOST_IP_HEADER = 'x-whistle-host-ip';
|
|
var GLOBAL_VALUE_HEAD = 'x-whistle-global-value';
|
|
var INTERVAL = 6000;
|
|
var CHECK_INTERVAL = 1000 * 60 * 60;
|
|
var portsField = typeof Symbol === 'undefined' ? '_ports' : Symbol('_ports'); // eslint-disable-line
|
|
var UTF8_OPTIONS = {encoding: 'utf8'};
|
|
var notLoadPlugins = config.networkMode || config.rulesOnlyMode;
|
|
var allPlugins = notLoadPlugins ? {} : getPluginsSync();
|
|
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 = {};
|
|
|
|
if (notLoadPlugins) {
|
|
getPlugin = function(cb) {
|
|
cb({});
|
|
};
|
|
}
|
|
|
|
/*eslint no-console: "off"*/
|
|
Object.keys(config).forEach(function(name) {
|
|
if (name === 'password') {
|
|
return;
|
|
}
|
|
var value = config[name];
|
|
if (name === 'passwordHash') {
|
|
conf.password = value;
|
|
} else {
|
|
var type = typeof value;
|
|
if (type == 'string' || type == 'number' || type === 'boolean') {
|
|
conf[name] = value;
|
|
}
|
|
}
|
|
});
|
|
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.on('updateRules', function() {
|
|
rulesMgr.clearAppend();
|
|
var hasRulesUrl;
|
|
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 (rules) {
|
|
rules = rules.replace(REMOTE_RULES_RE, function (_, apo, rulesUrl) {
|
|
hasRulesUrl = true;
|
|
rulesUrl = util.getPluginRulesUrl(rulesUrl);
|
|
if (apo) {
|
|
rulesUrl = util.setConfigVar(rulesUrl);
|
|
}
|
|
return httpMgr.add(rulesUrl, config.runtimeHeaders);
|
|
});
|
|
rulesMgr.append(rules, plugin.path);
|
|
}
|
|
});
|
|
if (!hasRulesUrl) {
|
|
httpMgr.clean();
|
|
}
|
|
});
|
|
pluginMgr.emit('updateRules');
|
|
|
|
function updateRules() {
|
|
pluginMgr.emit('updateRules');
|
|
}
|
|
|
|
httpMgr.addChangeListener(updateRules);
|
|
|
|
pluginMgr.updateRules = updateRules;
|
|
|
|
pluginMgr.on('update', function(result) {
|
|
Object.keys(result).forEach(function(name) {
|
|
pluginMgr.stopPlugin(result[name]);
|
|
});
|
|
});
|
|
pluginMgr.on('uninstall', function(result) {
|
|
Object.keys(result).forEach(function(name) {
|
|
pluginMgr.stopPlugin(result[name]);
|
|
});
|
|
});
|
|
|
|
function showVerbose(oldData, newData) {
|
|
if (!config.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(util.formatDate(new Date(uninstallData[name].mtime)) + ' [uninstall plugin] ' + name.slice(0, -1)));
|
|
});
|
|
installData && Object.keys(installData).forEach(function(name) {
|
|
console.log(colors.green(util.formatDate(new Date(installData[name].mtime)) + ' [install plugin] ' + name.slice(0, -1)));
|
|
});
|
|
updateData && Object.keys(updateData).forEach(function(name) {
|
|
console.log(colors.yellow(util.formatDate(new Date(updateData[name].mtime)) + ' [update plugin] ' + name.slice(0, -1)));
|
|
});
|
|
}
|
|
|
|
function readReqRules(dir, callback) {
|
|
fs.readFile(path.join(path.join(dir, '_rules.txt')), UTF8_OPTIONS, function(err, rulesText) {
|
|
if (err) {
|
|
fs.readFile(path.join(path.join(dir, 'reqRules.txt')), UTF8_OPTIONS, 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;
|
|
fse.readJson(newPkg.pkgPath, function(err, result) {
|
|
if (result && result.version) {
|
|
var conf = result.whistleConfig || '';
|
|
var hintList = util.getHintList(conf);
|
|
var simpleName = name.slice(0, -1);
|
|
newPkg.version = result.version;
|
|
newPkg.priority = parseInt(conf.priority, 10) || parseInt(result.pluginPriority, 10) || 0;
|
|
newPkg.rulesUrl = util.getCgiUrl(conf.rulesUrl);
|
|
newPkg.valuesUrl = util.getCgiUrl(conf.valuesUrl);
|
|
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[util.PLUGIN_MENU_CONFIG] = util.getPluginMenuConfig(conf);
|
|
newPkg.hintUrl = hintList ? undefined : util.getCgiUrl(conf.hintUrl);
|
|
newPkg.hintList = hintList;
|
|
newPkg.hideShortProtocol = !!conf.hideShortProtocol,
|
|
newPkg.hideLongProtocol = !!conf.hideLongProtocol,
|
|
newPkg.homepage = pluginUtil.getHomePageFromPackage(result);
|
|
newPkg.description = result.description;
|
|
newPkg.moduleName = result.name;
|
|
newPkg.pluginHomepage = pluginUtil.getPluginHomepage(result);
|
|
newPkg.registry = util.getRegistry(result);
|
|
newPkg.latest = pkg && pkg.latest;
|
|
_plugins[name] = newPkg;
|
|
fs.readFile(path.join(path.join(newPkg.path, 'rules.txt')), UTF8_OPTIONS, 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);
|
|
fs.readFile(path.join(path.join(newPkg.path, '_values.txt')), UTF8_OPTIONS, function(err, rulesText) {
|
|
newPkg[util.PLUGIN_VALUES] = pluginUtil.parseValues(util.renderPluginRules(rulesText, result, simpleName));
|
|
fs.readFile(path.join(path.join(newPkg.path, 'resRules.txt')), UTF8_OPTIONS, function(err, rulesText) {
|
|
newPkg.resRules = util.renderPluginRules(util.trim(rulesText), result, simpleName);
|
|
callbackHandler();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
} 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);
|
|
|
|
(function update() {
|
|
!config.inspectMode && setTimeout(function() {
|
|
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');
|
|
}
|
|
update();
|
|
});
|
|
});
|
|
}, INTERVAL);
|
|
})();
|
|
|
|
function getValue(rule) {
|
|
if (!rule) {
|
|
return;
|
|
}
|
|
var value = util.getMatcherValue(rule) || '';
|
|
value += (rule.port ? ':' + rule.port : '');
|
|
return encodeURIComponent(value);
|
|
}
|
|
|
|
function addRealUrl(req, newHeaders) {
|
|
if (!newHeaders) {
|
|
return;
|
|
}
|
|
var realUrl = req._realUrl;
|
|
if (!realUrl) {
|
|
var href = req.options && req.options.href;
|
|
realUrl = util.isUrl(href) ? href : null;
|
|
}
|
|
if (realUrl && realUrl != req.fullUrl) {
|
|
newHeaders[REAL_URL_HEADER] = encodeURIComponent(realUrl);
|
|
}
|
|
var rule = req.rules && req.rules.rule;
|
|
if (rule) {
|
|
if (rule.url !== rule.matcher) {
|
|
var relPath = rule.url.substring(rule.matcher.length);
|
|
newHeaders[RELATIVE_URL_HEADER] = encodeURIComponent(relPath);
|
|
}
|
|
if (rule.url) {
|
|
newHeaders[RULE_URL_HEADER] = encodeURIComponent(rule.url);
|
|
}
|
|
}
|
|
}
|
|
|
|
function addRuleHeaders(req, rules, headers) {
|
|
rules = rules || '';
|
|
headers = headers || req.headers;
|
|
var localHost = getValue(rules.host);
|
|
if (localHost) {
|
|
headers[LOCAL_HOST_HEADER] = localHost;
|
|
}
|
|
var ruleValue = util.getMatcherValue(rules.rule);
|
|
if (ruleValue) {
|
|
headers[RULE_VALUE_HEADER] = encodeURIComponent(ruleValue);
|
|
}
|
|
addRealUrl(req, headers);
|
|
var proxyRule = getValue(rules.proxy);
|
|
if (proxyRule) {
|
|
headers[PROXY_VALUE_HEADER] = proxyRule;
|
|
}
|
|
var pac = getValue(rules.pac);
|
|
if (pac) {
|
|
headers[PAC_VALUE_HEADER] = pac;
|
|
}
|
|
if (req.reqId) {
|
|
headers[REQ_ID_HEADER] = req.reqId;
|
|
}
|
|
if (req._pipeValue) {
|
|
headers[PIPE_VALUE_HEADER] = encodeURIComponent(req._pipeValue);
|
|
}
|
|
if (req.customParser) {
|
|
headers[CUSTOM_PARSER_HEADER] = req.customParser;
|
|
}
|
|
|
|
if (req.fullUrl) {
|
|
headers[FULL_URL_HEADER] = encodeURIComponent(req.fullUrl);
|
|
}
|
|
if (req.clientIp) {
|
|
headers[config.CLIENT_IP_HEAD] = req.clientIp;
|
|
}
|
|
if (req.clientPort) {
|
|
headers[config.CLIENT_PORT_HEAD] = req.clientPort;
|
|
}
|
|
if (req.globalValue) {
|
|
headers[config.GLOBAL_VALUE_HEAD] = encodeURIComponent(req.globalValue);
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
pluginMgr.addRuleHeaders = addRuleHeaders;
|
|
|
|
|
|
function loadPlugin(plugin, callback) {
|
|
if (!plugin) {
|
|
return callback();
|
|
}
|
|
var ports = plugin[portsField];
|
|
if (ports) {
|
|
return callback(null, ports);
|
|
}
|
|
util.getBoundIp(config.host, function(host) {
|
|
conf.host = host || LOCALHOST;
|
|
p.fork({
|
|
data: config.getPluginData(plugin.moduleName),
|
|
_inspect: config.inspectMode,
|
|
name: plugin.moduleName,
|
|
script: PLUGIN_MAIN,
|
|
value: plugin.path,
|
|
REQ_FROM_HEADER: REQ_FROM_HEADER,
|
|
RULE_VALUE_HEADER: RULE_VALUE_HEADER,
|
|
RULE_URL_HEADER: RULE_URL_HEADER,
|
|
MAX_AGE_HEADER: MAX_AGE_HEADER,
|
|
ETAG_HEADER: ETAG_HEADER,
|
|
FULL_URL_HEADER: FULL_URL_HEADER,
|
|
REAL_URL_HEADER: REAL_URL_HEADER,
|
|
RELATIVE_URL_HEADER: RELATIVE_URL_HEADER,
|
|
REQ_ID_HEADER: REQ_ID_HEADER,
|
|
PIPE_VALUE_HEADER: PIPE_VALUE_HEADER,
|
|
CUSTOM_PARSER_HEADER: CUSTOM_PARSER_HEADER,
|
|
STATUS_CODE_HEADER: STATUS_CODE_HEADER,
|
|
PLUGIN_REQUEST_HEADER: PLUGIN_REQUEST_HEADER,
|
|
LOCAL_HOST_HEADER: LOCAL_HOST_HEADER,
|
|
HOST_VALUE_HEADER: LOCAL_HOST_HEADER,
|
|
PROXY_VALUE_HEADER: PROXY_VALUE_HEADER,
|
|
PAC_VALUE_HEADER: PAC_VALUE_HEADER,
|
|
METHOD_HEADER: METHOD_HEADER,
|
|
CLIENT_IP_HEADER: config.CLIENT_IP_HEAD,
|
|
CLIENT_PORT_HEAD: CLIENT_PORT_HEAD,
|
|
GLOBAL_VALUE_HEAD: GLOBAL_VALUE_HEAD,
|
|
HOST_IP_HEADER: HOST_IP_HEADER,
|
|
debugMode: config.debugMode,
|
|
config: conf
|
|
}, function(err, ports, child, first) {
|
|
callback(err, ports);
|
|
if (!first) {
|
|
return;
|
|
}
|
|
if (err) {
|
|
logger.error(err);
|
|
var mode = process.env.PFORK_MODE;
|
|
if (config.debugMode || mode === 'inline' || mode === 'bind') {
|
|
console.log(err);
|
|
}
|
|
} else {
|
|
plugin[portsField] = ports;
|
|
child.on('close', function() {
|
|
delete plugin[portsField];
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
pluginMgr.loadPlugin = loadPlugin;
|
|
|
|
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 pluginIsDisabled(protocol.slice(0, -1)) ? null : allPlugins[protocol];
|
|
}
|
|
|
|
pluginMgr.isDisabled = pluginIsDisabled;
|
|
pluginMgr.getPlugin = _getPlugin;
|
|
|
|
function getPluginByName(name) {
|
|
return name && allPlugins[name + ':'];
|
|
}
|
|
|
|
pluginMgr.getPluginByName = getPluginByName;
|
|
|
|
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) {
|
|
plugins = plugins.map(function(plugin) {
|
|
return plugin.plugin;
|
|
});
|
|
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 parseRulesList(req, results, isResRules) {
|
|
var values = {};
|
|
results = results.filter(emptyFilter);
|
|
results.reverse().forEach(function(item) {
|
|
extend(values, item.values);
|
|
});
|
|
var pluginRulesMgr = new RulesMgr(values);
|
|
pluginRulesMgr.parse(results);
|
|
if (isResRules) {
|
|
req.curUrl = req.fullUrl;
|
|
util.mergeRules(req, pluginRulesMgr.resolveRules(req), true);
|
|
}
|
|
return pluginRulesMgr;
|
|
}
|
|
|
|
function getRulesFromPlugins(type, req, res, callback) {
|
|
var plugins = req.whistlePlugins;
|
|
loadPlugins(plugins, function(ports) {
|
|
var needNetworkData;
|
|
ports = ports.map(function(port, i) {
|
|
var plugin = plugins[i];
|
|
if (!needNetworkData && port && type === 'rules' &&
|
|
(port.statsPort || port.resRulesPort || port.resStatsPort)) {
|
|
needNetworkData = true;
|
|
}
|
|
return {
|
|
port: port && port[type + 'Port'],
|
|
plugin: plugin.plugin,
|
|
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 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 = extend({}, options);
|
|
opts.headers = extend({}, options.headers);
|
|
opts.port = item.port;
|
|
if (item.value) {
|
|
opts.headers[RULE_VALUE_HEADER] = encodeURIComponent(item.value);
|
|
} else {
|
|
opts.headers[RULE_VALUE_HEADER] = '';
|
|
}
|
|
addRealUrl(req, opts.headers);
|
|
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 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 getOptions(req, res, type) {
|
|
var fullUrl = req.fullUrl;
|
|
var options = util.parseUrl(fullUrl);
|
|
var isResRules = res && type === 'resRules';
|
|
var headers = extend({}, isResRules ? res.headers : req.headers);
|
|
delete headers.upgrade;
|
|
delete headers.connection;
|
|
|
|
options.headers = addRuleHeaders(req, req.rules, headers);
|
|
delete headers['content-length'];
|
|
headers[METHOD_HEADER] = encodeURIComponent(req.method || 'GET');
|
|
|
|
if (isResRules) {
|
|
headers.host = req.headers.host;
|
|
headers[HOST_IP_HEADER] = req.hostIp || LOCALHOST;
|
|
headers[STATUS_CODE_HEADER] = encodeURIComponent(res.statusCode == null ? '' : res.statusCode);
|
|
if (req.headers.cookie) {
|
|
headers.cookie = req.headers.cookie;
|
|
} else {
|
|
delete headers.cookie;
|
|
}
|
|
}
|
|
|
|
if (req.isPluginReq) {
|
|
headers[PLUGIN_REQUEST_HEADER] = 1;
|
|
}
|
|
|
|
options.protocol = 'http:';
|
|
options.host = LOCALHOST;
|
|
options.hostname = null;
|
|
options.agent = false;
|
|
|
|
return options;
|
|
}
|
|
|
|
function requestRules(options, callback, retryCount) {
|
|
retryCount = retryCount || 0;
|
|
util.request(options, function(err, body, res) {
|
|
if (err && retryCount < 5) {
|
|
return requestRules(options, callback, ++retryCount);
|
|
}
|
|
if (res && res.statusCode == 304) {
|
|
return callback(false, null, null, null, res);
|
|
}
|
|
body = body && body.trim();
|
|
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 && /^\{[\s\S]+\}$/.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;
|
|
if (req.headers[REQ_FROM_HEADER] === 'W2COMPOSER') {
|
|
delete req.headers[REQ_FROM_HEADER];
|
|
}
|
|
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');
|
|
options.headers[PLUGIN_HOOK_NAME_HEADER] = hookName;
|
|
options.headers['x-whistle-request-tunnel-ack'] = 1;
|
|
options.proxyHost = LOCALHOST;
|
|
options.proxyPort = port;
|
|
delete options.headers[CUSTOM_PARSER_HEADER];
|
|
if (req._websocketExtensions !== null) {
|
|
if (req._websocketExtensions) {
|
|
req.headers['sec-websocket-extensions'] = req._websocketExtensions;
|
|
} else {
|
|
delete req.headers['sec-websocket-extensions'];
|
|
}
|
|
}
|
|
var client;
|
|
var destroy = function() {
|
|
if (client) {
|
|
client.destroy();
|
|
client.socket && client.socket.destroy();
|
|
client = null;
|
|
}
|
|
};
|
|
var handleError = function(err) {
|
|
if (client) {
|
|
destroy();
|
|
if (err) {
|
|
config.debugMode && console.log(req._pipeRule.matcher, type);
|
|
(res || req).emit('error', err);
|
|
}
|
|
}
|
|
};
|
|
var handleConnect = function(socket, _res) {
|
|
if (req._hasError) {
|
|
return socket.destroy();
|
|
}
|
|
callback(socket);
|
|
};
|
|
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', 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;
|
|
var resRules = rulesMgr.resolveResRules(req, true);
|
|
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 {
|
|
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 = extend({}, options);
|
|
opts.headers = extend({}, options.headers);
|
|
opts.port = item.port;
|
|
if (item.value) {
|
|
opts.headers[RULE_VALUE_HEADER] = encodeURIComponent(item.value);
|
|
}
|
|
addRealUrl(req, opts.headers);
|
|
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, true);
|
|
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, true);
|
|
plugins.push({
|
|
plugin: plugin,
|
|
value: ruleValue,
|
|
url: ruleUrl == ruleValue ? undefined : ruleUrl
|
|
});
|
|
}
|
|
if (rules.plugin) {
|
|
var _plugins = [plugin];
|
|
rules.plugin.list.forEach(function(_plugin) {
|
|
_plugin = getPluginByPluginRule(_plugin);
|
|
if (_plugin && _plugins.indexOf(_plugin.plugin) == -1) {
|
|
_plugins.push(_plugin.plugin);
|
|
plugins.push(_plugin);
|
|
}
|
|
});
|
|
}
|
|
if (plugins.length) {
|
|
req.whistlePlugins = plugins;
|
|
}
|
|
return plugin;
|
|
}
|
|
|
|
pluginMgr.resolveWhistlePlugins = resolveWhistlePlugins;
|
|
|
|
pluginMgr.updatePluginRules = function(name) {
|
|
name && httpMgr.forceUpdate('http://127.0.0.1:' + config.port + '/whistle.' + name + '/');
|
|
};
|
|
|
|
module.exports = pluginMgr;
|