diff --git a/composer.json b/composer.json index d80f32ce60..4ebd5c7a32 100644 --- a/composer.json +++ b/composer.json @@ -25,11 +25,13 @@ "php": "^7.1", "ext-json": "*", "cboden/ratchet": "^0.4.1", - "illuminate/console": "5.6.*|5.7.*", - "illuminate/http": "5.6.*|5.7.*", - "illuminate/routing": "5.6.*|5.7.*", - "illuminate/support": "5.6.*|5.7.*", + "illuminate/console": "5.7.*", + "illuminate/http": "5.7.*", + "illuminate/routing": "5.7.*", + "illuminate/broadcasting": "5.7.*", + "illuminate/support": "5.7.*", "symfony/http-kernel": "~4.0", + "pusher/pusher-php-server": "~3.0", "symfony/psr-http-message-bridge": "^1.1" }, "require-dev": { diff --git a/config/websockets.php b/config/websockets.php index 85bdf28a32..8e1d003c54 100644 --- a/config/websockets.php +++ b/config/websockets.php @@ -1,5 +1,6 @@ [ [ + 'name' => env('APP_NAME'), 'app_id' => env('WEBSOCKETS_APP_ID'), 'app_key' => env('WEBSOCKETS_APP_KEY'), 'app_secret' => env('WEBSOCKETS_APP_SECRET') @@ -58,4 +60,20 @@ * `ClientProvier` interface. */ 'client_provider' => ConfigClientProvider::class, + + 'dashboard' => [ + + /* + * Path for the Websockets debug console + */ + 'path' => '/websockets', + + /* + * Middleware that will be applied to the dashboard routes. + */ + 'middleware' => [ + Authorize::class, + ], + + ] ]; \ No newline at end of file diff --git a/resources/views/console.blade.php b/resources/views/console.blade.php new file mode 100644 index 0000000000..1fc817f407 --- /dev/null +++ b/resources/views/console.blade.php @@ -0,0 +1,176 @@ + + + + WebSockets Console + + + + + + + +
+
+
+
+ + + + + + +
+
+
+
+
+

Event Creator

+
+
+
+ +
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+

Events

+ + + + + + + + + + + + + + + + + +
TypeSocketDetailsTime
@{{ log.type }}@{{ log.socketId }}@{{ log.details }}@{{ log.time }}
+
+
+
+ + + \ No newline at end of file diff --git a/src/ClientProviders/Client.php b/src/ClientProviders/Client.php index cd3f3c706d..688a3ef63d 100644 --- a/src/ClientProviders/Client.php +++ b/src/ClientProviders/Client.php @@ -16,6 +16,9 @@ class Client /** @var string */ public $appSecret; + /** @var string|null */ + public $name; + public static function findByAppId(int $appId) { return app(ClientProvider::class)->findByAppId($appId); @@ -26,7 +29,7 @@ public static function findByAppKey(string $appKey): ?Client return app(ClientProvider::class)->findByAppKey($appKey); } - public function __construct($appId, string $appKey, string $appSecret) + public function __construct($appId, string $appKey, string $appSecret, ?string $name) { if (!is_numeric($appId)) { throw InvalidClient::appIdIsNotNumeric($appId); @@ -45,6 +48,8 @@ public function __construct($appId, string $appKey, string $appSecret) $this->appKey = $appKey; $this->appSecret = $appSecret; + + $this->name = $name; } diff --git a/src/ClientProviders/ClientProvider.php b/src/ClientProviders/ClientProvider.php index f363f537de..4f67e0caf7 100644 --- a/src/ClientProviders/ClientProvider.php +++ b/src/ClientProviders/ClientProvider.php @@ -8,4 +8,6 @@ interface ClientProvider public function findByAppId(int $appId): ?Client; public function findByAppKey(string $appKey): ?Client; + + public function all(): array; } \ No newline at end of file diff --git a/src/ClientProviders/ConfigClientProvider.php b/src/ClientProviders/ConfigClientProvider.php index 6cf63d1679..6a4653ebb7 100644 --- a/src/ClientProviders/ConfigClientProvider.php +++ b/src/ClientProviders/ConfigClientProvider.php @@ -24,6 +24,15 @@ public function findByAppKey(string $appKey): ?Client return $this->instanciate($clientAttributes); } + public function all(): array + { + return $this->allClients() + ->map(function ($client) { + return $this->instanciate($client); + }) + ->toArray(); + } + protected function allClients(): Collection { return collect(config('websockets.clients')); @@ -38,7 +47,8 @@ protected function instanciate(?array $clientAttributes): ?Client return new Client( $clientAttributes['app_id'], $clientAttributes['app_key'], - $clientAttributes['app_secret'] + $clientAttributes['app_secret'], + $clientAttributes['name'] ?? null ); } } \ No newline at end of file diff --git a/src/Http/Controllers/AuthenticateConsole.php b/src/Http/Controllers/AuthenticateConsole.php new file mode 100644 index 0000000000..2dce0f7d3e --- /dev/null +++ b/src/Http/Controllers/AuthenticateConsole.php @@ -0,0 +1,14 @@ +validAuthenticationResponse($request, []); + } +} \ No newline at end of file diff --git a/src/Http/Controllers/SendMessage.php b/src/Http/Controllers/SendMessage.php new file mode 100644 index 0000000000..ff9b39bf44 --- /dev/null +++ b/src/Http/Controllers/SendMessage.php @@ -0,0 +1,21 @@ +key, $request->secret, + $request->appId, config('broadcasting.connections.pusher.options', []) + ); + + return (new PusherBroadcaster($pusher)) + ->broadcast([$request->channel], $request->event, json_decode($request->data, true)); + } +} \ No newline at end of file diff --git a/src/Http/Controllers/ShowConsole.php b/src/Http/Controllers/ShowConsole.php new file mode 100644 index 0000000000..e8fa6cefe6 --- /dev/null +++ b/src/Http/Controllers/ShowConsole.php @@ -0,0 +1,16 @@ + $clients->all() + ]); + } +} \ No newline at end of file diff --git a/src/Http/Middleware/Authorize.php b/src/Http/Middleware/Authorize.php new file mode 100644 index 0000000000..a2d80c4ce1 --- /dev/null +++ b/src/Http/Middleware/Authorize.php @@ -0,0 +1,13 @@ +verifySignature($request); foreach ($request->json()->get('channels', []) as $channelId) { + Dashboard::apiMessage($request->appId, $channelId, $request->json()->get('name'), $request->json()->get('data')); + $channel = $this->channelManager->find($request->appId, $channelId); optional($channel)->broadcastToEveryoneExcept([ diff --git a/src/LaravelEcho/Pusher/Channels/Channel.php b/src/LaravelEcho/Pusher/Channels/Channel.php index d84ea03cdc..0eb98649c2 100644 --- a/src/LaravelEcho/Pusher/Channels/Channel.php +++ b/src/LaravelEcho/Pusher/Channels/Channel.php @@ -2,6 +2,7 @@ namespace BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Channels; +use BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Dashboard; use BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Exceptions\InvalidSignatureException; use Illuminate\Support\Collection; use Ratchet\ConnectionInterface; @@ -56,11 +57,21 @@ public function subscribe(ConnectionInterface $connection, stdClass $payload) public function unsubscribe(ConnectionInterface $connection) { unset($this->subscriptions[$connection->socketId]); + + if (! $this->hasConnections()) { + Dashboard::vacated($connection, $this->channelId); + } } protected function saveConnection(ConnectionInterface $connection) { + if (! $this->hasConnections()) { + Dashboard::occupied($connection, $this->channelId); + } + $this->subscriptions[$connection->socketId] = $connection; + + Dashboard::subscribed($connection, $this->channelId); } public function broadcast($payload) diff --git a/src/LaravelEcho/Pusher/Dashboard.php b/src/LaravelEcho/Pusher/Dashboard.php new file mode 100644 index 0000000000..8a325e07e4 --- /dev/null +++ b/src/LaravelEcho/Pusher/Dashboard.php @@ -0,0 +1,100 @@ +httpRequest; + + self::log($connection->client->appId, self::TYPE_CONNECTION, [ + 'details' => "Origin: {$request->getUri()->getScheme()}://{$request->getUri()->getHost()}", + 'socketId' => $connection->socketId, + ]); + } + + public static function disconnection(ConnectionInterface $connection) + { + self::log($connection->client->appId, self::TYPE_DISCONNECTION, [ + 'socketId' => $connection->socketId + ]); + } + + public static function vacated(ConnectionInterface $connection, string $channelId) + { + self::log($connection->client->appId, self::TYPE_VACATED, [ + 'details' => "Channel: {$channelId}" + ]); + } + + public static function occupied(ConnectionInterface $connection, string $channelId) + { + self::log($connection->client->appId, self::TYPE_OCCUPIED, [ + 'details' => "Channel: {$channelId}" + ]); + } + + public static function subscribed(ConnectionInterface $connection, string $channelId) + { + self::log($connection->client->appId, self::TYPE_SUBSCRIBED, [ + 'socketId' => $connection->socketId, + 'details' => "Channel: {$channelId}" + ]); + } + + public static function clientMessage(ConnectionInterface $connection, stdClass $payload) + { + self::log($connection->client->appId, self::TYPE_CLIENT_MESSAGE, [ + 'details' => "Channel: {$payload->channel}, Event: {$payload->event}", + 'socketId' => $connection->socketId, + 'data' => json_encode($payload) + ]); + } + + public static function apiMessage($appId, string $channel, string $event, string $payload) + { + self::log($appId, self::TYPE_API_MESSAGE, [ + 'details' => "Channel: {$channel}, Event: {$event}", + 'data' => $payload + ]); + } + + public static function log($appId, string $type, array $attributes = []) + { + $channelId = self::LOG_CHANNEL_PREFIX . $type; + + $channel = app(ChannelManager::class)->find($appId, $channelId); + + optional($channel)->broadcast([ + 'event' => 'log_message', + 'channel' => $channelId, + 'data' => [ + 'type' => $type, + 'time' => strftime("%H:%M:%S") + ] + $attributes + ]); + } + +} \ No newline at end of file diff --git a/src/LaravelEcho/Pusher/Exceptions/InvalidConnectionException.php b/src/LaravelEcho/Pusher/Exceptions/InvalidConnectionException.php new file mode 100644 index 0000000000..143270caf1 --- /dev/null +++ b/src/LaravelEcho/Pusher/Exceptions/InvalidConnectionException.php @@ -0,0 +1,12 @@ +message = 'Invalid Connection'; + $this->code = 4009; + } +} \ No newline at end of file diff --git a/src/LaravelEcho/Pusher/Exceptions/UnknownAppKey.php b/src/LaravelEcho/Pusher/Exceptions/UnknownAppKeyException.php similarity index 81% rename from src/LaravelEcho/Pusher/Exceptions/UnknownAppKey.php rename to src/LaravelEcho/Pusher/Exceptions/UnknownAppKeyException.php index 76e856fe35..c154d279ed 100644 --- a/src/LaravelEcho/Pusher/Exceptions/UnknownAppKey.php +++ b/src/LaravelEcho/Pusher/Exceptions/UnknownAppKeyException.php @@ -2,7 +2,7 @@ namespace BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Exceptions; -class UnknownAppKey extends PusherException +class UnknownAppKeyException extends PusherException { public function __construct(string $appKey) { diff --git a/src/LaravelEcho/WebSocket/Message.php b/src/LaravelEcho/WebSocket/Message.php index ad089d2506..5617f59c4a 100644 --- a/src/LaravelEcho/WebSocket/Message.php +++ b/src/LaravelEcho/WebSocket/Message.php @@ -3,6 +3,7 @@ namespace BeyondCode\LaravelWebSockets\LaravelEcho\WebSocket; use BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Channels\ChannelManager; +use BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Dashboard; use Ratchet\ConnectionInterface; use stdClass; @@ -29,6 +30,8 @@ public function __construct(stdClass $payload, ConnectionInterface $connection, public function respond() { if (starts_with($this->payload->event, 'client-')) { + Dashboard::clientMessage($this->connection, $this->payload); + $channel = $this->channelManager->find($this->connection->client->appId, $this->payload->channel); optional($channel)->broadcast($this->payload); diff --git a/src/LaravelEcho/WebSocket/PusherServer.php b/src/LaravelEcho/WebSocket/PusherServer.php index f48027fbd8..b81127ec9e 100644 --- a/src/LaravelEcho/WebSocket/PusherServer.php +++ b/src/LaravelEcho/WebSocket/PusherServer.php @@ -2,14 +2,15 @@ namespace BeyondCode\LaravelWebSockets\LaravelEcho\WebSocket; -use BeyondCode\LaravelWebSockets\ClientProviders\Client; -use BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Exceptions\PusherException; -use BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Exceptions\UnknownAppKey; +use BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Dashboard; use Exception; use Ratchet\ConnectionInterface; use Ratchet\RFC6455\Messaging\MessageInterface; use BeyondCode\LaravelWebSockets\WebSocketController; +use BeyondCode\LaravelWebSockets\ClientProviders\Client; use BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Channels\ChannelManager; +use BeyondCode\LaravelWebsockets\LaravelEcho\Pusher\Exceptions\PusherException; +use BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Exceptions\UnknownAppKeyException; class PusherServer extends WebSocketController { @@ -49,7 +50,6 @@ function onError(ConnectionInterface $connection, Exception $exception) $exception->getPayload() )); } - dump($exception); } protected function verifyConnection(ConnectionInterface $connection) @@ -61,7 +61,7 @@ protected function verifyConnection(ConnectionInterface $connection) parse_str($request->getUri()->getQuery(), $queryParameters); if (! $client = Client::findByAppKey($queryParameters['appKey'])) { - throw new UnknownAppKey($queryParameters['appKey']); + throw new UnknownAppKeyException($queryParameters['appKey']); } $connection->client = $client; @@ -69,6 +69,8 @@ protected function verifyConnection(ConnectionInterface $connection) protected function establishConnection(ConnectionInterface $connection) { + Dashboard::connection($connection); + $connection->send(json_encode([ 'event' => 'pusher:connection_established', 'data' => json_encode([ diff --git a/src/LaravelWebSocketsServiceProvider.php b/src/LaravelWebSocketsServiceProvider.php index c96fe8ba2d..8d92ab0950 100644 --- a/src/LaravelWebSocketsServiceProvider.php +++ b/src/LaravelWebSocketsServiceProvider.php @@ -2,6 +2,8 @@ namespace BeyondCode\LaravelWebSockets; +use Illuminate\Support\Facades\Gate; +use Illuminate\Support\Facades\Route; use BeyondCode\LaravelWebSockets\ClientProviders\ClientProvider; use Illuminate\Support\ServiceProvider; use BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Channels\ChannelManager; @@ -10,15 +12,39 @@ class LaravelWebSocketsServiceProvider extends ServiceProvider { public function boot() { + Route::middlewareGroup('websockets', config('websockets.dashboard.middleware', [])); + $this->publishes([ __DIR__.'/../config/websockets.php' => base_path('config/websockets.php'), ], 'config'); + $this->registerRoutes(); + + $this->registerDashboardGate(); + + $this->loadViewsFrom(__DIR__.'/../resources/views/', 'websockets'); + $this->commands([ Console\StartWebSocketServer::class, ]); } + protected function registerRoutes() + { + Route::group($this->routeConfiguration(), function () { + $this->loadRoutesFrom(__DIR__.'/Http/routes.php'); + }); + } + + protected function routeConfiguration() + { + return [ + 'namespace' => 'BeyondCode\LaravelWebSockets\Http\Controllers', + 'prefix' => config('websockets.dashboard.path'), + 'middleware' => 'websockets' + ]; + } + public function register() { $this->mergeConfigFrom(__DIR__.'/../config/websockets.php', 'websockets'); @@ -35,4 +61,11 @@ public function register() return app(config('websockets.client_provider')); }); } + + protected function registerDashboardGate() + { + Gate::define('viewWebSocketDashboard', function ($user = null) { + return app()->environment('local'); + }); + } }