From 286aad2ab3df99e9f0c0f785b693be782ca1ce3b Mon Sep 17 00:00:00 2001 From: mateu Date: Fri, 5 Feb 2021 17:01:02 +0100 Subject: [PATCH 01/14] Client --- src/Client.php | 396 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 396 insertions(+) create mode 100644 src/Client.php diff --git a/src/Client.php b/src/Client.php new file mode 100644 index 0000000..48dc1ce --- /dev/null +++ b/src/Client.php @@ -0,0 +1,396 @@ + + * @version 2.0 + */ +class Client +{ + /** + * @var string $host + */ + private $host; + + /** + * @var int $port + */ + private $port; + + /** + * @var string $path + */ + private $path; + + /** + * @var string $origin + */ + private $origin; + + /** + * @var resource $socket + */ + private $socket = null; + + /** + * @var bool $connected + */ + private $connected = false; + + /** + * @var int $timeout_seconds + */ + private $timeout_seconds = 0; + + /** + * @var int $timeout_microseconds; + */ + private $timeout_microseconds = 10000; + + + public function __destruct() + { + $this->disconnect(); + } + + /** + * Set Timeouts + * + * @param int $timeout_seconds + * @param int $timeout_microseconds + * @return void + */ + public function setTimeout( int $timeout_seconds = 0, int $timeout_microseconds = 10000 ) : void + { + if( $timeout_seconds >= 0 ) { + $this->timeout_seconds = $timeout_seconds; + } + if( $timeout_microseconds >= 0 ) { + $this->timeout_microseconds = $timeout_microseconds; + } + } + + /** + * Sends data to remote server. + * + * @param string $data + * @param string $type + * @param bool $masked + * @return bool + */ + public function sendData(string $data, string $type = 'text', bool $masked = true): bool + { + if ($this->connected === false) { + trigger_error("Not connected: ".print_r( stream_get_meta_data( $this->socket ), true ), E_USER_WARNING); + return false; + } + if (!is_string($data)) { + trigger_error("Not a string data was given.", E_USER_WARNING); + return false; + } + if (strlen($data) === 0) { + return false; + } + $res = @fwrite($this->socket, $this->hybi10Encode($data, $type, $masked)); + if ($res === 0 || $res === false) { + return false; + } + $buffer = ' '; + while ($buffer !== '') { + $buffer = fread($this->socket, 512);// drop? + } + + return true; + } + + /** + * Connects to a websocket server. + * + * @param string $host + * @param int $port + * @param string $path + * @param string $origin + * @return bool + */ + public function connect(string $host, int $port, string $path, string $origin = ''): bool + { + $this->host = $host; + $this->port = $port; + $this->path = $path; + $this->origin = $origin; + + $key = base64_encode($this->generateRandomString(16, false, true)); + $header = "GET " . $path . " HTTP/1.1\r\n"; + $header .= "Host: " . $host . ":" . $port . "\r\n"; + $header .= "Upgrade: websocket\r\n"; + $header .= "Connection: Upgrade\r\n"; + $header .= "Sec-WebSocket-Key: " . $key . "\r\n"; + if (!empty($origin)) { + $header .= "Sec-WebSocket-Origin: " . $origin . "\r\n"; + } + $header .= "Sec-WebSocket-Version: 13\r\n\r\n"; + + $this->socket = fsockopen($host, $port, $errno, $errstr, 2); + if ($this->socket === false) { + return false; + } + + socket_set_timeout($this->socket, $this->timeout_seconds, $this->timeout_microseconds ); + @fwrite($this->socket, $header); + $response = @fread($this->socket, 1500); + + preg_match('#Sec-WebSocket-Accept:\s(.*)$#mUi', (string)$response, $matches); + + if ($matches) { + $keyAccept = trim($matches[1]); + $expectedResponse = base64_encode(pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))); + $this->connected = ($keyAccept === $expectedResponse) ? true : false; + } + + return $this->connected; + } + + /** + * Checks if connection to webserver is active. + * + * @return bool + */ + public function checkConnection(): bool + { + $this->connected = false; + + // send ping: + $data = 'ping?'; + @fwrite($this->socket, $this->hybi10Encode($data, 'ping', true)); + $response = @fread($this->socket, 300); + if (empty($response)) { + return false; + } + $response = $this->hybi10Decode($response); + if (!is_array($response)) { + return false; + } + if (!isset($response['type']) || $response['type'] !== 'pong') { + return false; + } + $this->connected = true; + + return true; + } + + + /** + * Disconnectes from websocket server. + * + * @return void + */ + public function disconnect(): void + { + $this->connected = false; + is_resource($this->socket) && fclose($this->socket); + } + + /** + * Reconnects to previously connected websocket server. + * + * @return void + */ + public function reconnect(): void + { + sleep(10); + $this->connected = false; + fclose($this->socket); + $this->connect($this->host, $this->port, $this->path, $this->origin); + } + + /** + * Generates a random string. + * + * @param int $length + * @param bool $addSpaces + * @param bool $addNumbers + * @return string + */ + private function generateRandomString(int $length = 10, bool $addSpaces = true, bool $addNumbers = true): string + { + $characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"§$%&/()=[]{}'; + $useChars = []; + // select some random chars: + for ($i = 0; $i < $length; $i++) { + $useChars[] = $characters[mt_rand(0, strlen($characters) - 1)]; + } + // add spaces and numbers: + if ($addSpaces === true) { + array_push($useChars, ' ', ' ', ' ', ' ', ' ', ' '); + } + if ($addNumbers === true) { + array_push($useChars, rand(0, 9), rand(0, 9), rand(0, 9)); + } + shuffle($useChars); + $randomString = trim(implode('', $useChars)); + $randomString = substr($randomString, 0, $length); + + return $randomString; + } + + /** + * Encodes data according to the WebSocket protocol standard. + * + * @param string $payload + * @param string $type + * @param bool $masked + * @return string + */ + private function hybi10Encode(string $payload, string $type = 'text', bool $masked = true): string + { + $frameHead = []; + $payloadLength = strlen($payload); + + switch ($type) { + case 'text': + // first byte indicates FIN, Text-Frame (10000001): + $frameHead[0] = 129; + break; + + case 'close': + // first byte indicates FIN, Close Frame(10001000): + $frameHead[0] = 136; + break; + + case 'ping': + // first byte indicates FIN, Ping frame (10001001): + $frameHead[0] = 137; + break; + + case 'pong': + // first byte indicates FIN, Pong frame (10001010): + $frameHead[0] = 138; + break; + } + + // set mask and payload length (using 1, 3 or 9 bytes) + if ($payloadLength > 65535) { + $payloadLengthBin = str_split(sprintf('%064b', $payloadLength), 8); + $frameHead[1] = ($masked === true) ? 255 : 127; + for ($i = 0; $i < 8; $i++) { + $frameHead[$i + 2] = bindec($payloadLengthBin[$i]); + } + // most significant bit MUST be 0 (close connection if frame too big) + if ($frameHead[2] > 127) { + $this->disconnect(); + throw new \RuntimeException('Invalid payload. Could not encode frame.'); + } + } elseif ($payloadLength > 125) { + $payloadLengthBin = str_split(sprintf('%016b', $payloadLength), 8); + $frameHead[1] = ($masked === true) ? 254 : 126; + $frameHead[2] = bindec($payloadLengthBin[0]); + $frameHead[3] = bindec($payloadLengthBin[1]); + } else { + $frameHead[1] = ($masked === true) ? $payloadLength + 128 : $payloadLength; + } + + // convert frame-head to string: + foreach (array_keys($frameHead) as $i) { + $frameHead[$i] = chr($frameHead[$i]); + } + if ($masked === true) { + // generate a random mask: + $mask = []; + for ($i = 0; $i < 4; $i++) { + $mask[$i] = chr(rand(0, 255)); + } + + $frameHead = array_merge($frameHead, $mask); + } + $frame = implode('', $frameHead); + + // append payload to frame: + for ($i = 0; $i < $payloadLength; $i++) { + $frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i]; + } + + return $frame; + } + + /** + * Decodes a received frame/sting according to the WebSocket protocol standards. + * + * @param string $data + * @return array + */ + private function hybi10Decode(string $data): array + { + $unmaskedPayload = ''; + $decodedData = []; + + // estimate frame type: + $firstByteBinary = sprintf('%08b', ord($data[0])); + $secondByteBinary = sprintf('%08b', ord($data[1])); + $opcode = bindec(substr($firstByteBinary, 4, 4)); + $isMasked = ($secondByteBinary[0] == '1') ? true : false; + $payloadLength = ord($data[1]) & 127; + + switch ($opcode) { + // text frame: + case 1: + $decodedData['type'] = 'text'; + break; + case 2: + $decodedData['type'] = 'binary'; + break; + // connection close frame: + case 8: + $decodedData['type'] = 'close'; + break; + // ping frame: + case 9: + $decodedData['type'] = 'ping'; + break; + // pong frame: + case 10: + $decodedData['type'] = 'pong'; + break; + default: + throw new \RuntimeException('Could not decode frame. Invalid type.'); + } + + if ($payloadLength === 126) { + $mask = substr($data, 4, 4); + $payloadOffset = 8; + $dataLength = bindec(sprintf('%08b', ord($data[2])) . sprintf('%08b', ord($data[3]))) + $payloadOffset; + } elseif ($payloadLength === 127) { + $mask = substr($data, 10, 4); + $payloadOffset = 14; + $tmp = ''; + for ($i = 0; $i < 8; $i++) { + $tmp .= sprintf('%08b', ord($data[$i + 2])); + } + $dataLength = bindec($tmp) + $payloadOffset; + unset($tmp); + } else { + $mask = substr($data, 2, 4); + $payloadOffset = 6; + $dataLength = $payloadLength + $payloadOffset; + } + + if ($isMasked === true) { + for ($i = $payloadOffset; $i < $dataLength; $i++) { + $j = $i - $payloadOffset; + if (isset($data[$i])) { + $unmaskedPayload .= $data[$i] ^ $mask[$j % 4]; + } + } + $decodedData['payload'] = $unmaskedPayload; + } else { + $payloadOffset = $payloadOffset - 4; + $decodedData['payload'] = substr($data, $payloadOffset); + } + + return $decodedData; + } +} From c5c8e4f7d719db8770ee6a20afed4a4261d1adb3 Mon Sep 17 00:00:00 2001 From: Sikander Iqbal Date: Tue, 9 Mar 2021 19:31:49 -0500 Subject: [PATCH 02/14] Minor typos fix --- README.md | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index ad1b7ee..a913e6d 100644 --- a/README.md +++ b/README.md @@ -63,13 +63,13 @@ $server->registerApplication('chat', \Bloatless\WebSocket\Examples\Application\C $server->run(); ``` -Assuming this code is in a file called `server.php` you can than start your server with the following command: +Assuming this code is in a file called `server.php` you can then start your server with the following command: ```shell php server.php ``` -The websocket server will than listen for new connection on the provided host and port. By default, this will be +The websocket server will then listen for new connections on the provided host and port. By default, this will be `localhost:8000`. This repositoy also includes a working example in [examples/server.php](examples/server.php) @@ -94,15 +94,15 @@ interface ApplicationInterface } ``` -`onConnet` and `onDisconnect` can be used to keep track of all the clients connected to your application. `onData` will -be called whenever the websocket server receives new data from one of the clients connect to the application. +`onConnect` and `onDisconnect` can be used to keep track of all the clients connected to your application. `onData` will +be called whenever the websocket server receives new data from one of the clients connected to the application. `onIPCData` will be called if data is provided by another process on your machine. (See [Push-Client (IPC)](#push-client-ipc)) A working example of an application can be found in [examples/Application/Chat.php](examples/Application/Chat.php) ### Timers -A common requirement to long-running processes such as a websocket server is to execute tasks periodically. This can +A common requirement for long-running processes such as a websocket server is to execute tasks periodically. This can be done using timers. Timers can execute methods within your server or application periodically. Here is an example: ```php @@ -119,12 +119,12 @@ This example would call the method `someMethod` within your chat application eve ### Push-Client (IPC) It is often required to push data into the websocket-server process from another application. Let's assume you run a -website containg a chat and an area containing news or a blog. Now every time a new article is published in your blog +website containing a chat and an area containing news or a blog. Now every time a new article is published in your blog you want to notify all users currently in your chat. To achieve this you somehow need to push data from your blog logic into the websocket server. This is where the Push-Client comes into play. When starting the websocket server, it opens a unix-domain-socket and listens for new messages. The Push-Client can -than be used to send these messages. Here is an example: +then be used to send these messages. Here is an example: ```php $pushClient = new \Bloatless\WebSocket\PushClient('//tmp/phpwss.sock'); @@ -134,17 +134,16 @@ $pushClient->sendToApplication('chat', [ ]); ``` -This code pushes data into your running websocket-server process. In this case the `echo` Method within the -chat-application is called and sends the provided message to all connected clients. +This code pushes data into your running websocket-server process. In this case the `echo` method within the +chat-application is called and it sends the provided message to all connected clients. You can find the full working example in: [examples/push.php](examples/push.php) -**Important Hint:** Push messages can be not larger than 64kb! +**Important Hint:** Push messages cannot be larger than 64kb! ### Client (Browser/JS) -Everything above this point was related to the server-side of things. But how to connect to the server from your -browser? +Everything above this point was related to the server-side of things. But how to connect to the server from your browser? Here is a simple example: @@ -168,8 +167,8 @@ A better example of the chat client can be found in: [examples/public/chat.html] ## Intended use and limitations -This project was mainly build for educational purposes. The code is relatively simple and easy to understand. This -server was **not tested in production**, so I strongly recommand to not use it on a live project. It should be totally +This project was mainly built for educational purposes. The code is relatively simple and easy to understand. This +server was **not tested in production**, so I strongly recommend not to use it in a live project. It should be totally fine for small educational projects or internal tools, but most probably will not handle huge amounts of traffic or connections very well. From a539ebfc8f67f1b93a5608fd8b11fec76729575a Mon Sep 17 00:00:00 2001 From: Michael Grinspan Date: Thu, 18 Mar 2021 15:07:14 -0400 Subject: [PATCH 03/14] Fix Warning on PHP8 PHP8 emits a warning when `final` and `private` are used on the same method. ``` Warning: Private methods cannot be final as they are never overridden by other classes in {file} on line {line} ``` Example: https://3v4l.org/p2Ccp --- src/Application/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application/Application.php b/src/Application/Application.php index dcbafda..d2c090a 100644 --- a/src/Application/Application.php +++ b/src/Application/Application.php @@ -16,7 +16,7 @@ protected function __construct() // singleton construct required this method to be protected/private } - final private function __clone() + final protected function __clone() { // singleton construct required this method to be protected/private } From cd5871992f7198635df1d24e43ee51bba08bf810 Mon Sep 17 00:00:00 2001 From: Haugli92 Date: Wed, 6 Oct 2021 09:40:29 +0200 Subject: [PATCH 04/14] Adding headers to private property and function to get headers of the connection --- src/Connection.php | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/Connection.php b/src/Connection.php index 38b091b..62d908f 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -49,6 +49,11 @@ class Connection * @var string $dataBuffer */ private string $dataBuffer = ''; + + /** + * @var array $headers + */ + private array $headers = []; /** * @param Server $server @@ -104,16 +109,15 @@ private function handshake(string $data): bool $this->application = $this->server->getApplication($applicationKey); // generate headers array: - $headers = []; foreach ($lines as $line) { $line = chop($line); if (preg_match('/\A(\S+): (.*)\z/', $line, $matches)) { - $headers[ strtolower($matches[1])] = $matches[2]; + $this->headers[ strtolower($matches[1])] = $matches[2]; } } // check for supported websocket version: - if (!isset($headers['sec-websocket-version']) || $headers['sec-websocket-version'] < 6) { + if (!isset($this->headers['sec-websocket-version']) || $this->headers['sec-websocket-version'] < 6) { $this->log('Unsupported websocket version.'); $this->sendHttpResponse(501); stream_socket_shutdown($this->socket, STREAM_SHUT_RDWR); @@ -123,8 +127,8 @@ private function handshake(string $data): bool // check origin: if ($this->server->getCheckOrigin() === true) { - $origin = (isset($headers['sec-websocket-origin'])) ? $headers['sec-websocket-origin'] : ''; - $origin = (isset($headers['origin'])) ? $headers['origin'] : $origin; + $origin = (isset($this->headers['sec-websocket-origin'])) ? $this->headers['sec-websocket-origin'] : ''; + $origin = (isset($this->headers['origin'])) ? $this->headers['origin'] : $origin; if (empty($origin)) { $this->log('No origin provided.'); $this->sendHttpResponse(401); @@ -143,13 +147,13 @@ private function handshake(string $data): bool } // do handyshake: (hybi-10) - $secKey = $headers['sec-websocket-key']; + $secKey = $this->headers['sec-websocket-key']; $secAccept = base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))); $response = "HTTP/1.1 101 Switching Protocols\r\n"; $response .= "Upgrade: websocket\r\n"; $response .= "Connection: Upgrade\r\n"; $response .= "Sec-WebSocket-Accept: " . $secAccept . "\r\n"; - if (isset($headers['sec-websocket-protocol']) && !empty($headers['sec-websocket-protocol'])) { + if (isset($this->headers['sec-websocket-protocol']) && !empty($this->headers['sec-websocket-protocol'])) { $response .= "Sec-WebSocket-Protocol: " . substr($path, 1) . "\r\n"; } $response .= "\r\n"; @@ -588,6 +592,15 @@ public function getClientSocket() { return $this->socket; } + + /** + * Return the headers of the connection + * @return array + */ + public function getHeaders(): array + { + return $this->headers; + } /** * Returns the application the client is connected to. From f69977aa8167726e39d9645fa8a4f17f5dfbdb88 Mon Sep 17 00:00:00 2001 From: Haugli92 Date: Wed, 6 Oct 2021 09:45:31 +0200 Subject: [PATCH 05/14] Renamed getHeaders to getClientHeaders --- src/Connection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Connection.php b/src/Connection.php index 62d908f..cbbae6d 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -597,7 +597,7 @@ public function getClientSocket() * Return the headers of the connection * @return array */ - public function getHeaders(): array + public function getClientHeaders(): array { return $this->headers; } From f5294a55778b9f7c9fd09d7e8a08f658757dde77 Mon Sep 17 00:00:00 2001 From: Haugli92 Date: Wed, 6 Oct 2021 10:40:50 +0200 Subject: [PATCH 06/14] Changed package-vendor --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 9d76a31..8aaeb3f 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "bloatless/php-websocket", + "name": "haugli92/php-websocket", "description": "A simple WebSocket Server and Client implementation in PHP.", "keywords": [ "php", From 1c9a98a150d46edc451c8542761ce74625dc4409 Mon Sep 17 00:00:00 2001 From: Haugli92 Date: Wed, 6 Oct 2021 10:42:29 +0200 Subject: [PATCH 07/14] Changed package-vendor --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 8aaeb3f..9d76a31 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "haugli92/php-websocket", + "name": "bloatless/php-websocket", "description": "A simple WebSocket Server and Client implementation in PHP.", "keywords": [ "php", From 5d9d5122e8160943c265d42ee068e74f15b1ae04 Mon Sep 17 00:00:00 2001 From: Ricardo Martins Date: Wed, 3 Nov 2021 10:53:25 +1100 Subject: [PATCH 08/14] Remove final from private magic method In order to fix PHP Warning: Private methods cannot be final as they are never overridden by other classes in .../src/Application/Application.php on line 19 --- src/Application/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application/Application.php b/src/Application/Application.php index d2c090a..8700415 100644 --- a/src/Application/Application.php +++ b/src/Application/Application.php @@ -16,7 +16,7 @@ protected function __construct() // singleton construct required this method to be protected/private } - final protected function __clone() + protected function __clone() { // singleton construct required this method to be protected/private } From 6ae8e1eebf1cb97fa873f96d418c9843c3b59066 Mon Sep 17 00:00:00 2001 From: Mateu Santandreu Date: Tue, 28 Dec 2021 15:19:18 +0100 Subject: [PATCH 09/14] Delete Client.php --- src/Client.php | 396 ------------------------------------------------- 1 file changed, 396 deletions(-) delete mode 100644 src/Client.php diff --git a/src/Client.php b/src/Client.php deleted file mode 100644 index 48dc1ce..0000000 --- a/src/Client.php +++ /dev/null @@ -1,396 +0,0 @@ - - * @version 2.0 - */ -class Client -{ - /** - * @var string $host - */ - private $host; - - /** - * @var int $port - */ - private $port; - - /** - * @var string $path - */ - private $path; - - /** - * @var string $origin - */ - private $origin; - - /** - * @var resource $socket - */ - private $socket = null; - - /** - * @var bool $connected - */ - private $connected = false; - - /** - * @var int $timeout_seconds - */ - private $timeout_seconds = 0; - - /** - * @var int $timeout_microseconds; - */ - private $timeout_microseconds = 10000; - - - public function __destruct() - { - $this->disconnect(); - } - - /** - * Set Timeouts - * - * @param int $timeout_seconds - * @param int $timeout_microseconds - * @return void - */ - public function setTimeout( int $timeout_seconds = 0, int $timeout_microseconds = 10000 ) : void - { - if( $timeout_seconds >= 0 ) { - $this->timeout_seconds = $timeout_seconds; - } - if( $timeout_microseconds >= 0 ) { - $this->timeout_microseconds = $timeout_microseconds; - } - } - - /** - * Sends data to remote server. - * - * @param string $data - * @param string $type - * @param bool $masked - * @return bool - */ - public function sendData(string $data, string $type = 'text', bool $masked = true): bool - { - if ($this->connected === false) { - trigger_error("Not connected: ".print_r( stream_get_meta_data( $this->socket ), true ), E_USER_WARNING); - return false; - } - if (!is_string($data)) { - trigger_error("Not a string data was given.", E_USER_WARNING); - return false; - } - if (strlen($data) === 0) { - return false; - } - $res = @fwrite($this->socket, $this->hybi10Encode($data, $type, $masked)); - if ($res === 0 || $res === false) { - return false; - } - $buffer = ' '; - while ($buffer !== '') { - $buffer = fread($this->socket, 512);// drop? - } - - return true; - } - - /** - * Connects to a websocket server. - * - * @param string $host - * @param int $port - * @param string $path - * @param string $origin - * @return bool - */ - public function connect(string $host, int $port, string $path, string $origin = ''): bool - { - $this->host = $host; - $this->port = $port; - $this->path = $path; - $this->origin = $origin; - - $key = base64_encode($this->generateRandomString(16, false, true)); - $header = "GET " . $path . " HTTP/1.1\r\n"; - $header .= "Host: " . $host . ":" . $port . "\r\n"; - $header .= "Upgrade: websocket\r\n"; - $header .= "Connection: Upgrade\r\n"; - $header .= "Sec-WebSocket-Key: " . $key . "\r\n"; - if (!empty($origin)) { - $header .= "Sec-WebSocket-Origin: " . $origin . "\r\n"; - } - $header .= "Sec-WebSocket-Version: 13\r\n\r\n"; - - $this->socket = fsockopen($host, $port, $errno, $errstr, 2); - if ($this->socket === false) { - return false; - } - - socket_set_timeout($this->socket, $this->timeout_seconds, $this->timeout_microseconds ); - @fwrite($this->socket, $header); - $response = @fread($this->socket, 1500); - - preg_match('#Sec-WebSocket-Accept:\s(.*)$#mUi', (string)$response, $matches); - - if ($matches) { - $keyAccept = trim($matches[1]); - $expectedResponse = base64_encode(pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))); - $this->connected = ($keyAccept === $expectedResponse) ? true : false; - } - - return $this->connected; - } - - /** - * Checks if connection to webserver is active. - * - * @return bool - */ - public function checkConnection(): bool - { - $this->connected = false; - - // send ping: - $data = 'ping?'; - @fwrite($this->socket, $this->hybi10Encode($data, 'ping', true)); - $response = @fread($this->socket, 300); - if (empty($response)) { - return false; - } - $response = $this->hybi10Decode($response); - if (!is_array($response)) { - return false; - } - if (!isset($response['type']) || $response['type'] !== 'pong') { - return false; - } - $this->connected = true; - - return true; - } - - - /** - * Disconnectes from websocket server. - * - * @return void - */ - public function disconnect(): void - { - $this->connected = false; - is_resource($this->socket) && fclose($this->socket); - } - - /** - * Reconnects to previously connected websocket server. - * - * @return void - */ - public function reconnect(): void - { - sleep(10); - $this->connected = false; - fclose($this->socket); - $this->connect($this->host, $this->port, $this->path, $this->origin); - } - - /** - * Generates a random string. - * - * @param int $length - * @param bool $addSpaces - * @param bool $addNumbers - * @return string - */ - private function generateRandomString(int $length = 10, bool $addSpaces = true, bool $addNumbers = true): string - { - $characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"§$%&/()=[]{}'; - $useChars = []; - // select some random chars: - for ($i = 0; $i < $length; $i++) { - $useChars[] = $characters[mt_rand(0, strlen($characters) - 1)]; - } - // add spaces and numbers: - if ($addSpaces === true) { - array_push($useChars, ' ', ' ', ' ', ' ', ' ', ' '); - } - if ($addNumbers === true) { - array_push($useChars, rand(0, 9), rand(0, 9), rand(0, 9)); - } - shuffle($useChars); - $randomString = trim(implode('', $useChars)); - $randomString = substr($randomString, 0, $length); - - return $randomString; - } - - /** - * Encodes data according to the WebSocket protocol standard. - * - * @param string $payload - * @param string $type - * @param bool $masked - * @return string - */ - private function hybi10Encode(string $payload, string $type = 'text', bool $masked = true): string - { - $frameHead = []; - $payloadLength = strlen($payload); - - switch ($type) { - case 'text': - // first byte indicates FIN, Text-Frame (10000001): - $frameHead[0] = 129; - break; - - case 'close': - // first byte indicates FIN, Close Frame(10001000): - $frameHead[0] = 136; - break; - - case 'ping': - // first byte indicates FIN, Ping frame (10001001): - $frameHead[0] = 137; - break; - - case 'pong': - // first byte indicates FIN, Pong frame (10001010): - $frameHead[0] = 138; - break; - } - - // set mask and payload length (using 1, 3 or 9 bytes) - if ($payloadLength > 65535) { - $payloadLengthBin = str_split(sprintf('%064b', $payloadLength), 8); - $frameHead[1] = ($masked === true) ? 255 : 127; - for ($i = 0; $i < 8; $i++) { - $frameHead[$i + 2] = bindec($payloadLengthBin[$i]); - } - // most significant bit MUST be 0 (close connection if frame too big) - if ($frameHead[2] > 127) { - $this->disconnect(); - throw new \RuntimeException('Invalid payload. Could not encode frame.'); - } - } elseif ($payloadLength > 125) { - $payloadLengthBin = str_split(sprintf('%016b', $payloadLength), 8); - $frameHead[1] = ($masked === true) ? 254 : 126; - $frameHead[2] = bindec($payloadLengthBin[0]); - $frameHead[3] = bindec($payloadLengthBin[1]); - } else { - $frameHead[1] = ($masked === true) ? $payloadLength + 128 : $payloadLength; - } - - // convert frame-head to string: - foreach (array_keys($frameHead) as $i) { - $frameHead[$i] = chr($frameHead[$i]); - } - if ($masked === true) { - // generate a random mask: - $mask = []; - for ($i = 0; $i < 4; $i++) { - $mask[$i] = chr(rand(0, 255)); - } - - $frameHead = array_merge($frameHead, $mask); - } - $frame = implode('', $frameHead); - - // append payload to frame: - for ($i = 0; $i < $payloadLength; $i++) { - $frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i]; - } - - return $frame; - } - - /** - * Decodes a received frame/sting according to the WebSocket protocol standards. - * - * @param string $data - * @return array - */ - private function hybi10Decode(string $data): array - { - $unmaskedPayload = ''; - $decodedData = []; - - // estimate frame type: - $firstByteBinary = sprintf('%08b', ord($data[0])); - $secondByteBinary = sprintf('%08b', ord($data[1])); - $opcode = bindec(substr($firstByteBinary, 4, 4)); - $isMasked = ($secondByteBinary[0] == '1') ? true : false; - $payloadLength = ord($data[1]) & 127; - - switch ($opcode) { - // text frame: - case 1: - $decodedData['type'] = 'text'; - break; - case 2: - $decodedData['type'] = 'binary'; - break; - // connection close frame: - case 8: - $decodedData['type'] = 'close'; - break; - // ping frame: - case 9: - $decodedData['type'] = 'ping'; - break; - // pong frame: - case 10: - $decodedData['type'] = 'pong'; - break; - default: - throw new \RuntimeException('Could not decode frame. Invalid type.'); - } - - if ($payloadLength === 126) { - $mask = substr($data, 4, 4); - $payloadOffset = 8; - $dataLength = bindec(sprintf('%08b', ord($data[2])) . sprintf('%08b', ord($data[3]))) + $payloadOffset; - } elseif ($payloadLength === 127) { - $mask = substr($data, 10, 4); - $payloadOffset = 14; - $tmp = ''; - for ($i = 0; $i < 8; $i++) { - $tmp .= sprintf('%08b', ord($data[$i + 2])); - } - $dataLength = bindec($tmp) + $payloadOffset; - unset($tmp); - } else { - $mask = substr($data, 2, 4); - $payloadOffset = 6; - $dataLength = $payloadLength + $payloadOffset; - } - - if ($isMasked === true) { - for ($i = $payloadOffset; $i < $dataLength; $i++) { - $j = $i - $payloadOffset; - if (isset($data[$i])) { - $unmaskedPayload .= $data[$i] ^ $mask[$j % 4]; - } - } - $decodedData['payload'] = $unmaskedPayload; - } else { - $payloadOffset = $payloadOffset - 4; - $decodedData['payload'] = substr($data, $payloadOffset); - } - - return $decodedData; - } -} From c31e172e3b3a7ce4aecc516d943c02ac91ef02e9 Mon Sep 17 00:00:00 2001 From: mateu Date: Tue, 28 Dec 2021 15:22:56 +0100 Subject: [PATCH 10/14] fix: Type: Error; Message: Typed property Bloatless\WebSocket\Server:: must not be accessed before initialization --- src/Server.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Server.php b/src/Server.php index 3f7579d..03d6c73 100644 --- a/src/Server.php +++ b/src/Server.php @@ -124,6 +124,7 @@ public function __construct( $this->host = $host; $this->port = $port; $this->ipcSocketPath = $ipcSocketPath; + $this->timers = new TimerCollection(); } /** @@ -137,7 +138,6 @@ public function run(): void ob_implicit_flush(); $this->createSocket($this->host, $this->port); $this->openIPCSocket($this->ipcSocketPath); - $this->timers = new TimerCollection(); $this->log('Server created'); while (true) { From 6edb79d5a660326074bedf6e2335845631f78548 Mon Sep 17 00:00:00 2001 From: Kevin Simon Date: Mon, 31 Jan 2022 15:45:37 +0100 Subject: [PATCH 11/14] Windows fix --- src/Server.php | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/Server.php b/src/Server.php index 03d6c73..4ad301b 100644 --- a/src/Server.php +++ b/src/Server.php @@ -598,10 +598,15 @@ public function writeBuffer($resource, string $string): int */ private function openIPCSocket(string $ipcSocketPath): void { - if (file_exists($ipcSocketPath)) { - unlink($ipcSocketPath); - } - $this->icpSocket = socket_create(AF_UNIX, SOCK_DGRAM, 0); + 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 ($this->icpSocket === false) { throw new \RuntimeException('Could not open ipc socket.'); } @@ -630,7 +635,13 @@ private function openIPCSocket(string $ipcSocketPath): void private function handleIPC(): void { $buffer = ''; - $bytesReceived = socket_recvfrom($this->icpSocket, $buffer, 65536, 0, $this->ipcSocketPath); + if (substr(php_uname(), 0, 7) == "Windows") { + $from = ''; + $port = 0; + $bytesReceived = socket_recvfrom($this->icpSocket, $buffer, 65536, 0, $from, $port); + } else { + $bytesReceived = socket_recvfrom($this->icpSocket, $buffer, 65536, 0, $this->ipcSocketPath); + } if ($bytesReceived === false) { return; } From 73a1b651471108e1a37f925ccf7ce6215dfcf7ec Mon Sep 17 00:00:00 2001 From: Haugli92 Date: Mon, 14 Mar 2022 08:33:16 +0100 Subject: [PATCH 12/14] Fixed a problem $timers must not be accessed before initialization --- src/Server.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Server.php b/src/Server.php index 3f7579d..03d6c73 100644 --- a/src/Server.php +++ b/src/Server.php @@ -124,6 +124,7 @@ public function __construct( $this->host = $host; $this->port = $port; $this->ipcSocketPath = $ipcSocketPath; + $this->timers = new TimerCollection(); } /** @@ -137,7 +138,6 @@ public function run(): void ob_implicit_flush(); $this->createSocket($this->host, $this->port); $this->openIPCSocket($this->ipcSocketPath); - $this->timers = new TimerCollection(); $this->log('Server created'); while (true) { From 697c2c8aff718a29bb9209b61c425f2fc0dae339 Mon Sep 17 00:00:00 2001 From: Ricardo Boss Date: Sun, 7 Aug 2022 01:27:20 +0200 Subject: [PATCH 13/14] Make IPC optional --- src/Server.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Server.php b/src/Server.php index 4ad301b..de7a408 100644 --- a/src/Server.php +++ b/src/Server.php @@ -37,9 +37,9 @@ class Server private $icpSocket; /** - * @var string $ipcSocketPath + * @var null|string $ipcSocketPath */ - private string $ipcSocketPath; + private ?string $ipcSocketPath; /** * @var string $ipcOwner If set, owner of the ipc socket will be changed to this value. @@ -114,12 +114,12 @@ class Server /** * @param string $host * @param int $port - * @param string $ipcSocketPath + * @param null|string $ipcSocketPath */ public function __construct( string $host = 'localhost', int $port = 8000, - string $ipcSocketPath = '/tmp/phpwss.sock' + ?string $ipcSocketPath = '/tmp/phpwss.sock' ) { $this->host = $host; $this->port = $port; @@ -137,12 +137,13 @@ public function run(): void { ob_implicit_flush(); $this->createSocket($this->host, $this->port); - $this->openIPCSocket($this->ipcSocketPath); + if ($this->ipcSocketPath) + $this->openIPCSocket($this->ipcSocketPath); $this->log('Server created'); while (true) { $this->timers->runAll(); - + $changed_sockets = $this->allsockets; @stream_select($changed_sockets, $write, $except, 0, 5000); foreach ($changed_sockets as $socket) { From 4de006c126644f8ff868f4249ecb648b207cb697 Mon Sep 17 00:00:00 2001 From: Simon Samtleben Date: Sat, 20 Aug 2022 10:39:14 +0200 Subject: [PATCH 14/14] Fix code style --- src/Server.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Server.php b/src/Server.php index de7a408..916d710 100644 --- a/src/Server.php +++ b/src/Server.php @@ -137,8 +137,9 @@ public function run(): void { ob_implicit_flush(); $this->createSocket($this->host, $this->port); - if ($this->ipcSocketPath) + if ($this->ipcSocketPath) { $this->openIPCSocket($this->ipcSocketPath); + } $this->log('Server created'); while (true) {