UNPKG

whistle

Version:

HTTP, HTTP2, HTTPS, Websocket debugging proxy

164 lines (153 loc) 4.61 kB
var EventEmitter = require('events').EventEmitter; var PassThrough = require('stream').PassThrough; var Socket = require('net').Socket; var http = require('http'); var https = require('https'); var hparser = require('hparser'); var httpRequest = http.request; var httpsRequest = https.request; var res = http.OutgoingMessage.prototype; var noop = function () {}; var INVALID_PATH_RE = /[^\u0021-\u00ff]/; var INVALID_PATH_RE_G = /[^\u0021-\u00ff]/g; var HOST_RE = /^host$/i; var STREAM_OPTS = { highWaterMark: 1 }; process.emitWarning = noop; //see: https://github.com/joyent/node/issues/9272 process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; var setHeader = res.setHeader; res.setHeader = function (field, val) { try { return setHeader.call(this, field, val); } catch (e) {} }; function listenerCount(emitter, eventName) { if (typeof emitter.listenerCount === 'function') { return emitter.listenerCount(eventName); } return EventEmitter.listenerCount(emitter, eventName); } exports.listenerCount = listenerCount; var proto = Socket.prototype; var destroy = proto.destroy; var on = proto.on; // 避免第三方模块没处理好异常导致程序crash proto.destroy = function (err) { if (this.destroyed) { return; } if (err && !listenerCount(this, 'error')) { this.on('error', noop); } return destroy.call(this, err); }; // 避免一些奇奇怪怪的异常,导致整个进程 crash // 如:Error: This socket has been ended by the other party var wrapOn = function () { var evt = arguments[0]; if (this.on === wrapOn) { this.on = on; } if (evt !== 'error' && !listenerCount(this, 'error')) { on.call(this, 'error', noop); } return on.apply(this, arguments); }; proto.on = wrapOn; function filterInvalidPath(options) { if (!options) { return options; } if (typeof options === 'string') { if (INVALID_PATH_RE.test(options)) { return options.replace(INVALID_PATH_RE_G, ''); } } else if (options.path && INVALID_PATH_RE.test(options.path)) { options.path = String(options.path).replace(INVALID_PATH_RE_G, ''); } return options; } function hackRequest(requestFn, self, args, isApply) { var client; try { client = requestFn[isApply ? 'apply' : 'call'](self, args); var end = client.end; client.end = function () { try { end.apply(this, arguments); } catch (e1) { client.emit('error', e1); } }; } catch (e2) { client = new PassThrough(STREAM_OPTS); process.nextTick(function () { client.emit('error', e2); }); } return client.on('error', noop); } https.request = function () { return hackRequest(httpsRequest, this, arguments, true); }; http.request = function (options) { var tunnelPath = options && options.method === 'CONNECT' && options.proxyTunnelPath; options = filterInvalidPath(options); if (!tunnelPath) { return hackRequest(httpRequest, this, arguments, true); } var client = hackRequest(httpRequest, this, options); var on = client.on; client.on = function (type, listener) { if (type !== 'connect') { return on.apply(this, arguments); } on.call(this, type, function (res, socket, head) { socket.on('error', noop); if (res.statusCode !== 200) { return listener.apply(this, arguments); } var headers = {}; var isHost; if (options.headers) { isHost = options.headers.Host; Object.keys(options.headers).forEach(function(key) { if (!HOST_RE.test(key)) { headers[key] = options.headers[key]; } }); } headers[isHost ? 'Host' : 'host'] = tunnelPath; if (options.enableIntercept) { headers['x-whistle-policy'] = 'intercept'; delete headers['X-Whistle-Policy']; } headers = hparser.getRawHeaders(headers); var rawData = 'CONNECT ' + tunnelPath + ' HTTP/1.1'; if (headers) { rawData += '\r\n' + headers; } rawData += '\r\n\r\n'; if (res.statusCode === 200 && res.headers['x-whistle-allow-tunnel-ack']) { rawData = '1' + rawData; } socket.write(rawData); hparser.parse( socket, function (err, _res) { if (err) { return client.emit('error', err); } res.statusCode = parseInt(_res.statusCode, 10); res.headers = _res.headers; !options.keepStreamResume && socket.pause(); listener.call(this, res, socket, head); }, true ); }); return this; }; return client; };