'use strict';

Object.defineProperty(exports, “__esModule”, {

value: true

}); exports.Server = exports.RequestError = undefined;

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 _underscore = require('underscore');

var _underscore2 = _interopRequireDefault(_underscore);

var _bluebird = require('bluebird');

var _bluebird2 = _interopRequireDefault(_bluebird);

var _tools = require('./tools');

var _handler_forward = require('./handler_forward');

var _handler_forward2 = _interopRequireDefault(_handler_forward);

var _handler_tunnel_direct = require('./handler_tunnel_direct');

var _handler_tunnel_direct2 = _interopRequireDefault(_handler_tunnel_direct);

var _handler_tunnel_chain = require('./handler_tunnel_chain');

var _handler_tunnel_chain2 = _interopRequireDefault(_handler_tunnel_chain);

var _handler_custom_response = require('./handler_custom_response');

var _handler_custom_response2 = _interopRequireDefault(_handler_custom_response);

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

// TODO: // - Fail gracefully if target proxy fails (invalid credentials or non-existent) // - Implement this requirement from rfc7230 // “A proxy MUST forward unrecognized header fields unless the field-name // is listed in the Connection header field (Section 6.1) or the proxy // is specifically configured to block, or otherwise transform, such // fields. Other recipients SHOULD ignore unrecognized header fields. // These requirements allow HTTP's functionality to be enhanced without // requiring prior update of deployed intermediaries.” // - Add param to prepareRequestFunction() that would allow the caller to kill a connection

// TODO: // - Use connection pooling and maybe other stuff from: // github.com/request/tunnel-agent/blob/master/index.js // github.com/request/request/blob/master/lib/tunnel.js

var DEFAULT_AUTH_REALM = 'ProxyChain'; var DEFAULT_PROXY_SERVER_PORT = 8000; var DEFAULT_TARGET_PORT = 80;

var REQUEST_ERROR_NAME = 'RequestError';

/**

* Represents custom request error. The message is emitted as HTTP response
* with a specific HTTP code and headers.
* If this error is thrown from the `prepareRequestFunction` function,
* the message and status code is sent to client.
* By default, the response will have Content-Type: text/plain
* and for the 407 status the Proxy-Authenticate header will be added.
*/

var RequestError = exports.RequestError = function (_Error) {

_inherits(RequestError, _Error);

function RequestError(message, statusCode, headers) {
    _classCallCheck(this, RequestError);

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

    _this.name = REQUEST_ERROR_NAME;
    _this.statusCode = statusCode;
    _this.headers = headers;

    Error.captureStackTrace(_this, RequestError);
    return _this;
}

return RequestError;

}(Error);

/**

* Represents the proxy server.
* It emits the 'requestFailed' event on unexpected request errors, with the following parameter `{ error, request }`.
* It emits the 'connectionClosed' event when connection to proxy server is closed, with parameter `{ connectionId, stats }`.
*/

var Server = exports.Server = function (_EventEmitter) {

_inherits(Server, _EventEmitter);

/**
 * Initializes a new instance of Server class.
 * @param options
 * @param [options.port] Port where the server will listen. By default 8000.
 * @param [options.prepareRequestFunction] Custom function to authenticate proxy requests,
 * provide URL to chained upstream proxy or potentially provide function that generates a custom response to HTTP requests.
 * It accepts a single parameter which is an object:
 * ```{
 *   connectionId: Number,
 *   request: Object,
 *   username: String,
 *   password: String,
 *   hostname: String,
 *   port: Number,
 *   isHttp: Boolean
 * }```
 * and returns an object (or promise resolving to the object) with following form:
 * ```{
 *   requestAuthentication: Boolean,
 *   upstreamProxyUrl: String,
 *   customResponseFunction: Function
 * }```
 * If `upstreamProxyUrl` is false-ish value, no upstream proxy is used.
 * If `prepareRequestFunction` is not set, the proxy server will not require any authentication
 * and will not use any upstream proxy.
 * If `customResponseFunction` is set, it will be called to generate a custom response to the HTTP request.
 * It should not be used together with `upstreamProxyUrl`.
 * @param [options.authRealm] Realm used in the Proxy-Authenticate header and also in the 'Server' HTTP header. By default it's `ProxyChain`.
 * @param [options.verbose] If true, the server logs
 */
function Server(options) {
    _classCallCheck(this, Server);

    var _this2 = _possibleConstructorReturn(this, (Server.__proto__ || Object.getPrototypeOf(Server)).call(this));

    options = options || {};

    _this2.port = options.port || DEFAULT_PROXY_SERVER_PORT;
    _this2.prepareRequestFunction = options.prepareRequestFunction;
    _this2.authRealm = options.authRealm || DEFAULT_AUTH_REALM;
    _this2.verbose = !!options.verbose;

    // Key is handler ID, value is HandlerXxx instance
    _this2.handlers = {};
    _this2.lastHandlerId = 0;

    _this2.server = _http2.default.createServer();
    _this2.server.on('clientError', _this2.onClientError.bind(_this2));
    _this2.server.on('request', _this2.onRequest.bind(_this2));
    _this2.server.on('connect', _this2.onConnect.bind(_this2));

    _this2.stats = {
        httpRequestCount: 0,
        connectRequestCount: 0
    };
    return _this2;
}

_createClass(Server, [{
    key: 'log',
    value: function log(handlerId, str) {
        if (this.verbose) {
            var logPrefix = handlerId ? handlerId + ' | ' : '';
            console.log('ProxyServer[' + this.port + ']: ' + logPrefix + str);
        }
    }
}, {
    key: 'onClientError',
    value: function onClientError(err, socket) {
        this.log(null, 'onClientError: ' + err);
        this.sendResponse(socket, 400, null, 'Invalid request');
    }

    /**
     * Handles normal HTTP request by forwarding it to target host or the upstream proxy.
     */

}, {
    key: 'onRequest',
    value: function onRequest(request, response) {
        var _this3 = this;

        var handlerOpts = void 0;
        this.prepareRequestHandling(request).then(function (result) {
            handlerOpts = result;
            handlerOpts.srcResponse = response;

            var handler = void 0;
            if (handlerOpts.customResponseFunction) {
                _this3.log(handlerOpts.id, 'Using HandlerCustomResponse');
                handler = new _handler_custom_response2.default(handlerOpts);
            } else {
                _this3.log(handlerOpts.id, 'Using HandlerForward');
                handler = new _handler_forward2.default(handlerOpts);
            }

            _this3.handlerRun(handler);
        }).catch(function (err) {
            _this3.failRequest(request, err, handlerOpts);
        });
    }

    /**
     * Handles HTTP CONNECT request by setting up a tunnel either to target host or to the upstream proxy.
     * @param request
     * @param socket
     * @param head The first packet of the tunneling stream (may be empty)
     */

}, {
    key: 'onConnect',
    value: function onConnect(request, socket, head) {
        var _this4 = this;

        var handlerOpts = void 0;
        this.prepareRequestHandling(request).then(function (result) {
            handlerOpts = result;
            handlerOpts.srcHead = head;

            var handler = void 0;
            if (handlerOpts.upstreamProxyUrlParsed) {
                _this4.log(handlerOpts.id, 'Using HandlerTunnelChain');
                handler = new _handler_tunnel_chain2.default(handlerOpts);
            } else {
                _this4.log(handlerOpts.id, 'Using HandlerTunnelDirect');
                handler = new _handler_tunnel_direct2.default(handlerOpts);
            }

            _this4.handlerRun(handler);
        }).catch(function (err) {
            _this4.failRequest(request, err, handlerOpts);
        });
    }

    /**
     * Authenticates a new request and determines upstream proxy URL using the user function.
     * Returns a promise resolving to an object that can be passed to construcot of one of the HandlerXxx classes.
     * @param request
     */

}, {
    key: 'prepareRequestHandling',
    value: function prepareRequestHandling(request) {
        var _this5 = this;

        // console.log('XXX prepareRequestHandling');
        // console.dir(_.pick(request, 'url', 'method'));
        // console.dir(url.parse(request.url));

        var handlerOpts = {
            server: this,
            id: ++this.lastHandlerId,
            srcRequest: request,
            srcHead: null,
            trgParsed: null,
            upstreamProxyUrlParsed: null
        };

        this.log(handlerOpts.id, '!!! Handling ' + request.method + ' ' + request.url + ' HTTP/' + request.httpVersion);

        var socket = request.socket;

        var isHttp = false;

        return _bluebird2.default.resolve().then(function () {
            // console.dir(_.pick(request, 'url', 'headers', 'method'));
            // Determine target hostname and port
            if (request.method === 'CONNECT') {
                // The request should look like:
                //   CONNECT server.example.com:80 HTTP/1.1
                // Note that request.url contains the "server.example.com:80" part
                handlerOpts.trgParsed = (0, _tools.parseHostHeader)(request.url);

                // If srcRequest.url does not match the regexp tools.HOST_HEADER_REGEX
                // or the url is too long it will not be parsed so we throw error here.
                if (!handlerOpts.trgParsed) {
                    throw new RequestError('Target "' + request.url + '" could not be parsed', 400);
                }

                _this5.stats.connectRequestCount++;
            } else {
                // The request should look like:
                //   GET http://server.example.com:80/some-path HTTP/1.1
                // Note that RFC 7230 says:
                // "When making a request to a proxy, other than a CONNECT or server-wide
                //  OPTIONS request (as detailed below), a client MUST send the target
                //  URI in absolute-form as the request-target"
                var parsed = (0, _tools.parseUrl)(request.url);

                // If srcRequest.url does not match the regexp tools.HOST_HEADER_REGEX
                // or the url is too long it will not be parsed so we throw error here.
                if (!parsed) {
                    throw new RequestError('Target "' + request.url + '" could not be parsed', 400);
                }

                // If srcRequest.url is something like '/some-path', this is most likely a normal HTTP request
                if (!parsed.protocol) {
                    throw new RequestError('Hey, good try, but I\'m a HTTP proxy, not your ordinary web server :)', 400);
                }
                // Only HTTP is supported, other protocols such as HTTP or FTP must use the CONNECT method
                if (parsed.protocol !== 'http:') {
                    throw new RequestError('Only HTTP protocol is supported (was ' + parsed.protocol + ')', 400);
                }

                handlerOpts.trgParsed = parsed;
                isHttp = true;

                _this5.stats.httpRequestCount++;
            }

            handlerOpts.trgParsed.port = handlerOpts.trgParsed.port || DEFAULT_TARGET_PORT;

            // Authenticate the request using a user function (if provided)
            if (!_this5.prepareRequestFunction) return { requestAuthentication: false, upstreamProxyUrlParsed: null };

            // Pause the socket so that no data is lost
            socket.pause();

            var funcOpts = {
                connectionId: handlerOpts.id,
                request: request,
                username: null,
                password: null,
                hostname: handlerOpts.trgParsed.hostname,
                port: handlerOpts.trgParsed.port,
                isHttp: isHttp
            };

            var proxyAuth = request.headers['proxy-authorization'];
            if (proxyAuth) {
                var auth = (0, _tools.parseProxyAuthorizationHeader)(proxyAuth);
                if (!auth) {
                    throw new RequestError('Invalid "Proxy-Authorization" header', 400);
                }
                if (auth.type !== 'Basic') {
                    throw new RequestError('The "Proxy-Authorization" header must have the "Basic" type.', 400);
                }
                funcOpts.username = auth.username;
                funcOpts.password = auth.password;
            }

            // User function returns a result directly or a promise
            return _this5.prepareRequestFunction(funcOpts);
        }).then(function (funcResult) {
            // If not authenticated, request client to authenticate
            if (funcResult && funcResult.requestAuthentication) {
                throw new RequestError(funcResult.failMsg || 'Proxy credentials required.', 407);
            }

            if (funcResult && funcResult.upstreamProxyUrl) {
                handlerOpts.upstreamProxyUrlParsed = (0, _tools.parseUrl)(funcResult.upstreamProxyUrl);

                if (handlerOpts.upstreamProxyUrlParsed) {
                    if (!handlerOpts.upstreamProxyUrlParsed.hostname || !handlerOpts.upstreamProxyUrlParsed.port) {
                        throw new Error('Invalid "upstreamProxyUrl" provided: URL must have hostname and port');
                    }
                    if (handlerOpts.upstreamProxyUrlParsed.scheme !== 'http') {
                        throw new Error('Invalid "upstreamProxyUrl" provided: URL must have the "http" scheme');
                    }
                }
            }

            if (funcResult && funcResult.customResponseFunction) {
                _this5.log(handlerOpts.id, 'Using custom response function');
                handlerOpts.customResponseFunction = funcResult.customResponseFunction;
                if (!isHttp) {
                    throw new Error('The "customResponseFunction" option can only be used for HTTP requests.');
                }
                if (typeof handlerOpts.customResponseFunction !== 'function') {
                    throw new Error('The "customResponseFunction" option must be a function.');
                }
            }

            if (handlerOpts.upstreamProxyUrlParsed) {
                _this5.log(handlerOpts.id, 'Using upstream proxy ' + (0, _tools.redactParsedUrl)(handlerOpts.upstreamProxyUrlParsed));
            }

            return handlerOpts;
        }).finally(function () {
            if (_this5.prepareRequestFunction) socket.resume();
        });
    }
}, {
    key: 'handlerRun',
    value: function handlerRun(handler) {
        var _this6 = this;

        this.handlers[handler.id] = handler;

        handler.once('close', function (_ref) {
            var stats = _ref.stats;

            _this6.emit('connectionClosed', {
                connectionId: handler.id,
                stats: stats
            });
            delete _this6.handlers[handler.id];
            _this6.log(handler.id, '!!! Closed and removed from server');
        });

        handler.run();
    }

    /**
     * Sends a HTTP error response to the client.
     * @param request
     * @param err
     */

}, {
    key: 'failRequest',
    value: function failRequest(request, err, handlerOpts) {
        var handlerId = handlerOpts ? handlerOpts.id : null;

        if (err.name === REQUEST_ERROR_NAME) {
            this.log(handlerId, 'Request failed (status ' + err.statusCode + '): ' + err.message);
            this.sendResponse(request.socket, err.statusCode, err.headers, err.message);
        } else {
            this.log(handlerId, 'Request failed with unknown error: ' + (err.stack || err));
            this.sendResponse(request.socket, 500, null, 'Internal error in proxy server');
            this.emit('requestFailed', { error: err, request: request });
        }

        // emit connection closed if request fails and connection was already reported
        if (handlerOpts) {
            this.log(handlerId, 'Closed because request failed with error');
            this.emit('connectionClosed', {
                connectionId: handlerOpts.id,
                stats: { srcTxBytes: 0, srcRxBytes: 0 }
            });
        }
    }

    /**
     * Sends a simple HTTP response to the client and forcibly closes the connection.
     * @param socket
     * @param statusCode
     * @param headers
     * @param message
     */

}, {
    key: 'sendResponse',
    value: function sendResponse(socket, statusCode, headers, message) {
        try {
            headers = headers || {};

            if (!headers['Content-Type']) {
                headers['Content-Type'] = 'text/html; charset=utf-8';
            }
            if (statusCode === 407 && !headers['Proxy-Authenticate']) {
                headers['Proxy-Authenticate'] = 'Basic realm="' + this.authRealm + '"';
            }
            if (!headers.Server) {
                headers.Server = this.authRealm;
            }
            // These headers are required by PhantomJS, otherwise the connection would timeout!
            if (!headers.Connection) {
                headers.Connection = 'close';
            }
            if (!headers['Content-Length']) {
                headers['Content-Length'] = Buffer.byteLength(message);
            }

            var msg = 'HTTP/1.1 ' + statusCode + ' ' + _http2.default.STATUS_CODES[statusCode] + '\r\n';
            _underscore2.default.each(headers, function (value, key) {
                msg += key + ': ' + value + '\r\n';
            });
            msg += '\r\n' + message;

            // console.log("RESPONSE:\n" + msg);

            socket.write(msg, function () {
                socket.end();

                // Unfortunately calling end() will not close the socket
                // if client refuses to close it. Hence calling destroy after a short while.
                setTimeout(function () {
                    socket.destroy();
                }, 100);
            });
        } catch (err) {
            this.log(null, 'Unhandled error in sendResponse(), will be ignored: ' + (err.stack || err));
        }
    }

    /**
     * Starts listening at a port specified in the constructor.
     * @param callback Optional callback
     * @return {*}
     */

}, {
    key: 'listen',
    value: function listen(callback) {
        var _this7 = this;

        return new _bluebird2.default(function (resolve, reject) {
            // Unfortunately server.listen() is not a normal function that fails on error,
            // so we need this trickery
            var onError = function onError(err) {
                _this7.log(null, 'Listen failed: ' + err);
                removeListeners();
                reject(err);
            };
            var onListening = function onListening() {
                _this7.log(null, 'Listening...');
                removeListeners();
                resolve();
            };
            var removeListeners = function removeListeners() {
                _this7.server.removeListener('error', onError);
                _this7.server.removeListener('listening', onListening);
            };

            _this7.server.on('error', onError);
            _this7.server.on('listening', onListening);
            _this7.server.listen(_this7.port);
        }).nodeify(callback);
    }

    /**
     * Gets array of IDs of all active connections.
     * @returns {*}
     */

}, {
    key: 'getConnectionIds',
    value: function getConnectionIds() {
        return _underscore2.default.keys(this.handlers);
    }

    /**
     * Gets data transfer statistics of a specific proxy connection.
     * @param {Number} connectionId ID of the connection handler.
     * It is passed to `prepareRequestFunction` function.
     * @return {Object} An object with statistics { srcTxBytes, srcRxBytes, trgTxBytes, trgRxBytes },
     * or null if connection does not exist or has been closed.
     */

}, {
    key: 'getConnectionStats',
    value: function getConnectionStats(connectionId) {
        var handler = this.handlers && this.handlers[connectionId];
        if (!handler) return undefined;

        return handler.getStats();
    }

    /**
     * Closes the proxy server.
     * @param [closeConnections] If true, then all the pending connections from clients
     * to targets and upstream proxies will be forcibly aborted.
     * @param callback
     */

}, {
    key: 'close',
    value: function close(closeConnections, callback) {
        if (typeof closeConnections === 'function') {
            callback = closeConnections;
            closeConnections = false;
        }

        if (closeConnections) {
            this.log(null, 'Closing pending handlers');
            var count = 0;
            _underscore2.default.each(this.handlers, function (handler) {
                count++;
                handler.close();
            });
            this.log(null, 'Destroyed ' + count + ' pending handlers');
        }

        // TODO: keep track of all handlers and close them if closeConnections=true
        if (this.server) {
            var server = this.server;

            this.server = null;
            return _bluebird2.default.promisify(server.close).bind(server)().nodeify(callback);
        }
    }
}]);

return Server;

}(_events2.default);