Added cors setting outside the app

This commit is contained in:
Alex Renoki 2020-08-18 16:04:52 +03:00
parent 5153568867
commit 11727e684f
9 changed files with 102 additions and 85 deletions

View File

@ -84,22 +84,11 @@ return [
'capacity' => null,
'enable_client_messages' => false,
'enable_statistics' => true,
],
],
/*
|--------------------------------------------------------------------------
| Allowed Origins
|--------------------------------------------------------------------------
|
| If not empty, you can whitelist certain origins that will be allowed
| to connect to the websocket server.
|
*/
'allowed_origins' => [
//
],
],
],
/*
|--------------------------------------------------------------------------

View File

@ -33,6 +33,9 @@ class App
/** @var bool */
public $statisticsEnabled = true;
/** @var array */
public $allowedOrigins = [];
public static function findById($appId)
{
return app(AppManager::class)->findById($appId);
@ -106,4 +109,11 @@ class App
return $this;
}
public function setAllowedOrigins(array $allowedOrigins)
{
$this->allowedOrigins = $allowedOrigins;
return $this;
}
}

View File

@ -78,7 +78,8 @@ class ConfigAppManager implements AppManager
$app
->enableClientMessages($appAttributes['enable_client_messages'])
->enableStatistics($appAttributes['enable_statistics'])
->setCapacity($appAttributes['capacity'] ?? null);
->setCapacity($appAttributes['capacity'] ?? null)
->setAllowedOrigins($appAttributes['allowed_origins'] ?? []);
return $app;
}

View File

@ -1,60 +0,0 @@
<?php
namespace BeyondCode\LaravelWebSockets\Server;
use Psr\Http\Message\RequestInterface;
use Ratchet\ConnectionInterface;
use Ratchet\Http\CloseResponseTrait;
use Ratchet\Http\HttpServerInterface;
use Ratchet\MessageComponentInterface;
class OriginCheck implements HttpServerInterface
{
use CloseResponseTrait;
/** @var \Ratchet\MessageComponentInterface */
protected $_component;
protected $allowedOrigins = [];
public function __construct(MessageComponentInterface $component, array $allowedOrigins = [])
{
$this->_component = $component;
$this->allowedOrigins = $allowedOrigins;
}
public function onOpen(ConnectionInterface $connection, RequestInterface $request = null)
{
if ($request->hasHeader('Origin')) {
$this->verifyOrigin($connection, $request);
}
return $this->_component->onOpen($connection, $request);
}
public function onMessage(ConnectionInterface $from, $msg)
{
return $this->_component->onMessage($from, $msg);
}
public function onClose(ConnectionInterface $connection)
{
return $this->_component->onClose($connection);
}
public function onError(ConnectionInterface $connection, \Exception $e)
{
return $this->_component->onError($connection, $e);
}
protected function verifyOrigin(ConnectionInterface $connection, RequestInterface $request)
{
$header = (string) $request->getHeader('Origin')[0];
$origin = parse_url($header, PHP_URL_HOST) ?: $header;
if (! empty($this->allowedOrigins) && ! in_array($origin, $this->allowedOrigins)) {
return $this->close($connection, 403);
}
}
}

View File

@ -79,11 +79,9 @@ class WebSocketServerFactory
$socket = new SecureServer($socket, $this->loop, config('websockets.ssl'));
}
$urlMatcher = new UrlMatcher($this->routes, new RequestContext);
$router = new Router($urlMatcher);
$app = new OriginCheck($router, config('websockets.allowed_origins', []));
$app = new Router(
new UrlMatcher($this->routes, new RequestContext)
);
$httpServer = new HttpServer($app, config('websockets.max_request_size_in_kb') * 1024);

View File

@ -0,0 +1,12 @@
<?php
namespace BeyondCode\LaravelWebSockets\WebSockets\Exceptions;
class OriginNotAllowed extends WebSocketException
{
public function __construct(string $appKey)
{
$this->message = "The origin is not allowed for `{$appKey}`.";
$this->code = 4009;
}
}

View File

@ -8,6 +8,7 @@ use BeyondCode\LaravelWebSockets\Facades\StatisticsLogger;
use BeyondCode\LaravelWebSockets\QueryParameters;
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\ConnectionsOverCapacity;
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\OriginNotAllowed;
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\UnknownAppKey;
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\WebSocketException;
use BeyondCode\LaravelWebSockets\WebSockets\Messages\PusherMessageFactory;
@ -30,6 +31,7 @@ class WebSocketHandler implements MessageComponentInterface
{
$this
->verifyAppKey($connection)
->verifyOrigin($connection)
->limitConcurrentConnections($connection)
->generateSocketId($connection)
->establishConnection($connection);
@ -77,6 +79,23 @@ class WebSocketHandler implements MessageComponentInterface
return $this;
}
protected function verifyOrigin(ConnectionInterface $connection)
{
if (! $connection->app->allowedOrigins) {
return $this;
}
$header = (string) ($connection->httpRequest->getHeader('Origin')[0] ?? null);
$origin = parse_url($header, PHP_URL_HOST) ?: $header;
if (! $header || ! in_array($origin, $connection->app->allowedOrigins)) {
throw new OriginNotAllowed($connection->app->key);
}
return $this;
}
protected function limitConcurrentConnections(ConnectionInterface $connection)
{
if (! is_null($capacity = $connection->app->capacity)) {

View File

@ -5,6 +5,7 @@ namespace BeyondCode\LaravelWebSockets\Tests;
use BeyondCode\LaravelWebSockets\Apps\App;
use BeyondCode\LaravelWebSockets\Tests\Mocks\Message;
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\ConnectionsOverCapacity;
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\OriginNotAllowed;
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\UnknownAppKey;
class ConnectionTest extends TestCase
@ -14,7 +15,7 @@ class ConnectionTest extends TestCase
{
$this->expectException(UnknownAppKey::class);
$this->pusherServer->onOpen($this->getWebSocketConnection('/?appKey=test'));
$this->pusherServer->onOpen($this->getWebSocketConnection('test'));
}
/** @test */
@ -65,4 +66,38 @@ class ConnectionTest extends TestCase
$connection->assertSentEvent('pusher:pong');
}
/** @test */
public function origin_validation_should_fail_for_no_origin()
{
$this->expectException(OriginNotAllowed::class);
$connection = $this->getWebSocketConnection('TestOrigin');
$this->pusherServer->onOpen($connection);
$connection->assertSentEvent('pusher:connection_established');
}
/** @test */
public function origin_validation_should_fail_for_wrong_origin()
{
$this->expectException(OriginNotAllowed::class);
$connection = $this->getWebSocketConnection('TestOrigin', ['Origin' => 'https://google.ro']);
$this->pusherServer->onOpen($connection);
$connection->assertSentEvent('pusher:connection_established');
}
/** @test */
public function origin_validation_should_pass_for_the_right_origin()
{
$connection = $this->getWebSocketConnection('TestOrigin', ['Origin' => 'https://test.origin.com']);
$this->pusherServer->onOpen($connection);
$connection->assertSentEvent('pusher:connection_established');
}
}

View File

@ -70,6 +70,19 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
'capacity' => null,
'enable_client_messages' => false,
'enable_statistics' => true,
'allowed_origins' => [],
],
[
'name' => 'Origin Test App',
'id' => '1234',
'key' => 'TestOrigin',
'secret' => 'TestSecret',
'capacity' => null,
'enable_client_messages' => false,
'enable_statistics' => true,
'allowed_origins' => [
'test.origin.com',
],
],
]);
@ -107,20 +120,20 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
}
}
protected function getWebSocketConnection(string $url = '/?appKey=TestKey'): Connection
protected function getWebSocketConnection(string $appKey = 'TestKey', array $headers = []): Connection
{
$connection = new Connection();
$connection->httpRequest = new Request('GET', $url);
$connection->httpRequest = new Request('GET', "/?appKey={$appKey}", $headers);
return $connection;
}
protected function getConnectedWebSocketConnection(array $channelsToJoin = [], string $url = '/?appKey=TestKey'): Connection
protected function getConnectedWebSocketConnection(array $channelsToJoin = [], string $appKey = 'TestKey', array $headers = []): Connection
{
$connection = new Connection();
$connection->httpRequest = new Request('GET', $url);
$connection->httpRequest = new Request('GET', "/?appKey={$appKey}", $headers);
$this->pusherServer->onOpen($connection);