UNPKG

whistle

Version:

HTTP, HTTP2, HTTPS, Websocket debugging proxy

286 lines (279 loc) 8.88 kB
var express = require('express'); var http = require('http'); var https = require('https'); var socks = require('sockx'); var extend = require('extend'); var EventEmitter = require('events'); var util = require('./util'); var logger = require('./util/logger'); var rules = require('./rules'); var setupHttps = require('./https').setup; var httpsUtil = require('./https/ca'); var rulesUtil = require('./rules/util'); var initDataServer = require('./util/data-server'); var pluginMgr = require('./plugins'); var config = require('./config'); var loadService = require('./service'); var initSocketMgr = require('./socket-mgr'); var tunnelProxy = require('./tunnel'); var upgradeProxy = require('./upgrade'); var proc = require('./util/process'); var perf = require('./util/perf'); var loadCert = require('./https/load-cert'); var common = require('./util/common'); function handleClientError(err, socket) { if (!socket.writable) { return socket.destroy(err); } var errCode = err && err.code; var statusCode = errCode === 'HPE_HEADER_OVERFLOW' ? 431 : 400; var stack = util.getErrorStack( 'clientError: Bad request' + (errCode ? ' (' + errCode + ')' : '') ); socket.end('HTTP/1.1 ' + statusCode + ' Bad Request\r\n\r\n' + stack); } function proxy(callback, _server) { var app = express(); var server = _server || http.createServer(); var proxyEvents = new EventEmitter(); var middlewares = ['./init', '../biz'] .concat(require('./inspectors')) .concat(config.middlewares) .concat(require('./handlers')); server.timeout = config.timeout; server.requestTimeout = 0; proxyEvents.config = config; proxyEvents.server = server; app.disable('x-powered-by'); app.logger = logger; middlewares.forEach(function (mw) { mw && app.use((typeof mw == 'string' ? require(mw) : mw).bind(proxyEvents)); }); server.on('clientError', handleClientError); pluginMgr.setProxy(proxyEvents); perf.setProxy(proxyEvents); initSocketMgr(proxyEvents); setupHttps(server, proxyEvents); exportInterfaces(proxyEvents); tunnelProxy(server, proxyEvents); upgradeProxy(server); initDataServer(proxyEvents); rulesUtil.setup(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(); var count = _server ? 1 : 2; var execCallback = function () { if (--count === 0) { process.whistleStarted = true; process.emit('whistleStarted', proxyEvents); typeof callback === 'function' && callback.call(server, proxyEvents); } }; !_server && util.getBoundIp(config.host, function (host) { util.checkPort(!config.INADDR_ANY && !host && config.port, function () { config.host = host; server.listen(config.port, host, execCallback); }); }); var createNormalServer = function (port, httpModule, opts) { if (!port) { return; } ++count; var optionServer = httpModule.createServer(opts); var isHttps = !!opts; proxyEvents[isHttps ? 'httpsServer' : 'httpServer'] = optionServer; optionServer.timeout = config.timeout; optionServer.requestTimeout = 0; optionServer.on('request', function (req, res) { req.isHttps = isHttps; app.handle(req, res); }); optionServer.isHttps = isHttps; tunnelProxy(optionServer, proxyEvents, isHttps ? 1 : 2); upgradeProxy(optionServer); optionServer.on('clientError', handleClientError); util.getBoundIp( config[isHttps ? 'httpsHost' : 'httpHost'], function (host) { util.checkPort(!config.INADDR_ANY && !host && port, function () { optionServer.listen(port, host, execCallback); }); } ); }; createNormalServer(config.httpPort, http); createNormalServer( config.httpsPort, https, extend( { SNICallback: function (servername, callback) { var curUrl = 'https://' + servername; loadCert( { isHttpsServer: true, fullUrl: curUrl, curUrl: curUrl, useSNI: true, headers: {}, servername: servername, serverName: servername, commonName: httpsUtil.getDomain(servername) }, function () { httpsUtil.SNICallback(servername, callback); } ); } }, httpsUtil.createCertificate('*.wproxy.org') ) ); if (config.socksPort) { ++count; var boundHost; var socksServer = socks.createServer(function (info, accept, deny) { var dstPort = info.dstPort; var dstAddr = info.dstAddr; var connPath = util.joinIpPort(dstAddr, dstPort); var headers = { host: connPath }; var clientIp = util.removeIPV6Prefix(info.srcAddr) || '127.0.0.1'; var clientPort = info.srcPort; headers['x-whistle-server'] = 'socks'; headers[config.CLIENT_INFO_HEADER] = [clientIp, clientPort, clientIp, clientPort].join(); if (config.socksMode) { headers[config.WHISTLE_POLICY_HEADER] = 'weakTunnel'; } var client = http.request({ method: 'CONNECT', agent: false, path: connPath, host: boundHost, port: config.port, headers: headers }); var destroy = function () { if (client) { client.abort(); client = null; deny(); } }; client.on('error', destroy); client.on('connect', function (res, socket) { socket.on('error', destroy); if (res.statusCode != 200) { return destroy(); } var reqSock = accept(true); if (reqSock) { reqSock.pipe(socket).pipe(reqSock); } else { destroy(); } }); client.end(); }); proxyEvents.socksServer = socksServer; util.getBoundIp(config.socksHost, function (host) { boundHost = host || '127.0.0.1'; util.checkPort( !config.INADDR_ANY && !host && config.socksPort, function () { socksServer.listen(config.socksPort, host, execCallback); } ); socksServer.useAuth(socks.auth.None()); }); } require('../biz/init')(proxyEvents, function () { server.on('request', app); execCallback(); }); return proxyEvents; } function exportInterfaces(obj) { obj.getWhistlePath = common.getWhistlePath; obj.rules = rules; obj.util = util; obj.rulesUtil = rulesUtil; obj.rulesMgr = rules; obj.httpsUtil = httpsUtil; obj.pluginMgr = pluginMgr; obj.logger = logger; obj.loadService = loadService; obj.setAuth = config.setAuth; obj.setUIHost = config.setUIHost; obj.setPluginUIHost = config.setPluginUIHost; obj.socketMgr = initSocketMgr; obj.getRuntimeInfo = function () { return proc; }; obj.getShadowRules = function () { return config.shadowRules; }; obj.setShadowRules = function (shadowRules) { if (typeof shadowRules === 'string') { config.shadowRules = shadowRules; rulesUtil.parseRules(); } }; return obj; } var HTTP2_ERR_RE = /^ERR_HTTP2_/; function handleGlobalException(err) { var code = err && err.code; if ( !config.diagnose && (code === 'EPIPE' || HTTP2_ERR_RE.test(code) || code === 'ENETUNREACH' || code === 'ERR_HTTP_TRAILER_INVALID' || code === 'ERR_INTERNAL_ASSERTION' || code === 'ERR_INVALID_ARG_TYPE' || (err && /finishwrite|'_hadError'/i.test(err.message))) ) { return; } if ( !err || (code !== 'ERR_IPC_CHANNEL_CLOSED' && code !== 'ERR_IPC_DISCONNECTED') ) { var stack = util.getErrorStack(err); common.writeLogSync('\r\n' + stack + '\r\n'); /*eslint no-console: "off"*/ console.error(stack); if ( typeof process.handleUncauthtWhistleErrorMessage === 'function' && process.handleUncauthtWhistleErrorMessage(stack, err) === false ) { return; } } setTimeout(function () { process.exit(1); }, 360); } process.on('unhandledRejection', handleGlobalException); process.on('uncaughtException', handleGlobalException); rulesUtil.setPluginMgr(pluginMgr); rulesUtil.parseRules(); module.exports = exportInterfaces(proxy);