/* lib/terminal/server/transmission/transmit_ws - A command utility for creating a websocket server or client. */ import agent_management from "../services/agent_management.js";
import agent_status from "../services/agent_status.js";
import browser from "../../test/application/browser.js";
import common from "../../../common/common.js";
import error from "../../utilities/error.js";
import getAddress from "../../utilities/getAddress.js";
import hash from "../../commands/library/hash.js";
import log from "../../utilities/log.js";
import mask from "../../utilities/mask.js";
import network from "./network.js";
import node from "../../utilities/node.js";
import settings from "../services/settings.js";
import vars from "../../utilities/vars.js";
/**
 * The websocket library
 * ```typescript
 * interface transmit_ws {
 *     agentClose      : (socket:websocket_client) => void;                                     // A uniform way to notify browsers when a remote agent goes offline
 *     clientReceiver  : websocket_messageHandler;                                              // Processes data from regular agent websocket tunnels into JSON for processing by receiver library.
 *     createSocket    : (config:config_websocket_create) => void;                              // Creates a new socket for use by openAgent and openService methods.
 *     ipAttempts      : {
 *         device: {
 *             [key:string]: string[];
 *         };
 *         user: {
 *             [key:string]: string[];
 *         };
 *     };                                                                                       // stores connection attempts as a list of ip addresses by agent hash
 *     listener        : (socket:websocket_client) => void;                                     // A handler attached to each socket to listen for incoming messages.
 *     open: {
 *         agent:   (config:config_websocket_openAgent) => void;   // Opens a long-term socket tunnel between known agents.
 *         service: (config:config_websocket_openService) => void; // Opens a service specific tunnel that ends when the service completes.
 *     };                                                                                       // methods to open sockets according to different security contexts
 *     queue           : (body:Buffer|socketData, socket:socketClient, opcode:number) => void;  // Pushes outbound data into a managed queue to ensure data frames are not intermixed.
 *     queueSend       : (socket:websocket_client) => void;                                     // Pushes messages stored from the agent's offline queue into the transmission queue.
 *     server          : (config:config_websocket_server) => node_net_Server;                   // Creates a websocket server.
 *     socketExtensions: (config:config_websocket_extensions) => void;                          // applies application specific extensions to sockets
 *     socketMap       : socketMap;                                                             // Stores open socket status information for all devices.
 *     socketStore      : {
 *         [key:string]: websocket_list;
 *     };                                                                                       // A store of open sockets by agent type.
 *     statusUpdate    : (socketData:socketData) => void;                                       // Receive socket status list updates from other devices.
 * }
 * ``` */ const transmit_ws = {
    // handling an agent socket collapse
    agentClose: function terminal_server_transmission_transmitWs_agentClose(socket) {
        const type = socket.type, agent = vars.agents[type][socket.hash];
        // ensures restarting the application does not process close signals from a prior execution instance
        agent_status({
            data: {
                agent: socket.hash,
                agentType: type,
                broadcast: false,
                respond: false,
                status: "offline"
            },
            service: "agent-status"
        });
        if (vars.settings.verbose === true) {
            log([
                `${common.capitalize(socket.role)}-side socket ${vars.text.angry}closed${vars.text.none} for ${vars.text.underline + type + vars.text.none} ${vars.text.cyan + agent.name + vars.text.none}.`
            ]);
        }
        socket.status = "closed";
        socket.destroy();
        if (transmit_ws.socketStore[type] !== undefined) {
            delete transmit_ws.socketStore[type][socket.hash];
        }
        if (type === "device") {
            delete transmit_ws.socketMap[socket.hash];
        }
    },
    // composes fragments from browsers and agents into JSON for processing by receiver library
    clientReceiver: function terminal_server_transmission_transmitWs_clientReceiver(bufferData) {
        const decoder = new node.stringDecoder.StringDecoder("utf8"), result = decoder.end(bufferData);
        // prevent parsing errors in the case of malformed or empty payloads
        if (result.charAt(0) === "{" && result.charAt(result.length - 1) === "}" && result.indexOf("\"data\":") > 0 && result.indexOf("\"service\":") > 0) {
            network.receiver(JSON.parse(result), {
                // eslint-disable-next-line
                socket: this,
                type: "ws"
            });
        }
    },
    // creates a new socket as a client to a remote server
    createSocket: function terminal_server_transmission_transmitWs_createSocket(config) {
        if (config.ip === "") {
            // an empty string defaults to loopback, which creates an endless feedback loop
            return;
        }
        hash({
            algorithm: "sha1",
            callback: function terminal_server_transmission_transmitWs_createSocket_hash(title, hashOutput) {
                let a = 0;
                const len = config.headers.length, client = vars.settings.secure === true ? node.tls.connect({
                    host: config.ip,
                    port: config.port,
                    rejectUnauthorized: false
                }) : node.net.connect({
                    host: config.ip,
                    port: config.port
                }), headerHash = config.socketType === "device" ? vars.identity.hashDevice : config.socketType === "user" ? vars.identity.hashUser : config.hash, header = [
                    "GET / HTTP/1.1",
                    config.ip.indexOf(":") > -1 ? `Host: [${config.ip}]:${config.port}` : `Host: ${config.ip}:${config.port}`,
                    "Upgrade: websocket",
                    "Connection: Upgrade",
                    "Sec-WebSocket-Version: 13",
                    `type: ${config.socketType}`,
                    `hash: ${headerHash}`,
                    `Sec-WebSocket-Key: ${hashOutput.hash}`
                ], callbackError = function terminal_server_transmission_transmitWs_createSocket_hash_error(errorMessage) {
                    if (config.socketType === "device" || config.socketType === "user") {
                        transmit_ws.ipAttempts[config.socketType][config.hash].push(config.ip);
                        client.hash = config.hash;
                        client.type = config.socketType;
                        client.destroy();
                        transmit_ws.open.agent({
                            agent: client.hash,
                            agentType: client.type,
                            callback: config.callback
                        });
                    }
                    if (vars.settings.verbose === true || vars.test.type === "browser_device" || vars.test.type === "browser_user") {
                        error([
                            "Error attempting websocket connect from client side."
                        ], errorMessage);
                    }
                }, callbackReady = function terminal_server_transmission_transmitWs_createSocket_hash_ready() {
                    header.push("");
                    header.push("");
                    client.write(header.join("\r\n"));
                    client.once("data", function terminal_server_transmission_transmitWs_createSocket_hash_ready_data() {
                        if (config.socketType === "device" || config.socketType === "user") {
                            transmit_ws.ipAttempts[config.socketType][config.hash] = [];
                            vars.agents[config.socketType][config.hash].ipSelected = config.ip;
                            settings({
                                data: {
                                    settings: vars.agents[config.socketType],
                                    type: config.socketType
                                },
                                service: "settings"
                            });
                        }
                        transmit_ws.socketExtensions({
                            callback: config.callback,
                            handler: config.handler,
                            identifier: config.hash,
                            role: "client",
                            socket: client,
                            type: config.socketType
                        });
                    });
                };
                if (len > 0) {
                    do {
                        header.push(config.headers[a]);
                        a = a + 1;
                    }while (a < len)
                }
                client.once("error", callbackError);
                if (config.socketType === "device" || config.socketType === "user") {
                    mask.mask(vars.agents[config.socketType][config.hash].secret, null, function terminal_server_transmission_transmitWs_createSocket_hash_maskDevice(key) {
                        header.push(`challenge: ${key}`);
                        client.once("ready", callbackReady);
                    });
                } else {
                    client.once("ready", callbackReady);
                }
            },
            digest: "base64",
            directInput: true,
            id: null,
            list: false,
            parent: null,
            source: Buffer.from(Math.random().toString(), "base64").toString(),
            stat: null
        });
    },
    getSocket: function terminal_server_transmission_transmitWs_getSocket(type, name) {
        if (transmit_ws.socketStore[type] === undefined || transmit_ws.socketStore[type] === null || transmit_ws.socketStore[type][name] === undefined || transmit_ws.socketStore[type][name] === null) {
            return null;
        }
        return transmit_ws.socketStore[type][name];
    },
    getSocketList: function terminal_server_transmission_transmitWs_getSocketList(type) {
        if (transmit_ws.socketStore[type] === undefined || transmit_ws.socketStore[type] === null) {
            return [];
        }
        const keys = Object.keys(transmit_ws.socketStore[type]), output = [];
        let index = keys.length;
        if (index > 0) {
            do {
                index = index - 1;
                output.push(transmit_ws.socketStore[type][keys[index]]);
            }while (index > 0)
        }
        return output;
    },
    ipAttempts: {
        device: {},
        user: {}
    },
    // processes incoming service data for agent sockets
    listener: function terminal_server_transmission_transmitWs_listener(socket) {
        const processor = function terminal_server_transmission_transmitWs_listener_processor(buf) {
            //    RFC 6455, 5.2.  Base Framing Protocol
            //     0                   1                   2                   3
            //     0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
            //    +-+-+-+-+-------+-+-------------+-------------------------------+
            //    |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
            //    |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
            //    |N|V|V|V|       |S|             |   (if payload len==126/127)   |
            //    | |1|2|3|       |K|             |                               |
            //    +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
            //    |     Extended payload length continued, if payload len == 127  |
            //    + - - - - - - - - - - - - - - - +-------------------------------+
            //    |                               |Masking-key, if MASK set to 1  |
            //    +-------------------------------+-------------------------------+
            //    | Masking-key (continued)       |          Payload Data         |
            //    +-------------------------------- - - - - - - - - - - - - - - - +
            //    :                     Payload Data continued ...                :
            //    + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
            //    |                     Payload Data continued ...                |
            //    +---------------------------------------------------------------+
            const data = function terminal_server_transmission_transmitWs_listener_processor_data() {
                if (buf !== null && buf !== undefined) {
                    socket.frame = Buffer.concat([
                        socket.frame,
                        buf
                    ]);
                }
                if (socket.frame.length < 2) {
                    return null;
                }
                return socket.frame;
            }(), extended = function terminal_server_transmission_transmitWs_listener_processor_extended(input) {
                const mask = input[1] > 127, len = mask === true ? input[1] - 128 : input[1], keyOffset = mask === true ? 4 : 0;
                if (len < 126) {
                    return {
                        lengthExtended: len,
                        lengthShort: len,
                        mask: mask,
                        startByte: 2 + keyOffset
                    };
                }
                if (len < 127) {
                    return {
                        lengthExtended: input.subarray(2, 4).readUInt16BE(0),
                        lengthShort: len,
                        mask: mask,
                        startByte: 4 + keyOffset
                    };
                }
                return {
                    lengthExtended: input.subarray(4, 10).readUIntBE(0, 6),
                    lengthShort: len,
                    mask: mask,
                    startByte: 10 + keyOffset
                };
            }, frame = function terminal_server_transmission_transmitWs_listener_processor_frame() {
                if (data === null) {
                    return null;
                }
                const bits0 = data[0].toString(2).padStart(8, "0"), meta = extended(data);
                return {
                    fin: data[0] > 127,
                    rsv1: bits0.charAt(1) === "1",
                    rsv2: bits0.charAt(2) === "1",
                    rsv3: bits0.charAt(3) === "1",
                    opcode: Number(bits0.charAt(4)) * 8 + Number(bits0.charAt(5)) * 4 + Number(bits0.charAt(6)) * 2 + Number(bits0.charAt(7)),
                    mask: meta.mask,
                    len: meta.lengthShort,
                    extended: meta.lengthExtended,
                    maskKey: meta.mask === true ? data.subarray(meta.startByte - 4, meta.startByte) : null,
                    startByte: meta.startByte
                };
            }(), unmask = function terminal_server_transmission_transmitWs_listener_processor_unmask(input) {
                if (frame.mask === true) {
                    // RFC 6455, 5.3.  Client-to-Server Masking
                    // j                   = i MOD 4
                    // transformed-octet-i = original-octet-i XOR masking-key-octet-j
                    input.forEach(function terminal_server_transmission_transmitWs_listener_processor_unmask_each(value, index) {
                        input[index] = value ^ frame.maskKey[index % 4];
                    });
                }
                return input;
            }, payload = function terminal_server_transmission_transmitWs_listener_processor_payload() {
                // Payload processing must contend with these 4 constraints:
                // 1. Message Fragmentation - RFC6455 allows messages to be fragmented from a single transmission into multiple transmission frames independently sent and received.
                // 2. Header Separation     - Firefox sends frame headers separated from frame bodies.
                // 3. Node Concatenation    - If Node.js receives message frames too quickly the various binary buffers are concatenated into a single deliverable to the processing application.
                // 4. TLS Max Packet Size   - TLS forces a maximum payload size of 65536 bytes.
                if (frame === null) {
                    return null;
                }
                let complete = null;
                const size = frame.extended + frame.startByte, len = socket.frame.length;
                if (len < size) {
                    return null;
                }
                complete = unmask(socket.frame.subarray(frame.startByte, size));
                socket.frame = socket.frame.subarray(size);
                network.logger({
                    direction: "receive",
                    size: data.length,
                    socketData: {
                        data: complete,
                        service: "response-no-action"
                    },
                    transmit: {
                        socket: socket,
                        type: "ws"
                    }
                });
                return complete;
            }();
            if (payload === null) {
                return;
            }
            if (frame.opcode === 8) {
                // socket close
                data[0] = 136;
                data[1] = data[1] > 127 ? data[1] - 128 : data[1];
                const payload = Buffer.concat([
                    data.subarray(0, 2),
                    unmask(data.subarray(2))
                ]);
                socket.write(payload);
                socket.off("data", processor);
                if (socket.type === "browser") {
                    delete transmit_ws.socketStore.browser[socket.hash];
                    socket.destroy();
                } else if (socket.type === "device" || socket.type === "user") {
                    transmit_ws.agentClose(socket);
                }
            } else if (frame.opcode === 9) {
                // respond to "ping" as "pong"
                transmit_ws.queue(data.subarray(frame.startByte), socket, 10);
            } else if (frame.opcode === 10) {
                // pong
                const payloadString = payload.toString(), pong = socket.pong[payloadString], time = process.hrtime.bigint();
                if (pong !== undefined) {
                    if (time < pong.start + pong.ttl) {
                        clearTimeout(pong.timeOut);
                        pong.callback(null, time - pong.start);
                    }
                    delete socket.pong[payloadString];
                }
            } else {
                const segment = Buffer.concat([
                    socket.fragment,
                    payload
                ]);
                // this block may include frame.opcode === 0 - a continuation frame
                socket.frameExtended = frame.extended;
                if (frame.fin === true) {
                    socket.handler(segment.subarray(0, socket.frameExtended));
                    socket.fragment = segment.subarray(socket.frameExtended);
                } else {
                    socket.fragment = segment;
                }
            }
            if (socket.frame.length > 2) {
                terminal_server_transmission_transmitWs_listener_processor(null);
            }
        };
        socket.on("data", processor);
    },
    open: {
        // open a long-term websocket tunnel between known agents
        agent: function terminal_server_transmission_transmitWs_openAgent(config) {
            if (vars.settings.secure === true || vars.test.type.indexOf("browser_") === 0) {
                if (vars.agents[config.agentType][config.agent] === undefined) {
                    if (config.callback !== null) {
                        config.callback(null);
                    }
                    return;
                }
                const socket = transmit_ws.getSocket(config.agentType, config.agent), agent = vars.agents[config.agentType][config.agent], attempts = transmit_ws.ipAttempts[config.agentType][config.agent] === undefined ? function terminal_server_transmission_transmitWs_openAgent_ip_ipList_attempts() {
                    transmit_ws.ipAttempts[config.agentType][config.agent] = [];
                    return transmit_ws.ipAttempts[config.agentType][config.agent];
                }() : transmit_ws.ipAttempts[config.agentType][config.agent], selfIP = vars.network.addresses, ip = function terminal_server_transmission_transmitWs_openAgent_ip() {
                    const ipList = function terminal_server_transmission_transmitWs_openAgent_ip_ipList(type) {
                        let a = agent.ipAll[type].length;
                        if (a > 0) {
                            do {
                                a = a - 1;
                                if (selfIP[type].indexOf(agent.ipAll[type][a]) > -1) {
                                    // don't bother attempting to send to an IP address present on the local device (ip collision)
                                    attempts.push(agent.ipAll[type][a]);
                                } else if (attempts.indexOf(agent.ipAll[type][a]) < 0) {
                                    return agent.ipAll[type][a];
                                }
                            }while (a > 0)
                        }
                        return null;
                    }, IPv6 = ipList("IPv6");
                    if (IPv6 === null) {
                        return ipList("IPv4");
                    }
                    if (attempts.length < 1) {
                        return agent.ipSelected;
                    }
                    return IPv6;
                }();
                if (agent === undefined) {
                    if (config.callback !== null) {
                        config.callback(null);
                    }
                    return;
                }
                if (socket !== null) {
                    if (config.callback !== null) {
                        config.callback(socket);
                    }
                    return;
                }
                if (transmit_ws.ipAttempts[config.agentType][config.agent] === undefined) {
                    transmit_ws.ipAttempts[config.agentType][config.agent] = [];
                }
                if (ip === null) {
                    const status = {
                        agent: config.agent,
                        agentType: config.agentType,
                        broadcast: false,
                        respond: false,
                        status: "offline"
                    };
                    agent.status = "offline";
                    transmit_ws.ipAttempts[config.agentType][config.agent] = [];
                    network.send({
                        data: status,
                        service: "agent-status"
                    }, "browser");
                } else {
                    transmit_ws.createSocket({
                        callback: config.callback,
                        headers: [],
                        hash: config.agent,
                        handler: transmit_ws.clientReceiver,
                        ip: ip,
                        port: agent.ports.ws,
                        socketType: config.agentType
                    });
                }
            }
        },
        // opens a service specific websocket tunnel between two points that closes when the service ends
        service: function terminal_server_transmission_transmitWs_openService(config) {
            transmit_ws.createSocket({
                callback: config.callback,
                handler: config.handler,
                headers: [],
                hash: config.hash,
                ip: config.ip,
                port: config.port,
                socketType: config.socketType
            });
        }
    },
    // manages queues, because websocket protocol limits one transmission per session in each direction
    queue: function terminal_server_transmission_transmitWs_queue(body, socketItem, opcode) {
        const writeSocket = vars.environment.command === "perf" && opcode === 2 ? false : true, writeFrame = function terminal_server_transmission_transmitWs_queue_writeFrame() {
            const writeCallback = function terminal_server_transmission_transmitWs_queue_writeFrame_writeCallback() {
                socketItem.queue.splice(0, 1);
                if (socketItem.queue.length > 0) {
                    terminal_server_transmission_transmitWs_queue_writeFrame();
                } else {
                    socketItem.status = "open";
                }
            };
            if (socketItem.status === "open") {
                socketItem.status = "pending";
            }
            if (writeSocket === true) {
                if (socketItem.write(socketItem.queue[0]) === true) {
                    writeCallback();
                } else {
                    socketItem.once("drain", writeCallback);
                }
            } else {
                writeCallback();
            }
        }, socketData = body, isBuffer = socketItem === undefined || socketItem === null ? false : socketData.service === undefined, stringBody = isBuffer === true ? null : JSON.stringify(socketData);
        let dataPackage = isBuffer === true ? body : Buffer.from(stringBody);
        if (socketItem === undefined || socketItem === null) {
            return;
        }
        // verify that payload to a user does not contain any device identifiers
        if (isBuffer === false && socketItem.type === "user") {
            const devices = Object.keys(vars.agents.device), violations = [
                `${vars.text.angry}Sending the following device identifier(s) on user socket at the indicated string index(es):${vars.text.none}`,
                `Service: ${vars.text.angry + socketData.service + vars.text.none}`,
                ""
            ], filler = "x".repeat(128);
            let index = devices.length, position = -1, tempString = stringBody;
            do {
                index = index - 1;
                position = tempString.indexOf(devices[index]);
                if (position > -1) {
                    do {
                        violations.push(`${vars.text.angry}*${vars.text.none} ${position} - ${devices[index]}`);
                        tempString = tempString.replace(devices[index], filler);
                        position = tempString.indexOf(devices[index]);
                    }while (position > -1)
                }
            }while (index > 0)
            if (violations.length > 3) {
                violations.push("");
                violations.push(stringBody);
                log(violations);
                return;
            }
        }
        // OPCODES
        // ## Messages
        // 0 - continuation - fragments of a message payload following an initial fragment
        // 1 - text message
        // 2 - binary message
        // 3-7 - reserved for future use
        //
        // ## Control Frames
        // 8 - close, the remote is destroying the socket
        // 9 - ping, a connectivity health check
        // a - pong, a response to a ping
        // b-f - reserved for future use
        //
        // ## Notes
        // * Message frame fragments must be transmitted in order and not interleaved with other messages.
        // * Message types may be supplied as buffer or socketData types, but will always be transmitted as buffers.
        // * Control frames are always granted priority and may occur between fragments of a single message.
        // * Control frames will always be supplied as buffer data types.
        //
        // ## Masking
        // * All traffic coming from the browser will be websocket masked.
        // * I have not tested if the browsers will process masked data as they shouldn't according to RFC 6455.
        // * This application supports both masked and unmasked transmission so long as the mask bit is set and a 32bit mask key is supplied.
        // * Mask bit is set as payload length (up to 127) + 128 assigned to frame header second byte.
        // * Mask key is first 4 bytes following payload length bytes (if any).
        if (opcode === 1 || opcode === 2 || opcode === 3 || opcode === 4 || opcode === 5 || opcode === 6 || opcode === 7) {
            const fragmentSize = socketItem.type === "browser" ? 0 : 1e6, op = isBuffer === true ? 2 : 1, fragmentation = function terminal_server_transmission_transmitWs_queue_fragmentation(first) {
                let finish = false;
                const frameBody = function terminal_server_transmission_transmitWs_queue_fragmentation_frameBody() {
                    if (fragmentSize < 1 || len === fragmentSize) {
                        finish = true;
                        return dataPackage;
                    }
                    const fragment = dataPackage.subarray(0, fragmentSize);
                    dataPackage = dataPackage.subarray(fragmentSize);
                    len = dataPackage.length;
                    if (len < fragmentSize) {
                        finish = true;
                    }
                    return fragment;
                }(), size = frameBody.length, frameHeader = function terminal_server_transmission_transmitWs_queue_fragmentation_frameHeader() {
                    // frame 0 is:
                    // * 128 bits for fin, 0 for unfinished plus opcode
                    // * opcode 0 - continuation of fragments
                    // * opcode 1 - text (total payload must be UTF8 and probably not contain hidden control characters)
                    // * opcode 2 - supposed to be binary, really anything that isn't 100& UTF8 text
                    // ** for fragmented data only first data frame gets a data opcode, others receive 0 (continuity)
                    const frame = size < 126 ? Buffer.alloc(2) : size < 65536 ? Buffer.alloc(4) : Buffer.alloc(10);
                    frame[0] = finish === true ? first === true ? 128 + op : 128 : first === true ? op : 0;
                    // frame 1 is length flag
                    frame[1] = size < 126 ? size : size < 65536 ? 126 : 127;
                    if (size > 125) {
                        if (size < 65536) {
                            frame.writeUInt16BE(size, 2);
                        } else {
                            frame.writeUIntBE(size, 4, 6);
                        }
                    }
                    return frame;
                }();
                socketItem.queue.push(Buffer.concat([
                    frameHeader,
                    frameBody
                ]));
                if (finish === true) {
                    if (socketItem.status === "open") {
                        writeFrame();
                    }
                } else {
                    terminal_server_transmission_transmitWs_queue_fragmentation(false);
                }
            };
            let len = dataPackage.length;
            network.logger({
                direction: "send",
                size: dataPackage.length,
                socketData: {
                    data: dataPackage,
                    service: isBuffer === true ? "response-no-action" : socketData.service
                },
                transmit: {
                    socket: socketItem,
                    type: "ws"
                }
            });
            fragmentation(true);
        } else if (opcode === 8 || opcode === 9 || opcode === 10 || opcode === 11 || opcode === 12 || opcode === 13 || opcode === 14 || opcode === 15) {
            const frameHeader = Buffer.alloc(2), frameBody = dataPackage.subarray(0, 125);
            frameHeader[0] = 128 + opcode;
            frameHeader[1] = frameBody.length;
            socketItem.queue.unshift(Buffer.concat([
                frameHeader,
                frameBody
            ]));
            network.logger({
                direction: "send",
                size: frameHeader[1] + 2,
                socketData: {
                    data: dataPackage,
                    service: "response-no-action"
                },
                transmit: {
                    socket: socketItem,
                    type: "ws"
                }
            });
            if (socketItem.status === "open") {
                writeFrame();
            }
        } else {
            error([
                `Error queueing message for socket transmission. Opcode ${vars.text.angry + String(opcode) + vars.text.none} is not supported.`
            ], null);
        }
    },
    // push an offline agent message queue into the transmission queue
    queueSend: function terminal_server_transmission_transmitWs_queueSend(socket) {
        const type = socket.type, queue = vars.settings.queue[type][socket.hash];
        if (queue !== undefined && queue.length > 0 && vars.test.type === "") {
            do {
                transmit_ws.queue(queue[0], socket, 1);
                queue.splice(0, 1);
            }while (queue.length > 0)
            const settingsData = {
                settings: vars.settings.queue,
                type: "queue"
            };
            settings({
                data: settingsData,
                service: "settings"
            });
        }
    },
    // websocket server and data receiver
    server: function terminal_server_transmission_transmitWs_server(config) {
        const connection = function terminal_server_transmission_transmitWs_server_connection(TLS_socket) {
            const socket = TLS_socket, handshake = function terminal_server_transmission_transmitWs_server_connection_handshake(data) {
                let browserType = null, hashKey = null, hashName = null, nonceHeader = null, type = null, userAgent = null;
                const dataString = data.toString(), testNonce = /^Sec-WebSocket-Protocol:\s*\w+-/, headerList = dataString.split("\r\n"), flags = {
                    hash: false,
                    key: false,
                    type: false,
                    userAgent: false
                }, headers = function terminal_server_transmission_transmitWs_server_connection_handshake_headers() {
                    const clientRespond = function terminal_server_transmission_transmitWs_server_connection_handshake_headers_clientRespond() {
                        const headers = [
                            "HTTP/1.1 101 Switching Protocols",
                            "Upgrade: websocket",
                            "Connection: Upgrade",
                            `Sec-WebSocket-Accept: ${hashKey}`
                        ];
                        if (type === "browser") {
                            headers.push(nonceHeader);
                        } else if (type === "testRemote") {
                            headers.push(`hash: ${hashName}`);
                        } else if (socket.type === "device" || socket.type === "user") {
                            const agent = vars.agents[socket.type][hashName];
                            if (agent === undefined || agent === null) {
                                socket.destroy();
                                return;
                            }
                            // provide all manners of notification
                            if (vars.settings.verbose === true) {
                                log([
                                    `Server-side socket ${vars.text.green + vars.text.bold}established${vars.text.none} for ${vars.text.underline + type + vars.text.none} ${vars.text.cyan + agent.name + vars.text.none}.`
                                ]);
                            }
                        }
                        headers.push("");
                        headers.push("");
                        socket.write(headers.join("\r\n"));
                        if (socket.type === "device" || socket.type === "user") {
                            // process offline message queues
                            transmit_ws.queueSend(socket);
                        }
                    }, testReset = function terminal_serveR_transmission_transmitWs_server_connection_handshake_headers_testReset() {
                        socket.write(hashName);
                        browser.methods.reset();
                    };
                    // some complexity is present because browsers will not have a "hash" heading
                    if (flags.type === true && flags.key === true && (type === "browser" && flags.userAgent === true || flags.hash === true)) {
                        const headerComplete = function terminal_serveR_transmission_transmitWs_server_connection_handshake_headers_headerComplete(test) {
                            const identifier = type === "browser" ? `${userAgent}-${browserType}-${hashKey}` : type === "testRemote" && vars.test.type === "browser_remote" ? "self" : hashName;
                            if (test === false || (type === "device" || type === "user") && transmit_ws.getSocket(type, identifier) !== null) {
                                socket.destroy();
                                return;
                            }
                            transmit_ws.socketExtensions({
                                callback: type === "testRemote" ? testReset : clientRespond,
                                handler: transmit_ws.clientReceiver,
                                identifier: identifier,
                                role: "server",
                                socket: socket,
                                type: type
                            });
                        };
                        if (type === "testRemote" && vars.test.type !== "browser_remote") {
                            socket.destroy();
                            return;
                        }
                        if (type === "device" || type === "user") {
                            if (vars.agents[type][hashName] === undefined || typeof vars.identity.hashDevice !== "string" || vars.identity.hashDevice.length !== 128) {
                                socket.destroy();
                                return;
                            }
                            const token = type === "device" ? vars.identity.secretDevice : vars.identity.secretUser;
                            let challenge = dataString.slice(dataString.indexOf("\r\nchallenge:")).replace(/(\s+challenge:\s*)/, "");
                            challenge = challenge.slice(0, challenge.indexOf("\r")).replace(/\s+/g, "");
                            mask.unmaskToken(challenge, token, headerComplete);
                        } else {
                            headerComplete(true);
                        }
                    }
                }, headerEach = function terminal_server_transmission_transmitWs_server_connection_handshake_headerEach(header) {
                    if (header.indexOf("Sec-WebSocket-Key") === 0) {
                        const key = header.slice(header.indexOf("-Key:") + 5).replace(/\s/g, "") + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
                        hash({
                            algorithm: "sha1",
                            callback: function terminal_server_transmission_transmitWs_server_connection_handshake_headerEach_callback(title, hashOutput) {
                                flags.key = true;
                                hashKey = hashOutput.hash;
                                headers();
                            },
                            digest: "base64",
                            directInput: true,
                            id: null,
                            list: false,
                            parent: null,
                            source: key,
                            stat: null
                        });
                    } else if (header.indexOf("User-Agent:") === 0) {
                        const subDec = /\s\w+\/\d+\.\d+\.\d+/g, browserName = function terminal_server_transmission_transmitWs_server_connection_handshake_headerEach_browserName(input) {
                            userAgent = input.slice(0, input.indexOf(".")).replace(/^\s+/, "");
                            return "";
                        }, ua = function terminal_server_transmission_transmitWs_server_connection_handshake_headerEach_ua() {
                            let matches = null;
                            header = header.replace(/User-Agent:\s+/, "");
                            header = header.slice(header.lastIndexOf(")"));
                            matches = header.match(subDec);
                            if (matches === null) {
                                return null;
                            }
                            return matches[matches.length - 1];
                        }();
                        if (ua === null) {
                            header.replace(/\s\w+\/\d+\.\d+/, browserName);
                        } else {
                            ua.replace(ua, browserName);
                        }
                        flags.userAgent = true;
                        headers();
                    } else if (header.indexOf("hash:") === 0) {
                        hashName = header.replace(/hash:\s+/, "");
                        flags.hash = true;
                        headers();
                    } else if (header.indexOf("type:") === 0) {
                        type = header.replace(/type:\s+/, "");
                        if (header.replace(/type:\s+/, "") === "perf" && (socket.remoteAddress === "::1" || socket.remoteAddress === "127.0.0.1")) {
                            flags.hash = true;
                        }
                        flags.type = true;
                        headers();
                    } else if (testNonce.test(header) === true) {
                        const noSpace = header.replace(/\s+/g, "").replace(testNonce, "");
                        if (noSpace === vars.identity.hashDevice || noSpace === "test-browser" && vars.test.type.indexOf("browser_") === 0) {
                            flags.type = true;
                            nonceHeader = header;
                            type = "browser";
                            header = header.slice(header.indexOf(":")).replace(/^:\s+/, "");
                            browserType = header.slice(0, header.indexOf("-"));
                            headers();
                        } else {
                            socket.destroy();
                        }
                    }
                };
                headerList.forEach(headerEach);
            };
            socket.once("data", handshake);
        }, wsServer = vars.settings.secure === true && config.options !== null ? node.tls.createServer({
            ca: config.options.options.ca,
            cert: config.options.options.cert,
            key: config.options.options.key
        }, connection) : node.net.createServer(), listenerCallback = function terminal_server_transmission_transmitWs_server_listenerCallback() {
            config.callback(wsServer.address());
        };
        if (vars.settings.secure === false) {
            wsServer.on("connection", connection);
        }
        if (config.host === "") {
            wsServer.listen({
                port: config.port
            }, listenerCallback);
        } else {
            wsServer.listen({
                host: config.host,
                port: config.port
            }, listenerCallback);
        }
        return wsServer;
    },
    // adds custom properties necessary to this application to newly created sockets
    socketExtensions: function terminal_server_transmission_transmitWs_socketExtension(config) {
        if (// permit test remote device
        config.type === "testRemote" && vars.test.type.indexOf("browser_") === 0 || // permit perf tests
        config.type === "perf" && (config.socket.remoteAddress === "::1" || config.socket.remoteAddress.replace("::ffff:", "") === "127.0.0.1") || // permit if the socket is not already created
        transmit_ws.getSocket(config.type, config.identifier) === null) {
            const ping = function terminal_server_transmission_transmitWs_socketExtension_ping(ttl, callback) {
                const errorObject = function terminal_server_transmission_transmitWs_socketExtension_ping_errorObject(code, message) {
                    const err = new Error(), agent = config.socket.type === "browser" ? null : vars.agents[config.socket.type][config.socket.hash], name = config.socket.type === "browser" ? config.socket.hash : agent === null || agent === undefined ? "unknown socket" : agent.name;
                    err.code = code;
                    err.message = `${message} Socket type ${config.socket.type} and name ${name}.`;
                    return err;
                };
                if (config.socket.status !== "open") {
                    callback(errorObject("ECONNABORTED", "Ping error on websocket without 'open' status."), null);
                } else {
                    const nameSlice = config.socket.hash.slice(0, 125);
                    // send ping
                    transmit_ws.queue(Buffer.from(nameSlice), config.socket, 9);
                    config.socket.pong[nameSlice] = {
                        callback: callback,
                        start: process.hrtime.bigint(),
                        timeOut: setTimeout(function terminal_server_transmission_transmitWs_socketExtension_ping_delay() {
                            callback(config.socket.pong[nameSlice].timeOutMessage, null);
                            delete config.socket.pong[nameSlice];
                        }, ttl),
                        timeOutMessage: errorObject("ETIMEDOUT", "Ping timeout on websocket."),
                        ttl: BigInt(ttl * 1e6)
                    };
                }
            }, list = function terminal_server_transmission_transmitWs_socketExtension_list() {
                const list = [], keysPrimary = Object.keys(transmit_ws.socketStore), // populate data from sockets that are children of a group
                child = function terminal_server_transmission_transmitWs_socketExtension_list_child() {
                    // @ts-ignore
                    const socketList = transmit_ws.getSocketList(keysPrimary[indexPrimary]);
                    let indexChild = socketList.length, socketItem, address = null;
                    if (indexChild < 1) {
                        return;
                    }
                    do {
                        indexChild = indexChild - 1;
                        socketItem = socketList[indexChild];
                        address = getAddress({
                            socket: socketItem,
                            type: "ws"
                        });
                        list.push({
                            localAddress: address.local.address,
                            localPort: address.local.port,
                            name: socketItem.hash,
                            remoteAddress: address.remote.address,
                            remotePort: address.remote.port,
                            status: socketItem.status,
                            type: keysPrimary[indexPrimary]
                        });
                    }while (indexChild > 0)
                };
                // loop through the socket groups
                let indexPrimary = keysPrimary.length;
                if (vars.identity.hashDevice === "") {
                    return;
                }
                do {
                    indexPrimary = indexPrimary - 1;
                    child();
                }while (indexPrimary > 0)
                // sort the raw data by group name
                list.sort(function terminal_server_transmission_transmitWs_socketExtension_list_sort(a, b) {
                    if (a.type < b.type) {
                        return -1;
                    }
                    return 1;
                });
                transmit_ws.socketMap[vars.identity.hashDevice] = list;
                network.send({
                    data: transmit_ws.socketMap,
                    service: "socket-map"
                }, "browser");
                network.send({
                    data: {
                        [vars.identity.hashDevice]: transmit_ws.socketMap[vars.identity.hashDevice]
                    },
                    service: "socket-map"
                }, "device");
            }, socketEnd = function terminal_server_transmission_transmitWs_socketExtension_socketEnd() {
                config.socket.status = "end";
                list();
            }, socketError = function terminal_server_transmission_transmitWs_socketExtension_socketError(errorMessage) {
                if (vars.settings.verbose === true || vars.test.type === "browser_device" || vars.test.type === "browser_user") {
                    error([
                        `Error on socket of type ${config.socket.type} at location ${config.socket.role} with identifier ${config.socket.hash}.`,
                        JSON.stringify(errorMessage),
                        JSON.stringify(getAddress({
                            socket: config.socket,
                            type: "ws"
                        }))
                    ], null);
                }
                if (config.socket.type === "device" || config.socket.type === "user") {
                    transmit_ws.agentClose(config.socket);
                }
                list();
            };
            config.socket.fragment = Buffer.from([]); // storehouse of complete data frames, which will comprise a frame header and payload body that may be fragmented
            config.socket.frame = Buffer.from([]); // stores pieces of frames, which can be divided due to TLS decoding or header separation from some browsers
            config.socket.frameExtended = 0; // stores the payload size of a given message payload as derived from the extended size bytes of a frame header
            config.socket.hash = config.identifier; // assigns a unique identifier to the socket based upon the socket's credentials
            config.socket.handler = config.handler; //  assigns an event handler to process incoming messages
            config.socket.ping = ping; // provides a means to insert a ping control frame and measure the round trip time of the returned pong frame
            config.socket.pong = {}; // stores termination times and callbacks for pong handling
            config.socket.queue = []; // stores messages for transmit, because websocket protocol cannot intermix messages
            config.socket.role = config.role; // assigns socket creation location
            config.socket.setKeepAlive(true, 0); // standard method to retain socket against timeouts from inactivity until a close frame comes in
            config.socket.status = "open"; // sets the status flag for the socket
            config.socket.type = config.type; // assigns the type name on the socket
            if (transmit_ws.socketStore[config.type] === undefined) {
                transmit_ws.socketStore[config.type] = {};
            }
            if (config.type === "browser" || config.type === "device" || config.type === "testRemote" || config.type === "user") {
                if (config.type !== "browser" || config.type === "browser" && config.identifier.indexOf("-primary-") > 0) {
                    transmit_ws.socketStore[config.type][config.identifier] = config.socket;
                } else {
                    if (transmit_ws.socketStore.browserOther === undefined) {
                        transmit_ws.socketStore.browserOther = {};
                    }
                    transmit_ws.socketStore.browserOther[config.identifier] = config.socket;
                }
                if (config.type === "device" || config.type === "user") {
                    vars.agents[config.type][config.identifier].ipSelected = getAddress({
                        socket: config.socket,
                        type: "ws"
                    }).remote.address;
                    config.socket.on("close", function terminal_server_transmission_transmitWs_socketExtension_close() {
                        const configData = {
                            agent: config.socket.hash,
                            agentType: config.socket.type,
                            callback: null
                        }, delay = function terminal_server_transmission_transmitWs_socketExtension_close_delay() {
                            transmit_ws.open.agent(configData);
                        };
                        transmit_ws.agentClose(config.socket);
                        setTimeout(delay, vars.settings.ui.statusTime);
                        list();
                    });
                    if (config.type === "user") {
                        const management = {
                            action: "modify",
                            agents: {
                                device: {},
                                user: {
                                    [config.identifier]: vars.agents.user[config.identifier]
                                }
                            },
                            agentFrom: config.identifier,
                            identity: null
                        };
                        agent_management({
                            data: management,
                            service: "agent-management"
                        });
                    }
                    if (config.role === "client") {
                        transmit_ws.queueSend(config.socket);
                    }
                } else {
                    config.socket.on("close", socketEnd);
                }
            } else {
                config.socket.on("close", socketEnd);
            }
            config.socket.on("end", socketEnd);
            config.socket.on("error", socketError);
            transmit_ws.listener(config.socket);
            if (config.callback !== null && config.callback !== undefined) {
                config.callback(config.socket);
            }
            list();
        }
    },
    // a map of which devices have which sockets open
    socketMap: {},
    socketMapUpdate: function terminal_server_transmission_transmitWs_socketMapUpdate(socketData) {
        const data = socketData.data, keys = Object.keys(data);
        transmit_ws.socketMap[keys[0]] = data[keys[0]];
        network.send({
            data: transmit_ws.socketMap,
            service: "socket-map"
        }, "browser");
    },
    // a store of local sockets
    socketStore: {}
};
export default transmit_ws;
