mirror of
https://github.com/avwo/whistle.git
synced 2026-10-18 22:51:32 +08:00
868 lines
32 KiB
JavaScript
868 lines
32 KiB
JavaScript
var socks = require('sockx');
|
|
var parseUrl = require('./util/parse-url-safe');
|
|
var net = require('net');
|
|
var extend = require('extend');
|
|
var LRU = require('lru-cache');
|
|
var EventEmitter = require('events').EventEmitter;
|
|
var hparser = require('hparser');
|
|
var dispatch = require('./https');
|
|
var util = require('./util');
|
|
var rules = require('./rules');
|
|
var socketMgr = require('./socket-mgr');
|
|
var rulesUtil = require('./rules/util');
|
|
var ca = require('./https/ca');
|
|
var pluginMgr = require('./plugins');
|
|
var config = require('./config');
|
|
|
|
var hasCustomCerts = ca.hasCustomCerts;
|
|
var existsCustomCert = ca.existsCustomCert;
|
|
var IP_CACHE = new LRU({ max: 600 });
|
|
var LOCALHOST = '127.0.0.1';
|
|
var TUNNEL_HOST_RE = /^[^:\/]+\.[^:\/]+:\d+$/;
|
|
var X_RE = /^x/;
|
|
var STATUS_CODES = require('http').STATUS_CODES || {};
|
|
var getRawHeaderNames = hparser.getRawHeaderNames;
|
|
var formatHeaders = hparser.formatHeaders;
|
|
var getRawHeaders = hparser.getRawHeaders;
|
|
|
|
function emitAborted(data, reqEmitter, code) {
|
|
data.reqError = true;
|
|
data.res.statusCode = code || 'aborted';
|
|
data.req.body = 'aborted';
|
|
reqEmitter.emit('abort', data);
|
|
}
|
|
|
|
function tunnelProxy(server, proxy, type) {
|
|
proxy.getTunnelIp = function (id) {
|
|
return IP_CACHE.get(id);
|
|
};
|
|
var fromHttpServer = type === 2;
|
|
var fromHttpsServer = type === 1;
|
|
server.on('connect', function (req, reqSocket) {
|
|
//ws, wss, https proxy
|
|
var headers = req.headers;
|
|
if (headers[config.WEBUI_HEAD]) {
|
|
delete headers[config.WEBUI_HEAD];
|
|
reqSocket.destroy();
|
|
return;
|
|
}
|
|
req.fromHttpServer = reqSocket.fromHttpServer = fromHttpServer;
|
|
req.fromHttpsServer = reqSocket.fromHttpsServer = fromHttpsServer;
|
|
var tunnelUrl = (req.fullUrl = util.setProtocol(
|
|
TUNNEL_HOST_RE.test(req.url) ? req.url : headers.host,
|
|
true
|
|
));
|
|
var options;
|
|
var socketErr;
|
|
var _emitError;
|
|
var _parseUrl = function (_url, port) {
|
|
_url = _url || tunnelUrl;
|
|
options = req.options = parseUrl(_url);
|
|
options.port = options.port || port || 443;
|
|
if (options.host && options.host.indexOf(':') === -1) {
|
|
options.host += ':' + options.port;
|
|
}
|
|
return options;
|
|
};
|
|
_parseUrl();
|
|
tunnelUrl = req.fullUrl = 'tunnel://' + options.host;
|
|
proxy.emit('_request', tunnelUrl);
|
|
var resSocket, proxyClient, responsed, reqEmitter, data, originPort;
|
|
var reqData, resData, res, rollBackTunnel, buf;
|
|
req.isTunnel = true;
|
|
req.method = util.toUpperCase(req.method) || 'CONNECT';
|
|
var clientInfo = util.parseClientInfo(req);
|
|
reqSocket._disabledProxyRules = req._disabledProxyRules;
|
|
reqSocket.clientIp = req.clientIp = clientInfo[0] || util.getClientIp(req);
|
|
reqSocket.clientPort = req.clientPort =
|
|
clientInfo[1] || util.getClientPort(req);
|
|
req._remoteAddr = clientInfo[2] || util.getRemoteAddr(req);
|
|
req._remotePort = clientInfo[3] || util.getRemotePort(req);
|
|
delete headers[config.CLIENT_PORT_HEAD];
|
|
var now = Date.now();
|
|
req.reqId = reqSocket.reqId = util.getReqId(now);
|
|
reqSocket.headers = headers;
|
|
reqSocket.fromTunnel = req.fromTunnel;
|
|
reqSocket.fromComposer = req.fromComposer;
|
|
req.isPluginReq = reqSocket.isPluginReq = util.checkPluginReqOnce(req, true);
|
|
util.onSocketEnd(reqSocket, function (err) {
|
|
// 处理可能的证书校验出错
|
|
if (config.captureData && dispatch.getTunnelDataOnce(reqSocket)) {
|
|
resData = {headers: {}};
|
|
data = {
|
|
id: req.reqId,
|
|
url: options.host,
|
|
startTime: now,
|
|
dnsTime: now,
|
|
captureError: true,
|
|
rules: req.rules,
|
|
req: {
|
|
_clientId: util.getComposerClientId(headers),
|
|
ip: req.clientIp,
|
|
port: req.clientPort,
|
|
method: req.method,
|
|
httpVersion: req.httpVersion || '1.1',
|
|
headers: headers,
|
|
rawHeaderNames: getRawHeaderNames(req.rawHeaders) || {}
|
|
},
|
|
res: resData,
|
|
isHttps: true
|
|
};
|
|
data.endTime = data.responseTime = data.requestTime = Date.now();
|
|
reqEmitter = new EventEmitter();
|
|
proxy.emit('request', reqEmitter, data);
|
|
emitAborted(data, reqEmitter, 'captureError');
|
|
pluginMgr.resolveWhistlePlugins(req);
|
|
pluginMgr.postStats(req);
|
|
pluginMgr.postStats(req, resData);
|
|
}
|
|
socketErr = err;
|
|
if (req.isLogRequests) {
|
|
--util.proc.tunnelRequests;
|
|
}
|
|
req.isLogRequests = false;
|
|
if (_emitError) {
|
|
_emitError(err);
|
|
} else {
|
|
reqSocket.destroy();
|
|
}
|
|
});
|
|
var hostname = options.hostname;
|
|
var isICloundCKDB = hostname === 'p22-ckdatabase.icloud.com';
|
|
var isIPHost = !isICloundCKDB && net.isIP(hostname);
|
|
var policy = headers[config.WHISTLE_POLICY_HEADER];
|
|
var weakTunnel = policy == 'weakTunnel';
|
|
var useTunnelPolicy = weakTunnel || policy == 'tunnel';
|
|
var inspect = useTunnelPolicy;
|
|
useTunnelPolicy = useTunnelPolicy || policy == 'connect';
|
|
var enableTunnelAck =
|
|
useTunnelPolicy && req.headers['x-whistle-request-tunnel-ack'];
|
|
var isLocalUIUrl = !useTunnelPolicy && config.isLocalUIUrl(hostname);
|
|
if (isLocalUIUrl ? isIPHost : util.isLocalHost(hostname)) {
|
|
isLocalUIUrl =
|
|
options.port == config.port || options.port == config.uiport;
|
|
}
|
|
var _rules = {};
|
|
if (isICloundCKDB || isLocalUIUrl) {
|
|
req.enable = req.disable = req._filters = _rules;
|
|
} else {
|
|
_rules = rules.initRules(req);
|
|
}
|
|
req.rules = reqSocket.rules = _rules;
|
|
reqSocket._globalPluginVars = req._globalPluginVars;
|
|
reqSocket.isLocalUIUrl = isLocalUIUrl;
|
|
rules.resolveRulesFile(req, function () {
|
|
var filter = req._filters;
|
|
var disable = req.disable;
|
|
var isDisabledProps = function() {
|
|
return disable.intercept || disable.https || disable.capture;
|
|
};
|
|
var isDisabeIntercept = function () {
|
|
return (
|
|
isICloundCKDB ||
|
|
useTunnelPolicy ||
|
|
isDisabledProps() ||
|
|
(req.isPluginReq && policy !== 'capture')
|
|
);
|
|
};
|
|
var isCustomIntercept = function () {
|
|
return (
|
|
filter.https ||
|
|
filter.capture ||
|
|
filter.intercept ||
|
|
policy === 'intercept' ||
|
|
policy === 'capture'
|
|
);
|
|
};
|
|
var isWebPort =
|
|
options.port == 80 || options.port == 443 || isCustomIntercept();
|
|
var matchCustomCert;
|
|
var isEnableIntercept = function () {
|
|
if (isCustomIntercept()) {
|
|
return true;
|
|
}
|
|
if (!rulesUtil.properties.isEnableCapture()) {
|
|
matchCustomCert = !!existsCustomCert(hostname);
|
|
return matchCustomCert;
|
|
}
|
|
return true;
|
|
};
|
|
var isIntercept = function () {
|
|
if (isLocalUIUrl || ((!isDisabeIntercept() && isEnableIntercept()) ||
|
|
(weakTunnel && !isDisabledProps() && isCustomIntercept()))) {
|
|
return true;
|
|
}
|
|
if (!isIPHost || !hasCustomCerts()) {
|
|
return false;
|
|
}
|
|
reqSocket.useProxifier =
|
|
(config.proxifier2 || (config.proxifier && isWebPort)) &&
|
|
!disable.proxifier;
|
|
return reqSocket.useProxifier;
|
|
};
|
|
var resolvedPlugin;
|
|
if (isIntercept()) {
|
|
if (util.isAuthCapture(req)) {
|
|
req.justAuth = true;
|
|
resolvedPlugin = true;
|
|
}
|
|
} else {
|
|
resolvedPlugin = true;
|
|
}
|
|
var plugin = resolvedPlugin ? pluginMgr.resolveWhistlePlugins(req) : null;
|
|
var handlePluginRules = function (_rulesMgr) {
|
|
if (_rulesMgr) {
|
|
req.pluginRules = _rulesMgr;
|
|
req.curUrl = tunnelUrl;
|
|
util.mergeRules(req, _rulesMgr.resolveReqRules(req));
|
|
util.filterWeakRule(req);
|
|
plugin = pluginMgr.getPluginByRuleUrl(util.rule.getUrl(_rules.rule));
|
|
} else {
|
|
util.filterWeakRule(req);
|
|
}
|
|
filter = req._filters;
|
|
disable = req.disable;
|
|
};
|
|
if (req.whistlePlugins) {
|
|
if (
|
|
matchCustomCert ||
|
|
(matchCustomCert == null && existsCustomCert(hostname))
|
|
) {
|
|
req._existsCustomCert = true;
|
|
req._enableCapture = true;
|
|
} else if (isEnableIntercept()) {
|
|
req._enableCapture = true;
|
|
}
|
|
}
|
|
pluginMgr.getTunnelRules(req, function (_rulesMgr) {
|
|
handlePluginRules(_rulesMgr);
|
|
policy = headers[config.WHISTLE_POLICY_HEADER];
|
|
if (!reqSocket._hasError && !req._authForbidden &&
|
|
!req.fromComposer && (req._forceCapture || isIntercept())) {
|
|
reqSocket.rulesHeaders = req.rulesHeaders;
|
|
reqSocket.enable = req.enable;
|
|
reqSocket.disable = req.disable;
|
|
reqSocket.tunnelHostname = hostname;
|
|
reqSocket.rules = _rules;
|
|
dispatch(
|
|
reqSocket,
|
|
function (chunk) {
|
|
if (
|
|
isLocalUIUrl ||
|
|
(isIPHost &&
|
|
util.isLocalAddress(hostname) &&
|
|
options.port == config.port)
|
|
) {
|
|
return reqSocket.destroy();
|
|
}
|
|
buf = chunk;
|
|
rollBackTunnel = true;
|
|
ensureLoadRules();
|
|
},
|
|
!req.enable.socket && isWebPort
|
|
);
|
|
util.setEstablished(reqSocket);
|
|
return;
|
|
}
|
|
|
|
ensureLoadRules();
|
|
|
|
function ensureLoadRules() {
|
|
if (!req.justAuth) {
|
|
if (resolvedPlugin) {
|
|
return handleTunnel();
|
|
}
|
|
} else {
|
|
req.justAuth = false;
|
|
}
|
|
if (!resolvedPlugin) {
|
|
plugin = pluginMgr.resolveWhistlePlugins(req);
|
|
}
|
|
pluginMgr.getTunnelRules(req, function (_rulesMgr2) {
|
|
handlePluginRules(_rulesMgr2);
|
|
handleTunnel();
|
|
});
|
|
}
|
|
|
|
function handleTunnel() {
|
|
if (req.isLogRequests !== false) {
|
|
++util.proc.tunnelRequests;
|
|
++util.proc.totalTunnelRequests;
|
|
req.isLogRequests = true;
|
|
}
|
|
var reqRawHeaderNames = getRawHeaderNames(req.rawHeaders) || {};
|
|
var enable = req.enable;
|
|
inspect =
|
|
inspect || util.isInspect(enable) || util.isCustomParser(req);
|
|
reqData = {
|
|
_clientId: util.getComposerClientId(headers),
|
|
ip: req.clientIp,
|
|
port: req.clientPort,
|
|
method: req.method,
|
|
httpVersion: req.httpVersion || '1.1',
|
|
headers: headers,
|
|
rawHeaderNames: reqRawHeaderNames
|
|
};
|
|
resData = { headers: {} };
|
|
reqEmitter = new EventEmitter();
|
|
data = {
|
|
id: req.reqId,
|
|
url: options.host,
|
|
startTime: Date.now(),
|
|
rules: _rules,
|
|
req: reqData,
|
|
res: resData,
|
|
isHttps: true,
|
|
inspect: inspect,
|
|
rulesHeaders: req.rulesHeaders
|
|
};
|
|
if (
|
|
util.showPluginReq(req) &&
|
|
config.captureData &&
|
|
(!filter.hide || disable.hide)
|
|
) {
|
|
data.abort = emitError;
|
|
if (req.isPluginReq) {
|
|
data.isPR = 1;
|
|
}
|
|
proxy.emit('request', reqEmitter, data);
|
|
}
|
|
if (reqSocket._hasError) {
|
|
return emitError(socketErr);
|
|
}
|
|
util.parseRuleJson(_rules.reqHeaders, function (reqHeaders) {
|
|
if (reqSocket._hasError) {
|
|
return emitError(socketErr);
|
|
}
|
|
_emitError = emitError;
|
|
var customXFF;
|
|
if (reqHeaders) {
|
|
reqHeaders = util.lowerCaseify(reqHeaders, reqRawHeaderNames);
|
|
customXFF = reqHeaders[config.CLIENT_IP_HEAD];
|
|
delete reqHeaders[config.CLIENT_IP_HEAD];
|
|
extend(headers, reqHeaders);
|
|
}
|
|
|
|
if (disable.clientIp || disable.clientIP) {
|
|
delete headers[config.CLIENT_IP_HEAD];
|
|
} else {
|
|
var forwardedFor = util.getMatcherValue(_rules.forwardedFor);
|
|
if (net.isIP(forwardedFor)) {
|
|
headers[config.CLIENT_IP_HEAD] = forwardedFor;
|
|
} else if (net.isIP(customXFF)) {
|
|
headers[config.CLIENT_IP_HEAD] = customXFF;
|
|
} else if (util.isLocalAddress(req.clientIp)) {
|
|
delete headers[config.CLIENT_IP_HEAD];
|
|
} else {
|
|
headers[config.CLIENT_IP_HEAD] = req.clientIp;
|
|
}
|
|
}
|
|
|
|
pluginMgr.postStats(req);
|
|
if (
|
|
!req.disable.abort &&
|
|
(enable.abort || filter.abort || disable.tunnel)
|
|
) {
|
|
return reqSocket.destroy();
|
|
}
|
|
|
|
res = util.getStatusCodeFromRule(_rules);
|
|
if (res) {
|
|
var statusCode = res.statusCode;
|
|
util.deleteReqHeaders(req);
|
|
if (statusCode == 200) {
|
|
resSocket = util.getEmptyRes();
|
|
var reqDelay = util.getMatcherValue(_rules.reqDelay);
|
|
data.requestTime = data.dnsTime = Date.now();
|
|
if (reqDelay > 0) {
|
|
setTimeout(handleConnect, reqDelay);
|
|
} else {
|
|
handleConnect();
|
|
}
|
|
return;
|
|
}
|
|
return sendEstablished(statusCode);
|
|
}
|
|
|
|
pluginMgr.loadPlugin(
|
|
req.isPluginReq ? null : plugin,
|
|
function (err, ports) {
|
|
if (reqSocket._hasError) {
|
|
return emitError(socketErr);
|
|
}
|
|
if (err) {
|
|
return sendEstablished(500);
|
|
}
|
|
var tunnelPort = ports && ports.tunnelPort;
|
|
var proxyUrl;
|
|
if (tunnelPort) {
|
|
proxyUrl = 'proxy://127.0.0.1:' + tunnelPort;
|
|
reqSocket.customParser = req.customParser =
|
|
util.getParserStatus(req);
|
|
pluginMgr.addRuleHeaders(req, _rules);
|
|
headers[config.PLUGIN_HOOK_NAME_HEADER] =
|
|
config.PLUGIN_HOOKS.TUNNEL;
|
|
socketMgr.setPending(req);
|
|
data.reqPlugin = 1;
|
|
}
|
|
|
|
var realUrl = _rules.rule && _rules.rule.url;
|
|
if (realUrl) {
|
|
var isHttp;
|
|
if (/^https?:/.test(realUrl)) {
|
|
isHttp = realUrl[4] === ':';
|
|
realUrl =
|
|
'tunnel' + realUrl.substring(realUrl.indexOf(':'));
|
|
}
|
|
if (/^tunnel:\/\//.test(realUrl) && realUrl != tunnelUrl) {
|
|
_parseUrl(realUrl, isHttp ? 80 : 443);
|
|
tunnelUrl = 'tunnel://' + options.host;
|
|
data.realUrl = tunnelUrl.replace('tunnel://', '');
|
|
}
|
|
}
|
|
originPort = options.port;
|
|
if (_rules.ua) {
|
|
var ua = util.getMatcherValue(_rules.ua);
|
|
headers['user-agent'] = ua;
|
|
}
|
|
rules.getProxy(
|
|
tunnelUrl,
|
|
proxyUrl ? null : req,
|
|
function (err, hostIp, hostPort) {
|
|
var isInternalProxy;
|
|
var proxyRule = _rules.proxy || '';
|
|
if (!proxyUrl) {
|
|
proxyUrl = proxyRule
|
|
? util.rule.getMatcher(proxyRule)
|
|
: null;
|
|
}
|
|
var isXProxy;
|
|
if (proxyUrl) {
|
|
isXProxy = X_RE.test(proxyUrl);
|
|
isInternalProxy = proxyRule.isInternal || util.isInternalProxy(req);
|
|
var isSocks = proxyRule.isSocks;
|
|
var isHttpsProxy = proxyRule.isHttps;
|
|
var _url = 'http:' + util.removeProtocol(proxyUrl);
|
|
data.proxy = true;
|
|
getServerIp(_url, function (ip) {
|
|
options = _parseUrl(
|
|
_url,
|
|
isSocks ? 1080 : isHttpsProxy ? 443 : 80
|
|
);
|
|
options.auth = options.auth || req._pacAuth;
|
|
var isProxyPort = util.isProxyPort(options.port);
|
|
if (isProxyPort && util.isLocalAddress(ip)) {
|
|
return emitError(
|
|
new Error(
|
|
'Self loop (' + ip + ':' + config.port + ')'
|
|
)
|
|
);
|
|
}
|
|
var handleProxy = function (proxySocket, _res) {
|
|
resSocket = proxySocket;
|
|
res = _res;
|
|
// 通知插件连接建立成功的回调
|
|
handleConnect();
|
|
handleError(resSocket);
|
|
delete headers['x-whistle-frame-parser'];
|
|
};
|
|
var dstOptions = parseUrl(tunnelUrl);
|
|
dstOptions.proxyHost = ip;
|
|
dstOptions.proxyPort = parseInt(options.port, 10);
|
|
dstOptions.port = dstOptions.port || 443;
|
|
resData.port = dstOptions.proxyPort;
|
|
dstOptions.host = dstOptions.hostname;
|
|
util.setClientId(
|
|
headers,
|
|
req.enable,
|
|
req.disable,
|
|
req.clientIp,
|
|
isInternalProxy
|
|
);
|
|
util.deleteReqHeaders(req);
|
|
var _headers = extend({}, headers);
|
|
_headers.host =
|
|
dstOptions.hostname + ':' + (dstOptions.port || 443);
|
|
if (disable.proxyUA) {
|
|
delete _headers['user-agent'];
|
|
}
|
|
dstOptions.headers = formatHeaders(
|
|
_headers,
|
|
reqRawHeaderNames
|
|
);
|
|
if (isSocks) {
|
|
dstOptions.proxyPort = options.port || 1080;
|
|
dstOptions.localDNS = false;
|
|
dstOptions.auths = config.getAuths(options);
|
|
} else {
|
|
dstOptions.proxyPort = options.port || 80;
|
|
dstOptions.proxyAuth = options.auth;
|
|
if (isProxyPort || util.isLocalPHost(req, true)) {
|
|
_headers[config.WEBUI_HEAD] = 1;
|
|
}
|
|
_headers['x-whistle-request-tunnel-ack'] = 1;
|
|
}
|
|
var netMgr = isSocks ? socks : config;
|
|
var reqDelay = util.getMatcherValue(_rules.reqDelay);
|
|
util.setProxyHost(req, dstOptions, true);
|
|
if (isHttpsProxy) {
|
|
dstOptions.proxyServername = options.hostname;
|
|
}
|
|
var connectProxy = function () {
|
|
if (responsed) {
|
|
return;
|
|
}
|
|
if (req.isPluginReq) {
|
|
var host = ip + ':' + dstOptions.proxyPort;
|
|
if (req._phost && req._phost.host) {
|
|
IP_CACHE.set(
|
|
req.isPluginReq,
|
|
req._phost.host + ', ' + host
|
|
);
|
|
} else {
|
|
IP_CACHE.set(req.isPluginReq, host);
|
|
}
|
|
} else if (req._phost) {
|
|
resData.phost = req._phost.host;
|
|
}
|
|
resData.port = dstOptions.proxyPort;
|
|
req.serverPort = reqSocket.serverPort = resData.port;
|
|
try {
|
|
if (!tunnelPort && req._phost && req._proxyTunnel) {
|
|
dstOptions.proxyTunnelPath = util.removeProtocol(
|
|
tunnelUrl,
|
|
true
|
|
);
|
|
}
|
|
var s = netMgr.connect(dstOptions, handleProxy);
|
|
proxyClient = s._sock || s;
|
|
s.on('error', function (err) {
|
|
if (isXProxy) {
|
|
resData.phost = undefined;
|
|
tunnel();
|
|
} else {
|
|
emitError(err);
|
|
}
|
|
});
|
|
} catch (e) {
|
|
emitError(e);
|
|
}
|
|
};
|
|
if (reqDelay > 0) {
|
|
setTimeout(connectProxy, reqDelay);
|
|
} else {
|
|
connectProxy();
|
|
}
|
|
});
|
|
} else {
|
|
tunnel(hostIp, hostPort);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
);
|
|
}, req);
|
|
}
|
|
var retryConnect;
|
|
var retryXHost = 0;
|
|
function tunnel(hostIp, hostPort) {
|
|
getServerIp(
|
|
tunnelUrl,
|
|
function (ip, port) {
|
|
if (port) {
|
|
req.hostIp = resData.ip = util.getHostIp(ip, port);
|
|
resData.port = port;
|
|
} else {
|
|
req.hostIp = resData.ip = ip;
|
|
// 不要赋值给port,否则重试时getServerIp会有端口
|
|
resData.port = port || originPort;
|
|
}
|
|
if (responsed) {
|
|
return;
|
|
}
|
|
if (req.isPluginReq) {
|
|
IP_CACHE.set(req.isPluginReq, req.hostIp);
|
|
}
|
|
req.serverPort = reqSocket.serverPort = resData.port;
|
|
resSocket = net.connect(resData.port, ip, handleConnect);
|
|
if (retryConnect) {
|
|
handleError(resSocket);
|
|
} else {
|
|
retryConnect = function () {
|
|
if (
|
|
retryXHost < 2 &&
|
|
_rules.host &&
|
|
X_RE.test(_rules.host.matcher)
|
|
) {
|
|
++retryXHost;
|
|
retryConnect = false;
|
|
if (retryXHost > 1) {
|
|
req.curUrl = tunnelUrl;
|
|
rules.lookupHost(req, function (_err, _ip) {
|
|
if (_err) {
|
|
return emitError(_err);
|
|
}
|
|
tunnel(_ip, originPort);
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
tunnel(ip, port);
|
|
};
|
|
var retried;
|
|
resSocket.on('error', function () {
|
|
if (!retried) {
|
|
retried = true;
|
|
this.destroy && this.destroy();
|
|
!responsed &&
|
|
!reqSocket._hasError &&
|
|
retryConnect &&
|
|
retryConnect();
|
|
}
|
|
});
|
|
}
|
|
},
|
|
hostIp,
|
|
hostPort
|
|
);
|
|
}
|
|
|
|
function handleConnect() {
|
|
if (reqSocket._hasError) {
|
|
return emitError(socketErr);
|
|
}
|
|
if (retryConnect) {
|
|
resSocket.removeListener('error', retryConnect);
|
|
handleError(resSocket);
|
|
retryConnect = null;
|
|
}
|
|
reqSocket.inspectFrames = inspect;
|
|
reqSocket.fullUrl = tunnelUrl;
|
|
reqSocket.enable = req.enable;
|
|
reqSocket.disable = req.disable;
|
|
reqSocket._filters = req._filters;
|
|
reqSocket.hostIp = req.hostIp;
|
|
reqSocket.method = req.method;
|
|
reqSocket.headerRulesMgr = req.headerRulesMgr;
|
|
reqSocket.clientPort = req.clientPort;
|
|
reqSocket.globalValue = req.globalValue;
|
|
resSocket.statusCode = resData.statusCode;
|
|
pluginMgr.resolvePipePlugin(reqSocket, function () {
|
|
if (reqSocket._hasError) {
|
|
return emitError(socketErr);
|
|
}
|
|
data.pipe = reqSocket._pipeRule;
|
|
if (reqSocket._pipePluginPorts) {
|
|
reqSocket.inspectFrames = data.inspect = true;
|
|
reqSocket.customParser = false;
|
|
}
|
|
var connHandler = function () {
|
|
if (buf) {
|
|
var _pipe = reqSocket.pipe;
|
|
reqSocket.pipe = function (stream) {
|
|
if (buf) {
|
|
stream.write(buf);
|
|
buf = null;
|
|
}
|
|
return _pipe.apply(this, arguments);
|
|
};
|
|
}
|
|
socketMgr.handleConnect(reqSocket, resSocket);
|
|
};
|
|
var handleEstablished = function () {
|
|
if (useTunnelPolicy) {
|
|
sendEstablished(200, connHandler);
|
|
} else {
|
|
connHandler();
|
|
sendEstablished();
|
|
}
|
|
};
|
|
var resDelay = util.getMatcherValue(_rules.resDelay);
|
|
if (resDelay > 0) {
|
|
setTimeout(handleEstablished, resDelay);
|
|
} else {
|
|
handleEstablished();
|
|
}
|
|
});
|
|
}
|
|
|
|
function getServerIp(url, callback, hostIp, hostPort, proxyRule) {
|
|
var hostHandler = function (err, ip, port, host) {
|
|
if (host) {
|
|
(proxyRule || _rules).host = host;
|
|
}
|
|
data.requestTime = data.dnsTime = Date.now();
|
|
req.hostIp = resData.ip = ip || LOCALHOST;
|
|
reqEmitter.emit('send', data);
|
|
err ? emitError(err) : callback(ip, port);
|
|
};
|
|
if (hostIp) {
|
|
hostHandler(null, hostIp, hostPort);
|
|
} else {
|
|
req.curUrl = url;
|
|
rules.resolveHost(
|
|
req,
|
|
hostHandler,
|
|
req.pluginRules,
|
|
req.rulesFileMgr,
|
|
req.headerRulesMgr
|
|
);
|
|
}
|
|
}
|
|
|
|
function handleError(socket) {
|
|
socket.on('error', emitError);
|
|
}
|
|
|
|
function sendEstablished(code, cb) {
|
|
if (res) {
|
|
code = res.statusCode || 200;
|
|
if (!res.headers['proxy-agent']) {
|
|
res.headers['proxy-agent'] = config.name;
|
|
res.rawHeaders = res.rawHeaders || [];
|
|
res.rawHeaders.push('proxy-agent', 'Proxy-Agent');
|
|
}
|
|
} else {
|
|
code = code || 200;
|
|
res = {
|
|
statusCode: code,
|
|
headers: {
|
|
'proxy-agent': config.name
|
|
},
|
|
rawHeaders: ['proxy-agent', 'Proxy-Agent']
|
|
};
|
|
}
|
|
var tunnelAck = enableTunnelAck && cb && code == 200;
|
|
if (tunnelAck) {
|
|
res.headers['x-whistle-allow-tunnel-ack'] = 1;
|
|
}
|
|
var resHeaders = res.headers;
|
|
pluginMgr.getResRules(req, res, function () {
|
|
var reqRules = req.rules;
|
|
util.parseRuleJson(
|
|
rollBackTunnel ? null : reqRules.resHeaders,
|
|
function (newResHeaders) {
|
|
if (rollBackTunnel) {
|
|
reqSocket.resume();
|
|
cb && cb();
|
|
} else {
|
|
var rawHeaderNames = getRawHeaderNames(res.rawHeaders) || {};
|
|
if (newResHeaders) {
|
|
util.lowerCaseify(newResHeaders, rawHeaderNames);
|
|
extend(resHeaders, newResHeaders);
|
|
}
|
|
var responseFor = util.getMatcherValue(reqRules.responseFor);
|
|
if (responseFor) {
|
|
resHeaders['x-whistle-response-for'] = responseFor;
|
|
}
|
|
util.setResponseFor(reqRules, resHeaders, req, req.hostIp, req._phost);
|
|
var delResHeaders =
|
|
util.parseDelProps(req).resHeaders;
|
|
Object.keys(delResHeaders).forEach(function (name) {
|
|
delete resHeaders[name];
|
|
});
|
|
code = util.getMatcherValue(reqRules.replaceStatus) || code;
|
|
var message =
|
|
code == 200
|
|
? 'Connection Established'
|
|
: STATUS_CODES[code] || 'unknown';
|
|
var statusLine = ['HTTP/1.1', code, message].join(' ');
|
|
var curHeaders = resHeaders;
|
|
if (req.fromComposer) {
|
|
curHeaders = extend({}, resHeaders);
|
|
curHeaders['x-whistle-req-id'] = req.reqId;
|
|
}
|
|
var rawData = [
|
|
statusLine,
|
|
getRawHeaders(formatHeaders(curHeaders, rawHeaderNames)),
|
|
'\r\n'
|
|
].join('\r\n');
|
|
try {
|
|
if (code != 200) {
|
|
reqSocket.end(rawData, cb);
|
|
} else {
|
|
if (tunnelAck) {
|
|
reqSocket.write(rawData);
|
|
reqSocket.once('data', function (chunk) {
|
|
buf = chunk.length > 1 ? chunk.slice(1) : null;
|
|
reqSocket.pause();
|
|
cb();
|
|
});
|
|
} else {
|
|
reqSocket.write(
|
|
rawData,
|
|
cb &&
|
|
function () {
|
|
setTimeout(cb, 16);
|
|
}
|
|
);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
reqSocket.emit('error', e);
|
|
}
|
|
}
|
|
if (reqEmitter) {
|
|
responsed = true;
|
|
data.responseTime = data.endTime = Date.now();
|
|
resData.rawHeaderNames = rawHeaderNames;
|
|
resData.statusCode = code || 200;
|
|
resData.ip = resData.ip || LOCALHOST;
|
|
resData.headers = resHeaders;
|
|
reqEmitter.emit('response', data);
|
|
reqEmitter.emit('end', data);
|
|
}
|
|
pluginMgr.postStats(req, res);
|
|
},
|
|
req
|
|
);
|
|
});
|
|
return reqSocket;
|
|
}
|
|
var reqDestroyed, resDestroyed;
|
|
function emitError(err) {
|
|
if (!reqDestroyed) {
|
|
reqDestroyed = true;
|
|
reqSocket.destroy();
|
|
}
|
|
if (resSocket && !resDestroyed) {
|
|
resDestroyed = true;
|
|
resSocket.destroy();
|
|
}
|
|
if (proxyClient) {
|
|
proxyClient.destroy && proxyClient.destroy();
|
|
proxyClient = null;
|
|
}
|
|
if (responsed) {
|
|
return;
|
|
}
|
|
responsed = true;
|
|
if (!reqEmitter) {
|
|
return;
|
|
}
|
|
data.responseTime = data.endTime = Date.now();
|
|
|
|
if (!resData.ip) {
|
|
req.hostIp = resData.ip = LOCALHOST;
|
|
}
|
|
|
|
if (!err && !resData.statusCode) {
|
|
emitAborted(data, reqEmitter);
|
|
} else {
|
|
data.resError = true;
|
|
resData.statusCode = resData.statusCode || 502;
|
|
resData.body = err ? util.getErrorStack(err) : 'Aborted';
|
|
util.emitError(reqEmitter, data);
|
|
}
|
|
resData.headers = { 'x-server': 'whistle' };
|
|
pluginMgr.postStats(req, resData);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
return server;
|
|
}
|
|
|
|
module.exports = tunnelProxy;
|