Merge pull request #1 from beyondcode/console

Add Debug Dashboard
This commit is contained in:
Marcel Pociot 2018-11-25 22:25:18 +01:00 committed by GitHub
commit f73883151d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 458 additions and 12 deletions

View File

@ -25,11 +25,13 @@
"php": "^7.1", "php": "^7.1",
"ext-json": "*", "ext-json": "*",
"cboden/ratchet": "^0.4.1", "cboden/ratchet": "^0.4.1",
"illuminate/console": "5.6.*|5.7.*", "illuminate/console": "5.7.*",
"illuminate/http": "5.6.*|5.7.*", "illuminate/http": "5.7.*",
"illuminate/routing": "5.6.*|5.7.*", "illuminate/routing": "5.7.*",
"illuminate/support": "5.6.*|5.7.*", "illuminate/broadcasting": "5.7.*",
"illuminate/support": "5.7.*",
"symfony/http-kernel": "~4.0", "symfony/http-kernel": "~4.0",
"pusher/pusher-php-server": "~3.0",
"symfony/psr-http-message-bridge": "^1.1" "symfony/psr-http-message-bridge": "^1.1"
}, },
"require-dev": { "require-dev": {

View File

@ -1,5 +1,6 @@
<?php <?php
use BeyondCode\LaravelWebSockets\Http\Middleware\Authorize;
use BeyondCode\LaravelWebSockets\ClientProviders\ConfigClientProvider; use BeyondCode\LaravelWebSockets\ClientProviders\ConfigClientProvider;
return [ return [
@ -44,6 +45,7 @@ return [
*/ */
'clients' => [ 'clients' => [
[ [
'name' => env('APP_NAME'),
'app_id' => env('WEBSOCKETS_APP_ID'), 'app_id' => env('WEBSOCKETS_APP_ID'),
'app_key' => env('WEBSOCKETS_APP_KEY'), 'app_key' => env('WEBSOCKETS_APP_KEY'),
'app_secret' => env('WEBSOCKETS_APP_SECRET') 'app_secret' => env('WEBSOCKETS_APP_SECRET')
@ -58,4 +60,20 @@ return [
* `ClientProvier` interface. * `ClientProvier` interface.
*/ */
'client_provider' => ConfigClientProvider::class, '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,
],
]
]; ];

View File

@ -0,0 +1,176 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>WebSockets Console</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<script
src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script>
<script src="https://js.pusher.com/4.3/pusher.min.js"></script>
</head>
<body>
<div class="container" id="app">
<div class="card col-xs-12">
<div class="card-header">
<form id="connect" class="form-inline" role="form">
<label class="my-1 mr-2" for="client">Client:</label>
<select class="form-control form-control-sm mr-2" name="client" id="client" v-model="client">
<option v-for="client in clients" :value="client">@{{ client.name }}</option>
</select>
<label class="my-1 mr-2" for="client">Port:</label>
<input class="form-control form-control-sm mr-2" v-model="port" placeholder="Port">
<button v-if="! connected" type="submit" @click.prevent="connect" class="mr-2 btn btn-sm btn-primary">Connect</button>
<button v-if="connected" type="submit" @click.prevent="disconnect" class="btn btn-sm btn-danger">Disconnect</button>
</form>
<div id="status"></div>
</div>
<div class="card-body">
<div v-if="connected">
<h4>Event Creator</h4>
<form>
<div class="row">
<div class="col">
<input type="text" class="form-control" v-model="form.channel" placeholder="Channel">
</div>
<div class="col">
<input type="text" class="form-control" v-model="form.event" placeholder="Event">
</div>
</div>
<div class="row mt-3">
<div class="col">
<div class="form-group">
<textarea placeholder="Data" v-model="form.data" class="form-control" id="data" rows="3"></textarea>
</div>
</div>
</div>
<div class="row text-right">
<div class="col">
<button type="submit" @click.prevent="sendEvent" class="btn btn-sm btn-primary">Send event</button>
</div>
</div>
</form>
</div>
<h4>Events</h4>
<table id="events" class="table table-striped table-hover">
<thead>
<tr>
<th>Type</th>
<th>Socket</th>
<th>Details</th>
<th>Time</th>
</tr>
</thead>
<tbody>
<tr v-for="log in logs.slice().reverse()">
<td><span class="badge" :class="getBadgeClass(log)">@{{ log.type }}</span></td>
<td>@{{ log.socketId }}</td>
<td>@{{ log.details }}</td>
<td>@{{ log.time }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
connected: false,
pusher: null,
port: 6001,
client: null,
clients: {!! json_encode($clients) !!},
form: {
channel: null,
event: null,
data: null
},
logs: [],
},
methods: {
connect() {
this.pusher = new Pusher(this.client.appKey, {
wsHost: window.location.hostname,
wsPort: this.port,
authEndpoint: '{{ config('websockets.dashboard.path') }}/auth',
enabledTransports: ['ws', 'flash']
});
this.pusher.connection.bind('state_change', states => {
$('div#status').text("Channels current state is " + states.current);
});
this.pusher.connection.bind('connected', () => {
this.connected = true;
});
this.pusher.connection.bind('disconnected', () => {
this.connected = false;
this.logs = [];
});
this.subscribeToChannel('disconnection');
this.subscribeToChannel('connection');
this.subscribeToChannel('vacated');
this.subscribeToChannel('occupied');
this.subscribeToChannel('subscribed');
this.subscribeToChannel('client_message');
this.subscribeToChannel('api_message');
},
disconnect() {
this.pusher.disconnect();
},
subscribeToChannel(channel) {
this.pusher.subscribe('{{ \BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Dashboard::LOG_CHANNEL_PREFIX }}'+channel)
.bind('log_message', (data) => {
this.logs.push(data);
});
},
getBadgeClass(log) {
if (log.type === 'occupied' || log.type === 'connection') {
return 'badge-primary';
}
if (log.type === 'vacated') {
return 'badge-warning';
}
if (log.type === 'disconnection') {
return 'badge-error';
}
if (log.type === 'api_message') {
return 'badge-info';
}
return 'badge-secondary';
},
sendEvent() {
$.post('{{ config('websockets.dashboard.path') }}/event', {
key: this.client.appKey,
secret: this.client.appSecret,
appId: this.client.appId,
channel: this.form.channel,
event: this.form.event,
data: this.form.data,
}).fail(e => {
alert('Error sending event.');
});
}
}
});
</script>
</body>
</html>

View File

@ -16,6 +16,9 @@ class Client
/** @var string */ /** @var string */
public $appSecret; public $appSecret;
/** @var string|null */
public $name;
public static function findByAppId(int $appId) public static function findByAppId(int $appId)
{ {
return app(ClientProvider::class)->findByAppId($appId); return app(ClientProvider::class)->findByAppId($appId);
@ -26,7 +29,7 @@ class Client
return app(ClientProvider::class)->findByAppKey($appKey); 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)) { if (!is_numeric($appId)) {
throw InvalidClient::appIdIsNotNumeric($appId); throw InvalidClient::appIdIsNotNumeric($appId);
@ -45,6 +48,8 @@ class Client
$this->appKey = $appKey; $this->appKey = $appKey;
$this->appSecret = $appSecret; $this->appSecret = $appSecret;
$this->name = $name;
} }

View File

@ -8,4 +8,6 @@ interface ClientProvider
public function findByAppId(int $appId): ?Client; public function findByAppId(int $appId): ?Client;
public function findByAppKey(string $appKey): ?Client; public function findByAppKey(string $appKey): ?Client;
public function all(): array;
} }

View File

@ -24,6 +24,15 @@ class ConfigClientProvider implements ClientProvider
return $this->instanciate($clientAttributes); return $this->instanciate($clientAttributes);
} }
public function all(): array
{
return $this->allClients()
->map(function ($client) {
return $this->instanciate($client);
})
->toArray();
}
protected function allClients(): Collection protected function allClients(): Collection
{ {
return collect(config('websockets.clients')); return collect(config('websockets.clients'));
@ -38,7 +47,8 @@ class ConfigClientProvider implements ClientProvider
return new Client( return new Client(
$clientAttributes['app_id'], $clientAttributes['app_id'],
$clientAttributes['app_key'], $clientAttributes['app_key'],
$clientAttributes['app_secret'] $clientAttributes['app_secret'],
$clientAttributes['name'] ?? null
); );
} }
} }

View File

@ -0,0 +1,14 @@
<?php
namespace BeyondCode\LaravelWebsockets\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Contracts\Broadcasting\Broadcaster;
class AuthenticateConsole
{
public function __invoke(Request $request, Broadcaster $broadcaster)
{
return $broadcaster->validAuthenticationResponse($request, []);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace BeyondCode\LaravelWebsockets\Http\Controllers;
use Pusher\Pusher;
use Illuminate\Http\Request;
use Illuminate\Broadcasting\Broadcasters\PusherBroadcaster;
class SendMessage
{
public function __invoke(Request $request)
{
$pusher = new Pusher(
$request->key, $request->secret,
$request->appId, config('broadcasting.connections.pusher.options', [])
);
return (new PusherBroadcaster($pusher))
->broadcast([$request->channel], $request->event, json_decode($request->data, true));
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace BeyondCode\LaravelWebsockets\Http\Controllers;
use Illuminate\Http\Request;
use BeyondCode\LaravelWebSockets\ClientProviders\ClientProvider;
class ShowConsole
{
public function __invoke(Request $request, ClientProvider $clients)
{
return view('websockets::console', [
'clients' => $clients->all()
]);
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace BeyondCode\LaravelWebsockets\Http\Middleware;
use Illuminate\Support\Facades\Gate;
class Authorize
{
public function handle($request, $next)
{
return Gate::check('viewWebSocketDashboard') ? $next($request) : abort(403);
}
}

5
src/Http/routes.php Normal file
View File

@ -0,0 +1,5 @@
<?php
Route::get('/', 'ShowConsole');
Route::post('/auth', 'AuthenticateConsole');
Route::post('/event', 'SendMessage');

View File

@ -2,6 +2,7 @@
namespace BeyondCode\LaravelWebSockets\LaravelEcho\Http\Controllers; namespace BeyondCode\LaravelWebSockets\LaravelEcho\Http\Controllers;
use BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Dashboard;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class TriggerEvent extends EchoController class TriggerEvent extends EchoController
@ -11,6 +12,8 @@ class TriggerEvent extends EchoController
$this->verifySignature($request); $this->verifySignature($request);
foreach ($request->json()->get('channels', []) as $channelId) { 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); $channel = $this->channelManager->find($request->appId, $channelId);
optional($channel)->broadcastToEveryoneExcept([ optional($channel)->broadcastToEveryoneExcept([

View File

@ -2,6 +2,7 @@
namespace BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Channels; namespace BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Channels;
use BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Dashboard;
use BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Exceptions\InvalidSignatureException; use BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Exceptions\InvalidSignatureException;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
@ -56,11 +57,21 @@ class Channel
public function unsubscribe(ConnectionInterface $connection) public function unsubscribe(ConnectionInterface $connection)
{ {
unset($this->subscriptions[$connection->socketId]); unset($this->subscriptions[$connection->socketId]);
if (! $this->hasConnections()) {
Dashboard::vacated($connection, $this->channelId);
}
} }
protected function saveConnection(ConnectionInterface $connection) protected function saveConnection(ConnectionInterface $connection)
{ {
if (! $this->hasConnections()) {
Dashboard::occupied($connection, $this->channelId);
}
$this->subscriptions[$connection->socketId] = $connection; $this->subscriptions[$connection->socketId] = $connection;
Dashboard::subscribed($connection, $this->channelId);
} }
public function broadcast($payload) public function broadcast($payload)

View File

@ -0,0 +1,100 @@
<?php
namespace BeyondCode\LaravelWebSockets\LaravelEcho\Pusher;
use Ratchet\ConnectionInterface;
use BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Channels\ChannelManager;
use stdClass;
class Dashboard
{
const LOG_CHANNEL_PREFIX = 'private-websockets-dashboard-';
const TYPE_DISCONNECTION = 'disconnection';
const TYPE_CONNECTION = 'connection';
const TYPE_VACATED = 'vacated';
const TYPE_OCCUPIED = 'occupied';
const TYPE_SUBSCRIBED = 'subscribed';
const TYPE_CLIENT_MESSAGE = 'client_message';
const TYPE_API_MESSAGE = 'api_message';
public static function connection(ConnectionInterface $connection)
{
/** @var \GuzzleHttp\Psr7\Request $request */
$request = $connection->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
]);
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Exceptions;
class InvalidConnectionException extends PusherException
{
public function __construct()
{
$this->message = 'Invalid Connection';
$this->code = 4009;
}
}

View File

@ -2,7 +2,7 @@
namespace BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Exceptions; namespace BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Exceptions;
class UnknownAppKey extends PusherException class UnknownAppKeyException extends PusherException
{ {
public function __construct(string $appKey) public function __construct(string $appKey)
{ {

View File

@ -3,6 +3,7 @@
namespace BeyondCode\LaravelWebSockets\LaravelEcho\WebSocket; namespace BeyondCode\LaravelWebSockets\LaravelEcho\WebSocket;
use BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Channels\ChannelManager; use BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Channels\ChannelManager;
use BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Dashboard;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use stdClass; use stdClass;
@ -29,6 +30,8 @@ class Message implements RespondableMessage
public function respond() public function respond()
{ {
if (starts_with($this->payload->event, 'client-')) { if (starts_with($this->payload->event, 'client-')) {
Dashboard::clientMessage($this->connection, $this->payload);
$channel = $this->channelManager->find($this->connection->client->appId, $this->payload->channel); $channel = $this->channelManager->find($this->connection->client->appId, $this->payload->channel);
optional($channel)->broadcast($this->payload); optional($channel)->broadcast($this->payload);

View File

@ -2,14 +2,15 @@
namespace BeyondCode\LaravelWebSockets\LaravelEcho\WebSocket; namespace BeyondCode\LaravelWebSockets\LaravelEcho\WebSocket;
use BeyondCode\LaravelWebSockets\ClientProviders\Client; use BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Dashboard;
use BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Exceptions\PusherException;
use BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Exceptions\UnknownAppKey;
use Exception; use Exception;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use Ratchet\RFC6455\Messaging\MessageInterface; use Ratchet\RFC6455\Messaging\MessageInterface;
use BeyondCode\LaravelWebSockets\WebSocketController; use BeyondCode\LaravelWebSockets\WebSocketController;
use BeyondCode\LaravelWebSockets\ClientProviders\Client;
use BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Channels\ChannelManager; 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 class PusherServer extends WebSocketController
{ {
@ -49,7 +50,6 @@ class PusherServer extends WebSocketController
$exception->getPayload() $exception->getPayload()
)); ));
} }
dump($exception);
} }
protected function verifyConnection(ConnectionInterface $connection) protected function verifyConnection(ConnectionInterface $connection)
@ -61,7 +61,7 @@ class PusherServer extends WebSocketController
parse_str($request->getUri()->getQuery(), $queryParameters); parse_str($request->getUri()->getQuery(), $queryParameters);
if (! $client = Client::findByAppKey($queryParameters['appKey'])) { if (! $client = Client::findByAppKey($queryParameters['appKey'])) {
throw new UnknownAppKey($queryParameters['appKey']); throw new UnknownAppKeyException($queryParameters['appKey']);
} }
$connection->client = $client; $connection->client = $client;
@ -69,6 +69,8 @@ class PusherServer extends WebSocketController
protected function establishConnection(ConnectionInterface $connection) protected function establishConnection(ConnectionInterface $connection)
{ {
Dashboard::connection($connection);
$connection->send(json_encode([ $connection->send(json_encode([
'event' => 'pusher:connection_established', 'event' => 'pusher:connection_established',
'data' => json_encode([ 'data' => json_encode([

View File

@ -2,6 +2,8 @@
namespace BeyondCode\LaravelWebSockets; namespace BeyondCode\LaravelWebSockets;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Route;
use BeyondCode\LaravelWebSockets\ClientProviders\ClientProvider; use BeyondCode\LaravelWebSockets\ClientProviders\ClientProvider;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Channels\ChannelManager; use BeyondCode\LaravelWebSockets\LaravelEcho\Pusher\Channels\ChannelManager;
@ -10,15 +12,39 @@ class LaravelWebSocketsServiceProvider extends ServiceProvider
{ {
public function boot() public function boot()
{ {
Route::middlewareGroup('websockets', config('websockets.dashboard.middleware', []));
$this->publishes([ $this->publishes([
__DIR__.'/../config/websockets.php' => base_path('config/websockets.php'), __DIR__.'/../config/websockets.php' => base_path('config/websockets.php'),
], 'config'); ], 'config');
$this->registerRoutes();
$this->registerDashboardGate();
$this->loadViewsFrom(__DIR__.'/../resources/views/', 'websockets');
$this->commands([ $this->commands([
Console\StartWebSocketServer::class, 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() public function register()
{ {
$this->mergeConfigFrom(__DIR__.'/../config/websockets.php', 'websockets'); $this->mergeConfigFrom(__DIR__.'/../config/websockets.php', 'websockets');
@ -35,4 +61,11 @@ class LaravelWebSocketsServiceProvider extends ServiceProvider
return app(config('websockets.client_provider')); return app(config('websockets.client_provider'));
}); });
} }
protected function registerDashboardGate()
{
Gate::define('viewWebSocketDashboard', function ($user = null) {
return app()->environment('local');
});
}
} }