Merge pull request #469 from beyondcode/refactor/cors
[2.x] Per-app CORS
This commit is contained in:
commit
3b49bcd5cb
|
|
@ -84,23 +84,12 @@ return [
|
||||||
'capacity' => null,
|
'capacity' => null,
|
||||||
'enable_client_messages' => false,
|
'enable_client_messages' => false,
|
||||||
'enable_statistics' => true,
|
'enable_statistics' => true,
|
||||||
|
'allowed_origins' => [
|
||||||
|
//
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Allowed Origins
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| If not empty, you can whitelist certain origins that will be allowed
|
|
||||||
| to connect to the websocket server.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'allowed_origins' => [
|
|
||||||
//
|
|
||||||
],
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Maximum Request Size
|
| Maximum Request Size
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ You may add additional apps in your `config/websockets.php` file.
|
||||||
'capacity' => null,
|
'capacity' => null,
|
||||||
'enable_client_messages' => false,
|
'enable_client_messages' => false,
|
||||||
'enable_statistics' => true,
|
'enable_statistics' => true,
|
||||||
|
'allowed_origins' => [],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,9 @@ class App
|
||||||
/** @var bool */
|
/** @var bool */
|
||||||
public $statisticsEnabled = true;
|
public $statisticsEnabled = true;
|
||||||
|
|
||||||
|
/** @var array */
|
||||||
|
public $allowedOrigins = [];
|
||||||
|
|
||||||
public static function findById($appId)
|
public static function findById($appId)
|
||||||
{
|
{
|
||||||
return app(AppManager::class)->findById($appId);
|
return app(AppManager::class)->findById($appId);
|
||||||
|
|
@ -106,4 +109,11 @@ class App
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setAllowedOrigins(array $allowedOrigins)
|
||||||
|
{
|
||||||
|
$this->allowedOrigins = $allowedOrigins;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,8 @@ class ConfigAppManager implements AppManager
|
||||||
$app
|
$app
|
||||||
->enableClientMessages($appAttributes['enable_client_messages'])
|
->enableClientMessages($appAttributes['enable_client_messages'])
|
||||||
->enableStatistics($appAttributes['enable_statistics'])
|
->enableStatistics($appAttributes['enable_statistics'])
|
||||||
->setCapacity($appAttributes['capacity'] ?? null);
|
->setCapacity($appAttributes['capacity'] ?? null)
|
||||||
|
->setAllowedOrigins($appAttributes['allowed_origins'] ?? []);
|
||||||
|
|
||||||
return $app;
|
return $app;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -79,11 +79,9 @@ class WebSocketServerFactory
|
||||||
$socket = new SecureServer($socket, $this->loop, config('websockets.ssl'));
|
$socket = new SecureServer($socket, $this->loop, config('websockets.ssl'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$urlMatcher = new UrlMatcher($this->routes, new RequestContext);
|
$app = new Router(
|
||||||
|
new UrlMatcher($this->routes, new RequestContext)
|
||||||
$router = new Router($urlMatcher);
|
);
|
||||||
|
|
||||||
$app = new OriginCheck($router, config('websockets.allowed_origins', []));
|
|
||||||
|
|
||||||
$httpServer = new HttpServer($app, config('websockets.max_request_size_in_kb') * 1024);
|
$httpServer = new HttpServer($app, config('websockets.max_request_size_in_kb') * 1024);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ use BeyondCode\LaravelWebSockets\Facades\StatisticsLogger;
|
||||||
use BeyondCode\LaravelWebSockets\QueryParameters;
|
use BeyondCode\LaravelWebSockets\QueryParameters;
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
|
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\ConnectionsOverCapacity;
|
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\ConnectionsOverCapacity;
|
||||||
|
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\OriginNotAllowed;
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\UnknownAppKey;
|
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\UnknownAppKey;
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\WebSocketException;
|
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\WebSocketException;
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Messages\PusherMessageFactory;
|
use BeyondCode\LaravelWebSockets\WebSockets\Messages\PusherMessageFactory;
|
||||||
|
|
@ -30,6 +31,7 @@ class WebSocketHandler implements MessageComponentInterface
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->verifyAppKey($connection)
|
->verifyAppKey($connection)
|
||||||
|
->verifyOrigin($connection)
|
||||||
->limitConcurrentConnections($connection)
|
->limitConcurrentConnections($connection)
|
||||||
->generateSocketId($connection)
|
->generateSocketId($connection)
|
||||||
->establishConnection($connection);
|
->establishConnection($connection);
|
||||||
|
|
@ -77,6 +79,23 @@ class WebSocketHandler implements MessageComponentInterface
|
||||||
return $this;
|
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)
|
protected function limitConcurrentConnections(ConnectionInterface $connection)
|
||||||
{
|
{
|
||||||
if (! is_null($capacity = $connection->app->capacity)) {
|
if (! is_null($capacity = $connection->app->capacity)) {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ class ConfigAppManagerTest extends TestCase
|
||||||
{
|
{
|
||||||
$apps = $this->appManager->all();
|
$apps = $this->appManager->all();
|
||||||
|
|
||||||
$this->assertCount(1, $apps);
|
$this->assertCount(2, $apps);
|
||||||
|
|
||||||
/** @var $app */
|
/** @var $app */
|
||||||
$app = $apps[0];
|
$app = $apps[0];
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ namespace BeyondCode\LaravelWebSockets\Tests;
|
||||||
use BeyondCode\LaravelWebSockets\Apps\App;
|
use BeyondCode\LaravelWebSockets\Apps\App;
|
||||||
use BeyondCode\LaravelWebSockets\Tests\Mocks\Message;
|
use BeyondCode\LaravelWebSockets\Tests\Mocks\Message;
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\ConnectionsOverCapacity;
|
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\ConnectionsOverCapacity;
|
||||||
|
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\OriginNotAllowed;
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\UnknownAppKey;
|
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\UnknownAppKey;
|
||||||
|
|
||||||
class ConnectionTest extends TestCase
|
class ConnectionTest extends TestCase
|
||||||
|
|
@ -14,7 +15,7 @@ class ConnectionTest extends TestCase
|
||||||
{
|
{
|
||||||
$this->expectException(UnknownAppKey::class);
|
$this->expectException(UnknownAppKey::class);
|
||||||
|
|
||||||
$this->pusherServer->onOpen($this->getWebSocketConnection('/?appKey=test'));
|
$this->pusherServer->onOpen($this->getWebSocketConnection('test'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
|
|
@ -65,4 +66,38 @@ class ConnectionTest extends TestCase
|
||||||
|
|
||||||
$connection->assertSentEvent('pusher:pong');
|
$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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,19 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
||||||
'capacity' => null,
|
'capacity' => null,
|
||||||
'enable_client_messages' => false,
|
'enable_client_messages' => false,
|
||||||
'enable_statistics' => true,
|
'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 = new Connection();
|
||||||
|
|
||||||
$connection->httpRequest = new Request('GET', $url);
|
$connection->httpRequest = new Request('GET', "/?appKey={$appKey}", $headers);
|
||||||
|
|
||||||
return $connection;
|
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 = new Connection();
|
||||||
|
|
||||||
$connection->httpRequest = new Request('GET', $url);
|
$connection->httpRequest = new Request('GET', "/?appKey={$appKey}", $headers);
|
||||||
|
|
||||||
$this->pusherServer->onOpen($connection);
|
$this->pusherServer->onOpen($connection);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue