( ′∀`)σ≡σ☆))Д′)レ(゚∀゚;)ヘ=З=З=Зε≡(ノ´_ゝ`)ノ
<?php
// CubicleSoft PHP WebSocketServer class.
// (C) 2021 CubicleSoft. All Rights Reserved.
// Make sure PHP doesn't introduce weird limitations.
ini_set("memory_limit", "-1");
set_time_limit(0);
// Requires the CubicleSoft PHP WebSocket class.
class WebSocketServer
{
protected $fp, $clients, $nextclientid, $websocketclass, $origins;
protected $defaultclosemode, $defaultmaxreadframesize, $defaultmaxreadmessagesize, $defaultkeepalive, $lasttimeoutcheck;
public function __construct()
{
$this->Reset();
}
public function Reset()
{
if (!class_exists("WebSocket", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/websocket.php";
$this->fp = false;
$this->clients = array();
$this->nextclientid = 1;
$this->websocketclass = "WebSocket";
$this->origins = false;
$this->defaultclosemode = WebSocket::CLOSE_IMMEDIATELY;
$this->defaultmaxreadframesize = 2000000;
$this->defaultmaxreadmessagesize = 10000000;
$this->defaultkeepalive = 30;
$this->lasttimeoutcheck = time();
}
public function __destruct()
{
$this->Stop();
}
public function SetWebSocketClass($newclass)
{
if (class_exists($newclass)) $this->websocketclass = $newclass;
}
public function SetAllowedOrigins($origins)
{
if (is_string($origins)) $origins = array($origins);
if (!is_array($origins)) $origins = false;
else if (isset($origins[0])) $origins = array_flip($origins);
$this->origins = $origins;
}
public function SetDefaultCloseMode($mode)
{
$this->defaultclosemode = $mode;
}
public function SetDefaultKeepAliveTimeout($keepalive)
{
$this->defaultkeepalive = (int)$keepalive;
}
public function SetDefaultMaxReadFrameSize($maxsize)
{
$this->defaultmaxreadframesize = (is_bool($maxsize) ? false : (int)$maxsize);
}
public function SetDefaultMaxReadMessageSize($maxsize)
{
$this->defaultmaxreadmessagesize = (is_bool($maxsize) ? false : (int)$maxsize);
}
// Starts the server on the host and port.
// $host is usually 0.0.0.0 or 127.0.0.1 for IPv4 and [::0] or [::1] for IPv6.
public function Start($host, $port)
{
$this->Stop();
$this->fp = stream_socket_server("tcp://" . $host . ":" . $port, $errornum, $errorstr);
if ($this->fp === false) return array("success" => false, "error" => self::WSTranslate("Bind() failed. Reason: %s (%d)", $errorstr, $errornum), "errorcode" => "bind_failed");
// Enable non-blocking mode.
stream_set_blocking($this->fp, 0);
return array("success" => true);
}
public function Stop()
{
foreach ($this->clients as $client)
{
if ($client->websocket !== false) $client->websocket->Disconnect();
else fclose($client->fp);
}
$this->clients = array();
if ($this->fp !== false)
{
fclose($this->fp);
$this->fp = false;
}
$this->nextclientid = 1;
}
// Dangerous but allows for stream_select() calls on multiple, separate stream handles.
public function GetStream()
{
return $this->fp;
}
// Return whatever response/headers are needed here.
protected function ProcessNewConnection($method, $path, $client)
{
$result = "";
if ($method !== "GET") $result .= "HTTP/1.1 405 Method Not Allowed\r\nConnection: close\r\n\r\n";
else if (!isset($client->headers["Host"]) || !isset($client->headers["Connection"]) || stripos($client->headers["Connection"], "upgrade") === false || !isset($client->headers["Upgrade"]) || stripos($client->headers["Upgrade"], "websocket") === false || !isset($client->headers["Sec-Websocket-Key"]))
{
$result .= "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n";
}
else if (!isset($client->headers["Sec-Websocket-Version"]) || $client->headers["Sec-Websocket-Version"] != 13)
{
$result .= "HTTP/1.1 426 Upgrade Required\r\nSec-WebSocket-Version: 13\r\nConnection: close\r\n\r\n";
}
else if (!isset($client->headers["Origin"]) || ($this->origins !== false && !isset($this->origins[strtolower($client->headers["Origin"])])))
{
$result .= "HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n";
}
return $result;
}
// Return whatever additional HTTP headers are needed here.
protected function ProcessAcceptedConnection($method, $path, $client)
{
return "";
}
protected function InitNewClient($fp)
{
$client = new stdClass();
$client->id = $this->nextclientid;
$client->readdata = "";
$client->writedata = "";
$client->request = false;
$client->path = "";
$client->url = "";
$client->headers = array();
$client->lastheader = "";
$client->websocket = false;
$client->fp = $fp;
$client->ipaddr = stream_socket_get_name($fp, true);
// Intended for application storage.
$client->appdata = false;
$this->clients[$this->nextclientid] = $client;
$this->nextclientid++;
return $client;
}
private function ProcessInitialResponse($method, $path, $client)
{
// Let a derived class handle the new connection (e.g. processing Origin and Host).
// Since the 'websocketclass' is instantiated AFTER this function, it is possible to switch classes on the fly.
$client->writedata .= $this->ProcessNewConnection($method, $path, $client);
// If an error occurs, the connection will still terminate.
$client->websocket = new $this->websocketclass();
$client->websocket->SetCloseMode($this->defaultclosemode);
$client->websocket->SetKeepAliveTimeout($this->defaultkeepalive);
// If nothing was output, accept the connection.
if ($client->writedata === "")
{
$client->writedata .= "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n";
$client->writedata .= "Sec-WebSocket-Accept: " . base64_encode(sha1($client->headers["Sec-Websocket-Key"] . WebSocket::KEY_GUID, true)) . "\r\n";
$client->writedata .= $this->ProcessAcceptedConnection($method, $path, $client);
$client->writedata .= "\r\n";
// Finish class initialization.
$client->websocket->SetServerMode();
$client->websocket->SetMaxReadFrameSize($this->defaultmaxreadframesize);
$client->websocket->SetMaxReadMessageSize($this->defaultmaxreadmessagesize);
// Set the socket in the WebSocket class.
$client->websocket->Connect("", "", array("connected_fp" => $client->fp));
}
$this->UpdateClientState($client->id);
}
public function UpdateStreamsAndTimeout($prefix, &$timeout, &$readfps, &$writefps)
{
if ($this->fp !== false) $readfps[$prefix . "ws_s"] = $this->fp;
if ($timeout === false || $timeout > $this->defaultkeepalive) $timeout = $this->defaultkeepalive;
foreach ($this->clients as $id => $client)
{
if ($client->writedata === "") $readfps[$prefix . "ws_c_" . $id] = $client->fp;
if ($client->writedata !== "" || ($client->websocket !== false && $client->websocket->NeedsWrite())) $writefps[$prefix . "ws_c_" . $id] = $client->fp;
if ($client->websocket !== false)
{
$timeout2 = $client->websocket->GetKeepAliveTimeout();
if ($timeout > $timeout2) $timeout = $timeout2;
}
}
}
// Sometimes keyed arrays don't work properly.
public static function FixedStreamSelect(&$readfps, &$writefps, &$exceptfps, $timeout)
{
// In order to correctly detect bad outputs, no '0' integer key is allowed.
if (isset($readfps[0]) || isset($writefps[0]) || ($exceptfps !== NULL && isset($exceptfps[0]))) return false;
$origreadfps = $readfps;
$origwritefps = $writefps;
$origexceptfps = $exceptfps;
$result2 = @stream_select($readfps, $writefps, $exceptfps, $timeout);
if ($result2 === false) return false;
if (isset($readfps[0]))
{
$fps = array();
foreach ($origreadfps as $key => $fp) $fps[(int)$fp] = $key;
foreach ($readfps as $num => $fp)
{
$readfps[$fps[(int)$fp]] = $fp;
unset($readfps[$num]);
}
}
if (isset($writefps[0]))
{
$fps = array();
foreach ($origwritefps as $key => $fp) $fps[(int)$fp] = $key;
foreach ($writefps as $num => $fp)
{
$writefps[$fps[(int)$fp]] = $fp;
unset($writefps[$num]);
}
}
if ($exceptfps !== NULL && isset($exceptfps[0]))
{
$fps = array();
foreach ($origexceptfps as $key => $fp) $fps[(int)$fp] = $key;
foreach ($exceptfps as $num => $fp)
{
$exceptfps[$fps[(int)$fp]] = $fp;
unset($exceptfps[$num]);
}
}
return true;
}
// Handles new connections, the initial conversation, basic packet management, and timeouts.
// Can wait on more streams than just sockets and/or more sockets. Useful for waiting on other resources.
// 'ws_s' and the 'ws_c_' prefix are reserved.
// Returns an array of clients that may need more processing.
public function Wait($timeout = false, $readfps = array(), $writefps = array(), $exceptfps = NULL)
{
$this->UpdateStreamsAndTimeout("", $timeout, $readfps, $writefps);
$result = array("success" => true, "clients" => array(), "removed" => array(), "readfps" => array(), "writefps" => array(), "exceptfps" => array(), "accepted" => array(), "read" => array(), "write" => array());
if (!count($readfps) && !count($writefps)) return $result;
$result2 = self::FixedStreamSelect($readfps, $writefps, $exceptfps, $timeout);
if ($result2 === false) return array("success" => false, "error" => self::WSTranslate("Wait() failed due to stream_select() failure. Most likely cause: Connection failure."), "errorcode" => "stream_select_failed");
// Return handles that were being waited on.
$result["readfps"] = $readfps;
$result["writefps"] = $writefps;
$result["exceptfps"] = $exceptfps;
$this->ProcessWaitResult($result);
return $result;
}
protected function ProcessWaitResult(&$result)
{
// Handle new connections.
if (isset($result["readfps"]["ws_s"]))
{
while (($fp = @stream_socket_accept($this->fp, 0)) !== false)
{
// Enable non-blocking mode.
stream_set_blocking($fp, 0);
$client = $this->InitNewClient($fp);
$result["accepted"][$client->id] = $client;
}
unset($result["readfps"]["ws_s"]);
}
// Handle clients in the read queue.
foreach ($result["readfps"] as $cid => $fp)
{
if (!is_string($cid) || strlen($cid) < 6 || substr($cid, 0, 5) !== "ws_c_") continue;
$id = (int)substr($cid, 5);
if (!isset($this->clients[$id])) continue;
$client = $this->clients[$id];
$result["read"][$id] = $client;
if ($client->websocket !== false)
{
$this->ProcessClientQueuesAndTimeoutState($result, $id, true, isset($result["writefps"][$cid]));
// Remove active WebSocket clients from the write queue.
unset($result["writefps"][$cid]);
}
else
{
$result2 = @fread($fp, 8192);
if ($result2 === false || ($result2 === "" && feof($fp)))
{
@fclose($fp);
unset($this->clients[$id]);
}
else
{
$client->readdata .= $result2;
if (strlen($client->readdata) > 100000)
{
// Bad header size. Just kill the connection.
@fclose($fp);
unset($this->clients[$id]);
}
else
{
while (($pos = strpos($client->readdata, "\n")) !== false)
{
// Retrieve the next line of input.
$line = rtrim(substr($client->readdata, 0, $pos));
$client->readdata = (string)substr($client->readdata, $pos + 1);
if ($client->request === false) $client->request = trim($line);
else if ($line !== "")
{
// Process the header.
if ($client->lastheader != "" && (substr($line, 0, 1) == " " || substr($line, 0, 1) == "\t")) $client->headers[$client->lastheader] .= $header;
else
{
$pos = strpos($line, ":");
if ($pos === false) $pos = strlen($line);
$client->lastheader = self::HeaderNameCleanup(substr($line, 0, $pos));
$client->headers[$client->lastheader] = ltrim(substr($line, $pos + 1));
}
}
else
{
// Headers have all been received. Process the client request.
$request = $client->request;
$pos = strpos($request, " ");
if ($pos === false) $pos = strlen($request);
$method = (string)substr($request, 0, $pos);
$request = trim(substr($request, $pos));
$pos = strrpos($request, " ");
if ($pos === false) $pos = strlen($request);
$path = (string)substr($request, 0, $pos);
if ($path === "") $path = "/";
if (isset($client->headers["Host"])) $client->headers["Host"] = preg_replace('/[^a-z0-9.:\[\]_-]/', "", strtolower($client->headers["Host"]));
$client->path = $path;
$client->url = "ws://" . (isset($client->headers["Host"]) ? $client->headers["Host"] : "localhost") . $path;
$this->ProcessInitialResponse($method, $path, $client);
break;
}
}
}
}
}
unset($result["readfps"][$cid]);
}
// Handle remaining clients in the write queue.
foreach ($result["writefps"] as $cid => $fp)
{
if (!is_string($cid) || strlen($cid) < 6 || substr($cid, 0, 5) !== "ws_c_") continue;
$id = (int)substr($cid, 5);
if (!isset($this->clients[$id])) continue;
$client = $this->clients[$id];
$result["write"][$id] = $client;
if ($client->writedata === "") $this->ProcessClientQueuesAndTimeoutState($result, $id, false, true);
else
{
$result2 = @fwrite($fp, $client->writedata);
if ($result2 === false || ($result2 === "" && feof($fp)))
{
@fclose($fp);
unset($this->clients[$id]);
}
else if ($result2 === 0) $this->ProcessClientQueuesAndTimeoutState($result, $id, true, false, 1);
else
{
$client->writedata = (string)substr($client->writedata, $result2);
// Let the application know about the new client or close the connection if the WebSocket Upgrade request failed.
if ($client->writedata === "")
{
if ($client->websocket->GetStream() !== false) $result["clients"][$id] = $client;
else
{
@fclose($fp);
unset($this->clients[$id]);
}
}
}
}
unset($result["writefps"][$cid]);
}
// Handle client timeouts.
$ts = time();
if ($this->lasttimeoutcheck <= $ts - 5)
{
foreach ($this->clients as $id => $client)
{
if (!isset($result["clients"][$id]) && $client->writedata === "" && $client->websocket !== false)
{
$this->ProcessClientQueuesAndTimeoutState($result, $id, false, false);
}
}
$this->lasttimeoutcheck = $ts;
}
}
protected function ProcessClientQueuesAndTimeoutState(&$result, $id, $read, $write, $readsize = 65536)
{
$client = $this->clients[$id];
$result2 = $client->websocket->ProcessQueuesAndTimeoutState($read, $write, $readsize);
if ($result2["success"]) $result["clients"][$id] = $client;
else
{
$result["removed"][$id] = array("result" => $result2, "client" => $client);
$this->RemoveClient($id);
}
}
public function GetClients()
{
return $this->clients;
}
public function NumClients()
{
return count($this->clients);
}
public function UpdateClientState($id)
{
}
public function GetClient($id)
{
return (isset($this->clients[$id]) ? $this->clients[$id] : false);
}
public function RemoveClient($id)
{
if (isset($this->clients[$id]))
{
$client = $this->clients[$id];
// Remove the client.
if ($client->websocket->GetStream() !== false)
{
$client->websocket->Disconnect();
$client->websocket = false;
$client->fp = false;
}
if ($client->fp !== false) @fclose($client->fp);
unset($this->clients[$id]);
}
}
public function ProcessWebServerClientUpgrade($webserver, $client)
{
if (!($client instanceof WebServer_Client)) return false;
if (!$client->requestcomplete || $client->mode === "handle_response") return false;
if ($client->request["method"] !== "GET" || !isset($client->headers["Connection"]) || stripos($client->headers["Connection"], "upgrade") === false || !isset($client->headers["Upgrade"]) || stripos($client->headers["Upgrade"], "websocket") === false) return false;
// Create an equivalent WebSocket server client class.
$webserver->DetachClient($client->id);
$method = $client->request["method"];
$path = $client->request["path"];
$client2 = $this->InitNewClient($client->fp);
$client2->request = $client->request["line"];
$client2->headers = $client->headers;
$client2->path = $path;
$client2->url = "ws://" . (isset($client->headers["Host"]) ? $client->headers["Host"] : "localhost") . $path;
$client2->appdata = $client->appdata;
$this->ProcessInitialResponse($method, $path, $client2);
return $client2->id;
}
public static function HeaderNameCleanup($name)
{
return preg_replace('/\s+/', "-", ucwords(strtolower(trim(preg_replace('/[^A-Za-z0-9 ]/', " ", $name)))));
}
public static function WSTranslate()
{
$args = func_get_args();
if (!count($args)) return "";
return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args);
}
}
?>