This commit is contained in:
Alex Renoki 2020-08-23 19:12:22 +03:00
parent b46cfadaa2
commit f62ac8fd56
11 changed files with 257 additions and 35 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ vendor
coverage coverage
.phpunit.result.cache .phpunit.result.cache
.idea/ .idea/
database.sqlite

View File

@ -0,0 +1,32 @@
<?php
namespace BeyondCode\LaravelWebSockets\Contracts;
use BeyondCode\LaravelWebSockets\PubSub\Broadcasters\RedisPusherBroadcaster;
use Illuminate\Broadcasting\Broadcasters\PusherBroadcaster;
use Pusher\Pusher;
trait PushesToPusher
{
/**
* Get the right Pusher broadcaster for the used driver.
*
* @param array $app
* @return \Illuminate\Broadcasting\Broadcasters\Broadcaster
*/
public function getPusherBroadcaster(array $app)
{
if (config('websockets.replication.driver') === 'redis') {
return new RedisPusherBroadcaster(
new Pusher($app['key'], $app['secret'], $app['id'], config('broadcasting.connections.websockets.options', [])),
$app['id'],
app('redis'),
config('broadcasting.connections.websockets.connection', null)
);
}
return new PusherBroadcaster(
new Pusher($app['key'], $app['secret'], $app['id'], config('broadcasting.connections.pusher.options', [])),
);
}
}

View File

@ -3,12 +3,15 @@
namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers; namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
use BeyondCode\LaravelWebSockets\Apps\App; use BeyondCode\LaravelWebSockets\Apps\App;
use BeyondCode\LaravelWebSockets\Contracts\PushesToPusher;
use Illuminate\Broadcasting\Broadcasters\PusherBroadcaster; use Illuminate\Broadcasting\Broadcasters\PusherBroadcaster;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Pusher\Pusher; use Pusher\Pusher;
class AuthenticateDashboard class AuthenticateDashboard
{ {
use PushesToPusher;
/** /**
* Find the app by using the header * Find the app by using the header
* and then reconstruct the PusherBroadcaster * and then reconstruct the PusherBroadcaster
@ -21,12 +24,11 @@ class AuthenticateDashboard
{ {
$app = App::findById($request->header('x-app-id')); $app = App::findById($request->header('x-app-id'));
$broadcaster = new PusherBroadcaster(new Pusher( $broadcaster = $this->getPusherBroadcaster([
$app->key, 'key' => $app->key,
$app->secret, 'secret' => $app->secret,
$app->id, 'id' =>$app->id,
[] ]);
));
/* /*
* Since the dashboard itself is already secured by the * Since the dashboard itself is already secured by the

View File

@ -3,12 +3,15 @@
namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers; namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
use BeyondCode\LaravelWebSockets\Statistics\Rules\AppId; use BeyondCode\LaravelWebSockets\Statistics\Rules\AppId;
use BeyondCode\LaravelWebSockets\Contracts\PushesToPusher;
use Illuminate\Broadcasting\Broadcasters\PusherBroadcaster; use Illuminate\Broadcasting\Broadcasters\PusherBroadcaster;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Pusher\Pusher; use Pusher\Pusher;
class SendMessage class SendMessage
{ {
use PushesToPusher;
/** /**
* Send the message to the requested channel. * Send the message to the requested channel.
* *
@ -17,7 +20,7 @@ class SendMessage
*/ */
public function __invoke(Request $request) public function __invoke(Request $request)
{ {
$validated = $request->validate([ $request->validate([
'appId' => ['required', new AppId], 'appId' => ['required', new AppId],
'key' => 'required|string', 'key' => 'required|string',
'secret' => 'required|string', 'secret' => 'required|string',
@ -26,30 +29,18 @@ class SendMessage
'data' => 'required|json', 'data' => 'required|json',
]); ]);
$this->getPusherBroadcaster($validated)->broadcast( $broadcaster = $this->getPusherBroadcaster([
[$validated['channel']], 'key' => $request->key,
$validated['event'], 'secret' => $request->secret,
json_decode($validated['data'], true) 'id' => $request->appId,
]);
$broadcaster->broadcast(
[$request->channel],
$request->event,
json_decode($request->data, true)
); );
return 'ok'; return 'ok';
} }
/**
* Get the pusher broadcaster for the current request.
*
* @param array $validated
* @return \Illuminate\Broadcasting\Broadcasters\PusherBroadcaster
*/
protected function getPusherBroadcaster(array $validated): PusherBroadcaster
{
$pusher = new Pusher(
$validated['key'],
$validated['secret'],
$validated['appId'],
config('broadcasting.connections.pusher.options', [])
);
return new PusherBroadcaster($pusher);
}
} }

View File

@ -125,8 +125,8 @@ class WebSocketsServiceProvider extends ServiceProvider
], function () { ], function () {
Route::get('/', ShowDashboard::class)->name('dashboard'); Route::get('/', ShowDashboard::class)->name('dashboard');
Route::get('/api/{appId}/statistics', [DashboardApiController::class, 'getStatistics'])->name('statistics'); Route::get('/api/{appId}/statistics', [DashboardApiController::class, 'getStatistics'])->name('statistics');
Route::post('auth', AuthenticateDashboard::class)->name('auth'); Route::post('/auth', AuthenticateDashboard::class)->name('auth');
Route::post('event', SendMessage::class)->name('send'); Route::post('/event', SendMessage::class)->name('send');
}); });
return $this; return $this;
@ -140,7 +140,7 @@ class WebSocketsServiceProvider extends ServiceProvider
protected function registerDashboardGate() protected function registerDashboardGate()
{ {
Gate::define('viewWebSocketsDashboard', function ($user = null) { Gate::define('viewWebSocketsDashboard', function ($user = null) {
return $this->app->environment(['local', 'testing']); return $this->app->environment('local');
}); });
return $this; return $this;

View File

@ -0,0 +1,100 @@
<?php
namespace BeyondCode\LaravelWebSockets\Tests\Dashboard;
use BeyondCode\LaravelWebSockets\Tests\TestCase;
use BeyondCode\LaravelWebSockets\Tests\Models\User;
use BeyondCode\LaravelWebSockets\Tests\Mocks\Message;
class AuthTest extends TestCase
{
/** @test */
public function can_authenticate_dashboard_over_channel()
{
$connection = $this->getConnectedWebSocketConnection(['test-channel']);
$this->pusherServer->onOpen($connection);
$this->actingAs(factory(User::class)->create())
->json('POST', route('laravel-websockets.auth'), [
'socket_id' => $connection->socketId,
'channel_name' => 'test-channel',
], ['x-app-id' => '1234'])
->seeJsonStructure([
'auth',
'channel_data',
]);
}
/** @test */
public function can_authenticate_dashboard_over_private_channel()
{
$connection = $this->getWebSocketConnection();
$this->pusherServer->onOpen($connection);
$signature = "{$connection->socketId}:private-channel";
$hashedAppSecret = hash_hmac('sha256', $signature, $connection->app->secret);
$message = new Message([
'event' => 'pusher:subscribe',
'data' => [
'auth' => "{$connection->app->key}:{$hashedAppSecret}",
'channel' => 'private-channel',
],
]);
$this->pusherServer->onMessage($connection, $message);
$connection->assertSentEvent('pusher_internal:subscription_succeeded', [
'channel' => 'private-channel',
]);
$this->actingAs(factory(User::class)->create())
->json('POST', route('laravel-websockets.auth'), [
'socket_id' => $connection->socketId,
'channel_name' => 'private-test-channel',
], ['x-app-id' => '1234'])
->seeJsonStructure([
'auth',
]);
}
/** @test */
public function can_authenticate_dashboard_over_presence_channel()
{
$connection = $this->getWebSocketConnection();
$this->pusherServer->onOpen($connection);
$channelData = [
'user_id' => 1,
'user_info' => [
'name' => 'Marcel',
],
];
$signature = "{$connection->socketId}:presence-channel:".json_encode($channelData);
$message = new Message([
'event' => 'pusher:subscribe',
'data' => [
'auth' => $connection->app->key.':'.hash_hmac('sha256', $signature, $connection->app->secret),
'channel' => 'presence-channel',
'channel_data' => json_encode($channelData),
],
]);
$this->pusherServer->onMessage($connection, $message);
$this->actingAs(factory(User::class)->create())
->json('POST', route('laravel-websockets.auth'), [
'socket_id' => $connection->socketId,
'channel_name' => 'presence-channel',
], ['x-app-id' => '1234'])
->seeJsonStructure([
'auth',
]);
}
}

View File

@ -3,14 +3,13 @@
namespace BeyondCode\LaravelWebSockets\Tests\Dashboard; namespace BeyondCode\LaravelWebSockets\Tests\Dashboard;
use BeyondCode\LaravelWebSockets\Tests\TestCase; use BeyondCode\LaravelWebSockets\Tests\TestCase;
use BeyondCode\LaravelWebSockets\Tests\Models\User;
class DashboardTest extends TestCase class DashboardTest extends TestCase
{ {
/** @test */ /** @test */
public function cant_see_dashboard_without_authorization() public function cant_see_dashboard_without_authorization()
{ {
config(['app.env' => 'production']);
$this->get(route('laravel-websockets.dashboard')) $this->get(route('laravel-websockets.dashboard'))
->assertResponseStatus(403); ->assertResponseStatus(403);
} }
@ -18,7 +17,8 @@ class DashboardTest extends TestCase
/** @test */ /** @test */
public function can_see_dashboard() public function can_see_dashboard()
{ {
$this->get(route('laravel-websockets.dashboard')) $this->actingAs(factory(User::class)->create())
->get(route('laravel-websockets.dashboard'))
->assertResponseOk() ->assertResponseOk()
->see('WebSockets Dashboard'); ->see('WebSockets Dashboard');
} }

16
tests/Models/User.php Normal file
View File

@ -0,0 +1,16 @@
<?php
namespace BeyondCode\LaravelWebSockets\Tests\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
protected $fillable = [
'name', 'email', 'password',
];
protected $hidden = [
'password', 'remember_token',
];
}

View File

@ -46,6 +46,12 @@ abstract class TestCase extends BaseTestCase
{ {
parent::setUp(); parent::setUp();
$this->resetDatabase();
$this->loadLaravelMigrations(['--database' => 'sqlite']);
$this->withFactories(__DIR__.'/database/factories');
$this->pusherServer = $this->app->make(config('websockets.handlers.websocket')); $this->pusherServer = $this->app->make(config('websockets.handlers.websocket'));
$this->channelManager = $this->app->make(ChannelManager::class); $this->channelManager = $this->app->make(ChannelManager::class);
@ -69,6 +75,7 @@ abstract class TestCase extends BaseTestCase
{ {
return [ return [
\BeyondCode\LaravelWebSockets\WebSocketsServiceProvider::class, \BeyondCode\LaravelWebSockets\WebSocketsServiceProvider::class,
TestServiceProvider::class,
]; ];
} }
@ -79,6 +86,16 @@ abstract class TestCase extends BaseTestCase
{ {
$app['config']->set('app.key', 'wslxrEFGWY6GfGhvN9L3wH3KSRJQQpBD'); $app['config']->set('app.key', 'wslxrEFGWY6GfGhvN9L3wH3KSRJQQpBD');
$app['config']->set('auth.providers.users.model', Models\User::class);
$app['config']->set('database.default', 'sqlite');
$app['config']->set('database.connections.sqlite', [
'driver' => 'sqlite',
'database' => __DIR__.'/database.sqlite',
'prefix' => '',
]);
$app['config']->set('websockets.apps', [ $app['config']->set('websockets.apps', [
[ [
'name' => 'Test App', 'name' => 'Test App',
@ -307,4 +324,14 @@ abstract class TestCase extends BaseTestCase
->make(ReplicationInterface::class) ->make(ReplicationInterface::class)
->getPublishClient(); ->getPublishClient();
} }
/**
* Reset the database.
*
* @return void
*/
protected function resetDatabase()
{
file_put_contents(__DIR__.'/database.sqlite', null);
}
} }

View File

@ -0,0 +1,31 @@
<?php
namespace BeyondCode\LaravelWebSockets\Tests;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;
class TestServiceProvider extends ServiceProvider
{
/**
* Boot the service provider.
*
* @return void
*/
public function boot()
{
Gate::define('viewWebSocketsDashboard', function ($user = null) {
return ! is_null($user);
});
}
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
//
}
}

View File

@ -0,0 +1,22 @@
<?php
/*
|--------------------------------------------------------------------------
| Model Factories
|--------------------------------------------------------------------------
|
| This directory should contain each of the model factory definitions for
| your application. Factories provide a convenient way to generate new
| model instances for testing / seeding your application's database.
|
*/
use Illuminate\Support\Str;
$factory->define(\BeyondCode\LaravelWebSockets\Tests\Models\User::class, function () {
return [
'name' => 'Name'.Str::random(5),
'email' => Str::random(5).'@gmail.com',
'password' => '$2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm', // secret
'remember_token' => Str::random(10),
];
});