'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _http = require('http'); var _http2 = _interopRequireDefault(_http); var _tools = require('./tools'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } /* globals Buffer */ // TODO: please rename this class to something else than "Handler", it makes it look like the class inherits from HandlerBase, which it doesn't /** * Represents a connection from source client to an external proxy using HTTP CONNECT tunnel, allows TCP connection. */ var HandlerTunnelTcpChain = function () { function HandlerTunnelTcpChain(_ref) { var srcSocket = _ref.srcSocket, trgParsed = _ref.trgParsed, upstreamProxyUrlParsed = _ref.upstreamProxyUrlParsed, log = _ref.log; _classCallCheck(this, HandlerTunnelTcpChain); this.log = log; // Bind all event handlers to this instance this.bindHandlersToThis(['onSrcSocketClose', 'onSrcSocketEnd', 'onSrcSocketError', 'onTrgSocket', 'onTrgSocketClose', 'onTrgSocketEnd', 'onTrgSocketError', 'onTrgRequestConnect', 'onTrgRequestAbort', 'onTrgRequestError']); if (!trgParsed.hostname) throw new Error('The "trgParsed.hostname" option is required'); if (!trgParsed.port) throw new Error('The "trgParsed.port" option is required'); this.trgRequest = null; this.trgSocket = null; this.trgParsed = trgParsed; this.trgParsed.port = this.trgParsed.port || DEFAULT_TARGET_PORT; this.srcSocket = srcSocket; this.srcSocket.once('close', this.onSrcSocketClose); this.srcSocket.once('end', this.onSrcSocketEnd); this.srcSocket.once('error', this.onSrcSocketError); this.upstreamProxyUrlParsed = upstreamProxyUrlParsed; this.isClosed = false; } _createClass(HandlerTunnelTcpChain, [{ key: 'bindHandlersToThis', value: function bindHandlersToThis(handlerNames) { var _this = this; handlerNames.forEach(function (evt) { _this[evt] = _this[evt].bind(_this); }); } }, { key: 'run', value: function run() { this.log('Connecting to upstream proxy...'); var options = { method: 'CONNECT', hostname: this.upstreamProxyUrlParsed.hostname, port: this.upstreamProxyUrlParsed.port, path: this.trgParsed.hostname + ':' + this.trgParsed.port, headers: {} }; (0, _tools.maybeAddProxyAuthorizationHeader)(this.upstreamProxyUrlParsed, options.headers); this.trgRequest = _http2.default.request(options); this.trgRequest.once('connect', this.onTrgRequestConnect); this.trgRequest.once('abort', this.onTrgRequestAbort); this.trgRequest.once('error', this.onTrgRequestError); this.trgRequest.on('socket', this.onTrgSocket); // Send the data this.trgRequest.end(); } // If the client closes the connection prematurely, // then immediately destroy the upstream socket, there's nothing we can do with it }, { key: 'onSrcSocketClose', value: function onSrcSocketClose() { if (this.isClosed) return; this.log('Source socket closed'); this.close(); } }, { key: 'onSrcSocketEnd', value: function onSrcSocketEnd() { if (this.isClosed) return; this.log('Source socket ended'); this.close(); } }, { key: 'onSrcSocketError', value: function onSrcSocketError(err) { if (this.isClosed) return; this.log('Source socket failed: ' + (err.stack || err)); this.close(); } }, { key: 'onTrgSocket', value: function onTrgSocket(socket) { if (this.isClosed) return; this.log('Target socket assigned'); this.trgSocket = socket; socket.once('close', this.onTrgSocketClose); socket.once('end', this.onTrgSocketEnd); socket.once('error', this.onTrgSocketError); } // Once target socket closes, we need to give time // to source socket to receive pending data, so we only call end() }, { key: 'onTrgSocketClose', value: function onTrgSocketClose() { var _this2 = this; if (this.isClosed) return; this.log('Target socket closed'); setTimeout(function () { if (_this2.srcSocket) _this2.srcSocket.end(); }, 100); } }, { key: 'onTrgSocketEnd', value: function onTrgSocketEnd() { var _this3 = this; if (this.isClosed) return; this.log('Target socket ended'); setTimeout(function () { if (_this3.srcSocket) _this3.srcSocket.end(); }, 100); } }, { key: 'onTrgSocketError', value: function onTrgSocketError(err) { if (this.isClosed) return; this.log('Target socket failed: ' + (err.stack || err)); this.fail(err); } }, { key: 'onTrgRequestConnect', value: function onTrgRequestConnect(response) { if (this.isClosed) return; this.log('Connected to upstream proxy'); if (this.checkUpstreamProxy407(response)) return; // Setup bi-directional tunnel this.trgSocket.pipe(this.srcSocket); this.srcSocket.pipe(this.trgSocket); this.srcSocket.resume(); } }, { key: 'onTrgRequestAbort', value: function onTrgRequestAbort() { if (this.isClosed) return; this.log('Target aborted'); this.close(); } }, { key: 'onTrgRequestError', value: function onTrgRequestError(err) { if (this.isClosed) return; this.log('Target request failed: ' + (err.stack || err)); this.fail(err); } /** * Checks whether response from upstream proxy is 407 Proxy Authentication Required * and if so, responds 502 Bad Gateway to client. * @param response * @return {boolean} */ }, { key: 'checkUpstreamProxy407', value: function checkUpstreamProxy407(response) { if (this.upstreamProxyUrlParsed && response.statusCode === 407) { this.fail('Invalid credentials provided for the upstream proxy.', 502); return true; } return false; } }, { key: 'fail', value: function fail(err, statusCode) { if (this.srcGotResponse) { this.log('Source already received a response, just destroying the socket...'); this.close(); } else if (statusCode) { // Manual error this.log(err + ', responding with custom status code ' + statusCode + ' to client'); } else if (err.code === 'ENOTFOUND' && !this.upstreamProxyUrlParsed) { this.log('Target server not found, sending 404 to client'); } else if (err.code === 'ENOTFOUND' && this.upstreamProxyUrlParsed) { this.log('Upstream proxy not found, sending 502 to client'); } else if (err.code === 'ECONNREFUSED') { this.log('Upstream proxy refused connection, sending 502 to client'); } else if (err.code === 'ETIMEDOUT') { this.log('Connection timed out, sending 502 to client'); } else if (err.code === 'ECONNRESET') { this.log('Connection lost, sending 502 to client'); } else if (err.code === 'EPIPE') { this.log('Socket closed before write, sending 502 to client'); } else { this.log('Unknown error, sending 500 to client'); } } /** * Detaches all listeners and destroys all sockets. */ }, { key: 'close', value: function close() { if (!this.isClosed) { this.log('Closing handler'); if (this.srcRequest) { this.srcRequest.destroy(); this.srcRequest = null; } if (this.srcSocket) { this.srcSocket.destroy(); this.srcSocket = null; } if (this.trgRequest) { this.trgRequest.abort(); this.trgRequest = null; } if (this.trgSocket) { this.trgSocket.destroy(); this.trgSocket = null; } this.isClosed = true; } } }]); return HandlerTunnelTcpChain; }(); exports.default = HandlerTunnelTcpChain;