UNPKG

whistle

Version:

HTTP, HTTP2, HTTPS, Websocket debugging proxy

1,327 lines (1,289 loc) 44 kB
var net = require('net'); var tls = require('tls'); var http = require('http'); var parseUrl = require('../util/parse-url-safe'); var socks = require('sockx'); var crypto = require('crypto'); var EventEmitter = require('events').EventEmitter; var LRU = require('lru-cache'); var checkSNI = require('sni'); var util = require('../util'); var extend = require('extend'); var config = require('../config'); var rules = require('../rules'); var pluginMgr = require('../plugins'); var socketMgr = require('../socket-mgr'); var hparser = require('hparser'); var properties = require('../rules/util').properties; var ca = require('./ca'); var loadCert = require('./load-cert'); var h2Consts = config.enableH2 ? require('http2').constants : {}; var STATUS_CODES = http.STATUS_CODES || {}; var getRawHeaders = hparser.getRawHeaders; var getRawHeaderNames = hparser.getRawHeaderNames; var formatHeaders = hparser.formatHeaders; var parseReq = hparser.parse; var getDomain = ca.getDomain; var serverAgent = ca.serverAgent; var getSNIServer = ca.getSNIServer; var getHttp2Server = ca.getHttp2Server; var LOCALHOST = '127.0.0.1'; var tunnelTmplData = new LRU({ max: 3000, maxAge: 30000 }); var uaCache = new LRU({ max: 1024 }); var TIMEOUT = 12000; var CONN_TIMEOUT = 60000; var X_RE = /^x/; var PROTO_SEP_RE = /,\s*/; var HTTP_PROTO_RE = /^(ws|http)s?:\/\//; var clientIpKey = config.CLIENT_IP_HEADER; var proxy, server; function handleWebsocket(socket, clientIp, clientPort) { var wss = socket.isHttps; clientIp = util.removeIPV6Prefix( clientIp || socket._remoteAddr || socket.remoteAddress ); socket.clientIp = clientIp; socket.method = 'GET'; socket.reqId = util.getReqId(); socket.clientPort = clientPort || socket._remotePort || socket.remotePort; rules.initHeaderRules(socket); pluginMgr.resolvePipePlugin(socket, function () { socket.rules = rules.initRules(socket); rules.resolveRulesFile(socket, function () { resolveWebsocket(socket, wss); }); }); } function setCaptureUA(ua) { if (ua && typeof ua === 'string' && ua.length < 1024) { uaCache.set(ua, 1); } } function isCaptureUA(ua) { return ua && uaCache.get(ua); } function getTransProto(str) { if (!str || typeof str !== 'string') { return; } if (str.indexOf(',') === -1) { return str; } return str.trim().split(PROTO_SEP_RE)[0]; } function handleFrames(socket, reqSocket, headers) { if (socket.enable.websocket || util.isWebSocket(headers)) { socketMgr.handleUpgrade(socket, reqSocket); } else { socketMgr.handleConnect(socket, reqSocket, true); } } function resolveWebsocket(socket, wss) { var headers = socket.headers; var reqEmitter = new EventEmitter(); var fullUrl = socket.fullUrl; var _rules = socket.rules; var clientIp = socket.clientIp; var filter = socket._filters; var now = Date.now(); var reqData = { ip: clientIp, port: socket.clientPort, method: 'GET', httpVersion: socket.httpVersion || '1.1', headers: headers, rawHeaderNames: socket.rawHeaderNames }; var resData = {}; var data = { _clientId: socket._clientId, id: socket.reqId, fc: socket.fromComposer ? 1 : undefined, url: fullUrl, startTime: now, sniPlugin: socket.sniPlugin, fwdHost: socket._fwdHost, rules: _rules, req: reqData, res: resData, pipe: socket._pipeRule, rulesHeaders: socket.rulesHeaders }; var reqSocket, options, isXProxy, isInternalProxy; var origProto, done, proxyUrl, clientKey, clientCert, isPfx, curStatus; var timeout = setTimeout(function () { destroy(util.TIMEOUT_ERR); }, CONN_TIMEOUT); var handleResponse = function () { var svrRes = util.getStatusCodeFromRule(_rules); if (!svrRes) { return; } var statusCode = svrRes.statusCode; var status = util.getStatusCode(statusCode); var resHeaders = resData.headers = svrRes.headers; var body = ''; var statusMsg; util.deleteReqHeaders(socket); resData.body = body; resData.ip = resData.ip || LOCALHOST; data.requestTime = data.dnsTime = Date.now(); getResRules(socket, resData, function () { status = curStatus || status; util.addMatchedRules(socket, resData); var isSuccess = status == 101; if (isSuccess) { statusMsg = 'HTTP/1.1 101 Switching Protocols'; var key = headers['sec-websocket-key']; var protocol = getTransProto(headers['sec-websocket-protocol']); headers.upgrade = getTransProto(headers.upgrade) || 'websocket'; if (key) { key += '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; resHeaders['Sec-WebSocket-Accept'] = crypto.createHash('sha1').update(key, 'binary').digest('base64'); } if (protocol) { resHeaders['Sec-WebSocket-Protocol'] = protocol; } resHeaders.Upgrade = headers.upgrade ; resHeaders.Connection = 'Upgrade'; reqSocket = util.getEmptyRes(); reqSocket.headers = resHeaders; handleFrames(socket, reqSocket, headers); } else { if (!status) { status = 502; body = 'Invalid status code: ' + statusCode; resHeaders['Content-Type'] = 'text/html; charset=utf8'; resHeaders['Content-Length'] = Buffer.byteLength(body) + ''; } resHeaders.Connection = 'close'; statusMsg = 'HTTP/1.1 ' + status; } resData.statusCode = status; var curHeaders = resHeaders; if (socket.fromComposer) { curHeaders = extend({}, resHeaders); curHeaders['x-whistle-req-id'] = socket.reqId; util.setFramesMode(curHeaders, isSuccess); } var rawData = statusMsg + '\r\n' + getRawHeaders(curHeaders) + '\r\n\r\n' + body; var end = function () { if (util.needAbortRes(socket)) { socket.__statusCode = status; return destroy(); } data.responseTime = data.endTime = Date.now(); if (isSuccess) { socket.write(rawData); } else { socket.end(rawData); } execCallback(null, socket); }; var reqDelay = util.getMatcherValue(_rules.reqDelay) || 0; var resDelay = util.getMatcherValue(_rules.resDelay) || 0; var delay = reqDelay + resDelay; if (delay > 0) { clearTimeout(timeout); setTimeout(end, delay); } else { end(); } }); return true; }; var plugin = pluginMgr.resolveWhistlePlugins(socket); handleEnd(socket); pluginMgr.getRules(socket, function (rulesMgr) { if (rulesMgr) { socket.pluginRules = rulesMgr; socket.curUrl = fullUrl; util.mergeRules(socket, rulesMgr.resolveReqRules(socket)); util.filterWeakRule(socket); if (util.isIgnored(filter, 'rule')) { plugin = null; } else { plugin = pluginMgr.getPluginByRuleUrl(util.rule.getUrl(_rules.rule)); } } else { util.filterWeakRule(socket); } var ruleUrlValue = plugin ? null : util.rule.getUrl(_rules.rule); if (ruleUrlValue && fullUrl !== ruleUrlValue && HTTP_PROTO_RE.test(ruleUrlValue)) { if (RegExp.$1 === 'http') { ruleUrlValue = ruleUrlValue.replace('http', 'ws'); } data.realUrl = fullUrl = util.encodeNonLatin1Char(ruleUrlValue); } if (_rules.referer) { var referer = util.getMatcherValue(_rules.referer); headers.referer = referer; } if (_rules.ua) { var ua = util.getMatcherValue(_rules.ua); headers['user-agent'] = ua; } if (util.showPluginReq(socket) && !socket.isInternalUrl) { if (socket.isLogRequests !== false) { ++util.proc.wsRequests; ++util.proc.totalWsRequests; socket.isLogRequests = true; } if (!util.isHide(socket)) { data.abort = destroy; if (socket.isPluginReq) { data.isPR = 1; } proxy.emit('request', reqEmitter, data); } } setupSocket(function () { if (util.needAbortReq(socket)) { return destroy(); } if (handleResponse()) { return; } pluginMgr.loadPlugin( socket.isPluginReq ? null : plugin, function (err, ports) { if (plugin) { data.dnsTime = Date.now(); } if (err) { return execCallback(err); } var port = ports && ports.upgrade && ports.port; if (port) { options.isPlugin = true; options.port = port; data.realUrl = util.changePort(fullUrl, options.port); socket.customParser = util.getParserStatus(socket); pluginMgr.addSessionInfo(socket, _rules); socketMgr.setPending(socket); options.protocol = 'ws:'; socket.headers[config.PLUGIN_HOOK_NAME_HEADER] = config.PLUGIN_HOOKS.HTTP; connectServer(); return; } plugin = null; rules.getClientCert(socket, function (_key, _cert, _isPfx) { clientKey = _key; clientCert = _cert; isPfx = _isPfx; rules.getProxy(fullUrl, socket, function (err, hostIp, hostPort) { var proxyRule = _rules.proxy; var connectProxy; var send = function () { data.requestTime = Date.now(); if (connectProxy) { connectProxy(); } else { connectServer(hostIp, hostPort); } }; var connect = function () { var reqDelay = util.getMatcherValue(_rules.reqDelay); if (reqDelay > 0) { clearTimeout(timeout); setTimeout(function () { timeout = setTimeout(function () { destroy(util.TIMEOUT_ERR); }, CONN_TIMEOUT); send(); }, reqDelay); } else { send(); } }; proxyUrl = proxyRule ? util.rule.getMatcher(proxyRule) : null; if (proxyUrl) { isXProxy = X_RE.test(proxyUrl); var isSocks = proxyRule.isSocks; isInternalProxy = proxyRule.isInternal || util.isInternalProxy(socket); var isHttpsProxy = proxyRule.isHttps; if (!isInternalProxy && !wss && proxyRule.isHttp2https) { wss = true; } else { wss = options.protocol === 'wss:'; } proxyUrl = 'http:' + util.removeProtocol(proxyUrl); getServerIp( proxyUrl, function (ip) { var proxyOptions = parseUrl(proxyUrl); proxyOptions.auth = proxyOptions.auth || socket._pacAuth; hostPort = proxyOptions.port; hostIp = ip; var proxyPort = (proxyOptions.port = parseInt(hostPort, 10) || (isSocks ? 1080 : isHttpsProxy ? 443 : 80)); var isProxyPort = util.isProxyPort(proxyPort); if (isProxyPort && util.isLocalAddress(ip)) { return execCallback( new Error('Self loop (' + util.joinIpPort(ip, proxyPort) + ')') ); } options.proxyHost = ip; resData.port = options.proxyPort = proxyPort; socket.serverPort = resData.port; options.host = options.hostname; if (!options.port) { options.port = wss ? 443 : 80; } var proxyOpts, tlsOpts; var handleProxy = function (proxySocket) { if (wss) { var conf = { socket: proxySocket }; if (tlsOpts) { extend(conf, tlsOpts); } var opts = util.setRejectUnauthorized(socket, conf); if (!socket.disable.secureOptions) { util.setSecureOptions(opts); } if (!socket.disable.servername) { opts.servername = options.hostname; } util.setClientCert(opts, clientKey, clientCert, isPfx); var handleProxyError = function (err) { if ( connectProxy && !tlsOpts && util.isCiphersError(err) ) { tlsOpts = util.getTlsOptions(_rules); connectProxy(); } else if ( connectProxy && util.checkTlsError(err) && util.checkAuto2Http(socket, ip, proxyUrl) ) { wss = false; data.httpsTime = data.httpsTime || Date.now(); data.useHttp = true; if (proxyOpts && options.port == 443) { proxyOpts.port = options.port = 80; proxyOpts.headers.host = util.joinIpPort(proxyOpts.host, proxyOpts.port); } connectProxy(); } else { execCallback(err); } }; try { proxySocket = tls.connect(opts); proxySocket.on('error', handleProxyError); reqSocket = proxySocket; proxySocket.on('secureConnect', function () { handleEnd(reqSocket); pipeData(); }); } catch (e) { handleProxyError(e); } return; } reqSocket = proxySocket; handleEnd(reqSocket); pipeData(); }; // 对应 internal-proxy 要用直接请求,方便用来穿透 nginx if (isInternalProxy && !socket._phost) { if (wss) { headers[config.HTTPS_FIELD] = 1; options.protocol = null; origProto = 'wss:'; wss = false; } } else { if (isSocks) { options.localDNS = false; options.auths = config.getAuths(proxyOptions); } else { var proxyHeaders = (options.headers = {}); pluginMgr.getTunnelKeys().forEach(function (k) { var val = headers[k]; if (val) { proxyHeaders[k] = val; } }); var auth = headers['proxy-authorization']; if (auth) { proxyHeaders['proxy-authorization'] = auth; } if (socket.disable.proxyUA) { delete proxyHeaders['user-agent']; } else if (headers['user-agent']) { proxyHeaders['user-agent'] = headers['user-agent']; } if (wss && isInternalProxy) { headers[config.HTTPS_FIELD] = 1; options.protocol = null; origProto = 'wss:'; wss = false; } if (!util.isLocalAddress(clientIp)) { proxyHeaders[clientIpKey] = clientIp; } if (isProxyPort) { proxyHeaders[config.WEBUI_HEAD] = 1; } if (util.isLocalPHost(socket, wss)) { headers[config.WEBUI_HEAD] = 1; } var clientId = headers[config.CLIENT_ID_HEADER]; if (clientId) { proxyHeaders[config.CLIENT_ID_HEADER] = clientId; } util.checkIfAddInterceptPolicy(proxyHeaders, headers); util.setClientId( proxyHeaders, socket.enable, socket.disable, clientIp, isInternalProxy ); options.proxyAuth = proxyOptions.auth; } var netMgr = isSocks ? socks : config; proxyOpts = util.setProxyHost(socket, options); if (isHttpsProxy) { proxyOpts.proxyServername = proxyOptions.hostname; } connectProxy = function () { if (destroyed) { return; } if (socket._phost) { resData.phost = socket._phost.host; } proxyOpts.enableIntercept = true; proxyOpts.proxyTunnelPath = util.getProxyTunnelPath( socket, wss ); try { var s = netMgr.connect(proxyOpts, handleProxy); s.on( 'error', isXProxy ? function () { if (isXProxy) { isXProxy = false; resData.phost = undefined; if (isInternalProxy) { options.protocol = origProto; } connectServer(); } } : execCallback ); } catch (e) { execCallback(e); } }; } connect(); }, null, null, proxyRule ); } else { connect(); } }); }); } ); }); }); var retryConnect, auto2http, newIp; var retryXHost = 0; function connectServer(hostIp, hostPort, tlsOpts) { getServerIp( fullUrl, function (ip, port) { var isWss = options.protocol === 'wss:'; var _port = port; resData.port = port = port || options.port || (isWss ? 443 : 80); socket.serverPort = port; if (destroyed) { return; } if (util.isProxyPort(port) && util.isLocalAddress(ip)) { return execCallback(new Error('Self loop (' + util.joinIpPort(ip, port) + ')')); } // checkHandUpError, retry try { var opts = util.setRejectUnauthorized(socket, { host: ip, port: port }); if (!socket.disable.servername) { opts.servername = util.parseHost(headers.host)[0] || options.hostname; } isWss && util.setClientCert(opts, clientKey, clientCert, isPfx); if (tlsOpts) { extend(opts, tlsOpts); } if (!socket.disable.secureOptions) { util.setSecureOptions(opts); } reqSocket = (isWss ? tls : net).connect(opts, pipeData); } catch (e) { return execCallback(e); } if (retryConnect) { handleEnd(reqSocket); } else { retryConnect = function (e) { if (!newIp && (newIp = util.getLocalhostIP(e, socket, socket._w2hostname, socket.hostIp))) { ip = newIp; } else if ( retryXHost < 2 && ((_rules.host && X_RE.test(_rules.host.matcher)) || (isInternalProxy && isXProxy)) ) { ++retryXHost; retryConnect = false; if (retryXHost > 1) { socket.curUrl = fullUrl; rules.lookupHost(socket, function (err, _ip) { socket.hostIp = resData.ip = _ip || LOCALHOST; if (err) { return execCallback(err); } connectServer(_ip); }); return; } } else if (isWss && util.checkAuto2Http(socket, ip, proxyUrl)) { if (auto2http || util.checkTlsError(e)) { options.protocol = null; data.httpsTime = data.httpsTime || Date.now(); data.useHttp = true; } else { retryConnect = false; auto2http = true; } } connectServer(ip, _port); }; var retried; // 不要用once,防止多次触发error导致crash reqSocket.on('error', function (err) { if (retried) { return; } retried = true; this.destroy && this.destroy(); if (destroyed || !retryConnect) { return; } if (!tlsOpts && isWss && util.isCiphersError(err)) { connectServer(hostIp, hostPort, util.getTlsOptions(_rules)); } else { retryConnect(err); } }); } }, hostIp, hostPort ); } function pipeData() { var enable = socket.enable; var disable = socket.disable; if (retryConnect) { reqSocket.removeListener('error', retryConnect); handleEnd(reqSocket); retryConnect = null; } clearTimeout(timeout); var clientId = headers[config.CLIENT_ID_HEADER]; if (clientId) { if (!options.isPlugin && !socket._customClientId && !util.isKeepClientId(socket, proxyUrl)) { socket._origClientId = clientId; util.removeClientId(headers); } data.clientId = clientId; } else { util.setClientId(headers, enable, disable, clientIp, isInternalProxy); } if (disable.clientIp || disable.clientIP) { delete headers[clientIpKey]; } else { var forwardedFor = util.getMatcherValue(_rules.forwardedFor); if (net.isIP(forwardedFor)) { headers[clientIpKey] = forwardedFor; } else if (socket._customXFF) { headers[clientIpKey] = socket._customXFF; } else if ( (!isInternalProxy && !plugin && !socket.enableXFF && !enable.clientIp && !enable.clientIP) || util.isLocalAddress(clientIp) ) { delete headers[clientIpKey]; } else { headers[clientIpKey] = clientIp; } } util.deleteReqHeaders(socket); util.addMatchedRules(socket); reqSocket.write(socket.getBuffer(headers, options.path)); reqSocket.resume(); delete headers['x-whistle-frame-parser']; delete headers[config.HTTPS_FIELD]; _pipeData(); } function setupSocket(cb) { var authObj = util.getAuthByRules(_rules); var list = [ _rules.reqHeaders, _rules.reqCors, _rules.reqCookies, _rules.params, _rules.urlReplace, _rules.urlParams, authObj ? null : _rules.auth ]; util.parseRuleJson( list, function ( reqHeaders, reqCors, reqCookies, params, urlReplace, urlParams, auth ) { if (params && urlParams) { extend(params, urlParams); } else { params = params || urlParams; } var newUrl = params ? util.replaceUrlQueryString(fullUrl, params) : fullUrl; var dProps = util.parseDelQuery(socket); newUrl = util.parsePathReplace(newUrl, urlReplace, dProps.paths) || newUrl; newUrl = util.deleteQuery(newUrl, dProps.query, socket._delQueryString); if (newUrl !== fullUrl) { fullUrl = newUrl; socket._realUrl = newUrl; } options = util.parseUrl(fullUrl); socket._w2hostname = options.hostname; data.realUrl = fullUrl; var host = (headers.host = options.host); socket._origin = headers.origin; if (reqHeaders) { reqHeaders = util.lowerCaseify(reqHeaders, socket.rawHeaderNames); var xff = reqHeaders[clientIpKey]; if (net.isIP(xff)) { socket._customXFF = xff; } socket._customClientId = reqHeaders[config.CLIENT_ID_HEADER]; delete reqHeaders[clientIpKey]; extend(headers, reqHeaders); headers.host = headers.host || host; } auth = util.getAuthBasic(auth || authObj); if (auth) { headers['authorization'] = auth; } var reqRuleData = { headers: headers }; util.setReqCors(reqRuleData, reqCors); util.setReqCookies(reqRuleData, reqCookies, headers.cookie, socket); if (_rules.referer) { headers.referer = util.getMatcherValue(_rules.referer); } util.disableReqProps(socket); pluginMgr.postStats(socket); cb(); }, socket ); } function getResRules(socket, res, callback) { socket.statusCode = res.statusCode || ''; var curResHeaders = (socket.resHeaders = res.headers); pluginMgr.getResRules(socket, res, function () { util.parseRuleJson( [_rules.resHeaders, _rules.resCors, _rules.resCookies], function (resHeaders, cors, cookies) { if (resHeaders) { resHeaders = util.lowerCaseify(resHeaders, res.rawHeaderNames); extend(res.headers, resHeaders); } var origin = curResHeaders['access-control-allow-origin']; if (socket._origin && origin && origin !== '*') { curResHeaders['access-control-allow-origin'] = socket._origin; } util.setResCors(res, cors, socket); util.setResCookies(res, cookies, socket); util.setResponseFor(_rules, curResHeaders, socket, socket.hostIp, socket._phost); util.deleteResHeaders(socket, curResHeaders); util.disableResProps(socket, curResHeaders); curStatus = util.getMatcherValue(_rules.replaceStatus); callback(); }, socket ); }); } function _pipeData() { parseReq( reqSocket, function (err, res) { if (err) { return execCallback(err); } reqSocket.pause(); socket.statusCode = res.statusCode || ''; var curResHeaders = (reqSocket.headers = res.headers); socket.resHeaders = curResHeaders; getResRules(socket, res, function () { util.delay(util.getMatcherValue(_rules.resDelay), function () { if (util.needAbortRes(socket)) { socket.__statusCode = res.statusCode; return destroy(); } reqSocket.reqId = data.id; var code = curStatus || res.statusCode; var isSuccess = code == 101; socket.statusCode = res.statusCode = code; util.addMatchedRules(socket, res); var curHeaders = curResHeaders; if (socket.fromComposer) { curHeaders = extend({}, curResHeaders); curHeaders['x-whistle-req-id'] = reqSocket.reqId; util.setFramesMode(curHeaders, isSuccess); } if (isSuccess) { socket.write(res.getHeaders(curHeaders, curStatus)); if (res.bodyBuffer.length) { reqSocket.unshift(res.bodyBuffer); } handleFrames(socket, reqSocket, curResHeaders); resData.body = ''; } else { socket.write(res.getBuffer(curHeaders, curStatus)); reqSocket.resume(); resData.body = res.body; } resData.headers = curResHeaders; resData.rawHeaderNames = res.rawHeaderNames; resData.statusCode = code; reqEmitter.emit('response', data); execCallback(null, reqSocket); }); }); }, true ); } function getServerIp(url, callback, hostIp, hostPort, proxyRule) { if (plugin) { return callback(LOCALHOST); } var hostHandler = function (err, ip, port, hostRule) { if (err) { return execCallback(err); } if (hostRule) { (proxyRule || _rules).host = hostRule; } socket.hostIp = resData.ip = util.joinIpPort(ip, port); data.requestTime = data.dnsTime = Date.now(); reqEmitter.emit('send', data); callback(ip, port); }; if (hostIp) { hostHandler(null, hostIp, hostPort); } else { socket.curUrl = url; rules.resolveHost( socket, hostHandler, socket.pluginRules, socket.rulesFileMgr, socket.headerRulesMgr ); } } function handleEnd(socket) { return util.onSocketEnd(socket, destroy); } var destroyed, reqDestroyed, resDestroyed; function destroy(err) { if (!reqDestroyed) { reqDestroyed = true; socket.destroy(); } if (reqSocket && !resDestroyed) { resDestroyed = true; reqSocket.destroy(); } if (destroyed) { return; } destroyed = true; execCallback(err); } function execCallback(err, _socket) { if (done) { return; } done = true; data.dnsTime = data.dnsTime || Date.now(); clearTimeout(timeout); data.responseTime = data.endTime = Date.now(); socket.hostIp = resData.ip = resData.ip || LOCALHOST; if (!err && !_socket) { err = new Error('Aborted'); data.reqError = true; resData.statusCode = 'aborted' + (socket.__statusCode ? ' (' + socket.__statusCode + ')' : ''); reqData.body = util.getErrorStack(err); reqEmitter.emit('abort', data); } else if (err) { data.resError = true; resData.statusCode = resData.statusCode || 502; resData.body = util.getErrorStack(err); util.emitError(reqEmitter, data); destroy(err); } else { reqEmitter.emit('end', data); } if (socket.resHeaders) { resData.headers = socket.resHeaders; } else if (err) { resData.headers = { 'x-server': 'whistle' }; } pluginMgr.postStats(socket, _socket || resData); } } function getTunnelData(socket, clientIp, clientPort, isHttpH2) { var enable = socket.enable || ''; var disable = socket.disable || ''; var headers = socket.headers; var tunnelData = headers[util.TUNNEL_DATA_HEADER]; var tdKey = config.tdKey; if (tdKey && (!tunnelData || config.overTdKey)) { tunnelData = headers[tdKey] || tunnelData; } var tunnelHeaders; var tunnelKeys = pluginMgr.getTunnelKeys(); tunnelKeys.forEach(function (k) { var val = headers[k]; if (val) { tunnelHeaders = tunnelHeaders || {}; tunnelHeaders[k] = val; } }); return { id: socket.reqId, clientIp: clientIp, clientPort: clientPort, remoteAddr: socket._remoteAddr, remotePort: socket._remotePort, clientId: headers[config.CLIENT_ID_HEADER], proxyAuth: disable.tunnelAuthHeader ? undefined : headers['proxy-authorization'], tunnelData: tunnelData, headers: tunnelHeaders, tunnelFirst: enable.tunnelHeadersFirst && !disable.tunnelHeadersFirst, isHttpH2: isHttpH2, sniPlugin: socket.sniPlugin }; } function addReqInfo(req) { var socket = req.socket; var remoteData = socket._remoteDataInfo || tunnelTmplData.get(socket.remotePort + ':' + socket.localPort); var headers = req.headers; if (remoteData) { req.isHttpH2 = remoteData.isHttpH2; req.reqId = remoteData.id; socket._remoteDataInfo = remoteData; remoteData._connected = true; headers[config.CLIENT_INFO_HEADER] = (remoteData.clientIp || LOCALHOST) + ',' + remoteData.clientPort + ',' + remoteData.remoteAddr + ',' + remoteData.remotePort; util.setTunnelHeaders(headers, remoteData); } if (!req.isHttpH2) { headers[config.HTTPS_FIELD] = 1; setCaptureUA(req.headers['user-agent']); } } function getStatusMessage(obj) { return obj.statusMessage || STATUS_CODES[obj.code] || 'Unknown'; } function isIllegalcHeader(name, value) { switch (name) { case h2Consts.HTTP2_HEADER_CONNECTION: case h2Consts.HTTP2_HEADER_UPGRADE: case h2Consts.HTTP2_HEADER_HOST: case h2Consts.HTTP2_HEADER_HTTP2_SETTINGS: case h2Consts.HTTP2_HEADER_KEEP_ALIVE: case h2Consts.HTTP2_HEADER_PROXY_CONNECTION: case h2Consts.HTTP2_HEADER_TRANSFER_ENCODING: return true; case h2Consts.HTTP2_HEADER_TE: return value !== 'trailers'; default: return false; } } function formatRawHeaders(obj, isH2) { var headers = obj.headers; if (isH2) { var newHeaders = {}; Object.keys(headers).forEach(function (name) { var value = headers[name]; if (!isIllegalcHeader(name, value)) { newHeaders[name] = value; } }); return newHeaders; } var rawNames = Array.isArray(obj.rawHeaders) && getRawHeaderNames(obj.rawHeaders); return formatHeaders(headers, rawNames); } function addStreamEvents(stream, handleAbort) { if (stream) { stream.on('error', handleAbort); stream.on('aborted', handleAbort); stream.on('close', handleAbort); } } function toHttp1(req, res) { var isH2 = req.httpVersion == 2; var client; var handleAbort = function () { if (client) { client.abort(); res.destroy(); client = null; } }; addReqInfo(req); addStreamEvents(req.stream, handleAbort); req.on('error', handleAbort); res.on('error', handleAbort); res.once('close', handleAbort); var host = req.headers[':authority']; var headers = req.headers; if (host) { req.headers.host = host; var newHeaders = { host: host }; Object.keys(headers).forEach(function (name) { if (name[0] !== ':') { newHeaders[name] = headers[name]; } }); headers = newHeaders; } else { headers = formatRawHeaders(req); } var options = util.parseUrl(util.getFullUrl(req)); options.protocol = null; options.hostname = null; options.agent = false; options.host = config.host || LOCALHOST; options.port = config.port; options.headers = headers; if (isH2) { headers[config.ALPN_PROTOCOL_HEADER] = (req.isHttpH2 ? 'httpH2' : 'h2') + (req.reqId ? '.' + req.reqId : ''); } options.method = req.method; client = http.request(options); client.on('error', handleAbort); client.on('response', function (svrRes) { svrRes.on('error', handleAbort); svrRes.once('end', function () { var trailers = svrRes.trailers; if (!util.isEmptyObject(trailers)) { var rawHeaderNames = svrRes.rawTrailers ? getRawHeaderNames(svrRes.rawTrailers) : null; try { res.addTrailers(formatHeaders(trailers, rawHeaderNames)); } catch (e) {} } }); var addHeaders = svrRes.headers[util.ADDITIONAL_HEAD]; if (addHeaders) { delete svrRes.headers[util.ADDITIONAL_HEAD]; if (isH2 && res.stream && typeof res.stream.additionalHeaders === 'function' && (addHeaders = util.parseRawJson(addHeaders))) { try { res.stream.additionalHeaders(addHeaders); } catch (e) {} } } try { var code = svrRes.statusCode; res.writeHead( code, getStatusMessage(svrRes), formatRawHeaders(svrRes, isH2) ); var write = res.write; res.write = function (chunk) { return write.call(res, chunk, function (e) { e && handleAbort(); }); }; svrRes.pipe(res); res.flushHeaders && res.flushHeaders(); } catch (e) { handleAbort(); } }); req.pipe(client); } var handlers = { request: function (req, res) { addReqInfo(req); server.emit('request', req, res); }, upgrade: function (req, socket) { addReqInfo(req); server.emit('upgrade', req, socket); } }; var h2Handlers = config.enableH2 ? { request: toHttp1, upgrade: handlers.upgrade } : handlers; var HTTP_RE = /^(\w+)\s+(\S+)\s+HTTP\/1.\d$/im; var HTTP2_RE = /^PRI\s\*\s+HTTP\/2.0$/im; var CONNECT_RE = /^CONNECT$/i; function addClientInfo(socket, chunk, statusLine, clientIp, clientPort) { var len = Buffer.byteLength(statusLine); chunk = chunk.slice(len); statusLine += '\r\n' + config.CLIENT_INFO_HEADER + ': ' + clientIp + ',' + clientPort + ',' + socket._remoteAddr + ',' + socket._remotePort; var tunnelData = getTunnelData(socket, clientIp, clientPort); if (tunnelData) { statusLine += '\r\n' + util.TEMP_TUNNEL_DATA_HEADER + ': ' + encodeURIComponent(JSON.stringify(tunnelData)); } return Buffer.concat([Buffer.from(statusLine), chunk]); } module.exports = function (socket, next, isWebPort) { var reqSocket, reqDestroyed, resDestroyed; var headersStr; var enable = socket.enable || ''; var disable = socket.disable || ''; var isCaptureIp = function() { if (disable.captureIp || disable.captureIP) { return false; } if (enable.capture || enable.captureIp || enable.captureIP) { return true; } return isCaptureUA(socket.headers['user-agent']); }; var isEnable = function(p1) { return enable[p1] && !disable[p1]; }; var destroy = function (err) { if (reqSocket) { if (!resDestroyed) { resDestroyed = true; reqSocket.destroy(err); } } else if (!reqDestroyed) { reqDestroyed = true; socket.destroy(err); } }; util.onSocketEnd(socket, destroy); var clientIp = socket.clientIp; var clientPort = socket.clientPort; util.readOneChunk(socket, function (chunk) { headersStr = chunk && chunk.toString(); var isHttp = chunk && HTTP_RE.test(headersStr); var statusLine = isHttp && RegExp['$&']; if (isHttp && CONNECT_RE.test(RegExp.$1)) { chunk = addClientInfo(socket, chunk, statusLine, clientIp, clientPort); util.connect( { port: config.port, host: config.host || LOCALHOST }, function (err, s) { reqSocket = s; if (err || socket._hasError) { return destroy(err); } reqSocket.on('error', destroy); reqSocket.write(chunk); reqSocket.pipe(socket).pipe(reqSocket); socket.resume(); } ); return; } if (!chunk) { //没有数据 return isWebPort ? socket.destroy() : next(chunk); } if (isHttp) { if (isEnable('forHttps') || disable.captureHttp) { next(chunk); } else { socket.resume(); server.emit('connection', socket); socket.emit( 'data', addClientInfo(socket, chunk, statusLine, clientIp, clientPort) ); } } else if (isEnable('forHttp') || disable.captureHttps) { return next(chunk); } else { var isHttpH2 = HTTP2_RE.test(headersStr); if (!isHttpH2 && chunk[0] != 22) { return next(chunk); } var useSNI, domain, serverKey; var handleConnect = function (port) { var promise = !isHttpH2 && !useSNI && serverAgent.existsServer(serverKey); var httpsServer = promise && promise.cert && promise.server; if (httpsServer && httpsServer.setSecureContext) { var cert = serverAgent.createCertificate(domain); if ( cert.key !== promise.cert.key || cert.cert !== promise.cert.cert ) { try { cert._ctx = cert._ctx || ca.createSecureContext(cert); httpsServer.setSecureContext(cert._ctx); promise.cert = cert; } catch (e) {} } } util.connect( { port: port, host: LOCALHOST, localAddress: LOCALHOST }, function (err, s) { reqSocket = s; if (err || socket._hasError) { return destroy(err); } reqSocket.on('error', destroy); socket._w2TunnelKey = reqSocket.localPort + ':' + reqSocket.remotePort; tunnelTmplData.set(socket._w2TunnelKey, getTunnelData(socket, clientIp, clientPort, isHttpH2)); reqSocket.write(chunk); reqSocket.pipe(socket).pipe(reqSocket); socket.resume(); } ); }; var useNoSNIServer = function () { serverKey = requestCert ? ':' + domain : domain; serverAgent.createServer(serverKey, handlers, handleConnect, 0, 0); }; var handleRequest = function () { if (useSNI) { var disableH2 = !properties.isEnableHttp2(); if (disable.http2) { disableH2 = true; } else if (enable.http2) { disableH2 = false; } getSNIServer(h2Handlers, handleConnect, disableH2, requestCert); } else if (isHttpH2) { getHttp2Server(h2Handlers, handleConnect); } else { useNoSNIServer(); } }; if (isHttpH2) { return handleRequest(); } useSNI = checkSNI(chunk); var servername = useSNI || socket.tunnelHostname; if ( !servername || (useSNI ? disable.captureSNI : (disable.captureNoSNI || (net.isIP(servername) && !isCaptureIp()))) || (socket.useProxifier && !ca.existsCustomCert(servername)) ) { return next(chunk); } var requestCert = (enable.clientCert || enable.requestCert) && !disable.clientCert && !disable.requestCert; domain = getDomain(servername); socket.curUrl = socket.fullUrl = 'https://' + servername; socket.useSNI = useSNI; socket.serverName = socket.servername = servername; socket.commonName = domain; loadCert(socket, function (cert) { if (socket._hasError) { return destroy(); } if (cert === false) { return next(chunk); } if (cert) { domain = servername; } handleRequest(); }); } }, isWebPort ? 0 : TIMEOUT ); }; module.exports.setup = function (s, p) { server = s; proxy = p; }; module.exports.handleWebsocket = handleWebsocket; module.exports.getTunnelDataOnce = function(socket) { if (!socket._w2TunnelKey) { return; } var data = tunnelTmplData.peek(socket._w2TunnelKey); return data && !data._connected ? data : null; };