ice framework documentation ice doc v1.10.1
Class Ice Cli Websocket

Websocket

    
namespace Ice\Cli\Websocket;

use Ice\Di;
use Ice\Cli\Console;

/**
 * A base class used in the server class.
 * It handles all encoding / decoding / masking / socket operations.
 *
 * @package     Ice/Cli
 * @category    Component
 * @author      Ice Team
 * @copyright   (c) 2014-2023 Ice Team
 * @license     http://iceframework.org/license
 */
class Websocket
{
    protected static opcodes = [
        "continuation": 0,
        "text": 1,
        "binary": 2,
        "close": 8,
        "ping": 9,
        "pong": 10
    ];

    protected static fragmentSize = 4096;
    protected static magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
    protected params;

    /**
     * Send data to a socket in clear form (basically fwrite).
     *
     * @param resource socket The socket to write to
     * @param string data The data to send
     * @return boolean Was the send successful
     */
    public function sendClear(resource socket, string data) -> boolean
    {
        return fwrite(socket, data) > 0;
    }

    /**
     * Send data to a socket.
     *
     * @param resource socket The socket to send to
     * @param string data The data to send
     * @param string opcode One of the opcodes (defaults to "text")
     * @param boolean masked Should the data be masked (per specs the server should not mask, defaults to false)
     * @return boolean Was the send successful
     */
    public function sendData(resource socket, string data, string opcode = "text", boolean masked = false) -> boolean
    {
        var tmp;

        while strlen(data) {
            let tmp = substr(data, 0, self::fragmentSize),
                data = strlen(data) < self::fragmentSize ? "" : substr(data, self::fragmentSize),
                tmp = this->encode(tmp, opcode, masked, strlen(data) === 0);

            if is_resource(socket) && get_resource_type(socket) == "stream" && !feof(socket) {
                if fwrite(socket, tmp) === false || stream_get_meta_data(socket)["timed_out"] {
                    return false;
                }
            } else {
                return false;
            }

            let opcode = "continuation";
        }

        return true;
    }

    /**
     * Read clear data from a socket (basically a fread).
     *
     * @param resource socket The socket to read from
     * @return string The data that was read
     */
    public function receiveClear(resource socket) -> string
    {
        var data, read, buff, meta;

        let data = "",
            read = self::fragmentSize;

        do {
            let buff = fread(socket, read);

            if buff === false {
                return "";
            }

            let data .= buff,
                meta = stream_get_meta_data(socket),
                read = min((int) meta["unread_bytes"], self::fragmentSize);

            usleep(1000);
        } while !feof(socket) && (int) meta["unread_bytes"] > 0;

        if strlen(data) === 1 {
            let data .= this->receiveClear(socket);
        }

        return data;
    }

    /**
     * Read data from a socket (in websocket format).
     *
     * @param resource socket The socket to read from
     * @return string The read data (decoded)
     */
    public function receive(resource socket) -> string | boolean
    {
        var opcode, length, tmp, buff, fin;
        boolean masked;
        string data, payload, mask;
        int i;

        let tmp = fread(socket, 2);

        if tmp === false {
            return false;
        }

        if strlen(tmp) === 1 {
            let tmp .= fread(socket, 1);
        }

        if tmp === false || strlen(tmp) < 2 {
            return false;
        }

        char d, f;
        var e;

        let data = tmp,
            f = data[0],
            e = ord(f),
            fin = (boolean) (e & 1 << 7),
            opcode = ord(f) & 31,
            d = data[1],
            masked = (boolean) (ord(d) >> 7),
            payload = "",
            length = (int) (ord(d) & 127); // Bits 1-7 in byte 1

        if length > 125 {
            let tmp = length === 126 ? fread(socket, 2) : fread(socket, 8);

            if tmp === false {
                return false;
            }

            let length = "",
                i = 0,
                data = (string) tmp;

            while i < strlen(tmp) {
                let length .= sprintf("%08b", ord(data[i])),
                    i++;
            }

            let length = bindec(length);
        }

        let mask = "";

        if masked {
            let tmp = fread(socket, 4);

            if tmp === false {
                return false;
            }

            let mask = (string) tmp;
        }

        if length > 0 {
            let tmp = "";

            do {
                let buff = fread(socket, min(length, self::fragmentSize));

                if buff === false {
                    return false;
                }

                let tmp .= buff;
            } while strlen(tmp) < length;

            let data = (string) tmp;

            if masked {
                int j;
                char c, d;
                var s, t;

                for i, c in data {
                    let j = (int) (i % 4),
                        d = mask[j],
                        s = (string) c,
                        t = (string) d,
                        payload .= s ^ t;
                }
            } else {
                let payload = data;
            }
        }

        if opcode === self::opcodes["close"] {
            return false;
        }

        return fin ? payload : payload . this->receive(socket);
    }

    /**
     * Encode the data.
     *
     * @param string data The data to send
     * @param string opcode One of the opcodes (defaults to "text")
     * @param boolean masked Should the data be masked
     * @param boolean fin Add 1 to the head
     * @return string
     */
    protected function encode(string data, string opcode = "text", boolean masked = true, boolean fin = true) -> string
    {
        var length, binstr;
        string head;
        int i;

        let length = strlen(data),
            head = "",
            head .= fin ? "1" : "0",
            head .= "000",
            head .= sprintf("%04b", self::opcodes[opcode]),
            head .= masked ? "1" : "0";

        if length > 65535 {
            let head .= decbin(127),
                head .= sprintf("%064b", length);
        } elseif length > 125 {
            let head .= decbin(126),
                head .= sprintf("%016b", length);
        } else {
            let head .= sprintf("%07b", length);
        }

        string frame = "";

        for binstr in str_split(head, 8) {
            let frame .= chr((int) bindec(binstr));
        }

        string mask = "";

        if masked {
            let i = 0;

            while i < 4 {
                let mask .= chr(rand(0, 255)),
                    i++;
            }

            let frame .= mask;
        }


        if masked {
            int j;
            char c, d;
            var s, t;

            for i, c in data {
                let j = (int) (i % 4),
                    d = mask[j],
                    s = (string) c,
                    t = (string) d,
                    frame .= s ^ t;
            }
        } else {
            let frame .= data;
        }

        return frame;
    }

    /**
     * Set server params.
     *
     * @param array params
     * @return self
     */
    public function setParams(array params)
    {
        let this->params = params;

        return this;
    }

    /**
     * Get server params.
     *
     * @return array
     */
    public function getParams() -> array
    {
        return this->params;
    }

    /**
     * Retrieve a single param.
     *
     * @param string key The data key
     * @param mixed defaultValue The value to return if data key does not exist
     * @return mixed
     */
    public function getParam(string key, defaultValue = null)
    {
        if isset this->params[key] {
            return this->params[key];
        }

        return defaultValue;
    }

    /**
     * Display text on the console.
     *
     * @param string text Text to display
     * @param int color The foreground color
     * @param int decoration Formatting type
     * @param int bgColor The background color
     * @param boolean exit Die if true
     * @return self
     */
    public function console(string text, int color = null, int decoration = Console::NORMAL, int bgColor = null, boolean exit = false)
    {
        var di, text;

        let text = date(this->getParam("date_format", "[Y-m-d H:i:s]")) . " " . text . "\r\n";

        if exit {
            die(text);
        }

        if this->getParam("verbose", false) {
            let di = Di::$fetch();

            echo di->get("console")->color(text, color, decoration, bgColor);
        }

        return this;
    }

    /**
     * Returns info text.
     *
     * @param string text
     * @return string
     */
    public function info(string text)
    {
        return this->console(text, Console::CYAN);
    }

    /**
     * Returns success text.
     *
     * @param string text
     * @return string
     */
    public function success(string text)
    {
        return this->console(text, Console::GREEN);
    }

    /**
     * Returns warning text.
     *
     * @param string text
     * @return string
     */
    public function warning(string text)
    {
        return this->console(text, Console::YELLOW);
    }

    /**
     * Returns error text.
     *
     * @param string text
     * @return string
     */
    public function error(string text)
    {
        return this->console(text, Console::RED);
    }

    /**
     * Get script uptime.
     *
     * @param boolean human In seconds if false
     * @return float|string
     */
    public function getUptime(boolean human = true)
    {
        var uptime, y, m, d, h, i, s;

        let uptime = microtime(true) - _SERVER["REQUEST_TIME_FLOAT"];

        if !human {
            return uptime;
        }

        let s = uptime % 60,
            i = floor((uptime % 3600) / 60),
            h = floor((uptime % 86400) / 3600),
            d = floor((uptime % 2592000) / 86400),
            m = floor(uptime / 2592000),
            y = floor(uptime / 2592000 * 12);

        return (y ? y . "Y " : "") .
               (m ? m . "M " : "") .
               (d ? d . "d " : "") .
               (h ? h . "h " : "") .
               (i ? i . "m " : "") .
               (s ? s . "s" : "");
    }

    /**
     * Get memory usage.
     *
     * @param boolean human In byetes if false
     * @return int|string
     */
    public function getMemoryUsage(boolean human = true)
    {
        var size, unit, i;

        let size = memory_get_usage();

        if !human {
            return size;
        }

        let unit = ["B", "KB", "MB", "GB", "TB", "PB"],
            i = floor(log(size, 1024));

        return round(size / pow(1024, (i)), 2) . unit[i];
    }
}