From 90243517f503405a56ce76db0e46a95b27c1fbb7 Mon Sep 17 00:00:00 2001 From: Ricardo Boss Date: Sun, 7 Aug 2022 11:49:43 +0200 Subject: [PATCH 1/3] Fixed some typos and code styling, dropped support for PHP 7.4 and use new PHP 8 only features. --- composer.json | 4 +- examples/Application/Chat.php | 6 +- src/Application/Application.php | 13 +- src/Application/ApplicationInterface.php | 8 +- src/Application/StatusApplication.php | 19 ++- src/Connection.php | 21 ++-- src/IPCPayloadFactory.php | 15 +-- src/Logger/StdOutLogger.php | 5 +- src/PushClient.php | 2 +- src/Server.php | 150 ++++++++++------------- 10 files changed, 113 insertions(+), 130 deletions(-) diff --git a/composer.json b/composer.json index 9d76a31..09e84b6 100644 --- a/composer.json +++ b/composer.json @@ -22,9 +22,9 @@ } }, "require": { - "php": "^7.4|^8.0", + "php": "^8.0", "ext-json": "*", "ext-sockets": "*", - "psr/log": "^1.1" + "psr/log": "^3.0" } } diff --git a/examples/Application/Chat.php b/examples/Application/Chat.php index 21e4fda..d1c671a 100644 --- a/examples/Application/Chat.php +++ b/examples/Application/Chat.php @@ -85,7 +85,7 @@ public function onIPCData(array $data): void $actionName = 'action' . ucfirst($data['action']); $message = 'System Message: ' . $data['data'] ?? ''; if (method_exists($this, $actionName)) { - call_user_func([$this, $actionName], $message); + $this->$actionName($message); } } @@ -98,8 +98,8 @@ public function onIPCData(array $data): void private function actionEcho(string $text): void { $encodedData = $this->encodeData('echo', $text); - foreach ($this->clients as $sendto) { - $sendto->send($encodedData); + foreach ($this->clients as $client) { + $client->send($encodedData); } } } diff --git a/src/Application/Application.php b/src/Application/Application.php index 8700415..eb5a037 100644 --- a/src/Application/Application.php +++ b/src/Application/Application.php @@ -28,7 +28,7 @@ protected function __clone() */ final public static function getInstance(): ApplicationInterface { - $calledClassName = get_called_class(); + $calledClassName = static::class; if (!isset(self::$instances[$calledClassName])) { self::$instances[$calledClassName] = new $calledClassName(); } @@ -40,12 +40,13 @@ final public static function getInstance(): ApplicationInterface * Decodes json data received from stream. * * @param string $data - * @throws \RuntimeException * @return array + * @throws \JsonException + * @throws \RuntimeException */ protected function decodeData(string $data): array { - $decodedData = json_decode($data, true); + $decodedData = json_decode($data, true, flags: JSON_THROW_ON_ERROR); if (empty($decodedData)) { throw new \RuntimeException('Could not decode data.'); } @@ -58,14 +59,14 @@ protected function decodeData(string $data): array } /** - * Enocdes data to be send to client. + * Encodes data to be sent to client. * * @param string $action * @param mixed $data - * @throws \InvalidArgumentException * @return string + * @throws \InvalidArgumentException */ - protected function encodeData(string $action, $data): string + protected function encodeData(string $action, mixed $data): string { if (empty($action)) { throw new \InvalidArgumentException('Action can not be empty.'); diff --git a/src/Application/ApplicationInterface.php b/src/Application/ApplicationInterface.php index 1613f96..c01ceca 100644 --- a/src/Application/ApplicationInterface.php +++ b/src/Application/ApplicationInterface.php @@ -9,21 +9,21 @@ interface ApplicationInterface { /** - * This method is tirggered when a new client connects to server/application. + * This method is triggered when a new client connects to server/application. * * @param Connection $connection */ public function onConnect(Connection $connection): void; /** - * This methods is triggered when a client disconnects from server/application. + * This method is triggered when a client disconnects from server/application. * * @param Connection $connection */ public function onDisconnect(Connection $connection): void; /** - * This method is triggered when the server recieves new data from a client. + * This method is triggered when the server receives new data from a client. * * @param string $data * @param Connection $client @@ -31,7 +31,7 @@ public function onDisconnect(Connection $connection): void; public function onData(string $data, Connection $client): void; /** - * This method is called when server recieves to for an application on the IPC socket. + * This method is called when server receives to for an application on the IPC socket. * * @param array $data */ diff --git a/src/Application/StatusApplication.php b/src/Application/StatusApplication.php index 067d9e1..564c920 100644 --- a/src/Application/StatusApplication.php +++ b/src/Application/StatusApplication.php @@ -46,7 +46,7 @@ public function onConnect(Connection $connection): void { $id = $connection->getClientId(); $this->clients[$id] = $connection; - $this->sendServerinfo($connection); + $this->sendServerInfo($connection); } /** @@ -62,7 +62,7 @@ public function onDisconnect(Connection $connection): void } /** - * This application does not expect any incomming client data. + * This application does not expect any incoming client data. * * @param string $data * @param Connection $client @@ -86,11 +86,8 @@ public function onIPCData(array $data): void */ public function setServerInfo(array $serverInfo): bool { - if (is_array($serverInfo)) { - $this->serverInfo = $serverInfo; - return true; - } - return false; + $this->serverInfo = $serverInfo; + return true; } /** @@ -153,7 +150,7 @@ public function clientActivity(string $client): void /** * Sends a status message to all clients connected to the application. * - * @param $text + * @param string $text * @param string $type */ public function statusMsg(string $text, string $type = 'info'): void @@ -172,7 +169,7 @@ public function statusMsg(string $text, string $type = 'info'): void * @param Connection $client * @return void */ - private function sendServerinfo(Connection $client): void + private function sendServerInfo(Connection $client): void { if (count($this->clients) < 1) { return; @@ -195,8 +192,8 @@ private function sendAll(string $encodedData): void if (count($this->clients) < 1) { return; } - foreach ($this->clients as $sendto) { - $sendto->send($encodedData); + foreach ($this->clients as $client) { + $client->send($encodedData); } } } diff --git a/src/Connection.php b/src/Connection.php index cbbae6d..a09b00c 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -49,7 +49,7 @@ class Connection * @var string $dataBuffer */ private string $dataBuffer = ''; - + /** * @var array $headers */ @@ -176,7 +176,7 @@ private function handshake(string $data): bool } /** - * Sends an http response to client. + * Sends a http response to client. * * @param int $httpStatusCode * @throws \RuntimeException @@ -245,11 +245,11 @@ private function handle(string $data): bool $this->waitingForData = true; $this->dataBuffer .= $data; return false; - } else { - $this->dataBuffer = ''; - $this->waitingForData = false; } + $this->dataBuffer = ''; + $this->waitingForData = false; + // trigger status application: if ($this->server->hasApplication('status')) { $client = $this->ip . ':' . $this->port; @@ -381,7 +381,7 @@ public function log(string $message, string $type = 'info'): void } /** - * Encodes a frame/message according the the WebSocket protocol standard. + * Encodes a frame/message according the WebSocket protocol standard. * * @param string $payload * @param string $type @@ -531,7 +531,7 @@ private function hybi10Decode(string $data): array /** * We have to check for large frames here. socket_recv cuts at 1024 bytes * so if websocket-frame is > 1024 bytes we have to wait until whole - * data is transferd. + * data is transferred. */ if (strlen($data) < $dataLength) { return []; @@ -546,7 +546,7 @@ private function hybi10Decode(string $data): array } $decodedData['payload'] = $unmaskedPayload; } else { - $payloadOffset = $payloadOffset - 4; + $payloadOffset -= 4; $decodedData['payload'] = substr($data, $payloadOffset); } @@ -584,7 +584,7 @@ public function getClientId(): string } /** - * Retuns the socket/resource of the connection. + * Returns the socket/resource of the connection. * * @return resource */ @@ -592,9 +592,10 @@ public function getClientSocket() { return $this->socket; } - + /** * Return the headers of the connection + * * @return array */ public function getClientHeaders(): array diff --git a/src/IPCPayloadFactory.php b/src/IPCPayloadFactory.php index b5a976b..3cf2440 100644 --- a/src/IPCPayloadFactory.php +++ b/src/IPCPayloadFactory.php @@ -31,7 +31,7 @@ public static function makeApplicationPayload(string $applicationName, array $da } /** - * Creates payload object from json ecoded string. + * Creates payload object from json encoded string. * * @param string $json * @return IPCPayload @@ -39,13 +39,10 @@ public static function makeApplicationPayload(string $applicationName, array $da public static function fromJson(string $json): IPCPayload { $data = json_decode($json, true); - switch ($data['type']) { - case IPCPayload::TYPE_SERVER: - return new IPCPayload(IPCPayload::TYPE_SERVER, $data['action'], $data['data']); - case IPCPayload::TYPE_APPLICATION: - return new IPCPayload(IPCPayload::TYPE_APPLICATION, $data['action'], $data['data']); - default: - throw new \RuntimeException('Can not create IPCPayload from invalid data type.'); - } + return match ($data['type']) { + IPCPayload::TYPE_SERVER => new IPCPayload(IPCPayload::TYPE_SERVER, $data['action'], $data['data']), + IPCPayload::TYPE_APPLICATION => new IPCPayload(IPCPayload::TYPE_APPLICATION, $data['action'], $data['data']), + default => throw new \RuntimeException('Can not create IPCPayload from invalid data type.'), + }; } } diff --git a/src/Logger/StdOutLogger.php b/src/Logger/StdOutLogger.php index de03cda..bfc0eae 100644 --- a/src/Logger/StdOutLogger.php +++ b/src/Logger/StdOutLogger.php @@ -27,11 +27,12 @@ class StdOutLogger extends AbstractLogger * @param mixed $level * @param string $message * @param array $context + * @return void */ - public function log($level, $message, array $context = []) + public function log($level, $message, array $context = []): void { $level = $level ?? LogLevel::ERROR; - $output = isset(self::OUTMAP[$level]) ? self::OUTMAP[$level] : STDERR; + $output = self::OUTMAP[$level] ?? STDERR; fwrite($output, date('Y-m-d H:i:s') . ' [' . $level . '] ' . $message . PHP_EOL); } } diff --git a/src/PushClient.php b/src/PushClient.php index f86c28b..04d7427 100644 --- a/src/PushClient.php +++ b/src/PushClient.php @@ -61,7 +61,7 @@ private function sendPayloadToServer(IPCPayload $payload): bool if ($dataLength > self::MAX_PAYLOAD_LENGTH) { throw new \RuntimeException( sprintf( - 'IPC payload exeeds max length of %d bytes. (%d bytes given.)', + 'IPC payload exceeds max length of %d bytes. (%d bytes given.)', self::MAX_PAYLOAD_LENGTH, $dataLength ) diff --git a/src/Server.php b/src/Server.php index 4ad301b..6b74bd3 100644 --- a/src/Server.php +++ b/src/Server.php @@ -5,6 +5,7 @@ namespace Bloatless\WebSocket; use Bloatless\WebSocket\Application\ApplicationInterface; +use Bloatless\WebSocket\Application\StatusApplication; use Psr\Log\LoggerInterface; /** @@ -21,26 +22,11 @@ class Server */ protected $master; - /** - * @var string $host Host the server will be bound to. - */ - private string $host = ''; - - /** - * @var int $port Port the server will listen on. - */ - private int $port = 0; - /** * @var resource $icpSocket */ private $icpSocket; - /** - * @var string $ipcSocketPath - */ - private string $ipcSocketPath; - /** * @var string $ipcOwner If set, owner of the ipc socket will be changed to this value. */ @@ -57,7 +43,7 @@ class Server private int $ipcMode = 0; /** - * @var array Holds all connected sockets + * @var resource[] Holds all connected sockets */ protected array $allsockets = []; @@ -67,17 +53,17 @@ class Server protected $context = null; /** - * @var array $clients + * @var Connection[] $clients */ protected array $clients = []; /** - * @var array $applications + * @var ApplicationInterface[] $applications */ protected array $applications = []; /** - * @var array $ipStorage + * @var string[] $ipStorage */ private array $ipStorage = []; @@ -87,7 +73,7 @@ class Server private bool $checkOrigin = true; /** - * @var array $allowedOrigins + * @var string[] $allowedOrigins */ private array $allowedOrigins = []; @@ -117,19 +103,16 @@ class Server * @param string $ipcSocketPath */ public function __construct( - string $host = 'localhost', - int $port = 8000, - string $ipcSocketPath = '/tmp/phpwss.sock' + private string $host = 'localhost', + private int $port = 8000, + private string $ipcSocketPath = '/tmp/phpwss.sock' ) { - $this->host = $host; - $this->port = $port; - $this->ipcSocketPath = $ipcSocketPath; $this->timers = new TimerCollection(); } /** * Main server loop. - * Listens for connections, handles connectes/disconnectes, e.g. + * Listens for connections, handles connects/disconnects, e.g. * * @return void */ @@ -142,7 +125,7 @@ public function run(): void while (true) { $this->timers->runAll(); - + $changed_sockets = $this->allsockets; @stream_select($changed_sockets, $write, $except, 0, 5000); foreach ($changed_sockets as $socket) { @@ -150,36 +133,42 @@ public function run(): void if (($ressource = stream_socket_accept($this->master)) === false) { $this->log('Socket error: ' . socket_strerror(socket_last_error($ressource))); continue; - } else { - $client = $this->createConnection($ressource); - $this->clients[(int)$ressource] = $client; - $this->allsockets[] = $ressource; - - if (count($this->clients) > $this->maxClients) { - $client->onDisconnect(); - if ($this->hasApplication('status')) { - $this->getApplication('status')->statusMsg( - 'Attention: Client Limit Reached!', - 'warning' - ); - } - continue; + } + + $client = $this->createConnection($ressource); + $this->clients[(int)$ressource] = $client; + $this->allsockets[] = $ressource; + + if (count($this->clients) > $this->maxClients) { + $client->onDisconnect(); + if ($this->hasApplication('status')) { + $app = $this->getApplication('status'); + + assert($app instanceof StatusApplication); + + $app->statusMsg( + 'Attention: Client Limit Reached!', + 'warning' + ); } + continue; + } + + $this->addIpToStorage($client->getClientIp()); + if ($this->checkMaxConnectionsPerIp($client->getClientIp()) === false) { + $client->onDisconnect(); + if ($this->hasApplication('status')) { + $app = $this->getApplication('status'); - $this->addIpToStorage($client->getClientIp()); - if ($this->checkMaxConnectionsPerIp($client->getClientIp()) === false) { - $client->onDisconnect(); - if ($this->hasApplication('status')) { - $this->getApplication('status')->statusMsg( - 'Connection/Ip limit for ip ' . $client->getClientIp() . ' was reached!', - 'warning' - ); - } - continue; + assert($app instanceof StatusApplication); + + $app->statusMsg( + 'Connection/Ip limit for ip ' . $client->getClientIp() . ' was reached!', + 'warning' + ); } } } else { - /** @var Connection $client */ $client = $this->clients[(int)$socket]; if (!is_object($client)) { unset($this->clients[(int)$socket]); @@ -207,7 +196,7 @@ public function run(): void } /** - * Checks if an application is registred. + * Checks if an application is registered. * * @param string $key * @return bool @@ -249,11 +238,14 @@ public function registerApplication(string $key, ApplicationInterface $applicati // status is kind of a system-app, needs some special cases: if ($key === 'status') { + assert($application instanceof StatusApplication); + $serverInfo = array( 'maxClients' => $this->maxClients, 'maxConnectionsPerIp' => $this->maxConnectionsPerIp, ); - $this->applications[$key]->setServerInfo($serverInfo); + + $application->setServerInfo($serverInfo); } } @@ -302,7 +294,11 @@ public function removeClientOnClose(Connection $client): void // trigger status application: if ($this->hasApplication('status')) { - $this->getApplication('status')->clientDisconnected($clientIp, $clientPort); + $app = $this->getApplication('status'); + + assert($app instanceof StatusApplication); + + $app->clientDisconnected($clientIp, $clientPort); } unset($clientIp, $clientPort, $resource); } @@ -332,10 +328,7 @@ public function removeClientOnError(Connection $client): void */ public function checkOrigin(string $domain): bool { - $domain = str_replace('http://', '', $domain); - $domain = str_replace('https://', '', $domain); - $domain = str_replace('www.', '', $domain); - $domain = str_replace('/', '', $domain); + $domain = str_replace(array('http://', 'https://', 'www.', '/'), '', $domain); return isset($this->allowedOrigins[$domain]); } @@ -400,20 +393,17 @@ private function checkMaxConnectionsPerIp(string $ip): bool if (!isset($this->ipStorage[$ip])) { return true; } - return ($this->ipStorage[$ip] > $this->maxConnectionsPerIp) ? false : true; + return $this->ipStorage[$ip] <= $this->maxConnectionsPerIp; } /** * Set whether the client origin should be checked on new connections. * * @param bool $doOriginCheck - * @return bool True if value could validated and set successfully. + * @return bool True if value could be validated and set successfully. */ public function setCheckOrigin(bool $doOriginCheck): bool { - if (is_bool($doOriginCheck) === false) { - return false; - } $this->checkOrigin = $doOriginCheck; return true; } @@ -435,9 +425,8 @@ public function getCheckOrigin(): bool */ public function setAllowedOrigin(string $domain): bool { - $domain = str_replace('http://', '', $domain); - $domain = str_replace('www.', '', $domain); - $domain = (strpos($domain, '/') !== false) ? substr($domain, 0, strpos($domain, '/')) : $domain; + $domain = str_replace(array('http://', 'www.'), '', $domain); + $domain = str_contains($domain, '/') ? substr($domain, 0, strpos($domain, '/')) : $domain; if (empty($domain)) { return false; } @@ -453,9 +442,6 @@ public function setAllowedOrigin(string $domain): bool */ public function setMaxConnectionsPerIp(int $limit): bool { - if (!is_int($limit)) { - return false; - } $this->maxConnectionsPerIp = $limit; return true; } @@ -463,7 +449,7 @@ public function setMaxConnectionsPerIp(int $limit): bool /** * Returns the max. connections per ip value. * - * @return int Max. simoultanous allowed connections for an ip to this server. + * @return int Max. simultaneous allowed connections for an ip to this server. */ public function getMaxConnectionsPerIp(): int { @@ -598,15 +584,15 @@ public function writeBuffer($resource, string $string): int */ private function openIPCSocket(string $ipcSocketPath): void { - if (substr(php_uname(), 0, 7) == "Windows"){ - $this->icpSocket = socket_create(AF_INET, SOCK_DGRAM, 0); - $ipcSocketPath = $this->host; - } else { - if (file_exists($ipcSocketPath)) { - unlink($ipcSocketPath); - } - $this->icpSocket = socket_create(AF_UNIX, SOCK_DGRAM, 0); - } + if (str_starts_with(php_uname(), "Windows")) { + $this->icpSocket = socket_create(AF_INET, SOCK_DGRAM, 0); + $ipcSocketPath = $this->host; + } else { + if (file_exists($ipcSocketPath)) { + unlink($ipcSocketPath); + } + $this->icpSocket = socket_create(AF_UNIX, SOCK_DGRAM, 0); + } if ($this->icpSocket === false) { throw new \RuntimeException('Could not open ipc socket.'); } @@ -635,7 +621,7 @@ private function openIPCSocket(string $ipcSocketPath): void private function handleIPC(): void { $buffer = ''; - if (substr(php_uname(), 0, 7) == "Windows") { + if (str_starts_with(php_uname(), "Windows")) { $from = ''; $port = 0; $bytesReceived = socket_recvfrom($this->icpSocket, $buffer, 65536, 0, $from, $port); From da11ff0907bbdedd0796a09ef50cd9e2eb1a64f8 Mon Sep 17 00:00:00 2001 From: Ricardo Boss Date: Sun, 7 Aug 2022 13:12:11 +0200 Subject: [PATCH 2/3] Replace soon-to-be deprecated strftime method with date --- src/Application/StatusApplication.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application/StatusApplication.php b/src/Application/StatusApplication.php index 564c920..ebd6e04 100644 --- a/src/Application/StatusApplication.php +++ b/src/Application/StatusApplication.php @@ -157,7 +157,7 @@ public function statusMsg(string $text, string $type = 'info'): void { $data = [ 'type' => $type, - 'text' => '[' . strftime('%m-%d %H:%M', time()) . '] ' . $text, + 'text' => '[' . date('m-d H:i') . '] ' . $text, ]; $encodedData = $this->encodeData('statusMsg', $data); $this->sendAll($encodedData); From fb1a3b581957b6d2e3082e1ad45238aa2f2801c5 Mon Sep 17 00:00:00 2001 From: Ricardo Boss Date: Sat, 22 Oct 2022 03:31:36 +0200 Subject: [PATCH 3/3] Fix invalid merge --- src/Server.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Server.php b/src/Server.php index ecb1903..c2d1ed1 100644 --- a/src/Server.php +++ b/src/Server.php @@ -27,11 +27,6 @@ class Server */ private $icpSocket; - /** - * @var null|string $ipcSocketPath - */ - private ?string $ipcSocketPath; - /** * @var string $ipcOwner If set, owner of the ipc socket will be changed to this value. */