Skip to content

Commit

Permalink
#45: Add origin hosts check to server + readme
Browse files Browse the repository at this point in the history
  • Loading branch information
arthurkushman committed Jan 11, 2020
1 parent cb269f2 commit 0b32dd9
Show file tree
Hide file tree
Showing 16 changed files with 153 additions and 33 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ just add

to your projects composer.json.

### Implement Your WebSocket handler class - ex.:
### Implement your WebSocket handler class - ex.:

```php
<?php
Expand Down Expand Up @@ -201,6 +201,16 @@ $conn->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:
Expand Down
11 changes: 5 additions & 6 deletions src/Components/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

class Connection implements ConnectionContract, CommonsContract
{

private $socketConnection;
private $clients;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand Down
56 changes: 56 additions & 0 deletions src/Components/OriginComponent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace WSSC\Components;

use PHPUnit\Framework\OutputError;
use WSSC\Exceptions\ConnectionException;

class OriginComponent
{
private $client;
private $config;

/**
* OriginComponent constructor.
* @param ServerConfig $config
* @param $client
*/
public function __construct(ServerConfig $config, $client)
{
$this->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();
}
}
56 changes: 55 additions & 1 deletion src/Components/ServerConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

class ServerConfig
{

private $clientsPerFork = WebSocketServerContract::CLIENTS_PER_FORK;
private $streamSelectTimeout = WebSocketServerContract::STREAM_SELECT_TIMEOUT;

Expand All @@ -17,6 +16,10 @@ class ServerConfig

private $processName = WebSocketServerContract::PROC_TITLE;

private $checkOrigin = false;
private $origins = [];
private $originHeader = false;

/**
* @return mixed
*/
Expand Down Expand Up @@ -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;
}
}
1 change: 0 additions & 1 deletion src/Components/WSClientTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

trait WSClientTrait
{

/**
* Validates whether server sent valid upgrade response
*
Expand Down
1 change: 0 additions & 1 deletion src/Components/WssMain.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
*/
class WssMain implements CommonsContract
{

private $isPcntlLoaded = false;

/**
Expand Down
1 change: 0 additions & 1 deletion src/Contracts/CommonsContract.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
*/
interface CommonsContract
{

// DADA types
public const EVENT_TYPE_PING = 'ping';
public const EVENT_TYPE_PONG = 'pong';
Expand Down
1 change: 0 additions & 1 deletion src/Contracts/ConnectionContract.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
*/
interface ConnectionContract
{

public function send(string $data): void;

public function close(): void;
Expand Down
1 change: 0 additions & 1 deletion src/Contracts/WebSocketServerContract.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
*/
interface WebSocketServerContract
{

// HOST/PORT
public const DEFAULT_HOST = '0.0.0.0';
public const DEFAULT_PORT = 8000;
Expand Down
1 change: 0 additions & 1 deletion src/Contracts/WscCommonsContract.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
*/
interface WscCommonsContract
{

public const TCP_SCHEME = 'tcp://';

public const MAX_BYTES_READ = 65535;
Expand Down
1 change: 0 additions & 1 deletion src/Exceptions/WebSocketException.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

class WebSocketException extends \Exception
{

public function printStack()
{
echo $this->getFile() . ' ' . $this->getLine() . ' ' . $this->getMessage() . PHP_EOL;
Expand Down
3 changes: 1 addition & 2 deletions src/WebSocketClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 21 additions & 10 deletions src/WebSocketServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,7 +20,6 @@
*/
class WebSocketServer extends WssMain implements WebSocketServerContract
{

private $clients = [];
// set any template You need ex.: GET /subscription/messenger/token
private $pathParams = [];
Expand All @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
Expand All @@ -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)
{
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
];
Expand Down
1 change: 0 additions & 1 deletion tests/ServerHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 0b32dd9

Please sign in to comment.