repo_description: HTTP, HTTP2, HTTPS, Websocket debugging proxy
repo_topics: charles debug fiddler hosts node nodejs proxy web weinre
code: 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;
  };
  