'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; 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 _events = require('events');

var _events2 = _interopRequireDefault(_events);

var _server = require('./server');

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”); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError(“this hasn't been initialised - super() hasn't been called”); } return call && (typeof call === “object” || typeof call === “function”) ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== “function” && superClass !== null) { throw new TypeError(“Super expression must either be null or a function, not ” + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

/**

* Base class for proxy connection handlers. It emits the `destroyed` event
* when the handler is no longer used.
*/

var HandlerBase = function (_EventEmitter) {

_inherits(HandlerBase, _EventEmitter);

function HandlerBase(_ref) {
    var server = _ref.server,
        id = _ref.id,
        srcRequest = _ref.srcRequest,
        srcHead = _ref.srcHead,
        srcResponse = _ref.srcResponse,
        trgParsed = _ref.trgParsed,
        upstreamProxyUrlParsed = _ref.upstreamProxyUrlParsed;

    _classCallCheck(this, HandlerBase);

    var _this = _possibleConstructorReturn(this, (HandlerBase.__proto__ || Object.getPrototypeOf(HandlerBase)).call(this));

    if (!server) throw new Error('The "server" option is required');
    if (!id) throw new Error('The "id" option is required');
    if (!srcRequest) throw new Error('The "srcRequest" option is required');
    if (!srcRequest.socket) throw new Error('"srcRequest.socket" cannot be null');
    if (!trgParsed.hostname) throw new Error('The "trgParsed.hostname" option is required');

    _this.server = server;
    _this.id = id;

    _this.srcRequest = srcRequest;
    _this.srcHead = srcHead;
    _this.srcResponse = srcResponse;
    _this.srcSocket = srcRequest.socket;

    _this.trgRequest = null;
    _this.trgSocket = null;
    _this.trgParsed = trgParsed;
    _this.trgParsed.port = _this.trgParsed.port || DEFAULT_TARGET_PORT;

    // Indicates that source socket might have received some data already
    _this.srcGotResponse = false;

    _this.isClosed = false;

    _this.upstreamProxyUrlParsed = upstreamProxyUrlParsed;

    // Create ServerResponse for the client HTTP request if it doesn't exist
    // NOTE: This is undocumented API, it might break in the future
    if (!_this.srcResponse) {
        _this.srcResponse = new _http2.default.ServerResponse(srcRequest);
        _this.srcResponse.shouldKeepAlive = false;
        _this.srcResponse.chunkedEncoding = false;
        _this.srcResponse.useChunkedEncodingByDefault = false;
        _this.srcResponse.assignSocket(_this.srcSocket);
    }

    // Bind all event handlers to this instance
    _this.bindHandlersToThis(['onSrcResponseFinish', 'onSrcResponseError', 'onSrcSocketEnd', 'onSrcSocketFinish', 'onSrcSocketClose', 'onSrcSocketError', 'onTrgSocket', 'onTrgSocketEnd', 'onTrgSocketFinish', 'onTrgSocketClose', 'onTrgSocketError']);

    _this.srcResponse.on('error', _this.onSrcResponseError);

    // Called for the ServerResponse's "finish" event
    // Normally, Node's "http" module has a "finish" event listener that would
    // take care of closing the socket once the HTTP response has completed, but
    // since we're making this ServerResponse instance manually, that event handler
    // never gets hooked up, so we must manually close the socket...
    _this.srcResponse.once('finish', _this.onSrcResponseFinish);

    // Forward data directly to source client without any delay
    _this.srcSocket.setNoDelay();

    _this.srcSocket.once('end', _this.onSrcSocketEnd);
    _this.srcSocket.once('close', _this.onSrcSocketClose);
    _this.srcSocket.once('finish', _this.onSrcSocketFinish);
    _this.srcSocket.on('error', _this.onSrcSocketError);
    return _this;
}

_createClass(HandlerBase, [{
    key: 'bindHandlersToThis',
    value: function bindHandlersToThis(handlerNames) {
        var _this2 = this;

        handlerNames.forEach(function (evt) {
            _this2[evt] = _this2[evt].bind(_this2);
        });
    }
}, {
    key: 'log',
    value: function log(str) {
        this.server.log(this.id, str);
    }

    // Abstract method, needs to be overridden

}, {
    key: 'run',
    value: function run() {} // eslint-disable-line

}, {
    key: 'onSrcSocketEnd',
    value: function onSrcSocketEnd() {
        if (this.isClosed) return;
        this.log('Source socket ended');
        this.close();
    }

    // On Node 10+, the 'close' event is called only after socket is destroyed,
    // so we also need to listen for the stream 'finish' event

}, {
    key: 'onSrcSocketFinish',
    value: function onSrcSocketFinish() {
        if (this.isClosed) return;
        this.log('Source socket finished');
        this.close();
    }

    // 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: 'onSrcSocketError',
    value: function onSrcSocketError(err) {
        if (this.isClosed) return;
        this.log('Source socket failed: ' + (err.stack || err));
        this.close();
    }

    // This is to address https://github.com/apifytech/proxy-chain/issues/27
    // It seems that when client closed the connection, the piped target socket
    // can still pump data to it, which caused unhandled "write after end" error

}, {
    key: 'onSrcResponseError',
    value: function onSrcResponseError(err) {
        if (this.isClosed) return;
        this.log('Source response failed: ' + (err.stack || err));
        this.close();
    }
}, {
    key: 'onSrcResponseFinish',
    value: function onSrcResponseFinish() {
        if (this.isClosed) return;
        this.log('Source response finished, ending source socket');
        // NOTE: We cannot destroy the socket, since there might be pending data that wouldn't be delivered!
        // This code is inspired by resOnFinish() in _http_server.js in Node.js code base.
        if (typeof this.srcSocket.destroySoon === 'function') {
            this.srcSocket.destroySoon();
        } else {
            this.srcSocket.end();
        }
    }
}, {
    key: 'onTrgSocket',
    value: function onTrgSocket(socket) {
        if (this.isClosed) return;
        this.log('Target socket assigned');

        this.trgSocket = socket;

        // Forward data directly to target server without any delay
        this.trgSocket.setNoDelay();

        socket.once('end', this.onTrgSocketEnd);
        socket.once('finish', this.onTrgSocketFinish);
        socket.once('close', this.onTrgSocketClose);
        socket.on('error', this.onTrgSocketError);
    }
}, {
    key: 'trgSocketShutdown',
    value: function trgSocketShutdown(msg) {
        if (this.isClosed) return;
        this.log(msg);
        // Once target socket closes, we need to give time
        // to source socket to receive pending data, so we only call end()
        // If socket is closed here instead of response, phantomjs does not properly parse the response as http response.
        if (this.srcResponse) {
            this.srcResponse.end();
        } else if (this.srcSocket) {
            // Handler tunnel chain does not use srcResponse, but needs to close srcSocket
            this.srcSocket.end();
        }
    }
}, {
    key: 'onTrgSocketEnd',
    value: function onTrgSocketEnd() {
        this.trgSocketShutdown('Target socket ended');
    }
}, {
    key: 'onTrgSocketFinish',
    value: function onTrgSocketFinish() {
        this.trgSocketShutdown('Target socket finished');
    }
}, {
    key: 'onTrgSocketClose',
    value: function onTrgSocketClose() {
        this.trgSocketShutdown('Target socket closed');
    }
}, {
    key: 'onTrgSocketError',
    value: function onTrgSocketError(err) {
        if (this.isClosed) return;
        this.log('Target socket 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(new _server.RequestError('Invalid credentials provided for the upstream proxy.', 502));
            return true;
        }
        return false;
    }
}, {
    key: 'fail',
    value: function fail(err) {
        if (this.srcGotResponse) {
            this.log('Source already received a response, just destroying the socket...');
            this.close();
        } else if (err.statusCode) {
            // Error is RequestError with HTTP status code
            this.log(err + ', responding with custom status code ' + err.statusCode + ' to client');
            this.srcResponse.writeHead(err.statusCode);
            this.srcResponse.end('' + err.message);
        } else if (err.code === 'ENOTFOUND' && !this.upstreamProxyUrlParsed) {
            this.log('Target server not found, sending 404 to client');
            this.srcResponse.writeHead(404);
            this.srcResponse.end('Target server not found');
        } else if (err.code === 'ENOTFOUND' && this.upstreamProxyUrlParsed) {
            this.log('Upstream proxy not found, sending 502 to client');
            this.srcResponse.writeHead(502);
            this.srcResponse.end('Upstream proxy was not found');
        } else if (err.code === 'ECONNREFUSED') {
            this.log('Upstream proxy refused connection, sending 502 to client');
            this.srcResponse.writeHead(502);
            this.srcResponse.end('Upstream proxy refused connection');
        } else if (err.code === 'ETIMEDOUT') {
            this.log('Connection timed out, sending 502 to client');
            this.srcResponse.writeHead(502);
            this.srcResponse.end('Connection to upstream proxy timed out');
        } else if (err.code === 'ECONNRESET') {
            this.log('Connection lost, sending 502 to client');
            this.srcResponse.writeHead(502);
            this.srcResponse.end('Connection lost');
        } else if (err.code === 'EPIPE') {
            this.log('Socket closed before write, sending 502 to client');
            this.srcResponse.writeHead(502);
            this.srcResponse.end('Connection interrupted');
        } else {
            this.log('Unknown error, sending 500 to client');
            this.srcResponse.writeHead(500);
            this.srcResponse.end('Internal error in proxy server');
        }
    }
}, {
    key: 'getStats',
    value: function getStats() {
        return {
            srcTxBytes: this.srcSocket ? this.srcSocket.bytesWritten : null,
            srcRxBytes: this.srcSocket ? this.srcSocket.bytesRead : null,
            trgTxBytes: this.trgSocket ? this.trgSocket.bytesWritten : null,
            trgRxBytes: this.trgSocket ? this.trgSocket.bytesRead : null
        };
    }

    /**
     * Detaches all listeners, destroys all sockets and emits the 'close' event.
     */

}, {
    key: 'close',
    value: function close() {
        if (this.isClosed) return;

        this.log('Closing handler');

        // Save stats before sockets are destroyed
        var stats = this.getStats();

        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;

        this.emit('close', { stats: stats });
    }
}]);

return HandlerBase;

}(_events2.default);

exports.default = HandlerBase;