"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createProxy = void 0;
const assert_1 = __importDefault(require("assert"));
const net = __importStar(require("net"));
const url = __importStar(require("url"));
const http = __importStar(require("http"));
const os = __importStar(require("os"));
const pkg_1 = __importDefault(require("./pkg"));
const debug_1 = __importDefault(require("debug"));
// log levels
const debug = {
    request: (0, debug_1.default)('proxy ← ← ←'),
    response: (0, debug_1.default)('proxy → → →'),
    proxyRequest: (0, debug_1.default)('proxy ↑ ↑ ↑'),
    proxyResponse: (0, debug_1.default)('proxy ↓ ↓ ↓'),
};
// hostname
const hostname = os.hostname();
/**
 * Sets up an `http.Server` or `https.Server` instance with the necessary
 * "request" and "connect" event listeners in order to make the server act
 * as an HTTP proxy.
 */
function createProxy(server) {
    if (!server)
        server = http.createServer();
    server.on('request', onrequest);
    server.on('connect', onconnect);
    return server;
}
exports.createProxy = createProxy;
/**
 * 13.5.1 End-to-end and Hop-by-hop Headers
 *
 * Hop-by-hop headers must be removed by the proxy before passing it on to the
 * next endpoint. Per-request basis hop-by-hop headers MUST be listed in a
 * Connection header, (section 14.10) to be introduced into HTTP/1.1 (or later).
 */
const hopByHopHeaders = [
    'Connection',
    'Keep-Alive',
    'Proxy-Authenticate',
    'Proxy-Authorization',
    'TE',
    'Trailers',
    'Transfer-Encoding',
    'Upgrade',
];
// create a case-insensitive RegExp to match "hop by hop" headers
const isHopByHop = new RegExp('^(' + hopByHopHeaders.join('|') + ')$', 'i');
/**
 * Iterator function for the request/response's "headers".
 */
function* eachHeader(obj) {
    // every even entry is a "key", every odd entry is a "value"
    let key = null;
    for (const v of obj.rawHeaders) {
        if (key === null) {
            key = v;
        }
        else {
            yield [key, v];
            key = null;
        }
    }
}
/**
 * HTTP GET/POST/DELETE/PUT, etc. proxy requests.
 */
async function onrequest(req, res) {
    debug.request('%s %s HTTP/%s ', req.method, req.url, req.httpVersion);
    const socket = req.socket;
    // pause the socket during authentication so no data is lost
    socket.pause();
    try {
        const success = await authenticate(this, req);
        if (!success)
            return requestAuthorization(req, res);
    }
    catch (_err) {
        const err = _err;
        // an error occured during login!
        res.writeHead(500);
        res.end((err.stack || err.message || err) + '\n');
        return;
    }
    socket.resume();
    const parsed = url.parse(req.url || '/');
    // setup outbound proxy request HTTP headers
    const headers = {};
    let hasXForwardedFor = false;
    let hasVia = false;
    const via = '1.1 ' + hostname + ' (proxy/' + pkg_1.default.version + ')';
    for (const header of eachHeader(req)) {
        debug.request('Request Header: %o', header);
        const key = header[0];
        let value = header[1];
        const keyLower = key.toLowerCase();
        if (!hasXForwardedFor && 'x-forwarded-for' === keyLower) {
            // append to existing "X-Forwarded-For" header
            // http://en.wikipedia.org/wiki/X-Forwarded-For
            hasXForwardedFor = true;
            if (typeof socket.remoteAddress === 'string') {
                value += ', ' + socket.remoteAddress;
                debug.proxyRequest('appending to existing "%s" header: "%s"', key, value);
            }
        }
        if (!hasVia && 'via' === keyLower) {
            // append to existing "Via" header
            hasVia = true;
            value += ', ' + via;
            debug.proxyRequest('appending to existing "%s" header: "%s"', key, value);
        }
        if (isHopByHop.test(key)) {
            debug.proxyRequest('ignoring hop-by-hop header "%s"', key);
        }
        else {
            const v = headers[key];
            if (Array.isArray(v)) {
                v.push(value);
            }
            else if (null != v) {
                headers[key] = [v, value];
            }
            else {
                headers[key] = value;
            }
        }
    }
    // add "X-Forwarded-For" header if it's still not here by now
    // http://en.wikipedia.org/wiki/X-Forwarded-For
    if (!hasXForwardedFor && typeof socket.remoteAddress === 'string') {
        headers['X-Forwarded-For'] = socket.remoteAddress;
        debug.proxyRequest('adding new "X-Forwarded-For" header: "%s"', headers['X-Forwarded-For']);
    }
    // add "Via" header if still not set by now
    if (!hasVia) {
        headers.Via = via;
        debug.proxyRequest('adding new "Via" header: "%s"', headers.Via);
    }
    // custom `http.Agent` support, set `server.agent`
    //let agent = server.agent;
    //if (null != agent) {
    //	debug.proxyRequest(
    //		'setting custom `http.Agent` option for proxy request: %s',
    //		agent
    //	);
    //	parsed.agent = agent;
    //	agent = null;
    //}
    //if (!parsed.port) {
    //	// default the port number if not specified, for >= node v0.11.6...
    //	// https://github.com/joyent/node/issues/6199
    //	parsed.port = 80;
    //}
    if (parsed.protocol !== 'http:') {
        // only "http://" is supported, "https://" should use CONNECT method
        res.writeHead(400);
        res.end(`Only "http:" protocol prefix is supported (got: "${parsed.protocol}")\n`);
        return;
    }
    let gotResponse = false;
    const proxyReq = http.request({
        ...parsed,
        method: req.method,
        headers,
        localAddress: this.localAddress,
    });
    debug.proxyRequest('%s %s HTTP/1.1 ', proxyReq.method, proxyReq.path);
    proxyReq.on('response', function (proxyRes) {
        debug.proxyResponse('HTTP/1.1 %s', proxyRes.statusCode);
        gotResponse = true;
        const headers = {};
        for (const [key, value] of eachHeader(proxyRes)) {
            debug.proxyResponse('Proxy Response Header: "%s: %s"', key, value);
            if (isHopByHop.test(key)) {
                debug.response('ignoring hop-by-hop header "%s"', key);
            }
            else {
                const v = headers[key];
                if (Array.isArray(v)) {
                    v.push(value);
                }
                else if (null != v) {
                    headers[key] = [v, value];
                }
                else {
                    headers[key] = value;
                }
            }
        }
        debug.response('HTTP/1.1 %s', proxyRes.statusCode);
        res.writeHead(proxyRes.statusCode || 200, headers);
        proxyRes.pipe(res);
        res.on('finish', onfinish);
    });
    proxyReq.on('error', function (err) {
        debug.proxyResponse('proxy HTTP request "error" event\n%s', err.stack || err);
        cleanup();
        if (gotResponse) {
            debug.response('already sent a response, just destroying the socket...');
            socket.destroy();
        }
        else if ('ENOTFOUND' == err.code) {
            debug.response('HTTP/1.1 404 Not Found');
            res.writeHead(404);
            res.end();
        }
        else {
            debug.response('HTTP/1.1 500 Internal Server Error');
            res.writeHead(500);
            res.end();
        }
    });
    // if the client closes the connection prematurely,
    // then close the upstream socket
    function onclose() {
        debug.request('client socket "close" event, aborting HTTP request to "%s"', req.url);
        proxyReq.abort();
        cleanup();
    }
    socket.on('close', onclose);
    function onfinish() {
        debug.response('"finish" event');
        cleanup();
    }
    function cleanup() {
        debug.response('cleanup');
        socket.removeListener('close', onclose);
        res.removeListener('finish', onfinish);
    }
    req.pipe(proxyReq);
}
/**
 * HTTP CONNECT proxy requests.
 */
async function onconnect(req, socket, head) {
    debug.request('%s %s HTTP/%s ', req.method, req.url, req.httpVersion);
    (0, assert_1.default)(!head || 0 == head.length, '"head" should be empty for proxy requests');
    let res;
    let gotResponse = false;
    // define request socket event listeners
    socket.on('close', function onclientclose() {
        debug.request('HTTP request %s socket "close" event', req.url);
    });
    socket.on('end', function onclientend() {
        debug.request('HTTP request %s socket "end" event', req.url);
    });
    socket.on('error', function onclienterror(err) {
        debug.request('HTTP request %s socket "error" event:\n%s', req.url, err.stack || err);
    });
    // define target socket event listeners
    function ontargetclose() {
        debug.proxyResponse('proxy target %s "close" event', req.url);
        socket.destroy();
    }
    function ontargetend() {
        debug.proxyResponse('proxy target %s "end" event', req.url);
    }
    function ontargeterror(err) {
        debug.proxyResponse('proxy target %s "error" event:\n%s', req.url, err.stack || err);
        if (gotResponse) {
            debug.response('already sent a response, just destroying the socket...');
            socket.destroy();
        }
        else if (err.code === 'ENOTFOUND') {
            debug.response('HTTP/1.1 404 Not Found');
            if (res) {
                res.writeHead(404);
                res.end();
            }
        }
        else {
            debug.response('HTTP/1.1 500 Internal Server Error');
            if (res) {
                res.writeHead(500);
                res.end();
            }
        }
    }
    function ontargetconnect() {
        debug.proxyResponse('proxy target %s "connect" event', req.url);
        debug.response('HTTP/1.1 200 Connection established');
        gotResponse = true;
        if (res) {
            res.removeListener('finish', onfinish);
            res.writeHead(200, 'Connection established');
            res.flushHeaders();
            // relinquish control of the `socket` from the ServerResponse instance
            res.detachSocket(socket);
            // nullify the ServerResponse object, so that it can be cleaned
            // up before this socket proxying is completed
            res = null;
        }
        socket.pipe(target);
        target.pipe(socket);
    }
    // create the `res` instance for this request since Node.js
    // doesn't provide us with one :(
    res = new http.ServerResponse(req);
    res.shouldKeepAlive = false;
    res.chunkedEncoding = false;
    res.useChunkedEncodingByDefault = false;
    res.assignSocket(socket);
    // called for the ServerResponse's "finish" event
    // XXX: 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...
    function onfinish() {
        debug.response('response "finish" event');
        if (res) {
            res.detachSocket(socket);
        }
        socket.end();
    }
    res.once('finish', onfinish);
    // pause the socket during authentication so no data is lost
    socket.pause();
    try {
        const success = await authenticate(this, req);
        if (!success)
            return requestAuthorization(req, res);
    }
    catch (_err) {
        const err = _err;
        // an error occured during login!
        res.writeHead(500);
        res.end((err.stack || err.message || err) + '\n');
        return;
    }
    socket.resume();
    if (!req.url) {
        throw new TypeError('No "url" provided');
    }
    // `req.url` should look like "example.com:443"
    const lastColon = req.url.lastIndexOf(':');
    const host = req.url.substring(0, lastColon);
    const port = parseInt(req.url.substring(lastColon + 1), 10);
    const localAddress = this.localAddress;
    const opts = { host: host.replace(/^\[|\]$/g, ''), port, localAddress };
    debug.proxyRequest('connecting to proxy target %o', opts);
    const target = net.connect(opts);
    target.on('connect', ontargetconnect);
    target.on('close', ontargetclose);
    target.on('error', ontargeterror);
    target.on('end', ontargetend);
}
/**
 * Checks `Proxy-Authorization` request headers. Same logic applied to CONNECT
 * requests as well as regular HTTP requests.
 */
async function authenticate(server, req) {
    if (typeof server.authenticate === 'function') {
        debug.request('authenticating request "%s %s"', req.method, req.url);
        return server.authenticate(req);
    }
    // no `server.authenticate()` function, so just allow the request
    return true;
}
/**
 * Sends a "407 Proxy Authentication Required" HTTP response to the `socket`.
 */
function requestAuthorization(req, res) {
    // request Basic proxy authorization
    debug.response('requesting proxy authorization for "%s %s"', req.method, req.url);
    // TODO: make "realm" and "type" (Basic) be configurable...
    const realm = 'proxy';
    const headers = {
        'Proxy-Authenticate': 'Basic realm="' + realm + '"',
    };
    res.writeHead(407, headers);
    res.end();
}
//# sourceMappingURL=proxy.js.map