From 0b32dd9a5cb177c53c9d02777275f69147d06066 Mon Sep 17 00:00:00 2001 From: akushman Date: Sat, 11 Jan 2020 12:48:20 +0300 Subject: [PATCH] #45: Add origin hosts check to server + readme --- README.md | 12 ++++- src/Components/Connection.php | 11 ++--- src/Components/OriginComponent.php | 56 +++++++++++++++++++++++ src/Components/ServerConfig.php | 56 ++++++++++++++++++++++- src/Components/WSClientTrait.php | 1 - src/Components/WssMain.php | 1 - src/Contracts/CommonsContract.php | 1 - src/Contracts/ConnectionContract.php | 1 - src/Contracts/WebSocketServerContract.php | 1 - src/Contracts/WscCommonsContract.php | 1 - src/Exceptions/WebSocketException.php | 1 - src/WebSocketClient.php | 3 +- src/WebSocketServer.php | 31 +++++++++---- tests/ServerHandler.php | 1 - tests/WebSocketClientTest.php | 8 ++-- tests/WebSocketServerTest.php | 1 - 16 files changed, 153 insertions(+), 33 deletions(-) create mode 100644 src/Components/OriginComponent.php diff --git a/README.md b/README.md index c938362..74ee660 100755 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ just add to your projects composer.json. -### Implement Your WebSocket handler class - ex.: +### Implement your WebSocket handler class - ex.: ```php broadCast('hey everybody...'); $conn->broadCastMany(['Hello', 'how are you today?', 'have a nice day'], 2); ``` +### Origin check +To let server check the Origin header with `n` hosts provided: +```php +$config = new ServerConfig(); +$config->setOrigins(["example.com", "otherexample.com"]); +$websocketServer = new WebSocketServer(new ServerHandler(), $config); +$websocketServer->run(); +``` +Server will automatically check those hosts proceeding to listen for other connections even if some failed to pass check. + ### How to test To run the Server - execute from the root of a project: diff --git a/src/Components/Connection.php b/src/Components/Connection.php index 6898538..67d9ac7 100755 --- a/src/Components/Connection.php +++ b/src/Components/Connection.php @@ -8,7 +8,6 @@ class Connection implements ConnectionContract, CommonsContract { - private $socketConnection; private $clients; @@ -65,8 +64,8 @@ public function broadCast(string $data): void /** * Broadcasting many messages with delay * - * @param array $data An array of messages (strings) sent to many clients - * @param int $delay Time in seconds to delay between messages + * @param array $data An array of messages (strings) sent to many clients + * @param int $delay Time in seconds to delay between messages * @throws \Exception */ public function broadCastMany(array $data, int $delay = 0): void @@ -110,9 +109,9 @@ private function encode($payload, string $type = self::EVENT_TYPE_TEXT, bool $ma // most significant bit MUST be 0 if ($frameHead[2] > self::MASK_127) { return [ - 'type' => $type, + 'type' => $type, 'payload' => $payload, - 'error' => WebSocketServerContract::ERR_FRAME_TOO_LARGE, + 'error' => WebSocketServerContract::ERR_FRAME_TOO_LARGE, ]; } } elseif ($payloadLength > self::MASK_125) { @@ -130,7 +129,7 @@ private function encode($payload, string $type = self::EVENT_TYPE_TEXT, bool $ma /** * Gets frame-head based on type of operation * - * @param string $type Types of operation encode-frames + * @param string $type Types of operation encode-frames * @return array */ private function getOpType(string $type): array diff --git a/src/Components/OriginComponent.php b/src/Components/OriginComponent.php new file mode 100644 index 0000000..20b0207 --- /dev/null +++ b/src/Components/OriginComponent.php @@ -0,0 +1,56 @@ +config = $config; + $this->client = $client; + } + + /** + * Checks if there is a compatible origin header came from client + * @param string $headers + * @return bool + */ + public function checkOrigin(string $headers): bool + { + preg_match('/Origin\:\s(.*?)\s/', $headers, $matches); + if (empty($matches[1])) { + $this->sendAndClose('No Origin header found.'); + return false; + } else { + $originHost = $matches[1]; + $allowedOrigins = $this->config->getOrigins(); + if (in_array($originHost, $allowedOrigins, true) === false) { + $this->sendAndClose('Host ' . $originHost . ' is not allowed to pass access control as origin.'); + return false; + } + } + return true; + } + + /** + * @param string $msg + * @throws \Exception + */ + private function sendAndClose(string $msg) + { + $conn = new Connection($this->client); + $conn->send($msg); + $conn->close(); + } +} \ No newline at end of file diff --git a/src/Components/ServerConfig.php b/src/Components/ServerConfig.php index d4a0f40..8f0f10a 100644 --- a/src/Components/ServerConfig.php +++ b/src/Components/ServerConfig.php @@ -6,7 +6,6 @@ class ServerConfig { - private $clientsPerFork = WebSocketServerContract::CLIENTS_PER_FORK; private $streamSelectTimeout = WebSocketServerContract::STREAM_SELECT_TIMEOUT; @@ -17,6 +16,10 @@ class ServerConfig private $processName = WebSocketServerContract::PROC_TITLE; + private $checkOrigin = false; + private $origins = []; + private $originHeader = false; + /** * @return mixed */ @@ -112,4 +115,55 @@ public function setProcessName(string $processName): void { $this->processName = $processName; } + + /** + * @return bool + */ + public function isCheckOrigin(): bool + { + return $this->checkOrigin; + } + + /** + * @param bool $checkOrigin + */ + public function setCheckOrigin(bool $checkOrigin): void + { + $this->checkOrigin = $checkOrigin; + } + + /** + * @return array + */ + public function getOrigins(): array + { + return $this->origins; + } + + /** + * @param array $origins + */ + public function setOrigins(array $origins): void + { + if (empty($origins) === false) { + $this->setCheckOrigin(true); + } + $this->origins = $origins; + } + + /** + * @return bool + */ + public function isOriginHeader(): bool + { + return $this->originHeader; + } + + /** + * @param bool $originHeader + */ + public function setOriginHeader(bool $originHeader): void + { + $this->originHeader = $originHeader; + } } \ No newline at end of file diff --git a/src/Components/WSClientTrait.php b/src/Components/WSClientTrait.php index 623cbc0..51d9cf7 100644 --- a/src/Components/WSClientTrait.php +++ b/src/Components/WSClientTrait.php @@ -9,7 +9,6 @@ trait WSClientTrait { - /** * Validates whether server sent valid upgrade response * diff --git a/src/Components/WssMain.php b/src/Components/WssMain.php index 53a7312..9aa7c05 100644 --- a/src/Components/WssMain.php +++ b/src/Components/WssMain.php @@ -16,7 +16,6 @@ */ class WssMain implements CommonsContract { - private $isPcntlLoaded = false; /** diff --git a/src/Contracts/CommonsContract.php b/src/Contracts/CommonsContract.php index a1ef365..b621eb2 100755 --- a/src/Contracts/CommonsContract.php +++ b/src/Contracts/CommonsContract.php @@ -8,7 +8,6 @@ */ interface CommonsContract { - // DADA types public const EVENT_TYPE_PING = 'ping'; public const EVENT_TYPE_PONG = 'pong'; diff --git a/src/Contracts/ConnectionContract.php b/src/Contracts/ConnectionContract.php index b0f5026..800cf05 100755 --- a/src/Contracts/ConnectionContract.php +++ b/src/Contracts/ConnectionContract.php @@ -8,7 +8,6 @@ */ interface ConnectionContract { - public function send(string $data): void; public function close(): void; diff --git a/src/Contracts/WebSocketServerContract.php b/src/Contracts/WebSocketServerContract.php index 3f925ee..20025bf 100755 --- a/src/Contracts/WebSocketServerContract.php +++ b/src/Contracts/WebSocketServerContract.php @@ -8,7 +8,6 @@ */ interface WebSocketServerContract { - // HOST/PORT public const DEFAULT_HOST = '0.0.0.0'; public const DEFAULT_PORT = 8000; diff --git a/src/Contracts/WscCommonsContract.php b/src/Contracts/WscCommonsContract.php index 7957602..ef9c743 100755 --- a/src/Contracts/WscCommonsContract.php +++ b/src/Contracts/WscCommonsContract.php @@ -8,7 +8,6 @@ */ interface WscCommonsContract { - public const TCP_SCHEME = 'tcp://'; public const MAX_BYTES_READ = 65535; diff --git a/src/Exceptions/WebSocketException.php b/src/Exceptions/WebSocketException.php index 01071e8..8d25001 100755 --- a/src/Exceptions/WebSocketException.php +++ b/src/Exceptions/WebSocketException.php @@ -4,7 +4,6 @@ class WebSocketException extends \Exception { - public function printStack() { echo $this->getFile() . ' ' . $this->getLine() . ' ' . $this->getMessage() . PHP_EOL; diff --git a/src/WebSocketClient.php b/src/WebSocketClient.php index 998cca3..14051e0 100755 --- a/src/WebSocketClient.php +++ b/src/WebSocketClient.php @@ -7,12 +7,11 @@ class WebSocketClient extends WscMain { - /** * Sets parameters for Web Socket Client intercommunication * * @param string $url string representation of a socket utf, ex.: tcp://www.example.com:8000 or udp://example.com:13 - * @param ClientConfig $config Client configuration settings e.g.: connection - timeout, ssl options, fragment message size to send etc. + * @param ClientConfig $config Client configuration settings e.g.: connection - timeout, ssl options, fragment message size to send etc. * @throws \InvalidArgumentException * @throws Exceptions\BadUriException * @throws Exceptions\ConnectionException diff --git a/src/WebSocketServer.php b/src/WebSocketServer.php index 70e6b96..8a09d7f 100755 --- a/src/WebSocketServer.php +++ b/src/WebSocketServer.php @@ -3,6 +3,7 @@ namespace WSSC; use WSSC\Components\Connection; +use WSSC\Components\OriginComponent; use WSSC\Components\ServerConfig; use WSSC\Components\WssMain; use WSSC\Contracts\CommonsContract; @@ -19,7 +20,6 @@ */ class WebSocketServer extends WssMain implements WebSocketServerContract { - private $clients = []; // set any template You need ex.: GET /subscription/messenger/token private $pathParams = []; @@ -34,7 +34,7 @@ class WebSocketServer extends WssMain implements WebSocketServerContract // for the very 1st time must be true private $stepRecursion = true; - private const MAX_BYTES_READ = 8192; + private const MAX_BYTES_READ = 8192; private const HEADER_BYTES_READ = 1024; // stream non-blocking @@ -49,7 +49,8 @@ class WebSocketServer extends WssMain implements WebSocketServerContract public function __construct( WebSocket $handler, ServerConfig $config - ) { + ) + { ini_set('default_socket_timeout', 5); // this should be >= 5 sec, otherwise there will be broken pipe - tested $this->handler = $handler; @@ -85,7 +86,7 @@ public function run() * and when forks equals true which prevents it from infinite recursive iterations * * @param resource $server server connection - * @param bool $fork flag to fork or run event loop + * @param bool $fork flag to fork or run event loop * @throws WebSocketException * @throws ConnectionException */ @@ -153,6 +154,9 @@ private function looping($server) //new client if (in_array($server, $readSocks, false)) { $this->acceptNewClient($server, $readSocks); + if ($this->config->isCheckOrigin() && $this->config->isOriginHeader() === false) { + continue; + } } //message from existing client @@ -169,9 +173,16 @@ private function acceptNewClient($server, array &$readSocks) { $newClient = stream_socket_accept($server, 0); // must be 0 to non-block if ($newClient) { - // important to read from headers here coz later client will change and there will be only msgs on pipe $headers = fread($newClient, self::HEADER_BYTES_READ); + if ($this->config->isCheckOrigin()) { + $hasOrigin = (new OriginComponent($this->config, $newClient))->checkOrigin($headers); + $this->config->setOriginHeader($hasOrigin); + if ($hasOrigin === false) { + return; + } + } + if (empty($this->handler->pathParams[0]) === false) { $this->setPathParams($headers); } @@ -189,10 +200,10 @@ private function acceptNewClient($server, array &$readSocks) } /** - * @uses onMessage + * @param array $readSocks * @uses onPing * @uses onPong - * @param array $readSocks + * @uses onMessage */ private function messagesWorker(array $readSocks) { @@ -233,7 +244,7 @@ private function messagesWorker(array $readSocks) * Handshakes/upgrade and key parse * * @param resource $client Source client socket to write - * @param string $headers Headers that client has been sent + * @param string $headers Headers that client has been sent * @return string socket handshake key (Sec-WebSocket-Key)| false on parse error * @throws ConnectionException */ @@ -266,8 +277,8 @@ private function handshake($client, string $headers): string private function setHeadersUpgrade($secWebSocketAccept) { $this->headersUpgrade = [ - self::HEADERS_UPGRADE_KEY => self::HEADERS_UPGRADE_VALUE, - self::HEADERS_CONNECTION_KEY => self::HEADERS_CONNECTION_VALUE, + self::HEADERS_UPGRADE_KEY => self::HEADERS_UPGRADE_VALUE, + self::HEADERS_CONNECTION_KEY => self::HEADERS_CONNECTION_VALUE, self::HEADERS_SEC_WEBSOCKET_ACCEPT_KEY => ' ' . $secWebSocketAccept // the space before key is really important ]; diff --git a/tests/ServerHandler.php b/tests/ServerHandler.php index d2cfdea..c78f96b 100755 --- a/tests/ServerHandler.php +++ b/tests/ServerHandler.php @@ -10,7 +10,6 @@ class ServerHandler extends WebSocket { - /* * if You need to parse URI context like /messanger/chat/JKN324jn4213 * You can do so by placing URI parts into an array - $pathParams, when Socket will receive a connection diff --git a/tests/WebSocketClientTest.php b/tests/WebSocketClientTest.php index 4a5db4c..d690711 100644 --- a/tests/WebSocketClientTest.php +++ b/tests/WebSocketClientTest.php @@ -9,11 +9,10 @@ class WebSocketClientTest extends TestCase { - private const WS_SCHEME = 'ws://'; - private const WS_HOST = 'localhost'; - private const WS_PORT = ':8000'; - private const WS_URI = '/notifications/messanger/vkjsndfvjn23243'; + private const WS_HOST = 'localhost'; + private const WS_PORT = ':8000'; + private const WS_URI = '/notifications/messanger/vkjsndfvjn23243'; private $url; @@ -50,6 +49,7 @@ public function it_sends_with_headers_via_constructor() $config->setTimeout(15); $config->setHeaders([ 'X-Custom-Header' => 'Foo Bar Baz', +// 'Origin' => 'example.com' ]); $recvMsg = '{"user_id" : 123}'; diff --git a/tests/WebSocketServerTest.php b/tests/WebSocketServerTest.php index ed23ec7..17b0a98 100644 --- a/tests/WebSocketServerTest.php +++ b/tests/WebSocketServerTest.php @@ -8,7 +8,6 @@ class WebSocketServerTest extends TestCase { - /** * @test * @throws \WSSC\Exceptions\WebSocketException