"use strict";
/*
Websockets Passes: Array of passes.

A `pass` is just a function that is executed on `req, socket, options`
so that you can easily add new checks while still keeping the base
flexible.

The names of passes are exported as WS_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;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WS_PASSES = void 0;
exports.numOpenSockets = numOpenSockets;
exports.checkMethodAndHeader = checkMethodAndHeader;
exports.XHeaders = XHeaders;
exports.stream = stream;
const http = __importStar(require("node:http"));
const https = __importStar(require("node:https"));
const common = __importStar(require("../common"));
const web_outgoing_1 = require("./web-outgoing");
const debug_1 = __importDefault(require("debug"));
const log = (0, debug_1.default)("http-proxy-3:ws-incoming");
const web_o = Object.values(web_outgoing_1.OUTGOING_PASSES);
function createSocketCounter(name) {
    let sockets = new Set();
    return ({ add, rm, } = {}) => {
        if (add) {
            if (!add.id) {
                add.id = Math.random();
            }
            if (!sockets.has(add.id)) {
                sockets.add(add.id);
            }
        }
        if (rm) {
            if (!rm.id) {
                rm.id = Math.random();
            }
            if (sockets.has(rm.id)) {
                sockets.delete(rm.id);
            }
        }
        log("socket counter:", { [name]: sockets.size }, add ? "add" : rm ? "rm" : "");
        return sockets.size;
    };
}
const socketCounter = createSocketCounter("socket");
const proxySocketCounter = createSocketCounter("proxySocket");
/* MockResponse
   when a websocket gets a regular HTTP Response,
   apply proxied headers
*/
class MockResponse {
    constructor() {
        this.headers = {};
        this.statusCode = 200;
        this.statusMessage = "";
    }
    setHeader(key, value) {
        this.headers[key] = value;
        return this;
    }
    ;
}
function numOpenSockets() {
    return socketCounter() + proxySocketCounter();
}
// WebSocket requests must have the `GET` method and
// the `upgrade:websocket` header
function checkMethodAndHeader(req, socket) {
    log("websocket: checkMethodAndHeader");
    if (req.method !== "GET" || !req.headers.upgrade) {
        socket.destroy();
        return true;
    }
    if (req.headers.upgrade.toLowerCase() !== "websocket") {
        socket.destroy();
        return true;
    }
}
// Sets `x-forwarded-*` headers if specified in config.
function XHeaders(req, _socket, options) {
    if (!options.xfwd)
        return;
    log("websocket: XHeaders");
    const values = {
        for: req.connection.remoteAddress || req.socket.remoteAddress,
        port: common.getPort(req),
        proto: common.hasEncryptedConnection(req) ? "wss" : "ws",
    };
    for (const header of ["for", "port", "proto"]) {
        req.headers["x-forwarded-" + header] =
            (req.headers["x-forwarded-" + header] || "") +
                (req.headers["x-forwarded-" + header] ? "," : "") +
                values[header];
    }
}
// Do the actual proxying. Make the request and upgrade it.
// Send the Switching Protocols request and pipe the sockets.
function stream(req, socket, options, head, server, cb) {
    log("websocket: new stream");
    const proxySockets = [];
    socketCounter({ add: socket });
    const cleanUpProxySockets = () => {
        for (const p of proxySockets) {
            p.end();
        }
    };
    socket.on("close", () => {
        socketCounter({ rm: socket });
        cleanUpProxySockets();
    });
    // The pipe below will end proxySocket if socket closes cleanly, but not
    // if it errors (eg, vanishes from the net and starts returning
    // EHOSTUNREACH). We need to do that explicitly.
    socket.on("error", cleanUpProxySockets);
    const createHttpHeader = (line, headers) => {
        return (Object.keys(headers)
            .reduce((head, key) => {
            const value = headers[key];
            if (!Array.isArray(value)) {
                head.push(key + ": " + value);
                return head;
            }
            for (let i = 0; i < value.length; i++) {
                head.push(key + ": " + value[i]);
            }
            return head;
        }, [line])
            .join("\r\n") + "\r\n\r\n");
    };
    common.setupSocket(socket);
    if (head && head.length) {
        socket.unshift(head);
    }
    // @ts-expect-error FIXME: options.target may be undefined
    const proto = common.isSSL.test(options.target.protocol) ? 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
    if (server) {
        server.emit("proxyReqWs", proxyReq, req, socket, options, head);
    }
    // Error Handler
    proxyReq.on("error", onOutgoingError);
    proxyReq.on("upgrade", (proxyRes, proxySocket, proxyHead) => {
        log("upgrade");
        proxySocketCounter({ add: proxySocket });
        proxySockets.push(proxySocket);
        proxySocket.on("close", () => {
            proxySocketCounter({ rm: proxySocket });
        });
        proxySocket.on("error", onOutgoingError);
        // Allow us to listen for when the websocket has completed.
        proxySocket.on("end", () => {
            server.emit("close", proxyRes, proxySocket, proxyHead);
        });
        proxySocket.on("close", () => {
            socket.end();
        });
        common.setupSocket(proxySocket);
        if (proxyHead && proxyHead.length) {
            proxySocket.unshift(proxyHead);
        }
        // Remark: Handle writing the headers to the socket when switching protocols
        // Also handles when a header is an array.
        socket.write(createHttpHeader("HTTP/1.1 101 Switching Protocols", proxyRes.headers));
        proxySocket.pipe(socket).pipe(proxySocket);
        server.emit("open", proxySocket);
    });
    function onOutgoingError(err) {
        if (cb) {
            cb(err, req, socket);
        }
        else {
            server.emit("error", err, req, socket);
        }
        // I changed this from "socket.end()" which is what node-http-proxy does to destroySoon() due to getting
        // the unit test "should close client socket if upstream is closed before upgrade" from lib/http-proxy.test.ts
        // to work.  Just doing socket.end() leaves things half open for a while if proxySocket errors out,
        // which may be another leak type situation and definitely doesn't work for unit testing.
        socket.destroySoon();
    }
    // if we get a response, backend is not a websocket endpoint,
    // relay HTTP response and close the socket
    proxyReq.on("response", (proxyRes) => {
        log("got non-ws HTTP response", {
            statusCode: proxyRes.statusCode,
            statusMessage: proxyRes.statusMessage,
        });
        const res = new MockResponse();
        for (const pass of web_o) {
            // note: none of these return anything
            pass(req, res, proxyRes, options);
        }
        // implement HTTP/1.1 chunked transfer unless content-length is defined
        // matches proxyRes.pipe(res) behavior,
        // but we are piping directly to the socket instead, so it's our job.
        let writeChunk = (chunk) => {
            socket.write(chunk);
        };
        if (req.httpVersion === "1.1" && proxyRes.headers["content-length"] === undefined) {
            res.headers["transfer-encoding"] = "chunked";
            writeChunk = (chunk) => {
                socket.write(chunk.length.toString(16));
                socket.write("\r\n");
                socket.write(chunk);
                socket.write("\r\n");
            };
        }
        const proxyHead = createHttpHeader(`HTTP/${req.httpVersion} ${proxyRes.statusCode} ${proxyRes.statusMessage}`, res.headers);
        if (!socket.destroyed) {
            socket.write(proxyHead);
            proxyRes.on("data", (chunk) => {
                writeChunk(chunk);
            });
            proxyRes.on("end", () => {
                writeChunk("");
                socket.destroySoon();
            });
        }
        else {
            // make sure response is consumed
            proxyRes.resume();
        }
    });
    proxyReq.end();
}
exports.WS_PASSES = { checkMethodAndHeader, XHeaders, stream };
