UNPKG

whistle

Version:

HTTP, HTTPS, Websocket debugging proxy

345 lines (322 loc) 12.7 kB
var express = require('express'); var url = require('url'); var net = require('net'); var path = require('path'); var tls = require('tls'); var fs = require('fs'); var socks = require('socksv5'); var EventEmitter = require('events').EventEmitter; var extend = require('util')._extend; var util = require('./util'); var logger = require('./util/logger'); var rules = require('./rules'); var dispatch = require('./https'); var httpsUtil = require('./https/util'); var rulesUtil = require('./rules/util'); var initDataServer = require('./util/data-server'); var initLogServer = require('./util/log-server'); var pluginMgr = require('./plugins'); var config = require('./config'); var loadService = require('./service'); var LOCALHOST = '127.0.0.1'; var TUNNEL_HOST_RE = /^[^:\/]+\.[^:\/]+:\d+$/; var STATUS_CODES = require('http').STATUS_CODES || {}; var index = 0; function tunnelProxy(server, proxy) { //see: https://github.com/joyent/node/issues/9272 if (typeof tls.checkServerIdentity == 'function') { var checkServerIdentity = tls.checkServerIdentity; tls.checkServerIdentity = function() { try { return checkServerIdentity.apply(this, arguments); } catch(err) { logger.error(err); return err; } }; } process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0'; server.on('connect', function(req, reqSocket, head) {//ws, wss, https proxy var tunnelUrl = req.fullUrl = util.setProtocol(TUNNEL_HOST_RE.test(req.url) ? req.url : req.headers.host, true); var options; var parseUrl = function (_url, port) { _url = _url || tunnelUrl; options = req.options = url.parse(_url); options.port = options.port || port || 443; return options; }; parseUrl(); tunnelUrl = req.fullUrl = 'tunnel://' + options.host; proxy.emit('_request', tunnelUrl); var resSocket, proxySocket, responsed, reqEmitter, data, rulesMgr; req.isTunnel = true; req.clientIp = util.getClientIp(req) || LOCALHOST; req.reqId = ++index; var hostname = options.hostname; var policy = req.headers[config.WHISTLE_POLICY_HEADER]; var useTunnelPolicy = policy == 'tunnel'; var isLocalUIUrl = !useTunnelPolicy && (config.isLocalUIUrl(hostname) || config.isPluginUrl(hostname)); var _rules = req.rules = isLocalUIUrl ? {} : rules.resolveRules(tunnelUrl); rules.resolveFileRules(req, function() { var plugin = pluginMgr.resolveWhistlePlugins(req); pluginMgr.getTunnelRules(req, function(_rulesMgr) { if (_rulesMgr) { rulesMgr = _rulesMgr; util.mergeRules(req, rulesMgr.resolveRules(tunnelUrl)); plugin = pluginMgr.getPluginByRuleUrl(util.rule.getUrl(_rules.rule)); } var filter = req.filter; var disable = req.disable; abortIfUnavailable(reqSocket); if (req.headers[config.WEBUI_HEAD]) { return reqSocket.destroy(); } if (!useTunnelPolicy && (policy === 'intercept' || isLocalUIUrl || ((filter.https || filter.intercept || rulesUtil.properties.get('interceptHttpsConnects')) && !disable.intercept && !disable.https))) { dispatch(reqSocket, hostname, proxy, function(_resSocket) { resSocket = _resSocket; }); sendEstablished(); return; } pluginMgr.postStats(req); reqEmitter = new EventEmitter(); var headers = req.headers; var reqData = { ip: req.clientIp, method: util.toUpperCase(req.method) || 'CONNECT', httpVersion: req.httpVersion || '1.1', headers: headers }; var resData = {headers: {}}; data = reqEmitter.data = { url: options.host, startTime: Date.now(), rules: _rules, req: reqData, res: resData, isHttps: true }; if (!filter.hide) { proxy.emit('request', reqEmitter); } if (disable.tunnel) { return reqSocket.destroy(); } var statusCode = util.getMatcherValue(_rules.statusCode); if (statusCode) { return sendEstablished(statusCode); } pluginMgr.loadPlugin(plugin, function() { var tunnelPort, proxyUrl; if(plugin) { tunnelPort = plugin.ports && plugin.ports.tunnelPort; if (!tunnelPort) { return emitError(new Error('No plugin.tunnelServer')); } proxyUrl = 'proxy://127.0.0.1:' + tunnelPort; pluginMgr.addRuleHeaders(req, _rules); } var realUrl = _rules.rule && _rules.rule.url.replace('https:', 'tunnel:'); if (/^tunnel:\/\//.test(realUrl) && realUrl != tunnelUrl) { tunnelUrl = realUrl; data.realUrl = realUrl.replace('tunnel://', ''); parseUrl(); } rules.getProxy(tunnelUrl, proxyUrl ? null : req, function(err, hostIp, hostPort) { if (!proxyUrl) { proxyUrl = _rules.proxy ? _rules.proxy.matcher : null; } if (proxyUrl) { var isSocks = /^socks:\/\//.test(proxyUrl); var _url = 'http:' + util.removeProtocol(proxyUrl); data.proxy = true; resolveHost(_url, function(ip) { options = parseUrl(_url, isSocks ? 1080 : 80); var isProxyPort = options.port == config.port; if (isProxyPort && util.isLocalAddress(ip)) { return emitError(new Error('Unable to proxy to itself')); } var onConnect = function(_proxySocket) { proxySocket = _proxySocket; abortIfUnavailable(proxySocket); var transform = util.getEventTransform(proxy, 'tunnelRequest', tunnelUrl); if (transform) { reqSocket.pipe(transform).pipe(proxySocket); transform = util.getEventTransform(proxy, 'tunnelRequest', tunnelUrl); proxySocket.pipe(transform).pipe(reqSocket); } else { reqSocket.pipe(proxySocket).pipe(reqSocket); } sendEstablished(); }; var dstOptions = url.parse(tunnelUrl); dstOptions.proxyHost = ip; dstOptions.proxyPort = parseInt(options.port, 10); dstOptions.port = dstOptions.port || 443; dstOptions.host = dstOptions.hostname; var _headers = extend({}, headers); pluginMgr.addRuleHeaders(req, _rules, _headers); _headers.host = dstOptions.hostname + ':' + (dstOptions.port || 443); dstOptions.headers = _headers; if (isSocks) { dstOptions.proxyPort = options.port || 1080; dstOptions.localDNS = false; dstOptions.auths = config.getAuths(options); socks.connect(dstOptions, onConnect).on('error', emitError); } else { dstOptions.proxyPort = options.port || 80; dstOptions.proxyAuth = options.auth; if (isProxyPort) { _headers[config.WEBUI_HEAD] = 1; } config.connect(dstOptions, onConnect).on('error', emitError); } }); } else { tunnel(hostIp, hostPort); } }); }); function tunnel(hostIp, hostPort) { resolveHost(tunnelUrl, function(ip, port) { resData.ip = port ? ip + ':' + port : ip; resSocket = net.connect(port || options.port, ip, function() { var transform = util.getEventTransform(proxy, 'tunnelRequest', tunnelUrl); if (transform) { resSocket.pipe(transform).pipe(reqSocket); transform = util.getEventTransform(proxy, 'tunnelRequest', tunnelUrl); reqSocket.pipe(transform).pipe(resSocket); } else { resSocket.pipe(reqSocket).pipe(resSocket); } sendEstablished(); }); abortIfUnavailable(resSocket); }, hostIp, hostPort); } function resolveHost(url, callback, hostIp, hostPort) { var hostHandler = function(err, ip, port, host) { if (host) { _rules.host = host; } data.requestTime = data.dnsTime = Date.now(); resData.ip = ip || LOCALHOST; reqEmitter.emit('send', data); err ? emitError(err) : callback(ip, port); }; if (hostIp) { hostHandler(null, hostIp, hostPort); } else { rules.resolveHost(url, hostHandler, rulesMgr, req.rulesFileMgr); } } function abortIfUnavailable(socket) { socket.on('error', emitError).on('close', emitError); } function sendEstablished(code) { if (code) { reqSocket.end('HTTP/1.1 ' + code + ' ' + (STATUS_CODES[code] || 'unknown') + '\r\nProxy-Agent: ' + config.name + '\r\n\r\n'); } else { reqSocket.write('HTTP/1.1 200 Connection Established\r\nProxy-Agent: ' + config.name + '\r\n\r\n'); } if (reqEmitter) { responsed = true; data.responseTime = data.endTime = Date.now(); resData.statusCode = code || 200; reqEmitter.emit('response', data); reqEmitter.emit('end', data); } return reqSocket; } function emitError(err) { if (responsed) { return; } responsed = true; resSocket && resSocket.destroy(); proxySocket && proxySocket.destroy(); reqSocket.destroy(); if (!reqEmitter) { return; } data.responseTime = data.endTime = Date.now(); if (!resData.ip) { resData.ip = LOCALHOST; } if (!err) { err = new Error('Aborted'); data.reqError = true; resData.statusCode ='aborted'; reqData.body = util.getErrorStack(err); reqEmitter.emit('abort', data); } else { data.resError = true; resData.statusCode = resData.statusCode || 502; resData.body = util.getErrorStack(err); util.emitError(reqEmitter, data); } } }); }); }); return server; } function proxy(callback) { var app = express(); var server = app.listen(config.port, callback); var proxyEvents = new EventEmitter(); var middlewares = ['./init', '../biz'] .concat(require('./inspectors')) .concat(config.middlewares) .concat(require('./handlers')); proxyEvents.config = config; app.logger = logger; middlewares.forEach(function(mw) { mw && app.use((typeof mw == 'string' ? require(mw) : mw).bind(proxyEvents)); }); exportInterfaces(proxyEvents); tunnelProxy(server, proxyEvents); initDataServer(proxyEvents); initLogServer(proxyEvents); require('../biz/init')(proxyEvents); var properties = rulesUtil.properties; if (config.disableAllRules) { properties.set('disabledAllRules', true); } else if (config.disableAllRules === false) { properties.set('disabledAllRules', false); } if (config.disableAllPlugins) { properties.set('disabledAllPlugins', true); } else if (config.disableAllPlugins === false) { properties.set('disabledAllPlugins', false); } if (config.allowMultipleChoice) { properties.set('allowMultipleChoice', true); } else if (config.allowMultipleChoice === false) { properties.set('allowMultipleChoice', false); } rulesUtil.addValues(config.values, config.replaceExistValue); rulesUtil.addRules(config.rules, config.replaceExistRule); config.debug && rules.disableDnsCache(); return proxyEvents; } function exportInterfaces(obj) { obj.rules = rules; obj.util = util; obj.rulesUtil = rulesUtil; obj.httpsUtil = httpsUtil; obj.pluginMgr = pluginMgr; obj.logger = logger; obj.loadService = loadService; obj.setAuth = config.setAuth; return obj; } process.on('uncaughtException', function(err){ var stack = util.getErrorStack(err); fs.writeFileSync(path.join(process.cwd(), config.name + '.log'), '\r\n' + stack + '\r\n', {flag: 'a'}); /*eslint no-console: "off"*/ console.error(stack); process.exit(1); }); module.exports = exportInterfaces(proxy);