"use strict";
/*
A `pass` is just a function that is executed on `req, res, options`
so that you can easily add new checks while still keeping the base
flexible.

The names of passes are exported as WEB_PASSES from this module.

*/
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 () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.WEB_PASSES = void 0;
exports.deleteLength = deleteLength;
exports.timeout = timeout;
exports.XHeaders = XHeaders;
exports.stream = stream;
const http = __importStar(require("node:http"));
const https = __importStar(require("node:https"));
const followRedirects = __importStar(require("follow-redirects"));
const common = __importStar(require("../common"));
const web_outgoing_1 = require("./web-outgoing");
const node_stream_1 = require("node:stream");
const web_o = Object.values(web_outgoing_1.OUTGOING_PASSES);
const nativeAgents = { http, https };
//  Sets `content-length` to '0' if request is of DELETE type.
function deleteLength(req) {
    if ((req.method === "DELETE" || req.method === "OPTIONS") && !req.headers["content-length"]) {
        req.headers["content-length"] = "0";
        delete req.headers["transfer-encoding"];
    }
}
// Sets timeout in request socket if it was specified in options.
function timeout(req, _res, options) {
    if (options.timeout) {
        req.socket.setTimeout(options.timeout);
    }
}
// Sets `x-forwarded-*` headers if specified in config.
function XHeaders(req, _res, options) {
    if (!options.xfwd) {
        return;
    }
    const encrypted = common.hasEncryptedConnection(req);
    const values = {
        for: req.connection.remoteAddress || req.socket.remoteAddress,
        port: common.getPort(req),
        proto: encrypted ? "https" : "http",
    };
    for (const header of ["for", "port", "proto"]) {
        req.headers["x-forwarded-" + header] =
            (req.headers["x-forwarded-" + header] || "") + (req.headers["x-forwarded-" + header] ? "," : "") + values[header];
    }
    req.headers["x-forwarded-host"] = req.headers["x-forwarded-host"] || req.headers[":authority"] || req.headers["host"] || "";
}
// Does the actual proxying. If `forward` is enabled fires up
// a ForwardStream (there is NO RESPONSE), same happens for ProxyStream. The request
// just dies otherwise.
function stream(req, res, options, _, server, cb) {
    // And we begin!
    server.emit("start", req, res, options.target || options.forward);
    if (options.fetch || options.fetchOptions || process.env.FORCE_FETCH_PATH === "true") {
        return stream2(req, res, options, _, server, cb);
    }
    const agents = options.followRedirects ? followRedirects : nativeAgents;
    const http = agents.http;
    const https = agents.https;
    if (options.forward) {
        // forward enabled, so just pipe the request
        const proto = options.forward.protocol === "https:" ? https : http;
        const outgoingOptions = common.setupOutgoing(options.ssl || {}, options, req, "forward");
        const forwardReq = proto.request(outgoingOptions);
        // error handler (e.g. ECONNRESET, ECONNREFUSED)
        // Handle errors on incoming request as well as it makes sense to
        const forwardError = createErrorHandler(forwardReq, options.forward);
        req.on("error", forwardError);
        forwardReq.on("error", forwardError);
        (options.buffer || req).pipe(forwardReq);
        if (!options.target) {
            // no target, so we do not send anything back to the client.
            // If target is set, we do a separate proxy below, which might be to a
            // completely different server.
            return res.end();
        }
    }
    // Request initalization
    const proto = options.target.protocol === "https:" ? https : http;
    const outgoingOptions = common.setupOutgoing(options.ssl || {}, options, req);
    const proxyReq = proto.request(outgoingOptions);
    // Enable developers to modify the proxyReq before headers are sent
    proxyReq.on("socket", (socket) => {
        if (server && !proxyReq.getHeader("expect")) {
            server.emit("proxyReq", proxyReq, req, res, options, socket);
        }
    });
    // allow outgoing socket to timeout so that we could
    // show an error page at the initial request
    if (options.proxyTimeout) {
        proxyReq.setTimeout(options.proxyTimeout, () => {
            proxyReq.destroy();
        });
    }
    // Ensure we abort proxy if request is aborted
    res.on("close", () => {
        const aborted = !res.writableFinished;
        if (aborted) {
            proxyReq.destroy();
        }
    });
    // handle errors in proxy and incoming request, just like for forward proxy
    const proxyError = createErrorHandler(proxyReq, options.target);
    req.on("error", proxyError);
    proxyReq.on("error", proxyError);
    function createErrorHandler(proxyReq, url) {
        return (err) => {
            if (req.socket.destroyed && err.code === "ECONNRESET") {
                server.emit("econnreset", err, req, res, url);
                proxyReq.destroy();
                return;
            }
            if (cb) {
                cb(err, req, res, url);
            }
            else {
                server.emit("error", err, req, res, url);
            }
        };
    }
    (options.buffer || req).pipe(proxyReq);
    proxyReq.on("response", (proxyRes) => {
        server?.emit("proxyRes", proxyRes, req, res);
        if (!res.headersSent && !options.selfHandleResponse) {
            for (const pass of web_o) {
                // note: none of these return anything
                pass(req, res, proxyRes, options);
            }
        }
        if (!res.finished) {
            // Allow us to listen for when the proxy has completed
            proxyRes.on("end", () => {
                server?.emit("end", req, res, proxyRes);
            });
            // We pipe to the response unless its expected to be handled by the user
            if (!options.selfHandleResponse) {
                proxyRes.pipe(res);
            }
        }
        else {
            server?.emit("end", req, res, proxyRes);
        }
    });
}
async function stream2(req, res, options, _, server, cb) {
    // Helper function to handle errors consistently throughout the fetch path
    const handleError = (err, target) => {
        const e = err;
        // Copy code from cause if available and missing on err
        if (e.code === undefined && e.cause?.code) {
            e.code = e.cause.code;
        }
        if (cb) {
            cb(err, req, res, target);
        }
        else {
            server.emit("error", err, req, res, target);
        }
    };
    req.on("error", (err) => {
        if (req.socket.destroyed && err.code === "ECONNRESET") {
            const target = options.target || options.forward;
            if (target) {
                server.emit("econnreset", err, req, res, target);
            }
            return;
        }
        handleError(err);
    });
    const customFetch = options.fetch || fetch;
    const fetchOptions = options.fetchOptions ?? {};
    const prepareRequest = (outgoing) => {
        const requestOptions = {
            method: outgoing.method,
            ...fetchOptions.requestOptions,
        };
        const headers = new Headers(fetchOptions.requestOptions?.headers);
        if (!fetchOptions.requestOptions?.headers && outgoing.headers) {
            for (const [key, value] of Object.entries(outgoing.headers)) {
                if (typeof key === "string") {
                    if (Array.isArray(value)) {
                        for (const v of value) {
                            headers.append(key, v);
                        }
                    }
                    else if (value != null) {
                        headers.append(key, value);
                    }
                }
            }
        }
        if (options.auth) {
            headers.set("authorization", `Basic ${Buffer.from(options.auth).toString("base64")}`);
        }
        if (options.proxyTimeout) {
            requestOptions.signal = AbortSignal.timeout(options.proxyTimeout);
        }
        requestOptions.headers = headers;
        if (options.buffer) {
            requestOptions.body = options.buffer;
        }
        else if (req.method !== "GET" && req.method !== "HEAD") {
            requestOptions.body = req;
            requestOptions.duplex = "half";
        }
        return requestOptions;
    };
    if (options.forward) {
        const outgoingOptions = common.setupOutgoing(options.ssl || {}, options, req, "forward");
        const requestOptions = prepareRequest(outgoingOptions);
        let targetUrl = new URL(outgoingOptions.url).origin + outgoingOptions.path;
        if (targetUrl.startsWith("ws")) {
            targetUrl = targetUrl.replace("ws", "http");
        }
        // Call onBeforeRequest callback before making the forward request
        if (fetchOptions.onBeforeRequest) {
            try {
                await fetchOptions.onBeforeRequest(requestOptions, req, res, options);
            }
            catch (err) {
                handleError(err, options.forward);
                return;
            }
        }
        try {
            const result = await customFetch(targetUrl, requestOptions);
            // Call onAfterResponse callback for forward requests (though they typically don't expect responses)
            if (fetchOptions.onAfterResponse) {
                try {
                    await fetchOptions.onAfterResponse(result, req, res, options);
                }
                catch (err) {
                    handleError(err, options.forward);
                    return;
                }
            }
        }
        catch (err) {
            handleError(err, options.forward);
        }
        if (!options.target) {
            return res.end();
        }
    }
    const outgoingOptions = common.setupOutgoing(options.ssl || {}, options, req);
    const requestOptions = prepareRequest(outgoingOptions);
    let targetUrl = new URL(outgoingOptions.url).origin + outgoingOptions.path;
    if (targetUrl.startsWith("ws")) {
        targetUrl = targetUrl.replace("ws", "http");
    }
    // Call onBeforeRequest callback before making the request
    if (fetchOptions.onBeforeRequest) {
        try {
            await fetchOptions.onBeforeRequest(requestOptions, req, res, options);
        }
        catch (err) {
            handleError(err, options.target);
            return;
        }
    }
    try {
        const response = await customFetch(targetUrl, requestOptions);
        // Call onAfterResponse callback after receiving the response
        if (fetchOptions.onAfterResponse) {
            try {
                await fetchOptions.onAfterResponse(response, req, res, options);
            }
            catch (err) {
                handleError(err, options.target);
                return;
            }
        }
        // ProxyRes is used in the outgoing passes
        // But since only certain properties are used, we can fake it here
        // to avoid having to refactor everything.
        const fakeProxyRes = {
            statusCode: response.status,
            statusMessage: response.statusText,
            headers: Object.fromEntries(response.headers.entries()),
            rawHeaders: Object.entries(response.headers).flatMap(([key, value]) => {
                if (Array.isArray(value)) {
                    return value.flatMap((v) => (v != null ? [key, v] : []));
                }
                return value != null ? [key, value] : [];
            }),
        };
        server?.emit("proxyRes", fakeProxyRes, req, res);
        if (!res.headersSent && !options.selfHandleResponse) {
            for (const pass of web_o) {
                // note: none of these return anything
                pass(req, res, fakeProxyRes, options);
            }
        }
        if (!res.writableEnded) {
            // Allow us to listen for when the proxy has completed
            const nodeStream = response.body ? node_stream_1.Readable.from(response.body) : null;
            if (nodeStream) {
                nodeStream.on("error", (err) => {
                    handleError(err, options.target);
                });
                nodeStream.on("end", () => {
                    server?.emit("end", req, res, fakeProxyRes);
                });
                // We pipe to the response unless its expected to be handled by the user
                if (!options.selfHandleResponse) {
                    nodeStream.pipe(res, { end: true });
                }
                else {
                    nodeStream.resume();
                }
            }
            else {
                server?.emit("end", req, res, fakeProxyRes);
            }
        }
        else {
            server?.emit("end", req, res, fakeProxyRes);
        }
    }
    catch (err) {
        handleError(err, options.target);
    }
}
exports.WEB_PASSES = { deleteLength, timeout, XHeaders, stream };
