ice framework documentation ice doc v1.10.1
    
namespace Ice\Cli\Websocket;

use Ice\Exception;

/**
 * Websocket server.
 *
 * @package     Ice/Cli
 * @category    Component
 * @author      Ice Team
 * @copyright   (c) 2014-2023 Ice Team
 * @license     http://iceframework.org/license
 */
class Server extends Websocket
{
    protected verbose = false { set };
    protected address  { get };
    protected server { get };
    protected sockets = [];
    protected clients = [] { get };
    protected callbacks = [];

    /**
     * Create an instance.
     *
     * @param string address Where to create the server, defaults to "ws://127.0.0.1:8080"
     * @param array options Stream context options
     */
    public function __construct(string address = "ws://127.0.0.1:8080", array options = [])
    {
        var addr, context, key, value;

        let addr = parse_url(address);

        if addr === false || !isset addr["scheme"] || !isset addr["host"] || !isset addr["port"] {
            throw new Exception("Invalid address");
        }

        let context = stream_context_create();

        if count(options) {
            for key, value in options {
                stream_context_set_option(context, "ssl", key, value);
            }
        }

        let this->address = address;

        let this->server = stream_socket_server(
            (in_array(addr["scheme"], ["wss", "tls"]) ? "tls" : "tcp") . "://" . addr["host"] . ":" . addr["port"],
            null,
            null,
            STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
            context
        );

        if this->server === false {
            throw new Exception("Could not create server");
        }
    }

    /**
     * Start processing requests. This method runs in an infinite loop.
     *
     * @return void
     */
    public function run() -> void
    {
        var changed, write, except, stream, messages, message, socket, tmp;

        let this->sockets[] = this->server;

        if isset this->callbacks["boot"] {
            call_user_func(this->callbacks["boot"], this);
        }

        while 1 {
            if isset this->callbacks["tick"] {
                if call_user_func(this->callbacks["tick"], this) === false {
                    break;
                }
            }

            let changed = this->sockets,
                write = [],
                except = [],
                stream = stream_select(changed, write, except, (isset this->callbacks["tick"] ? 0 : null));

            if stream > 0 {
                let messages = [];

                for socket in changed {
                    if socket === this->server {
                        let tmp = stream_socket_accept(this->server);

                        if tmp !== false {
                            if this->connect(tmp) {
                                if isset this->callbacks["connect"] {
                                    call_user_func(this->callbacks["connect"], this->clients[(int) tmp], this);
                                }
                            }
                        }
                    } else {
                        let message = this->receive(socket);

                        if message === false || in_array(message, ["quit", "exit", "close"]) {
                            if isset this->callbacks["disconnect"] {
                                call_user_func(this->callbacks["disconnect"], this->clients[(int) socket], this);
                            }

                            this->disconnect(socket);
                        } else {
                            let messages[] = [
                                "client": this->clients[(int) socket],
                                "message": message
                            ];
                        }
                    }
                }

                for message in messages {
                    if isset this->callbacks["message"] {
                        call_user_func(this->callbacks["message"], message["client"], message["message"], this);
                    }
                }
            }

            usleep(this->getParam("sleep", 5000));
        }
    }

    /**
     * Connect a socket to the server.
     *
     * @param resource socket The resource
     * @return boolean
     */
    protected function connect(resource socket) -> boolean
    {
        var headers, header, request, tmp, cookies, value, client, response;

        let headers = this->receiveClear(socket);

        if !headers {
            return false;
        }

        let headers = str_replace(["\r\n", "\n"], ["\n", "\r\n"], headers),
            headers = array_filter(explode("\r\n", preg_replace("(\r\n\s+)", " ", headers))),
            request = explode(" ", array_shift(headers));

        if strtoupper(request[0]) !== "GET" {
            this->sendClear(socket, "HTTP/1.1 405 Method Not Allowed\r\n\r\n");

            return false;
        }

        let tmp = [];

        for header in headers {
            let header = explode(":", header, 2),
                tmp[trim(strtolower(header[0]))] = trim(header[1]);
        }

        let headers = tmp;

        if !isset headers["sec-websocket-key"] || !isset headers["upgrade"] || !isset headers["connection"] ||
            strtolower(headers["upgrade"]) != "websocket" || strpos(strtolower(headers["connection"]), "upgrade") === false {
            this->sendClear(socket, "HTTP/1.1 400 Bad Request\r\n\r\n");

            return false;
        }

        let cookies = [];

        if isset headers["cookie"] {
            let tmp = explode(";", headers["cookie"]);

            for value in tmp {
                if trim(value) !== "" && strpos(value, "=") !== false {
                    let value = explode("=", value, 2),
                        cookies[trim(value[0])] = value[1];
                }
            }
        }

        let client = [
            "socket": socket,
            "headers": headers,
            "resource": request[1],
            "cookies": cookies
        ];

        if isset this->callbacks["validate"] && !call_user_func(this->callbacks["validate"], client, this) {
            this->sendClear(socket, "HTTP/1.1 400 Bad Request\r\n\r\n");

            return false;
        }

        let response = [
            "HTTP/1.1 101 WebSocket Protocol Handshake",
            "Upgrade: WebSocket",
            "Connection: Upgrade",
            "Sec-WebSocket-Version: 13",
            "Sec-WebSocket-Location: " . this->address,
            "Sec-WebSocket-Accept: " . base64_encode(sha1(headers["sec-websocket-key"] . self::magic, true))
        ];

        if isset headers["origin"] {
            let response[] = "Sec-WebSocket-Origin: " . headers["origin"];
        }

        let this->sockets[(int) socket] = socket,
            this->clients[(int) socket] = client;

        return this->sendClear(socket, implode("\r\n", response) . "\r\n\r\n");
    }

    /**
     * Disconnect a socket from the server.
     *
     * @param resource socket The resource
     * @return void
     */
    public function disconnect(resource socket) -> void
    {
        unset this->clients[(int) socket];
        unset this->sockets[(int) socket];
    }

    /**
     * Set a callback to be executed when a client connects, returning `false` will prevent the client from connecting.
     * The callable will receive:
     *  - an associative array with client data
     *  - the current server instance
     * The callable should return `true` if the client should be allowed to connect or `false` otherwise.
     *
     * @param callable callback The callback
     * @return self
     */
    public function onValidate(callable callback)
    {
        return this->callback("validate", callback);
    }

    /**
     * Set a callback to be executed when a client is connected.
     * The callable will receive:
     *  - an associative array with client data
     *  - the current server instance
     *
     * @param callable callback The callback
     * @return self
     */
    public function onConnect(callable callback)
    {
        return this->callback("connect", callback);
    }

    /**
     * Set a callback to execute when a client disconnects.
     * The callable will receive:
     *  - an associative array with client data
     *  - the current server instance
     *
     * @param callable callback The callback
     * @return self
     */
    public function onDisconnect(callable callback)
    {
        return this->callback("disconnect", callback);
    }

    /**
     * Set a callback to execute when a client sends a message.
     * The callable will receive:
     *  - an associative array with client data
     *  - the message string
     *  - the current server instance
     *
     * @param callable callback The callback
     * @return self
     */
    public function onMessage(callable callback)
    {
        return this->callback("message", callback);
    }

    /**
     * Set a callback to execute every few milliseconds.
     * The callable will receive the server instance. If it returns boolean `false` the server will stop listening.
     *
     * @param callable callback The callback
     * @return self
     */
    public function onTick(callable callback)
    {
        return this->callback("tick", callback);
    }

    /**
     * Set a callback to execute on boot the server.
     * The callable will receive the server instance.
     *
     * @param callable callback The callback
     * @return self
     */
    public function onBoot(callable callback)
    {
        return this->callback("boot", callback);
    }

    /**
     * Register a callback to execute.
     *
     * @param string key A callback key
     * @param callable callback The callback
     * @return self
     */
    public function callback(string key, callable callback)
    {
        let this->callbacks[key] = callback;

        return this;
    }
}