Merge 2.x changes (#1043)
* Resolve conflicts * Apply fixes from StyleCI (#1042)
This commit is contained in:
parent
c53e78d5a0
commit
fb958fb851
|
|
@ -20,41 +20,16 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
php:
|
php:
|
||||||
- '7.3'
|
|
||||||
- '7.4'
|
|
||||||
- '8.0'
|
- '8.0'
|
||||||
- '8.1'
|
- '8.1'
|
||||||
laravel:
|
laravel:
|
||||||
- 6.*
|
- 9.*
|
||||||
- 7.*
|
|
||||||
- 8.*
|
|
||||||
prefer:
|
prefer:
|
||||||
- 'prefer-lowest'
|
- 'prefer-lowest'
|
||||||
- 'prefer-stable'
|
- 'prefer-stable'
|
||||||
include:
|
include:
|
||||||
- laravel: '6.*'
|
- laravel: '9.*'
|
||||||
testbench: '4.*'
|
testbench: '7.*'
|
||||||
phpunit: '^8.5.8|^9.3.3'
|
|
||||||
- laravel: '7.*'
|
|
||||||
testbench: '5.*'
|
|
||||||
phpunit: '^8.5.8|^9.3.3'
|
|
||||||
- laravel: '8.*'
|
|
||||||
testbench: '6.*'
|
|
||||||
phpunit: '^9.3.3'
|
|
||||||
exclude:
|
|
||||||
- php: '8.0'
|
|
||||||
laravel: 6.*
|
|
||||||
prefer: 'prefer-lowest'
|
|
||||||
- php: '8.0'
|
|
||||||
laravel: 7.*
|
|
||||||
prefer: 'prefer-lowest'
|
|
||||||
- php: '8.1'
|
|
||||||
laravel: 6.*
|
|
||||||
- php: '8.1'
|
|
||||||
laravel: 7.*
|
|
||||||
- php: '8.1'
|
|
||||||
laravel: 8.*
|
|
||||||
prefer: 'prefer-lowest'
|
|
||||||
|
|
||||||
name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} --${{ matrix.prefer }}
|
name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} --${{ matrix.prefer }}
|
||||||
|
|
||||||
|
|
@ -68,6 +43,13 @@ jobs:
|
||||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
|
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
|
||||||
coverage: pcov
|
coverage: pcov
|
||||||
|
|
||||||
|
- name: Setup MySQL
|
||||||
|
uses: haltuf/mysql-action@master
|
||||||
|
with:
|
||||||
|
mysql version: '8.0'
|
||||||
|
mysql database: 'websockets_test'
|
||||||
|
mysql root password: 'password'
|
||||||
|
|
||||||
- name: Setup Redis
|
- name: Setup Redis
|
||||||
uses: supercharge/redis-github-action@1.1.0
|
uses: supercharge/redis-github-action@1.1.0
|
||||||
with:
|
with:
|
||||||
|
|
@ -81,7 +63,7 @@ jobs:
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
composer require "laravel/framework:${{ matrix.laravel }}" "phpunit/phpunit:${{ matrix.phpunit }}" "orchestra/testbench-browser-kit:${{ matrix.testbench }}" "orchestra/database:${{ matrix.testbench }}" --no-interaction --no-update
|
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench-browser-kit:${{ matrix.testbench }}" "orchestra/database:${{ matrix.testbench }}" --no-interaction --no-update
|
||||||
composer update --${{ matrix.prefer }} --prefer-dist --no-interaction --no-suggest
|
composer update --${{ matrix.prefer }} --prefer-dist --no-interaction --no-suggest
|
||||||
|
|
||||||
- name: Run tests for Local
|
- name: Run tests for Local
|
||||||
|
|
|
||||||
|
|
@ -29,27 +29,33 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"cboden/ratchet": "^0.4.1",
|
"php": "^8.0|^8.1",
|
||||||
"clue/redis-react": "^2.5",
|
"cboden/ratchet": "^0.4.4",
|
||||||
|
"clue/block-react": "^1.5",
|
||||||
|
"clue/reactphp-sqlite": "^1.0",
|
||||||
|
"clue/redis-react": "^2.6",
|
||||||
|
"doctrine/dbal": "^2.9",
|
||||||
"evenement/evenement": "^2.0|^3.0",
|
"evenement/evenement": "^2.0|^3.0",
|
||||||
"facade/ignition-contracts": "^1.0",
|
"facade/ignition-contracts": "^1.0",
|
||||||
"guzzlehttp/psr7": "^1.7|^2.0",
|
"guzzlehttp/psr7": "^1.5",
|
||||||
"illuminate/broadcasting": "^6.3|^7.0|^8.0|^9.0",
|
"illuminate/broadcasting": "^9.0",
|
||||||
"illuminate/console": "^6.3|^7.0|^8.0|^9.0",
|
"illuminate/console": "^9.0",
|
||||||
"illuminate/http": "^6.3|^7.0|^8.0|^9.0",
|
"illuminate/http": "^9.0",
|
||||||
"illuminate/queue": "^6.3|^7.0|^8.0|^9.0",
|
"illuminate/queue": "^9.0",
|
||||||
"illuminate/routing": "^6.3|^7.0|^8.0|^9.0",
|
"illuminate/routing": "^9.0",
|
||||||
"illuminate/support": "^6.3|^7.0|^8.0|^9.0",
|
"illuminate/support": "^9.0",
|
||||||
"pusher/pusher-php-server": "^3.0|^4.0|^5.0|^6.0|^7.0",
|
"pusher/pusher-php-server": "^6.0|^7.0",
|
||||||
|
"react/mysql": "^0.5",
|
||||||
"react/promise": "^2.8",
|
"react/promise": "^2.8",
|
||||||
"symfony/http-kernel": "^4.4|^5.4|^6.0",
|
"symfony/http-kernel": "^5.0|^6.0",
|
||||||
"symfony/psr-http-message-bridge": "^1.1|^2.0"
|
"symfony/psr-http-message-bridge": "^1.1|^2.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"clue/block-react": "^1.4",
|
"clue/buzz-react": "^2.9",
|
||||||
"laravel/legacy-factories": "^1.1",
|
"laravel/legacy-factories": "^1.1",
|
||||||
"orchestra/testbench-browser-kit": "^4.0|^5.0|^6.0",
|
"orchestra/testbench-browser-kit": "^7.0",
|
||||||
"phpunit/phpunit": "^8.5.8|^9.3.3"
|
"phpunit/phpunit": "^9.0",
|
||||||
|
"ratchet/pawl": "^0.3.5"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
"ext-pcntl": "Running the server needs pcntl to listen to command signals and soft-shutdown.",
|
"ext-pcntl": "Running the server needs pcntl to listen to command signals and soft-shutdown.",
|
||||||
|
|
@ -71,7 +77,8 @@
|
||||||
"config": {
|
"config": {
|
||||||
"sort-packages": true
|
"sort-packages": true
|
||||||
},
|
},
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "dev",
|
||||||
|
"prefer-stable": true,
|
||||||
"extra": {
|
"extra": {
|
||||||
"laravel": {
|
"laravel": {
|
||||||
"providers": [
|
"providers": [
|
||||||
|
|
|
||||||
|
|
@ -37,13 +37,40 @@ return [
|
||||||
| the use of the TCP protocol based on, for example, a list of allowed
|
| the use of the TCP protocol based on, for example, a list of allowed
|
||||||
| applications.
|
| applications.
|
||||||
| By default, it uses the defined array in the config file, but you can
|
| By default, it uses the defined array in the config file, but you can
|
||||||
| anytime implement the same interface as the class and add your own
|
| choose to use SQLite or MySQL application managers, or define a
|
||||||
| custom method to retrieve the apps.
|
| custom application manager.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'app' => \BeyondCode\LaravelWebSockets\Apps\ConfigAppManager::class,
|
'app' => \BeyondCode\LaravelWebSockets\Apps\ConfigAppManager::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| SQLite application manager
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The SQLite database to use when using the SQLite application manager.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'sqlite' => [
|
||||||
|
'database' => storage_path('laravel-websockets.sqlite'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| MySql application manager
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The MySQL database connection to use.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'mysql' => [
|
||||||
|
'connection' => env('DB_CONNECTION', 'mysql'),
|
||||||
|
|
||||||
|
'table' => 'websockets_apps',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateWebSocketsAppsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('websockets_apps', function (Blueprint $table) {
|
||||||
|
$table->string('id')->index();
|
||||||
|
$table->string('key');
|
||||||
|
$table->string('secret');
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('host')->nullable();
|
||||||
|
$table->string('path')->nullable();
|
||||||
|
$table->boolean('enable_client_messages')->default(false);
|
||||||
|
$table->boolean('enable_statistics')->default(true);
|
||||||
|
$table->unsignedInteger('capacity')->nullable();
|
||||||
|
$table->string('allowed_origins');
|
||||||
|
$table->nullableTimestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('websockets_apps');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,9 +16,9 @@ class CreateWebSocketsStatisticsEntriesTable extends Migration
|
||||||
Schema::create('websockets_statistics_entries', function (Blueprint $table) {
|
Schema::create('websockets_statistics_entries', function (Blueprint $table) {
|
||||||
$table->increments('id');
|
$table->increments('id');
|
||||||
$table->string('app_id');
|
$table->string('app_id');
|
||||||
$table->integer('peak_connection_count');
|
$table->integer('peak_connections_count');
|
||||||
$table->integer('websocket_message_count');
|
$table->integer('websocket_messages_count');
|
||||||
$table->integer('api_message_count');
|
$table->integer('api_messages_count');
|
||||||
$table->nullableTimestamps();
|
$table->nullableTimestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
class RenameStatisticsCounters extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function up()
|
|
||||||
{
|
|
||||||
Schema::table('websockets_statistics_entries', function (Blueprint $table) {
|
|
||||||
$table->renameColumn('peak_connection_count', 'peak_connections_count');
|
|
||||||
});
|
|
||||||
Schema::table('websockets_statistics_entries', function (Blueprint $table) {
|
|
||||||
$table->renameColumn('websocket_message_count', 'websocket_messages_count');
|
|
||||||
});
|
|
||||||
Schema::table('websockets_statistics_entries', function (Blueprint $table) {
|
|
||||||
$table->renameColumn('api_message_count', 'api_messages_count');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function down()
|
|
||||||
{
|
|
||||||
Schema::table('websockets_statistics_entries', function (Blueprint $table) {
|
|
||||||
$table->renameColumn('peak_connections_count', 'peak_connection_count');
|
|
||||||
});
|
|
||||||
Schema::table('websockets_statistics_entries', function (Blueprint $table) {
|
|
||||||
$table->renameColumn('websocket_messages_count', 'websocket_message_count');
|
|
||||||
});
|
|
||||||
Schema::table('websockets_statistics_entries', function (Blueprint $table) {
|
|
||||||
$table->renameColumn('api_messages_count', 'api_message_count');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS apps (
|
||||||
|
id STRING NOT NULL,
|
||||||
|
key STRING NOT NULL,
|
||||||
|
secret STRING NOT NULL,
|
||||||
|
name STRING NOT NULL,
|
||||||
|
host STRING NULLABLE,
|
||||||
|
path STRING NULLABLE,
|
||||||
|
enable_client_messages BOOLEAN DEFAULT 0,
|
||||||
|
enable_statistics BOOLEAN DEFAULT 1,
|
||||||
|
capacity INTEGER NULLABLE,
|
||||||
|
allowed_origins STRING NULLABLE
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
@extends('websockets::layout')
|
||||||
|
|
||||||
|
@section('title')
|
||||||
|
Apps
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="flex flex-col py-8" id="app">
|
||||||
|
<form action="{{ route('laravel-websockets.apps.store') }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
Add new app
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($errors->isNotEmpty())
|
||||||
|
<div class="bg-red-500 text-white my-4 p-4 rounded">
|
||||||
|
@foreach($errors->all() as $error)
|
||||||
|
{{ $error }}<br>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="mt-6 sm:mt-5">
|
||||||
|
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
|
||||||
|
<label for="name"
|
||||||
|
class="block text-sm font-medium leading-5 text-gray-700 sm:mt-px sm:pt-2">
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
|
<div class="max-w-lg flex rounded-md shadow-sm">
|
||||||
|
<input id="name" name="name"
|
||||||
|
class="flex-1 form-input block w-full rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
|
||||||
|
<label for="allowed_origins"
|
||||||
|
class="block text-sm font-medium leading-5 text-gray-700 sm:mt-px sm:pt-2">
|
||||||
|
Allowed origins (comma separated)
|
||||||
|
</label>
|
||||||
|
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
|
<div class="max-w-lg flex rounded-md shadow-sm">
|
||||||
|
<input id="allowed_origins" name="allowed_origins"
|
||||||
|
class="flex-1 form-input block w-full rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
|
||||||
|
<label for="enable_statistics"
|
||||||
|
class="block text-sm font-medium leading-5 text-gray-700 sm:mt-px sm:pt-2">
|
||||||
|
Enable Statistics
|
||||||
|
</label>
|
||||||
|
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
|
<div class="mt-2 flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input id="enable_statistics"
|
||||||
|
name="enable_statistics"
|
||||||
|
value="1" type="checkbox" class="form-checkbox h-4 w-4 text-indigo-600 transition duration-150 ease-in-out" />
|
||||||
|
<label for="enable_statistics" class="ml-2 block text-sm leading-5 text-gray-900">
|
||||||
|
Yes
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
|
||||||
|
<label for="enable_client_messages"
|
||||||
|
class="block text-sm font-medium leading-5 text-gray-700 sm:mt-px sm:pt-2">
|
||||||
|
Enable Client Messages
|
||||||
|
</label>
|
||||||
|
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
|
<div class="mt-2 flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input id="enable_client_messages"
|
||||||
|
name="enable_client_messages"
|
||||||
|
value="1" type="checkbox" class="form-checkbox h-4 w-4 text-indigo-600 transition duration-150 ease-in-out" />
|
||||||
|
<label for="enable_client_messages" class="ml-2 block text-sm leading-5 text-gray-900">
|
||||||
|
Yes
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-8 border-t border-gray-200 pt-5">
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<span class="ml-3 inline-flex rounded-md shadow-sm">
|
||||||
|
<button type="submit"
|
||||||
|
@click.prevent="saveUser"
|
||||||
|
class="inline-flex justify-center py-2 px-4 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:shadow-outline-indigo active:bg-indigo-700 transition duration-150 ease-in-out">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
|
||||||
|
<div
|
||||||
|
class="align-middle inline-block min-w-full shadow overflow-hidden sm:rounded-lg border-b border-gray-200">
|
||||||
|
<table class="min-w-full" v-if="apps.length > 0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Allowed origins
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Statistics
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Client Messages
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white">
|
||||||
|
<tr v-for="app in apps">
|
||||||
|
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 text-sm leading-5 font-medium text-gray-900">
|
||||||
|
@{{ app.name }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 text-sm leading-5 text-gray-500">
|
||||||
|
@{{ app.allowed_origins || '*' }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 text-sm leading-5 text-gray-500">
|
||||||
|
<span v-if="app.enable_statistics">Yes</span>
|
||||||
|
<span v-else>No</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 text-sm leading-5 text-gray-500">
|
||||||
|
<span v-if="app.enable_client_messages">Yes</span>
|
||||||
|
<span v-else>No</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-no-wrap text-right border-b border-gray-200 text-sm leading-5 font-medium">
|
||||||
|
<a href="#" @click.prevent="showInstructions(app)"
|
||||||
|
class="pl-4 text-gray-600 hover:text-gray-900">Installation instructions</a>
|
||||||
|
<a href="#" @click.prevent="deleteUser(user)"
|
||||||
|
class="pl-4 text-red-600 hover:text-red-900">Delete</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-4" v-if="app">
|
||||||
|
<p class="pb-1">Modify your <code>.env</code> file:</p>
|
||||||
|
<pre class="bg-gray-100 p-4 rounded">PUSHER_APP_HOST=@{{ app.host === null ? window.location.hostname : app.host }}
|
||||||
|
PUSHER_APP_PORT={{ $port }}
|
||||||
|
PUSHER_APP_KEY=@{{ app.key }}
|
||||||
|
PUSHER_APP_ID=@{{ app.id }}
|
||||||
|
PUSHER_APP_SECRET=@{{ app.secret }}
|
||||||
|
PUSHER_APP_SCHEME=https
|
||||||
|
MIX_PUSHER_APP_HOST="${PUSHER_APP_HOST}"
|
||||||
|
MIX_PUSHER_APP_PORT="${PUSHER_APP_PORT}"
|
||||||
|
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('scripts')
|
||||||
|
<script>
|
||||||
|
new Vue({
|
||||||
|
el: '#app',
|
||||||
|
data: {
|
||||||
|
app: null,
|
||||||
|
apps: @json($apps),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showInstructions(app) {
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -1,32 +1,13 @@
|
||||||
<html>
|
@extends('websockets::layout')
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
|
|
||||||
|
|
||||||
<title>WebSockets Dashboard</title>
|
@section('title')
|
||||||
|
Dashboard
|
||||||
|
@endsection
|
||||||
|
|
||||||
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
|
@section('content')
|
||||||
<link href="https://cdn.jsdelivr.net/npm/vue-json-editor@1.4.2/assets/jsoneditor.min.css" rel="stylesheet">
|
|
||||||
|
|
||||||
<script src="https://js.pusher.com/7.0/pusher.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js"></script>
|
|
||||||
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/v-jsoneditor@1.4.1/dist/v-jsoneditor.min.js"></script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
window.baseURL = '{{ url(request()->path()) }}';
|
|
||||||
axios.defaults.baseURL = baseURL;
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="px-6">
|
|
||||||
<div
|
<div
|
||||||
id="app"
|
id="app"
|
||||||
class="mx-auto"
|
class="mx-auto"
|
||||||
:class="{
|
|
||||||
'max-w-xl': ! connected,
|
|
||||||
'max-w-6xl': connected,
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<div class="w-full my-6 rounded-lg bg-gray-100 p-6">
|
<div class="w-full my-6 rounded-lg bg-gray-100 p-6">
|
||||||
<div class="font-semibold uppercase text-gray-700 mb-6">
|
<div class="font-semibold uppercase text-gray-700 mb-6">
|
||||||
|
|
@ -235,6 +216,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('scripts')
|
||||||
<script>
|
<script>
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
|
|
@ -448,5 +432,4 @@
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
@endsection
|
||||||
</html>
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
|
||||||
|
|
||||||
|
<title>Laravel WebSockets</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tailwindcss/ui@latest/dist/tailwind-ui.min.css">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/vue-json-editor@1.4.2/assets/jsoneditor.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<script src="https://js.pusher.com/7.0/pusher.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js"></script>
|
||||||
|
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/v-jsoneditor@1.4.1/dist/v-jsoneditor.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.baseURL = '{{ url(request()->path()) }}';
|
||||||
|
axios.defaults.baseURL = baseURL;
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="min-h-screen bg-white">
|
||||||
|
<nav class="bg-white border-b border-gray-200">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex justify-between h-16">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0 flex items-center font-bold">
|
||||||
|
Laravel WebSockets
|
||||||
|
</div>
|
||||||
|
<div class="hidden sm:-my-px sm:ml-6 sm:flex">
|
||||||
|
<a href="{{ route('laravel-websockets.dashboard') }}"
|
||||||
|
class="
|
||||||
|
@if(Route::is('laravel-websockets.dashboard'))
|
||||||
|
border-indigo-500 focus:border-indigo-700 text-gray-900
|
||||||
|
@else
|
||||||
|
border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:text-gray-700 focus:border-gray-300
|
||||||
|
@endif
|
||||||
|
inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium leading-5 focus:outline-none transition duration-150 ease-in-out">
|
||||||
|
Dashboard
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('laravel-websockets.apps') }}"
|
||||||
|
class="
|
||||||
|
@if(Route::is('laravel-websockets.apps'))
|
||||||
|
border-indigo-500 focus:border-indigo-700 text-gray-900
|
||||||
|
@else
|
||||||
|
border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:text-gray-700 focus:border-gray-300
|
||||||
|
@endif
|
||||||
|
ml-8 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium leading-5 focus:outline-none transition duration-150 ease-in-out">
|
||||||
|
Apps
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="-mr-2 flex items-center sm:hidden">
|
||||||
|
<!-- Mobile menu button -->
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
|
||||||
|
<!-- Menu open: "hidden", Menu closed: "block" -->
|
||||||
|
<svg class="block h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"/>
|
||||||
|
</svg>
|
||||||
|
<!-- Menu open: "block", Menu closed: "hidden" -->
|
||||||
|
<svg class="hidden h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="py-10">
|
||||||
|
<header>
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<h1 class="text-3xl font-bold leading-tight text-gray-900">
|
||||||
|
@yield('title')
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||||
|
@yield('content')
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@yield('scripts')
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -17,6 +17,7 @@ use Psr\Http\Message\RequestInterface;
|
||||||
use Pusher\Pusher;
|
use Pusher\Pusher;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
use Ratchet\Http\HttpServerInterface;
|
use Ratchet\Http\HttpServerInterface;
|
||||||
|
use React\Promise\Deferred;
|
||||||
use React\Promise\PromiseInterface;
|
use React\Promise\PromiseInterface;
|
||||||
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
|
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
|
|
@ -52,13 +53,6 @@ abstract class Controller implements HttpServerInterface
|
||||||
*/
|
*/
|
||||||
protected $channelManager;
|
protected $channelManager;
|
||||||
|
|
||||||
/**
|
|
||||||
* The app attached with this request.
|
|
||||||
*
|
|
||||||
* @var \BeyondCode\LaravelWebSockets\Apps\App|null
|
|
||||||
*/
|
|
||||||
protected $app;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the request.
|
* Initialize the request.
|
||||||
*
|
*
|
||||||
|
|
@ -184,11 +178,25 @@ abstract class Controller implements HttpServerInterface
|
||||||
|
|
||||||
$laravelRequest = Request::createFromBase((new HttpFoundationFactory)->createRequest($serverRequest));
|
$laravelRequest = Request::createFromBase((new HttpFoundationFactory)->createRequest($serverRequest));
|
||||||
|
|
||||||
$this->ensureValidAppId($laravelRequest->get('appId'))
|
$this
|
||||||
->ensureValidSignature($laravelRequest);
|
->ensureValidAppId($laravelRequest->appId)
|
||||||
|
->then(function ($app) use ($laravelRequest, $connection) {
|
||||||
|
try {
|
||||||
|
$this->ensureValidSignature($app, $laravelRequest);
|
||||||
|
} catch (HttpException $exception) {
|
||||||
|
$this->onError($connection, $exception);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Invoke the controller action
|
// Invoke the controller action
|
||||||
|
try {
|
||||||
$response = $this($laravelRequest);
|
$response = $this($laravelRequest);
|
||||||
|
} catch (HttpException $exception) {
|
||||||
|
$this->onError($connection, $exception);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Allow for async IO in the controller action
|
// Allow for async IO in the controller action
|
||||||
if ($response instanceof PromiseInterface) {
|
if ($response instanceof PromiseInterface) {
|
||||||
|
|
@ -200,10 +208,13 @@ abstract class Controller implements HttpServerInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($response instanceof HttpException) {
|
if ($response instanceof HttpException) {
|
||||||
throw $response;
|
$this->onError($connection, $response);
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->sendAndClose($connection, $response);
|
$this->sendAndClose($connection, $response);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -222,29 +233,34 @@ abstract class Controller implements HttpServerInterface
|
||||||
* Ensure app existence.
|
* Ensure app existence.
|
||||||
*
|
*
|
||||||
* @param mixed $appId
|
* @param mixed $appId
|
||||||
* @return $this
|
* @return PromiseInterface
|
||||||
*
|
*
|
||||||
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
|
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
|
||||||
*/
|
*/
|
||||||
public function ensureValidAppId($appId)
|
public function ensureValidAppId($appId)
|
||||||
{
|
{
|
||||||
if (! $appId || ! $this->app = App::findById($appId)) {
|
$deferred = new Deferred();
|
||||||
|
|
||||||
|
App::findById($appId)
|
||||||
|
->then(function ($app) use ($appId, $deferred) {
|
||||||
|
if (! $app) {
|
||||||
throw new HttpException(401, "Unknown app id `{$appId}` provided.");
|
throw new HttpException(401, "Unknown app id `{$appId}` provided.");
|
||||||
}
|
}
|
||||||
|
$deferred->resolve($app);
|
||||||
|
});
|
||||||
|
|
||||||
return $this;
|
return $deferred->promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure signature integrity coming from an
|
* Ensure signature integrity coming from an
|
||||||
* authorized application.
|
* authorized application.
|
||||||
*
|
*
|
||||||
* @param \GuzzleHttp\Psr7\ServerRequest $request
|
* @param App $app
|
||||||
|
* @param Request $request
|
||||||
* @return $this
|
* @return $this
|
||||||
*
|
|
||||||
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
|
|
||||||
*/
|
*/
|
||||||
protected function ensureValidSignature(Request $request)
|
protected function ensureValidSignature(App $app, Request $request)
|
||||||
{
|
{
|
||||||
// The `auth_signature` & `body_md5` parameters are not included when calculating the `auth_signature` value.
|
// The `auth_signature` & `body_md5` parameters are not included when calculating the `auth_signature` value.
|
||||||
// The `appId`, `appKey` & `channelName` parameters are actually route parameters and are never supplied by the client.
|
// The `appId`, `appKey` & `channelName` parameters are actually route parameters and are never supplied by the client.
|
||||||
|
|
@ -261,7 +277,7 @@ abstract class Controller implements HttpServerInterface
|
||||||
|
|
||||||
$signature = "{$request->getMethod()}\n/{$request->path()}\n".Pusher::array_implode('=', '&', $params);
|
$signature = "{$request->getMethod()}\n/{$request->path()}\n".Pusher::array_implode('=', '&', $params);
|
||||||
|
|
||||||
$authSignature = hash_hmac('sha256', $signature, $this->app->secret);
|
$authSignature = hash_hmac('sha256', $signature, $app->secret);
|
||||||
|
|
||||||
if ($authSignature !== $request->get('auth_signature')) {
|
if ($authSignature !== $request->get('auth_signature')) {
|
||||||
throw new HttpException(401, 'Invalid auth signature provided.');
|
throw new HttpException(401, 'Invalid auth signature provided.');
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ namespace BeyondCode\LaravelWebSockets\API;
|
||||||
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
||||||
use BeyondCode\LaravelWebSockets\Facades\StatisticsCollector;
|
use BeyondCode\LaravelWebSockets\Facades\StatisticsCollector;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use React\Promise\Deferred;
|
||||||
|
|
||||||
class TriggerEvent extends Controller
|
class TriggerEvent extends Controller
|
||||||
{
|
{
|
||||||
|
|
@ -16,11 +17,15 @@ class TriggerEvent extends Controller
|
||||||
*/
|
*/
|
||||||
public function __invoke(Request $request)
|
public function __invoke(Request $request)
|
||||||
{
|
{
|
||||||
|
if ($request->has('channel')) {
|
||||||
|
$channels = [$request->get('channel')];
|
||||||
|
} else {
|
||||||
$channels = $request->channels ?: [];
|
$channels = $request->channels ?: [];
|
||||||
|
|
||||||
if (is_string($channels)) {
|
if (is_string($channels)) {
|
||||||
$channels = [$channels];
|
$channels = [$channels];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($channels as $channelName) {
|
foreach ($channels as $channelName) {
|
||||||
// Here you can use the ->find(), even if the channel
|
// Here you can use the ->find(), even if the channel
|
||||||
|
|
@ -49,7 +54,11 @@ class TriggerEvent extends Controller
|
||||||
$request->appId, $request->socket_id, $channelName, (object) $payload
|
$request->appId, $request->socket_id, $channelName, (object) $payload
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($this->app->statisticsEnabled) {
|
$deferred = new Deferred();
|
||||||
|
|
||||||
|
$this->ensureValidAppId($request->appId)
|
||||||
|
->then(function ($app) use ($request, $channelName, $deferred) {
|
||||||
|
if ($app->statisticsEnabled) {
|
||||||
StatisticsCollector::apiMessage($request->appId);
|
StatisticsCollector::apiMessage($request->appId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,8 +67,11 @@ class TriggerEvent extends Controller
|
||||||
'channel' => $channelName,
|
'channel' => $channelName,
|
||||||
'payload' => $request->data,
|
'payload' => $request->data,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$deferred->resolve((object) []);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (object) [];
|
return $deferred->promise();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace BeyondCode\LaravelWebSockets\Apps;
|
namespace BeyondCode\LaravelWebSockets\Apps;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||||
|
use React\Promise\PromiseInterface;
|
||||||
|
|
||||||
class App
|
class App
|
||||||
{
|
{
|
||||||
|
|
@ -40,7 +41,7 @@ class App
|
||||||
* Find the app by id.
|
* Find the app by id.
|
||||||
*
|
*
|
||||||
* @param string|int $appId
|
* @param string|int $appId
|
||||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
* @return PromiseInterface
|
||||||
*/
|
*/
|
||||||
public static function findById($appId)
|
public static function findById($appId)
|
||||||
{
|
{
|
||||||
|
|
@ -51,9 +52,9 @@ class App
|
||||||
* Find the app by app key.
|
* Find the app by app key.
|
||||||
*
|
*
|
||||||
* @param string $appKey
|
* @param string $appKey
|
||||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
* @return PromiseInterface
|
||||||
*/
|
*/
|
||||||
public static function findByKey($appKey): ?self
|
public static function findByKey($appKey): PromiseInterface
|
||||||
{
|
{
|
||||||
return app(AppManager::class)->findByKey($appKey);
|
return app(AppManager::class)->findByKey($appKey);
|
||||||
}
|
}
|
||||||
|
|
@ -62,9 +63,9 @@ class App
|
||||||
* Find the app by app secret.
|
* Find the app by app secret.
|
||||||
*
|
*
|
||||||
* @param string $appSecret
|
* @param string $appSecret
|
||||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
* @return PromiseInterface
|
||||||
*/
|
*/
|
||||||
public static function findBySecret($appSecret): ?self
|
public static function findBySecret($appSecret): PromiseInterface
|
||||||
{
|
{
|
||||||
return app(AppManager::class)->findBySecret($appSecret);
|
return app(AppManager::class)->findBySecret($appSecret);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
namespace BeyondCode\LaravelWebSockets\Apps;
|
namespace BeyondCode\LaravelWebSockets\Apps;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||||
|
use React\Promise\PromiseInterface;
|
||||||
|
use function React\Promise\resolve as resolvePromise;
|
||||||
|
|
||||||
class ConfigAppManager implements AppManager
|
class ConfigAppManager implements AppManager
|
||||||
{
|
{
|
||||||
|
|
@ -26,54 +28,64 @@ class ConfigAppManager implements AppManager
|
||||||
/**
|
/**
|
||||||
* Get all apps.
|
* Get all apps.
|
||||||
*
|
*
|
||||||
* @return array[\BeyondCode\LaravelWebSockets\Apps\App]
|
* @return PromiseInterface
|
||||||
*/
|
*/
|
||||||
public function all(): array
|
public function all(): PromiseInterface
|
||||||
{
|
{
|
||||||
return $this->apps
|
return resolvePromise($this->apps
|
||||||
->map(function (array $appAttributes) {
|
->map(function (array $appAttributes) {
|
||||||
return $this->convertIntoApp($appAttributes);
|
return $this->convertIntoApp($appAttributes);
|
||||||
})
|
})
|
||||||
->toArray();
|
->toArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get app by id.
|
* Get app by id.
|
||||||
*
|
*
|
||||||
* @param string|int $appId
|
* @param string|int $appId
|
||||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
* @return PromiseInterface
|
||||||
*/
|
*/
|
||||||
public function findById($appId): ?App
|
public function findById($appId): PromiseInterface
|
||||||
{
|
{
|
||||||
return $this->convertIntoApp(
|
return resolvePromise($this->convertIntoApp(
|
||||||
$this->apps->firstWhere('id', $appId)
|
$this->apps->firstWhere('id', $appId)
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get app by app key.
|
* Get app by app key.
|
||||||
*
|
*
|
||||||
* @param string $appKey
|
* @param string $appKey
|
||||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
* @return PromiseInterface
|
||||||
*/
|
*/
|
||||||
public function findByKey($appKey): ?App
|
public function findByKey($appKey): PromiseInterface
|
||||||
{
|
{
|
||||||
return $this->convertIntoApp(
|
return resolvePromise($this->convertIntoApp(
|
||||||
$this->apps->firstWhere('key', $appKey)
|
$this->apps->firstWhere('key', $appKey)
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get app by secret.
|
* Get app by secret.
|
||||||
*
|
*
|
||||||
* @param string $appSecret
|
* @param string $appSecret
|
||||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
* @return PromiseInterface
|
||||||
*/
|
*/
|
||||||
public function findBySecret($appSecret): ?App
|
public function findBySecret($appSecret): PromiseInterface
|
||||||
{
|
{
|
||||||
return $this->convertIntoApp(
|
return resolvePromise($this->convertIntoApp(
|
||||||
$this->apps->firstWhere('secret', $appSecret)
|
$this->apps->firstWhere('secret', $appSecret)
|
||||||
);
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function createApp($appData): PromiseInterface
|
||||||
|
{
|
||||||
|
$this->apps->push($appData);
|
||||||
|
|
||||||
|
return resolvePromise();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -107,8 +119,8 @@ class ConfigAppManager implements AppManager
|
||||||
}
|
}
|
||||||
|
|
||||||
$app
|
$app
|
||||||
->enableClientMessages($appAttributes['enable_client_messages'])
|
->enableClientMessages((bool) $appAttributes['enable_client_messages'])
|
||||||
->enableStatistics($appAttributes['enable_statistics'])
|
->enableStatistics((bool) $appAttributes['enable_statistics'])
|
||||||
->setCapacity($appAttributes['capacity'] ?? null)
|
->setCapacity($appAttributes['capacity'] ?? null)
|
||||||
->setAllowedOrigins($appAttributes['allowed_origins'] ?? []);
|
->setAllowedOrigins($appAttributes['allowed_origins'] ?? []);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,171 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Apps;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||||
|
use React\MySQL\ConnectionInterface;
|
||||||
|
use React\MySQL\QueryResult;
|
||||||
|
use React\Promise\Deferred;
|
||||||
|
use React\Promise\PromiseInterface;
|
||||||
|
|
||||||
|
class MysqlAppManager implements AppManager
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The database connection.
|
||||||
|
*
|
||||||
|
* @var ConnectionInterface
|
||||||
|
*/
|
||||||
|
protected $database;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the class.
|
||||||
|
*
|
||||||
|
* @param ConnectionInterface $database
|
||||||
|
*/
|
||||||
|
public function __construct(ConnectionInterface $database)
|
||||||
|
{
|
||||||
|
$this->database = $database;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getTableName(): string
|
||||||
|
{
|
||||||
|
return config('websockets.managers.mysql.table');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all apps.
|
||||||
|
*
|
||||||
|
* @return PromiseInterface
|
||||||
|
*/
|
||||||
|
public function all(): PromiseInterface
|
||||||
|
{
|
||||||
|
$deferred = new Deferred();
|
||||||
|
|
||||||
|
$this->database->query('SELECT * FROM `'.$this->getTableName().'`')
|
||||||
|
->then(function (QueryResult $result) use ($deferred) {
|
||||||
|
$deferred->resolve($result->resultRows);
|
||||||
|
}, function ($error) use ($deferred) {
|
||||||
|
$deferred->reject($error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return $deferred->promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get app by id.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @return PromiseInterface
|
||||||
|
*/
|
||||||
|
public function findById($appId): PromiseInterface
|
||||||
|
{
|
||||||
|
$deferred = new Deferred();
|
||||||
|
|
||||||
|
$this->database->query('SELECT * from `'.$this->getTableName().'` WHERE `id` = ?', [$appId])
|
||||||
|
->then(function (QueryResult $result) use ($deferred) {
|
||||||
|
$deferred->resolve($this->convertIntoApp($result->resultRows[0]));
|
||||||
|
}, function ($error) use ($deferred) {
|
||||||
|
$deferred->reject($error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return $deferred->promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get app by app key.
|
||||||
|
*
|
||||||
|
* @param string $appKey
|
||||||
|
* @return PromiseInterface
|
||||||
|
*/
|
||||||
|
public function findByKey($appKey): PromiseInterface
|
||||||
|
{
|
||||||
|
$deferred = new Deferred();
|
||||||
|
|
||||||
|
$this->database->query('SELECT * from `'.$this->getTableName().'` WHERE `key` = ?', [$appKey])
|
||||||
|
->then(function (QueryResult $result) use ($deferred) {
|
||||||
|
$deferred->resolve($this->convertIntoApp($result->resultRows[0]));
|
||||||
|
}, function ($error) use ($deferred) {
|
||||||
|
$deferred->reject($error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return $deferred->promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get app by secret.
|
||||||
|
*
|
||||||
|
* @param string $appSecret
|
||||||
|
* @return PromiseInterface
|
||||||
|
*/
|
||||||
|
public function findBySecret($appSecret): PromiseInterface
|
||||||
|
{
|
||||||
|
$deferred = new Deferred();
|
||||||
|
|
||||||
|
$this->database->query('SELECT * from `'.$this->getTableName().'` WHERE `secret` = ?', [$appSecret])
|
||||||
|
->then(function (QueryResult $result) use ($deferred) {
|
||||||
|
$deferred->resolve($this->convertIntoApp($result->resultRows[0]));
|
||||||
|
}, function ($error) use ($deferred) {
|
||||||
|
$deferred->reject($error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return $deferred->promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map the app into an App instance.
|
||||||
|
*
|
||||||
|
* @param array|null $app
|
||||||
|
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
||||||
|
*/
|
||||||
|
protected function convertIntoApp(?array $appAttributes): ?App
|
||||||
|
{
|
||||||
|
if (! $appAttributes) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$app = new App(
|
||||||
|
$appAttributes['id'],
|
||||||
|
$appAttributes['key'],
|
||||||
|
$appAttributes['secret']
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isset($appAttributes['name'])) {
|
||||||
|
$app->setName($appAttributes['name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($appAttributes['host'])) {
|
||||||
|
$app->setHost($appAttributes['host']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($appAttributes['path'])) {
|
||||||
|
$app->setPath($appAttributes['path']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$app
|
||||||
|
->enableClientMessages((bool) $appAttributes['enable_client_messages'])
|
||||||
|
->enableStatistics((bool) $appAttributes['enable_statistics'])
|
||||||
|
->setCapacity($appAttributes['capacity'] ?? null)
|
||||||
|
->setAllowedOrigins(array_filter(explode(',', $appAttributes['allowed_origins'])));
|
||||||
|
|
||||||
|
return $app;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function createApp($appData): PromiseInterface
|
||||||
|
{
|
||||||
|
$deferred = new Deferred();
|
||||||
|
|
||||||
|
$this->database->query(
|
||||||
|
'INSERT INTO `'.$this->getTableName().'` (`id`, `key`, `secret`, `name`, `enable_client_messages`, `enable_statistics`, `allowed_origins`, `capacity`) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||||
|
[$appData['id'], $appData['key'], $appData['secret'], $appData['name'], $appData['enable_client_messages'], $appData['enable_statistics'], $appData['allowed_origins'] ?? '', $appData['capacity'] ?? null])
|
||||||
|
->then(function () use ($deferred) {
|
||||||
|
$deferred->resolve();
|
||||||
|
}, function ($error) use ($deferred) {
|
||||||
|
$deferred->reject($error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return $deferred->promise();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Apps;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||||
|
use Clue\React\SQLite\DatabaseInterface;
|
||||||
|
use Clue\React\SQLite\Result;
|
||||||
|
use React\Promise\Deferred;
|
||||||
|
use React\Promise\PromiseInterface;
|
||||||
|
|
||||||
|
class SQLiteAppManager implements AppManager
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The database connection.
|
||||||
|
*
|
||||||
|
* @var DatabaseInterface
|
||||||
|
*/
|
||||||
|
protected $database;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the class.
|
||||||
|
*
|
||||||
|
* @param DatabaseInterface $database
|
||||||
|
*/
|
||||||
|
public function __construct(DatabaseInterface $database)
|
||||||
|
{
|
||||||
|
$this->database = $database;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all apps.
|
||||||
|
*
|
||||||
|
* @return PromiseInterface
|
||||||
|
*/
|
||||||
|
public function all(): PromiseInterface
|
||||||
|
{
|
||||||
|
$deferred = new Deferred();
|
||||||
|
|
||||||
|
$this->database->query('SELECT * FROM `apps`')
|
||||||
|
->then(function (Result $result) use ($deferred) {
|
||||||
|
$deferred->resolve($result->rows);
|
||||||
|
}, function ($error) use ($deferred) {
|
||||||
|
$deferred->reject($error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return $deferred->promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get app by id.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @return PromiseInterface
|
||||||
|
*/
|
||||||
|
public function findById($appId): PromiseInterface
|
||||||
|
{
|
||||||
|
$deferred = new Deferred();
|
||||||
|
|
||||||
|
$this->database->query('SELECT * from apps WHERE `id` = :id', ['id' => $appId])
|
||||||
|
->then(function (Result $result) use ($deferred) {
|
||||||
|
$deferred->resolve($this->convertIntoApp($result->rows[0]));
|
||||||
|
}, function ($error) use ($deferred) {
|
||||||
|
$deferred->reject($error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return $deferred->promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get app by app key.
|
||||||
|
*
|
||||||
|
* @param string $appKey
|
||||||
|
* @return PromiseInterface
|
||||||
|
*/
|
||||||
|
public function findByKey($appKey): PromiseInterface
|
||||||
|
{
|
||||||
|
$deferred = new Deferred();
|
||||||
|
|
||||||
|
$this->database->query('SELECT * from apps WHERE `key` = :key', ['key' => $appKey])
|
||||||
|
->then(function (Result $result) use ($deferred) {
|
||||||
|
$deferred->resolve($this->convertIntoApp($result->rows[0]));
|
||||||
|
}, function ($error) use ($deferred) {
|
||||||
|
$deferred->reject($error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return $deferred->promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get app by secret.
|
||||||
|
*
|
||||||
|
* @param string $appSecret
|
||||||
|
* @return PromiseInterface
|
||||||
|
*/
|
||||||
|
public function findBySecret($appSecret): PromiseInterface
|
||||||
|
{
|
||||||
|
$deferred = new Deferred();
|
||||||
|
|
||||||
|
$this->database->query('SELECT * from apps WHERE `secret` = :secret', ['secret' => $appSecret])
|
||||||
|
->then(function (Result $result) use ($deferred) {
|
||||||
|
$deferred->resolve($this->convertIntoApp($result->rows[0]));
|
||||||
|
}, function ($error) use ($deferred) {
|
||||||
|
$deferred->reject($error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return $deferred->promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map the app into an App instance.
|
||||||
|
*
|
||||||
|
* @param array|null $app
|
||||||
|
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
||||||
|
*/
|
||||||
|
protected function convertIntoApp(?array $appAttributes): ?App
|
||||||
|
{
|
||||||
|
if (! $appAttributes) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$app = new App(
|
||||||
|
$appAttributes['id'],
|
||||||
|
$appAttributes['key'],
|
||||||
|
$appAttributes['secret']
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isset($appAttributes['name'])) {
|
||||||
|
$app->setName($appAttributes['name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($appAttributes['host'])) {
|
||||||
|
$app->setHost($appAttributes['host']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($appAttributes['path'])) {
|
||||||
|
$app->setPath($appAttributes['path']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$app
|
||||||
|
->enableClientMessages((bool) $appAttributes['enable_client_messages'])
|
||||||
|
->enableStatistics((bool) $appAttributes['enable_statistics'])
|
||||||
|
->setCapacity($appAttributes['capacity'] ?? null)
|
||||||
|
->setAllowedOrigins(array_filter(explode(',', $appAttributes['allowed_origins'])));
|
||||||
|
|
||||||
|
return $app;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function createApp($appData): PromiseInterface
|
||||||
|
{
|
||||||
|
$deferred = new Deferred();
|
||||||
|
|
||||||
|
$this->database->query('
|
||||||
|
INSERT INTO apps (id, key, secret, name, host, path, enable_client_messages, enable_statistics, capacity, allowed_origins)
|
||||||
|
VALUES (:id, :key, :secret, :name, :host, :path, :enable_client_messages, :enable_statistics, :capacity, :allowed_origins)
|
||||||
|
', $appData)
|
||||||
|
->then(function (Result $result) use ($deferred) {
|
||||||
|
$deferred->resolve();
|
||||||
|
}, function ($error) use ($deferred) {
|
||||||
|
$deferred->reject($error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return $deferred->promise();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Cache;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Helpers;
|
||||||
|
use Illuminate\Cache\ArrayLock as LaravelLock;
|
||||||
|
use React\Promise\PromiseInterface;
|
||||||
|
|
||||||
|
class ArrayLock extends Lock
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The parent array cache store.
|
||||||
|
*
|
||||||
|
* @var \Illuminate\Cache\ArrayStore
|
||||||
|
*/
|
||||||
|
protected $store;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal Laravel Array Lock.
|
||||||
|
*
|
||||||
|
* @var \Illuminate\Cache\ArrayLock
|
||||||
|
*/
|
||||||
|
protected $lock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new lock instance.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Cache\ArrayStore $store
|
||||||
|
* @param string $name
|
||||||
|
* @param int $seconds
|
||||||
|
* @param string|null $owner
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct($store, $name, $seconds, $owner = null)
|
||||||
|
{
|
||||||
|
parent::__construct($name, $seconds, $owner);
|
||||||
|
|
||||||
|
$this->lock = new LaravelLock($store, $name, $seconds, $owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function acquire(): PromiseInterface
|
||||||
|
{
|
||||||
|
return Helpers::createFulfilledPromise($this->lock->acquire());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get($callback = null): PromiseInterface
|
||||||
|
{
|
||||||
|
return $this->lock->get($callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function release(): PromiseInterface
|
||||||
|
{
|
||||||
|
return Helpers::createFulfilledPromise($this->lock->release());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Cache;
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use React\Promise\PromiseInterface;
|
||||||
|
|
||||||
|
abstract class Lock
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name of the lock.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of seconds the lock should be maintained.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $seconds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scope identifier of this lock.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $owner;
|
||||||
|
|
||||||
|
public function __construct($name, $seconds, $owner = null)
|
||||||
|
{
|
||||||
|
if (is_null($owner)) {
|
||||||
|
$owner = Str::random();
|
||||||
|
}
|
||||||
|
$this->name = $name;
|
||||||
|
$this->seconds = $seconds;
|
||||||
|
$this->owner = $owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract public function acquire(): PromiseInterface;
|
||||||
|
|
||||||
|
abstract public function get($callback = null): PromiseInterface;
|
||||||
|
|
||||||
|
abstract public function release(): PromiseInterface;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Cache;
|
||||||
|
|
||||||
|
use Clue\React\Redis\Client;
|
||||||
|
use Illuminate\Cache\LuaScripts;
|
||||||
|
use React\Promise\Deferred;
|
||||||
|
use React\Promise\PromiseInterface;
|
||||||
|
|
||||||
|
class RedisLock extends Lock
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The asynchronous redis client.
|
||||||
|
*
|
||||||
|
* @var Client
|
||||||
|
*/
|
||||||
|
protected $redis;
|
||||||
|
|
||||||
|
public function __construct(Client $redis, $name, $seconds, $owner = null)
|
||||||
|
{
|
||||||
|
parent::__construct($name, $seconds, $owner);
|
||||||
|
|
||||||
|
$this->redis = $redis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function acquire(): PromiseInterface
|
||||||
|
{
|
||||||
|
$promise = new Deferred();
|
||||||
|
|
||||||
|
if ($this->seconds > 0) {
|
||||||
|
$this->redis
|
||||||
|
->set($this->name, $this->owner, 'EX', $this->seconds, 'NX')
|
||||||
|
->then(function ($result) use ($promise) {
|
||||||
|
$promise->resolve($result === true);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$this->redis
|
||||||
|
->setnx($this->name, $this->owner)
|
||||||
|
->then(function ($result) use ($promise) {
|
||||||
|
$promise->resolve($result === 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return $promise->promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get($callback = null): PromiseInterface
|
||||||
|
{
|
||||||
|
$promise = new Deferred();
|
||||||
|
|
||||||
|
$this->acquire()
|
||||||
|
->then(function ($result) use ($callback, $promise) {
|
||||||
|
if ($result) {
|
||||||
|
try {
|
||||||
|
$callback();
|
||||||
|
} finally {
|
||||||
|
$promise->resolve($this->release());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return $promise->promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function release(): PromiseInterface
|
||||||
|
{
|
||||||
|
return $this->redis->eval(LuaScripts::releaseLock(), 1, $this->name, $this->owner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,17 +2,18 @@
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\ChannelManagers;
|
namespace BeyondCode\LaravelWebSockets\ChannelManagers;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Cache\ArrayLock;
|
||||||
use BeyondCode\LaravelWebSockets\Channels\Channel;
|
use BeyondCode\LaravelWebSockets\Channels\Channel;
|
||||||
use BeyondCode\LaravelWebSockets\Channels\PresenceChannel;
|
use BeyondCode\LaravelWebSockets\Channels\PresenceChannel;
|
||||||
use BeyondCode\LaravelWebSockets\Channels\PrivateChannel;
|
use BeyondCode\LaravelWebSockets\Channels\PrivateChannel;
|
||||||
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
use BeyondCode\LaravelWebSockets\Helpers;
|
use BeyondCode\LaravelWebSockets\Helpers;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Cache\ArrayLock;
|
|
||||||
use Illuminate\Cache\ArrayStore;
|
use Illuminate\Cache\ArrayStore;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
use React\EventLoop\LoopInterface;
|
use React\EventLoop\LoopInterface;
|
||||||
|
use function React\Promise\all;
|
||||||
use React\Promise\PromiseInterface;
|
use React\Promise\PromiseInterface;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
|
|
||||||
|
|
@ -226,9 +227,7 @@ class LocalChannelManager implements ChannelManager
|
||||||
{
|
{
|
||||||
$channel = $this->findOrCreate($connection->app->id, $channelName);
|
$channel = $this->findOrCreate($connection->app->id, $channelName);
|
||||||
|
|
||||||
return Helpers::createFulfilledPromise(
|
return $channel->unsubscribe($connection, $payload);
|
||||||
$channel->unsubscribe($connection, $payload)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -439,26 +438,24 @@ class LocalChannelManager implements ChannelManager
|
||||||
*/
|
*/
|
||||||
public function removeObsoleteConnections(): PromiseInterface
|
public function removeObsoleteConnections(): PromiseInterface
|
||||||
{
|
{
|
||||||
$lock = $this->lock();
|
return $this->lock()->get(function () {
|
||||||
try {
|
return $this->getLocalConnections()
|
||||||
if (! $lock->acquire()) {
|
->then(function ($connections) {
|
||||||
return Helpers::createFulfilledPromise(false);
|
$promises = [];
|
||||||
}
|
|
||||||
|
|
||||||
$this->getLocalConnections()->then(function ($connections) {
|
|
||||||
foreach ($connections as $connection) {
|
foreach ($connections as $connection) {
|
||||||
$differenceInSeconds = $connection->lastPongedAt->diffInSeconds(Carbon::now());
|
$differenceInSeconds = $connection->lastPongedAt->diffInSeconds(Carbon::now());
|
||||||
|
|
||||||
if ($differenceInSeconds > 120) {
|
if ($differenceInSeconds > 120) {
|
||||||
$this->unsubscribeFromAllChannels($connection);
|
$promises[] = $this->unsubscribeFromAllChannels($connection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
return Helpers::createFulfilledPromise(true);
|
return all($promises);
|
||||||
} finally {
|
})->then(function () {
|
||||||
optional($lock)->forceRelease();
|
$this->lock()->release();
|
||||||
}
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -557,7 +554,7 @@ class LocalChannelManager implements ChannelManager
|
||||||
/**
|
/**
|
||||||
* Get a new ArrayLock instance to avoid race conditions.
|
* Get a new ArrayLock instance to avoid race conditions.
|
||||||
*
|
*
|
||||||
* @return \Illuminate\Cache\CacheLock
|
* @return ArrayLock
|
||||||
*/
|
*/
|
||||||
protected function lock()
|
protected function lock()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,19 @@
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\ChannelManagers;
|
namespace BeyondCode\LaravelWebSockets\ChannelManagers;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Cache\RedisLock;
|
||||||
|
use BeyondCode\LaravelWebSockets\Channels\Channel;
|
||||||
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
||||||
use BeyondCode\LaravelWebSockets\Helpers;
|
use BeyondCode\LaravelWebSockets\Helpers;
|
||||||
use BeyondCode\LaravelWebSockets\Server\MockableConnection;
|
use BeyondCode\LaravelWebSockets\Server\MockableConnection;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Clue\React\Redis\Client;
|
use Clue\React\Redis\Client;
|
||||||
use Clue\React\Redis\Factory;
|
use Clue\React\Redis\Factory;
|
||||||
use Illuminate\Cache\RedisLock;
|
|
||||||
use Illuminate\Support\Facades\Redis;
|
use Illuminate\Support\Facades\Redis;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
use React\EventLoop\LoopInterface;
|
use React\EventLoop\LoopInterface;
|
||||||
|
use function React\Promise\all;
|
||||||
use React\Promise\PromiseInterface;
|
use React\Promise\PromiseInterface;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
|
|
||||||
|
|
@ -100,9 +102,12 @@ class RedisChannelManager extends LocalChannelManager
|
||||||
{
|
{
|
||||||
return $this->getGlobalChannels($connection->app->id)
|
return $this->getGlobalChannels($connection->app->id)
|
||||||
->then(function ($channels) use ($connection) {
|
->then(function ($channels) use ($connection) {
|
||||||
|
$promises = [];
|
||||||
foreach ($channels as $channel) {
|
foreach ($channels as $channel) {
|
||||||
$this->unsubscribeFromChannel($connection, $channel, new stdClass);
|
$promises[] = $this->unsubscribeFromChannel($connection, $channel, new stdClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return all($promises);
|
||||||
})
|
})
|
||||||
->then(function () use ($connection) {
|
->then(function () use ($connection) {
|
||||||
return parent::unsubscribeFromAllChannels($connection);
|
return parent::unsubscribeFromAllChannels($connection);
|
||||||
|
|
@ -144,19 +149,37 @@ class RedisChannelManager extends LocalChannelManager
|
||||||
*/
|
*/
|
||||||
public function unsubscribeFromChannel(ConnectionInterface $connection, string $channelName, stdClass $payload): PromiseInterface
|
public function unsubscribeFromChannel(ConnectionInterface $connection, string $channelName, stdClass $payload): PromiseInterface
|
||||||
{
|
{
|
||||||
return parent::unsubscribeFromChannel($connection, $channelName, $payload)
|
return $this->getGlobalConnectionsCount($connection->app->id, $channelName)
|
||||||
->then(function () use ($connection, $channelName) {
|
->then(function ($count) use ($connection, $channelName) {
|
||||||
return $this->decrementSubscriptionsCount($connection->app->id, $channelName);
|
if ($count === 0) {
|
||||||
})
|
// Make sure to not stay subscribed to the PubSub topic
|
||||||
|
// if there are no connections.
|
||||||
|
return $this->unsubscribeFromTopic($connection->app->id, $channelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Helpers::createFulfilledPromise(null);
|
||||||
|
})
|
||||||
|
->then(function () use ($connection, $channelName) {
|
||||||
|
return $this->decrementSubscriptionsCount($connection->app->id, $channelName)
|
||||||
->then(function ($count) use ($connection, $channelName) {
|
->then(function ($count) use ($connection, $channelName) {
|
||||||
$this->removeConnectionFromSet($connection);
|
|
||||||
// If the total connections count gets to 0 after unsubscribe,
|
// If the total connections count gets to 0 after unsubscribe,
|
||||||
// try again to check & unsubscribe from the PubSub topic if needed.
|
// try again to check & unsubscribe from the PubSub topic if needed.
|
||||||
if ($count < 1) {
|
if ($count < 1) {
|
||||||
$this->removeChannelFromSet($connection->app->id, $channelName);
|
$promises = [];
|
||||||
$this->unsubscribeFromTopic($connection->app->id, $channelName);
|
|
||||||
|
$promises[] = $this->unsubscribeFromTopic($connection->app->id, $channelName);
|
||||||
|
$promises[] = $this->removeChannelFromSet($connection->app->id, $channelName);
|
||||||
|
|
||||||
|
return all($promises);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
->then(function () use ($connection) {
|
||||||
|
return $this->removeConnectionFromSet($connection);
|
||||||
|
})
|
||||||
|
->then(function () use ($connection, $channelName, $payload) {
|
||||||
|
return parent::unsubscribeFromChannel($connection, $channelName, $payload);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -371,23 +394,21 @@ class RedisChannelManager extends LocalChannelManager
|
||||||
*/
|
*/
|
||||||
public function removeObsoleteConnections(): PromiseInterface
|
public function removeObsoleteConnections(): PromiseInterface
|
||||||
{
|
{
|
||||||
$lock = $this->lock();
|
return $this->lock()->get(function () {
|
||||||
try {
|
return $this->getConnectionsFromSet(0, now()->subMinutes(2)->format('U'))
|
||||||
$lock->get(function () {
|
|
||||||
$this->getConnectionsFromSet(0, now()->subMinutes(2)->format('U'))
|
|
||||||
->then(function ($connections) {
|
->then(function ($connections) {
|
||||||
|
$promises = [];
|
||||||
foreach ($connections as $socketId => $appId) {
|
foreach ($connections as $socketId => $appId) {
|
||||||
$connection = $this->fakeConnectionForApp($appId, $socketId);
|
$connection = $this->fakeConnectionForApp($appId, $socketId);
|
||||||
|
|
||||||
$this->unsubscribeFromAllChannels($connection);
|
$promises[] = $this->unsubscribeFromAllChannels($connection);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
return all($promises);
|
||||||
|
});
|
||||||
|
})->then(function () {
|
||||||
return parent::removeObsoleteConnections();
|
return parent::removeObsoleteConnections();
|
||||||
} finally {
|
});
|
||||||
optional($lock)->forceRelease();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -846,11 +867,11 @@ class RedisChannelManager extends LocalChannelManager
|
||||||
/**
|
/**
|
||||||
* Get a new RedisLock instance to avoid race conditions.
|
* Get a new RedisLock instance to avoid race conditions.
|
||||||
*
|
*
|
||||||
* @return \Illuminate\Cache\CacheLock
|
* @return RedisLock
|
||||||
*/
|
*/
|
||||||
protected function lock()
|
protected function lock()
|
||||||
{
|
{
|
||||||
return new RedisLock($this->redis, static::$lockName, 0);
|
return new RedisLock($this->publishClient, static::$lockName, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,11 @@ use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
||||||
use BeyondCode\LaravelWebSockets\Events\SubscribedToChannel;
|
use BeyondCode\LaravelWebSockets\Events\SubscribedToChannel;
|
||||||
use BeyondCode\LaravelWebSockets\Events\UnsubscribedFromChannel;
|
use BeyondCode\LaravelWebSockets\Events\UnsubscribedFromChannel;
|
||||||
|
use BeyondCode\LaravelWebSockets\Helpers;
|
||||||
use BeyondCode\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
use BeyondCode\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
|
use React\Promise\PromiseInterface;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
|
|
||||||
class Channel
|
class Channel
|
||||||
|
|
@ -116,12 +118,12 @@ class Channel
|
||||||
* Unsubscribe connection from the channel.
|
* Unsubscribe connection from the channel.
|
||||||
*
|
*
|
||||||
* @param \Ratchet\ConnectionInterface $connection
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
* @return bool
|
* @return PromiseInterface
|
||||||
*/
|
*/
|
||||||
public function unsubscribe(ConnectionInterface $connection): bool
|
public function unsubscribe(ConnectionInterface $connection): PromiseInterface
|
||||||
{
|
{
|
||||||
if (! $this->hasConnection($connection)) {
|
if (! $this->hasConnection($connection)) {
|
||||||
return false;
|
return Helpers::createFulfilledPromise(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
unset($this->connections[$connection->socketId]);
|
unset($this->connections[$connection->socketId]);
|
||||||
|
|
@ -132,7 +134,7 @@ class Channel
|
||||||
$this->getName()
|
$this->getName()
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
return Helpers::createFulfilledPromise(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,10 @@ namespace BeyondCode\LaravelWebSockets\Channels;
|
||||||
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
||||||
use BeyondCode\LaravelWebSockets\Events\SubscribedToChannel;
|
use BeyondCode\LaravelWebSockets\Events\SubscribedToChannel;
|
||||||
use BeyondCode\LaravelWebSockets\Events\UnsubscribedFromChannel;
|
use BeyondCode\LaravelWebSockets\Events\UnsubscribedFromChannel;
|
||||||
|
use BeyondCode\LaravelWebSockets\Helpers;
|
||||||
use BeyondCode\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
use BeyondCode\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
|
use React\Promise\PromiseInterface;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
|
|
||||||
class PresenceChannel extends PrivateChannel
|
class PresenceChannel extends PrivateChannel
|
||||||
|
|
@ -100,30 +102,30 @@ class PresenceChannel extends PrivateChannel
|
||||||
* Unsubscribe connection from the channel.
|
* Unsubscribe connection from the channel.
|
||||||
*
|
*
|
||||||
* @param \Ratchet\ConnectionInterface $connection
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
* @return bool
|
* @return PromiseInterface
|
||||||
*/
|
*/
|
||||||
public function unsubscribe(ConnectionInterface $connection): bool
|
public function unsubscribe(ConnectionInterface $connection): PromiseInterface
|
||||||
{
|
{
|
||||||
$truth = parent::unsubscribe($connection);
|
$truth = parent::unsubscribe($connection);
|
||||||
|
|
||||||
$this->channelManager
|
return $this->channelManager
|
||||||
->getChannelMember($connection, $this->getName())
|
->getChannelMember($connection, $this->getName())
|
||||||
->then(function ($user) {
|
->then(function ($user) {
|
||||||
return @json_decode($user);
|
return @json_decode($user);
|
||||||
})
|
})
|
||||||
->then(function ($user) use ($connection) {
|
->then(function ($user) use ($connection) {
|
||||||
if (! $user) {
|
if (! $user) {
|
||||||
return;
|
return Helpers::createFulfilledPromise(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->channelManager
|
return $this->channelManager
|
||||||
->userLeftPresenceChannel($connection, $user, $this->getName())
|
->userLeftPresenceChannel($connection, $user, $this->getName())
|
||||||
->then(function () use ($connection, $user) {
|
->then(function () use ($connection, $user) {
|
||||||
// The `pusher_internal:member_removed` is triggered when a user leaves a channel.
|
// The `pusher_internal:member_removed` is triggered when a user leaves a channel.
|
||||||
// It's quite possible that a user can have multiple connections to the same channel
|
// It's quite possible that a user can have multiple connections to the same channel
|
||||||
// (for example by having multiple browser tabs open)
|
// (for example by having multiple browser tabs open)
|
||||||
// and in this case the events will only be triggered when the last one is closed.
|
// and in this case the events will only be triggered when the last one is closed.
|
||||||
$this->channelManager
|
return $this->channelManager
|
||||||
->getMemberSockets($user->user_id, $connection->app->id, $this->getName())
|
->getMemberSockets($user->user_id, $connection->app->id, $this->getName())
|
||||||
->then(function ($sockets) use ($connection, $user) {
|
->then(function ($sockets) use ($connection, $user) {
|
||||||
if (count($sockets) === 0) {
|
if (count($sockets) === 0) {
|
||||||
|
|
@ -149,8 +151,9 @@ class PresenceChannel extends PrivateChannel
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
|
->then(function () use ($truth) {
|
||||||
return $truth;
|
return $truth;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ use BeyondCode\LaravelWebSockets\ServerFactory;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use React\EventLoop\Factory as LoopFactory;
|
use React\EventLoop\Factory as LoopFactory;
|
||||||
|
use React\EventLoop\LoopInterface;
|
||||||
|
use function React\Promise\all;
|
||||||
|
|
||||||
class StartServer extends Command
|
class StartServer extends Command
|
||||||
{
|
{
|
||||||
|
|
@ -69,6 +71,10 @@ class StartServer extends Command
|
||||||
*/
|
*/
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
|
$this->laravel->singleton(LoopInterface::class, function () {
|
||||||
|
return $this->loop;
|
||||||
|
});
|
||||||
|
|
||||||
$this->configureLoggers();
|
$this->configureLoggers();
|
||||||
|
|
||||||
$this->configureManagers();
|
$this->configureManagers();
|
||||||
|
|
@ -311,9 +317,13 @@ class StartServer extends Command
|
||||||
// be automatically be unsubscribed from all channels.
|
// be automatically be unsubscribed from all channels.
|
||||||
$channelManager->getLocalConnections()
|
$channelManager->getLocalConnections()
|
||||||
->then(function ($connections) {
|
->then(function ($connections) {
|
||||||
foreach ($connections as $connection) {
|
return all(collect($connections)->map(function ($connection) {
|
||||||
|
return app('websockets.handler')
|
||||||
|
->onClose($connection)
|
||||||
|
->then(function () use ($connection) {
|
||||||
$connection->close();
|
$connection->close();
|
||||||
}
|
});
|
||||||
|
})->toArray());
|
||||||
})
|
})
|
||||||
->then(function () {
|
->then(function () {
|
||||||
$this->loop->stop();
|
$this->loop->stop();
|
||||||
|
|
|
||||||
|
|
@ -3,37 +3,46 @@
|
||||||
namespace BeyondCode\LaravelWebSockets\Contracts;
|
namespace BeyondCode\LaravelWebSockets\Contracts;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Apps\App;
|
use BeyondCode\LaravelWebSockets\Apps\App;
|
||||||
|
use React\Promise\PromiseInterface;
|
||||||
|
|
||||||
interface AppManager
|
interface AppManager
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get all apps.
|
* Get all apps.
|
||||||
*
|
*
|
||||||
* @return array[\BeyondCode\LaravelWebSockets\Apps\App]
|
* @return PromiseInterface
|
||||||
*/
|
*/
|
||||||
public function all(): array;
|
public function all(): PromiseInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get app by id.
|
* Get app by id.
|
||||||
*
|
*
|
||||||
* @param string|int $appId
|
* @param string|int $appId
|
||||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
* @return PromiseInterface
|
||||||
*/
|
*/
|
||||||
public function findById($appId): ?App;
|
public function findById($appId): PromiseInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get app by app key.
|
* Get app by app key.
|
||||||
*
|
*
|
||||||
* @param string $appKey
|
* @param string $appKey
|
||||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
* @return PromiseInterface
|
||||||
*/
|
*/
|
||||||
public function findByKey($appKey): ?App;
|
public function findByKey($appKey): PromiseInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get app by secret.
|
* Get app by secret.
|
||||||
*
|
*
|
||||||
* @param string $appSecret
|
* @param string $appSecret
|
||||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
* @return PromiseInterface
|
||||||
*/
|
*/
|
||||||
public function findBySecret($appSecret): ?App;
|
public function findBySecret($appSecret): PromiseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new app.
|
||||||
|
*
|
||||||
|
* @param $appData
|
||||||
|
* @return PromiseInterface
|
||||||
|
*/
|
||||||
|
public function createApp($appData): PromiseInterface;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@ namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Apps\App;
|
use BeyondCode\LaravelWebSockets\Apps\App;
|
||||||
use BeyondCode\LaravelWebSockets\Concerns\PushesToPusher;
|
use BeyondCode\LaravelWebSockets\Concerns\PushesToPusher;
|
||||||
|
use function Clue\React\Block\await;
|
||||||
use Illuminate\Broadcasting\Broadcasters\PusherBroadcaster;
|
use Illuminate\Broadcasting\Broadcasters\PusherBroadcaster;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use React\EventLoop\LoopInterface;
|
||||||
|
|
||||||
class AuthenticateDashboard
|
class AuthenticateDashboard
|
||||||
{
|
{
|
||||||
|
|
@ -21,7 +23,7 @@ class AuthenticateDashboard
|
||||||
*/
|
*/
|
||||||
public function __invoke(Request $request)
|
public function __invoke(Request $request)
|
||||||
{
|
{
|
||||||
$app = App::findById($request->header('X-App-Id'));
|
$app = await(App::findById($request->header('X-App-Id')), app(LoopInterface::class));
|
||||||
|
|
||||||
$broadcaster = $this->getPusherBroadcaster([
|
$broadcaster = $this->getPusherBroadcaster([
|
||||||
'key' => $app->key,
|
'key' => $app->key,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||||
|
use function Clue\React\Block\await;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use React\EventLoop\LoopInterface;
|
||||||
|
|
||||||
|
class ShowApps
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Show the configured apps.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \BeyondCode\LaravelWebSockets\Contracts\AppManager $apps
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __invoke(Request $request, AppManager $apps)
|
||||||
|
{
|
||||||
|
return view('websockets::apps', [
|
||||||
|
'apps' => await($apps->all(), app(LoopInterface::class), 2.0),
|
||||||
|
'port' => config('websockets.dashboard.port', 6001),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,9 @@ namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||||
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
||||||
|
use function Clue\React\Block\await;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use React\EventLoop\LoopInterface;
|
||||||
|
|
||||||
class ShowDashboard
|
class ShowDashboard
|
||||||
{
|
{
|
||||||
|
|
@ -18,7 +20,7 @@ class ShowDashboard
|
||||||
public function __invoke(Request $request, AppManager $apps)
|
public function __invoke(Request $request, AppManager $apps)
|
||||||
{
|
{
|
||||||
return view('websockets::dashboard', [
|
return view('websockets::dashboard', [
|
||||||
'apps' => $apps->all(),
|
'apps' => await($apps->all(), app(LoopInterface::class), 2.0),
|
||||||
'port' => config('websockets.dashboard.port', 6001),
|
'port' => config('websockets.dashboard.port', 6001),
|
||||||
'channels' => DashboardLogger::$channels,
|
'channels' => DashboardLogger::$channels,
|
||||||
'logPrefix' => DashboardLogger::LOG_CHANNEL_PREFIX,
|
'logPrefix' => DashboardLogger::LOG_CHANNEL_PREFIX,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||||
|
use BeyondCode\LaravelWebSockets\Dashboard\Http\Requests\StoreAppRequest;
|
||||||
|
use function Clue\React\Block\await;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use React\EventLoop\LoopInterface;
|
||||||
|
|
||||||
|
class StoreApp
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Show the configured apps.
|
||||||
|
*
|
||||||
|
* @param StoreAppRequest $request
|
||||||
|
* @param \BeyondCode\LaravelWebSockets\Contracts\AppManager $apps
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __invoke(StoreAppRequest $request, AppManager $apps)
|
||||||
|
{
|
||||||
|
$appData = [
|
||||||
|
'id' => (string) Str::uuid(),
|
||||||
|
'key' => (string) Str::uuid(),
|
||||||
|
'secret' => (string) Str::uuid(),
|
||||||
|
'name' => $request->get('name'),
|
||||||
|
'enable_client_messages' => $request->has('enable_client_messages'),
|
||||||
|
'enable_statistics' => $request->has('enable_statistics'),
|
||||||
|
'allowed_origins' => $request->get('allowed_origins'),
|
||||||
|
];
|
||||||
|
|
||||||
|
await($apps->createApp($appData), app(LoopInterface::class));
|
||||||
|
|
||||||
|
return redirect()->route('laravel-websockets.apps');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StoreAppRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authenticate()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => 'required',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,9 @@
|
||||||
namespace BeyondCode\LaravelWebSockets\Rules;
|
namespace BeyondCode\LaravelWebSockets\Rules;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||||
|
use function Clue\React\Block\await;
|
||||||
use Illuminate\Contracts\Validation\Rule;
|
use Illuminate\Contracts\Validation\Rule;
|
||||||
|
use React\EventLoop\Factory;
|
||||||
|
|
||||||
class AppId implements Rule
|
class AppId implements Rule
|
||||||
{
|
{
|
||||||
|
|
@ -18,7 +20,7 @@ class AppId implements Rule
|
||||||
{
|
{
|
||||||
$manager = app(AppManager::class);
|
$manager = app(AppManager::class);
|
||||||
|
|
||||||
return $manager->findById($value) ? true : false;
|
return await($manager->findById($value), Factory::create()) ? true : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ class Router
|
||||||
*/
|
*/
|
||||||
public function registerRoutes()
|
public function registerRoutes()
|
||||||
{
|
{
|
||||||
$this->get('/app/{appKey}', config('websockets.handlers.websocket'));
|
$this->get('/app/{appKey}', 'websockets.handler');
|
||||||
$this->post('/apps/{appId}/events', config('websockets.handlers.trigger_event'));
|
$this->post('/apps/{appId}/events', config('websockets.handlers.trigger_event'));
|
||||||
$this->get('/apps/{appId}/channels', config('websockets.handlers.fetch_channels'));
|
$this->get('/apps/{appId}/channels', config('websockets.handlers.fetch_channels'));
|
||||||
$this->get('/apps/{appId}/channels/{channelName}', config('websockets.handlers.fetch_channel'));
|
$this->get('/apps/{appId}/channels/{channelName}', config('websockets.handlers.fetch_channel'));
|
||||||
|
|
@ -191,9 +191,10 @@ class Router
|
||||||
*/
|
*/
|
||||||
protected function getRoute(string $method, string $uri, $action): Route
|
protected function getRoute(string $method, string $uri, $action): Route
|
||||||
{
|
{
|
||||||
|
$action = app($action);
|
||||||
$action = is_subclass_of($action, MessageComponentInterface::class)
|
$action = is_subclass_of($action, MessageComponentInterface::class)
|
||||||
? $this->createWebSocketsServer($action)
|
? $this->createWebSocketsServer($action)
|
||||||
: app($action);
|
: $action;
|
||||||
|
|
||||||
return new Route($uri, ['_controller' => $action], [], [], null, [], [$method]);
|
return new Route($uri, ['_controller' => $action], [], [], null, [], [$method]);
|
||||||
}
|
}
|
||||||
|
|
@ -201,13 +202,11 @@ class Router
|
||||||
/**
|
/**
|
||||||
* Create a new websockets server to handle the action.
|
* Create a new websockets server to handle the action.
|
||||||
*
|
*
|
||||||
* @param string $action
|
* @param MessageComponentInterface $app
|
||||||
* @return \Ratchet\WebSocket\WsServer
|
* @return \Ratchet\WebSocket\WsServer
|
||||||
*/
|
*/
|
||||||
protected function createWebSocketsServer(string $action): WsServer
|
protected function createWebSocketsServer($app): WsServer
|
||||||
{
|
{
|
||||||
$app = app($action);
|
|
||||||
|
|
||||||
if (WebsocketsLogger::isEnabled()) {
|
if (WebsocketsLogger::isEnabled()) {
|
||||||
$app = WebsocketsLogger::decorate($app);
|
$app = WebsocketsLogger::decorate($app);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,14 @@ use BeyondCode\LaravelWebSockets\Events\ConnectionClosed;
|
||||||
use BeyondCode\LaravelWebSockets\Events\NewConnection;
|
use BeyondCode\LaravelWebSockets\Events\NewConnection;
|
||||||
use BeyondCode\LaravelWebSockets\Events\WebSocketMessageReceived;
|
use BeyondCode\LaravelWebSockets\Events\WebSocketMessageReceived;
|
||||||
use BeyondCode\LaravelWebSockets\Facades\StatisticsCollector;
|
use BeyondCode\LaravelWebSockets\Facades\StatisticsCollector;
|
||||||
|
use BeyondCode\LaravelWebSockets\Helpers;
|
||||||
|
use BeyondCode\LaravelWebSockets\Server\Exceptions\WebSocketException;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
use Ratchet\RFC6455\Messaging\MessageInterface;
|
use Ratchet\RFC6455\Messaging\MessageInterface;
|
||||||
use Ratchet\WebSocket\MessageComponentInterface;
|
use Ratchet\WebSocket\MessageComponentInterface;
|
||||||
|
use React\Promise\Deferred;
|
||||||
|
use React\Promise\PromiseInterface;
|
||||||
|
|
||||||
class WebSocketHandler implements MessageComponentInterface
|
class WebSocketHandler implements MessageComponentInterface
|
||||||
{
|
{
|
||||||
|
|
@ -47,7 +51,9 @@ class WebSocketHandler implements MessageComponentInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->verifyAppKey($connection)
|
$this->verifyAppKey($connection)
|
||||||
->verifyOrigin($connection)
|
->then(function () use ($connection) {
|
||||||
|
try {
|
||||||
|
$this->verifyOrigin($connection)
|
||||||
->limitConcurrentConnections($connection)
|
->limitConcurrentConnections($connection)
|
||||||
->generateSocketId($connection)
|
->generateSocketId($connection)
|
||||||
->establishConnection($connection);
|
->establishConnection($connection);
|
||||||
|
|
@ -71,6 +77,12 @@ class WebSocketHandler implements MessageComponentInterface
|
||||||
|
|
||||||
NewConnection::dispatch($connection->app->id, $connection->socketId);
|
NewConnection::dispatch($connection->app->id, $connection->socketId);
|
||||||
}
|
}
|
||||||
|
} catch (WebSocketException $exception) {
|
||||||
|
$this->onError($connection, $exception);
|
||||||
|
}
|
||||||
|
}, function ($exception) use ($connection) {
|
||||||
|
$this->onError($connection, $exception);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -105,11 +117,11 @@ class WebSocketHandler implements MessageComponentInterface
|
||||||
* Handle the websocket close.
|
* Handle the websocket close.
|
||||||
*
|
*
|
||||||
* @param \Ratchet\ConnectionInterface $connection
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
* @return void
|
* @return PromiseInterface
|
||||||
*/
|
*/
|
||||||
public function onClose(ConnectionInterface $connection)
|
public function onClose(ConnectionInterface $connection)
|
||||||
{
|
{
|
||||||
$this->channelManager
|
return $this->channelManager
|
||||||
->unsubscribeFromAllChannels($connection)
|
->unsubscribeFromAllChannels($connection)
|
||||||
->then(function (bool $unsubscribed) use ($connection) {
|
->then(function (bool $unsubscribed) use ($connection) {
|
||||||
if (isset($connection->app)) {
|
if (isset($connection->app)) {
|
||||||
|
|
@ -117,8 +129,13 @@ class WebSocketHandler implements MessageComponentInterface
|
||||||
StatisticsCollector::disconnection($connection->app->id);
|
StatisticsCollector::disconnection($connection->app->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->channelManager->unsubscribeFromApp($connection->app->id);
|
return $this->channelManager->unsubscribeFromApp($connection->app->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Helpers::createFulfilledPromise(true);
|
||||||
|
})
|
||||||
|
->then(function () use ($connection) {
|
||||||
|
if (isset($connection->app)) {
|
||||||
DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_DISCONNECTED, [
|
DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_DISCONNECTED, [
|
||||||
'socketId' => $connection->socketId,
|
'socketId' => $connection->socketId,
|
||||||
]);
|
]);
|
||||||
|
|
@ -160,21 +177,28 @@ class WebSocketHandler implements MessageComponentInterface
|
||||||
* Verify the app key validity.
|
* Verify the app key validity.
|
||||||
*
|
*
|
||||||
* @param \Ratchet\ConnectionInterface $connection
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
* @return $this
|
* @return PromiseInterface
|
||||||
*/
|
*/
|
||||||
protected function verifyAppKey(ConnectionInterface $connection)
|
protected function verifyAppKey(ConnectionInterface $connection): PromiseInterface
|
||||||
{
|
{
|
||||||
|
$deferred = new Deferred();
|
||||||
|
|
||||||
$query = QueryParameters::create($connection->httpRequest);
|
$query = QueryParameters::create($connection->httpRequest);
|
||||||
|
|
||||||
$appKey = $query->get('appKey');
|
$appKey = $query->get('appKey');
|
||||||
|
|
||||||
if (! $app = App::findByKey($appKey)) {
|
App::findByKey($appKey)
|
||||||
throw new Exceptions\UnknownAppKey($appKey);
|
->then(function ($app) use ($appKey, $connection, $deferred) {
|
||||||
|
if (! $app) {
|
||||||
|
$deferred->reject(new Exceptions\UnknownAppKey($appKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
$connection->app = $app;
|
$connection->app = $app;
|
||||||
|
|
||||||
return $this;
|
$deferred->resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
return $deferred->promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -92,14 +92,15 @@ class MemoryCollector implements StatisticsCollector
|
||||||
{
|
{
|
||||||
$this->getStatistics()->then(function ($statistics) {
|
$this->getStatistics()->then(function ($statistics) {
|
||||||
foreach ($statistics as $appId => $statistic) {
|
foreach ($statistics as $appId => $statistic) {
|
||||||
if (! $statistic->isEnabled()) {
|
$statistic->isEnabled()->then(function ($isEnabled) use ($appId, $statistic) {
|
||||||
continue;
|
if (! $isEnabled) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($statistic->shouldHaveTracesRemoved()) {
|
if ($statistic->shouldHaveTracesRemoved()) {
|
||||||
$this->resetAppTraces($appId);
|
$this->resetAppTraces($appId);
|
||||||
|
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->createRecord($statistic, $appId);
|
$this->createRecord($statistic, $appId);
|
||||||
|
|
@ -111,6 +112,7 @@ class MemoryCollector implements StatisticsCollector
|
||||||
is_null($connections) ? 0 : $connections
|
is_null($connections) ? 0 : $connections
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
namespace BeyondCode\LaravelWebSockets\Statistics;
|
namespace BeyondCode\LaravelWebSockets\Statistics;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Apps\App;
|
use BeyondCode\LaravelWebSockets\Apps\App;
|
||||||
|
use React\Promise\Deferred;
|
||||||
|
use React\Promise\PromiseInterface;
|
||||||
|
|
||||||
class Statistic
|
class Statistic
|
||||||
{
|
{
|
||||||
|
|
@ -118,11 +120,17 @@ class Statistic
|
||||||
/**
|
/**
|
||||||
* Check if the app has statistics enabled.
|
* Check if the app has statistics enabled.
|
||||||
*
|
*
|
||||||
* @return bool
|
* @return PromiseInterface
|
||||||
*/
|
*/
|
||||||
public function isEnabled(): bool
|
public function isEnabled(): PromiseInterface
|
||||||
{
|
{
|
||||||
return App::findById($this->appId)->statisticsEnabled;
|
$deferred = new Deferred();
|
||||||
|
|
||||||
|
App::findById($this->appId)->then(function ($app) use ($deferred) {
|
||||||
|
$deferred->resolve($app->statisticsEnabled);
|
||||||
|
});
|
||||||
|
|
||||||
|
return $deferred->promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,25 @@ use BeyondCode\LaravelWebSockets\Contracts\StatisticsCollector;
|
||||||
use BeyondCode\LaravelWebSockets\Contracts\StatisticsStore;
|
use BeyondCode\LaravelWebSockets\Contracts\StatisticsStore;
|
||||||
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\AuthenticateDashboard;
|
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\AuthenticateDashboard;
|
||||||
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\SendMessage;
|
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\SendMessage;
|
||||||
|
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\ShowApps;
|
||||||
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\ShowDashboard;
|
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\ShowDashboard;
|
||||||
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\ShowStatistics;
|
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\ShowStatistics;
|
||||||
|
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\StoreApp;
|
||||||
use BeyondCode\LaravelWebSockets\Dashboard\Http\Middleware\Authorize as AuthorizeDashboard;
|
use BeyondCode\LaravelWebSockets\Dashboard\Http\Middleware\Authorize as AuthorizeDashboard;
|
||||||
use BeyondCode\LaravelWebSockets\Queue\AsyncRedisConnector;
|
use BeyondCode\LaravelWebSockets\Queue\AsyncRedisConnector;
|
||||||
use BeyondCode\LaravelWebSockets\Server\Router;
|
use BeyondCode\LaravelWebSockets\Server\Router;
|
||||||
|
use Clue\React\SQLite\DatabaseInterface;
|
||||||
|
use Clue\React\SQLite\Factory as SQLiteFactory;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Illuminate\Support\Facades\Queue;
|
use Illuminate\Support\Facades\Queue;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use React\EventLoop\Factory;
|
||||||
|
use React\EventLoop\LoopInterface;
|
||||||
|
use React\MySQL\ConnectionInterface;
|
||||||
|
use React\MySQL\Factory as MySQLFactory;
|
||||||
|
use SplFileInfo;
|
||||||
|
use Symfony\Component\Finder\Finder;
|
||||||
|
|
||||||
class WebSocketsServiceProvider extends ServiceProvider
|
class WebSocketsServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
|
|
@ -38,8 +48,16 @@ class WebSocketsServiceProvider extends ServiceProvider
|
||||||
__DIR__.'/../database/migrations/0000_00_00_000000_rename_statistics_counters.php' => database_path('migrations/0000_00_00_000000_rename_statistics_counters.php'),
|
__DIR__.'/../database/migrations/0000_00_00_000000_rename_statistics_counters.php' => database_path('migrations/0000_00_00_000000_rename_statistics_counters.php'),
|
||||||
], 'migrations');
|
], 'migrations');
|
||||||
|
|
||||||
|
$this->registerEventLoop();
|
||||||
|
|
||||||
|
$this->registerSQLiteDatabase();
|
||||||
|
|
||||||
|
$this->registerMySqlDatabase();
|
||||||
|
|
||||||
$this->registerAsyncRedisQueueDriver();
|
$this->registerAsyncRedisQueueDriver();
|
||||||
|
|
||||||
|
$this->registerWebSocketHandler();
|
||||||
|
|
||||||
$this->registerRouter();
|
$this->registerRouter();
|
||||||
|
|
||||||
$this->registerManagers();
|
$this->registerManagers();
|
||||||
|
|
@ -61,6 +79,13 @@ class WebSocketsServiceProvider extends ServiceProvider
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function registerEventLoop()
|
||||||
|
{
|
||||||
|
$this->app->singleton(LoopInterface::class, function () {
|
||||||
|
return Factory::create();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register the async, non-blocking Redis queue driver.
|
* Register the async, non-blocking Redis queue driver.
|
||||||
*
|
*
|
||||||
|
|
@ -73,6 +98,47 @@ class WebSocketsServiceProvider extends ServiceProvider
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function registerSQLiteDatabase()
|
||||||
|
{
|
||||||
|
$this->app->singleton(DatabaseInterface::class, function () {
|
||||||
|
$factory = new SQLiteFactory($this->app->make(LoopInterface::class));
|
||||||
|
|
||||||
|
$database = $factory->openLazy(
|
||||||
|
config('websockets.managers.sqlite.database', ':memory:')
|
||||||
|
);
|
||||||
|
|
||||||
|
$migrations = (new Finder())
|
||||||
|
->files()
|
||||||
|
->ignoreDotFiles(true)
|
||||||
|
->in(__DIR__.'/../database/migrations/sqlite')
|
||||||
|
->name('*.sql');
|
||||||
|
|
||||||
|
/** @var SplFileInfo $migration */
|
||||||
|
foreach ($migrations as $migration) {
|
||||||
|
$database->exec($migration->getContents());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $database;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function registerMySqlDatabase()
|
||||||
|
{
|
||||||
|
$this->app->singleton(ConnectionInterface::class, function () {
|
||||||
|
$factory = new MySQLFactory($this->app->make(LoopInterface::class));
|
||||||
|
|
||||||
|
$connectionKey = 'database.connections.'.config('websockets.managers.mysql.connection');
|
||||||
|
|
||||||
|
$auth = trim(config($connectionKey.'.username').':'.config($connectionKey.'.password'), ':');
|
||||||
|
$connection = trim(config($connectionKey.'.host').':'.config($connectionKey.'.port'), ':');
|
||||||
|
$database = config($connectionKey.'.database');
|
||||||
|
|
||||||
|
$database = $factory->createLazyConnection(trim("{$auth}@{$connection}/{$database}", '@'));
|
||||||
|
|
||||||
|
return $database;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register the statistics-related contracts.
|
* Register the statistics-related contracts.
|
||||||
*
|
*
|
||||||
|
|
@ -98,7 +164,7 @@ class WebSocketsServiceProvider extends ServiceProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Regsiter the dashboard components.
|
* Register the dashboard components.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
|
|
@ -165,6 +231,8 @@ class WebSocketsServiceProvider extends ServiceProvider
|
||||||
'middleware' => config('websockets.dashboard.middleware', [AuthorizeDashboard::class]),
|
'middleware' => config('websockets.dashboard.middleware', [AuthorizeDashboard::class]),
|
||||||
], function () {
|
], function () {
|
||||||
Route::get('/', ShowDashboard::class)->name('dashboard');
|
Route::get('/', ShowDashboard::class)->name('dashboard');
|
||||||
|
Route::get('/apps', ShowApps::class)->name('apps');
|
||||||
|
Route::post('/apps', StoreApp::class)->name('apps.store');
|
||||||
Route::get('/api/{appId}/statistics', ShowStatistics::class)->name('statistics');
|
Route::get('/api/{appId}/statistics', ShowStatistics::class)->name('statistics');
|
||||||
Route::post('/auth', AuthenticateDashboard::class)->name('auth');
|
Route::post('/auth', AuthenticateDashboard::class)->name('auth');
|
||||||
Route::post('/event', SendMessage::class)->name('event');
|
Route::post('/event', SendMessage::class)->name('event');
|
||||||
|
|
@ -182,4 +250,11 @@ class WebSocketsServiceProvider extends ServiceProvider
|
||||||
return $this->app->environment('local');
|
return $this->app->environment('local');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function registerWebSocketHandler()
|
||||||
|
{
|
||||||
|
$this->app->singleton('websockets.handler', function () {
|
||||||
|
return app(config('websockets.handlers.websocket'));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Test\Apps;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Apps\App;
|
||||||
|
use BeyondCode\LaravelWebSockets\Apps\ConfigAppManager;
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||||
|
use BeyondCode\LaravelWebSockets\Test\TestCase;
|
||||||
|
|
||||||
|
class ConfigAppManagerTest extends TestCase
|
||||||
|
{
|
||||||
|
/** @var AppManager */
|
||||||
|
protected $apps;
|
||||||
|
|
||||||
|
public function getEnvironmentSetUp($app)
|
||||||
|
{
|
||||||
|
parent::getEnvironmentSetUp($app);
|
||||||
|
|
||||||
|
$app['config']->set('websockets.managers.app', ConfigAppManager::class);
|
||||||
|
$app['config']->set('websockets.apps', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->apps = app()->make(AppManager::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_can_return_all_apps()
|
||||||
|
{
|
||||||
|
$apps = $this->await($this->apps->all());
|
||||||
|
$this->assertCount(0, $apps);
|
||||||
|
|
||||||
|
$this->await($this->apps->createApp([
|
||||||
|
'id' => 1,
|
||||||
|
'key' => 'test',
|
||||||
|
'secret' => 'secret',
|
||||||
|
'name' => 'Test',
|
||||||
|
'enable_client_messages' => true,
|
||||||
|
'enable_statistics' => false,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$apps = $this->await($this->apps->all());
|
||||||
|
$this->assertCount(1, $apps);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_can_find_apps_by_id()
|
||||||
|
{
|
||||||
|
$this->await($this->apps->createApp([
|
||||||
|
'id' => 1,
|
||||||
|
'key' => 'test',
|
||||||
|
'secret' => 'secret',
|
||||||
|
'name' => 'Test',
|
||||||
|
'enable_client_messages' => true,
|
||||||
|
'enable_statistics' => false,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$app = $this->await($this->apps->findById(1));
|
||||||
|
|
||||||
|
$this->assertInstanceOf(App::class, $app);
|
||||||
|
$this->assertSame('test', $app->key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_can_find_apps_by_key()
|
||||||
|
{
|
||||||
|
$this->await($this->apps->createApp([
|
||||||
|
'id' => 1,
|
||||||
|
'key' => 'key',
|
||||||
|
'secret' => 'secret',
|
||||||
|
'name' => 'Test',
|
||||||
|
'enable_client_messages' => true,
|
||||||
|
'enable_statistics' => false,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$app = $this->await($this->apps->findByKey('key'));
|
||||||
|
|
||||||
|
$this->assertInstanceOf(App::class, $app);
|
||||||
|
$this->assertSame('key', $app->key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_can_find_apps_by_secret()
|
||||||
|
{
|
||||||
|
$this->await($this->apps->createApp([
|
||||||
|
'id' => 1,
|
||||||
|
'key' => 'key',
|
||||||
|
'secret' => 'secret',
|
||||||
|
'name' => 'Test',
|
||||||
|
'enable_client_messages' => true,
|
||||||
|
'enable_statistics' => false,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$app = $this->await($this->apps->findBySecret('secret'));
|
||||||
|
|
||||||
|
$this->assertInstanceOf(App::class, $app);
|
||||||
|
$this->assertSame('key', $app->key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Test\Apps;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Apps\App;
|
||||||
|
use BeyondCode\LaravelWebSockets\Apps\MysqlAppManager;
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||||
|
use BeyondCode\LaravelWebSockets\Test\TestCase;
|
||||||
|
|
||||||
|
class MysqlAppManagerTest extends TestCase
|
||||||
|
{
|
||||||
|
/** @var AppManager */
|
||||||
|
protected $apps;
|
||||||
|
|
||||||
|
public function getEnvironmentSetUp($app)
|
||||||
|
{
|
||||||
|
parent::getEnvironmentSetUp($app);
|
||||||
|
|
||||||
|
$app['config']->set('websockets.managers.app', MysqlAppManager::class);
|
||||||
|
$app['config']->set('database.connections.mysql.database', 'websockets_test');
|
||||||
|
$app['config']->set('database.connections.mysql.username', 'root');
|
||||||
|
$app['config']->set('database.connections.mysql.password', 'password');
|
||||||
|
|
||||||
|
$app['config']->set('websockets.managers.mysql.table', 'websockets_apps');
|
||||||
|
$app['config']->set('websockets.managers.mysql.connection', 'mysql');
|
||||||
|
$app['config']->set('database.connections.default', 'mysql');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->artisan('migrate:fresh', [
|
||||||
|
'--database' => 'mysql',
|
||||||
|
'--realpath' => true,
|
||||||
|
'--path' => __DIR__.'/../../database/migrations/',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->apps = app()->make(AppManager::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_can_return_all_apps()
|
||||||
|
{
|
||||||
|
$apps = $this->await($this->apps->all());
|
||||||
|
$this->assertCount(0, $apps);
|
||||||
|
|
||||||
|
$this->await($this->apps->createApp([
|
||||||
|
'id' => 1,
|
||||||
|
'key' => 'test',
|
||||||
|
'secret' => 'secret',
|
||||||
|
'name' => 'Test',
|
||||||
|
'enable_client_messages' => true,
|
||||||
|
'enable_statistics' => false,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$apps = $this->await($this->apps->all());
|
||||||
|
$this->assertCount(1, $apps);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_can_find_apps_by_id()
|
||||||
|
{
|
||||||
|
$this->await($this->apps->createApp([
|
||||||
|
'id' => 1,
|
||||||
|
'key' => 'test',
|
||||||
|
'secret' => 'secret',
|
||||||
|
'name' => 'Test',
|
||||||
|
'enable_client_messages' => true,
|
||||||
|
'enable_statistics' => false,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$app = $this->await($this->apps->findById(1));
|
||||||
|
|
||||||
|
$this->assertInstanceOf(App::class, $app);
|
||||||
|
$this->assertSame('test', $app->key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_can_find_apps_by_key()
|
||||||
|
{
|
||||||
|
$this->await($this->apps->createApp([
|
||||||
|
'id' => 1,
|
||||||
|
'key' => 'key',
|
||||||
|
'secret' => 'secret',
|
||||||
|
'name' => 'Test',
|
||||||
|
'enable_client_messages' => true,
|
||||||
|
'enable_statistics' => false,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$app = $this->await($this->apps->findByKey('key'));
|
||||||
|
|
||||||
|
$this->assertInstanceOf(App::class, $app);
|
||||||
|
$this->assertSame('key', $app->key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_can_find_apps_by_secret()
|
||||||
|
{
|
||||||
|
$this->await($this->apps->createApp([
|
||||||
|
'id' => 1,
|
||||||
|
'key' => 'key',
|
||||||
|
'secret' => 'secret',
|
||||||
|
'name' => 'Test',
|
||||||
|
'enable_client_messages' => true,
|
||||||
|
'enable_statistics' => false,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$app = $this->await($this->apps->findBySecret('secret'));
|
||||||
|
|
||||||
|
$this->assertInstanceOf(App::class, $app);
|
||||||
|
$this->assertSame('key', $app->key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Test\Apps;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Apps\App;
|
||||||
|
use BeyondCode\LaravelWebSockets\Apps\SQLiteAppManager;
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||||
|
use BeyondCode\LaravelWebSockets\Test\TestCase;
|
||||||
|
|
||||||
|
class SqliteAppManagerTest extends TestCase
|
||||||
|
{
|
||||||
|
/** @var AppManager */
|
||||||
|
protected $apps;
|
||||||
|
|
||||||
|
public function getEnvironmentSetUp($app)
|
||||||
|
{
|
||||||
|
parent::getEnvironmentSetUp($app);
|
||||||
|
|
||||||
|
$app['config']->set('websockets.managers.app', SQLiteAppManager::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->apps = app()->make(AppManager::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_can_return_all_apps()
|
||||||
|
{
|
||||||
|
$apps = $this->await($this->apps->all());
|
||||||
|
$this->assertCount(0, $apps);
|
||||||
|
|
||||||
|
$this->await($this->apps->createApp([
|
||||||
|
'id' => 1,
|
||||||
|
'key' => 'test',
|
||||||
|
'secret' => 'secret',
|
||||||
|
'name' => 'Test',
|
||||||
|
'enable_client_messages' => true,
|
||||||
|
'enable_statistics' => false,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$apps = $this->await($this->apps->all());
|
||||||
|
$this->assertCount(1, $apps);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_can_find_apps_by_id()
|
||||||
|
{
|
||||||
|
$this->await($this->apps->createApp([
|
||||||
|
'id' => 1,
|
||||||
|
'key' => 'test',
|
||||||
|
'secret' => 'secret',
|
||||||
|
'name' => 'Test',
|
||||||
|
'enable_client_messages' => true,
|
||||||
|
'enable_statistics' => false,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$app = $this->await($this->apps->findById(1));
|
||||||
|
|
||||||
|
$this->assertInstanceOf(App::class, $app);
|
||||||
|
$this->assertSame('test', $app->key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_can_find_apps_by_key()
|
||||||
|
{
|
||||||
|
$this->await($this->apps->createApp([
|
||||||
|
'id' => 1,
|
||||||
|
'key' => 'key',
|
||||||
|
'secret' => 'secret',
|
||||||
|
'name' => 'Test',
|
||||||
|
'enable_client_messages' => true,
|
||||||
|
'enable_statistics' => false,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$app = $this->await($this->apps->findByKey('key'));
|
||||||
|
|
||||||
|
$this->assertInstanceOf(App::class, $app);
|
||||||
|
$this->assertSame('key', $app->key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_can_find_apps_by_secret()
|
||||||
|
{
|
||||||
|
$this->await($this->apps->createApp([
|
||||||
|
'id' => 1,
|
||||||
|
'key' => 'key',
|
||||||
|
'secret' => 'secret',
|
||||||
|
'name' => 'Test',
|
||||||
|
'enable_client_messages' => true,
|
||||||
|
'enable_statistics' => false,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$app = $this->await($this->apps->findBySecret('secret'));
|
||||||
|
|
||||||
|
$this->assertInstanceOf(App::class, $app);
|
||||||
|
$this->assertSame('key', $app->key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,43 +2,43 @@
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Test;
|
namespace BeyondCode\LaravelWebSockets\Test;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Server\Exceptions\OriginNotAllowed;
|
|
||||||
use BeyondCode\LaravelWebSockets\Server\Exceptions\UnknownAppKey;
|
use BeyondCode\LaravelWebSockets\Server\Exceptions\UnknownAppKey;
|
||||||
|
|
||||||
class ConnectionTest extends TestCase
|
class ConnectionTest extends TestCase
|
||||||
{
|
{
|
||||||
public function test_cannot_connect_with_a_wrong_app_key()
|
public function test_cannot_connect_with_a_wrong_app_key()
|
||||||
{
|
{
|
||||||
$this->expectException(UnknownAppKey::class);
|
$this->startServer();
|
||||||
|
|
||||||
$this->newActiveConnection(['public-channel'], 'NonWorkingKey');
|
$response = $this->await($this->joinWebSocketServer(['public-channel'], 'NonWorkingKey'));
|
||||||
|
$this->assertSame('{"event":"pusher:error","data":{"message":"Could not find app key `NonWorkingKey`.","code":4001}}', (string) $response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_unconnected_app_cannot_store_statistics()
|
public function test_unconnected_app_cannot_store_statistics()
|
||||||
{
|
{
|
||||||
$this->expectException(UnknownAppKey::class);
|
$this->startServer();
|
||||||
|
|
||||||
$this->newActiveConnection(['public-channel'], 'NonWorkingKey');
|
$response = $this->await($this->joinWebSocketServer(['public-channel'], 'NonWorkingKey'));
|
||||||
|
$this->assertSame('{"event":"pusher:error","data":{"message":"Could not find app key `NonWorkingKey`.","code":4001}}', (string) $response);
|
||||||
|
|
||||||
$this->assertCount(0, $this->statisticsCollector->getStatistics());
|
$count = $this->await($this->statisticsCollector->getStatistics());
|
||||||
|
$this->assertCount(0, $count);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_origin_validation_should_fail_for_no_origin()
|
public function test_origin_validation_should_fail_for_no_origin()
|
||||||
{
|
{
|
||||||
$this->expectException(OriginNotAllowed::class);
|
$this->startServer();
|
||||||
|
|
||||||
$connection = $this->newConnection('TestOrigin');
|
$response = $this->await($this->joinWebSocketServer(['public-channel'], 'TestOrigin'));
|
||||||
|
$this->assertSame('{"event":"pusher:error","data":{"message":"The origin is not allowed for `TestOrigin`.","code":4009}}', (string) $response);
|
||||||
$this->pusherServer->onOpen($connection);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_origin_validation_should_fail_for_wrong_origin()
|
public function test_origin_validation_should_fail_for_wrong_origin()
|
||||||
{
|
{
|
||||||
$this->expectException(OriginNotAllowed::class);
|
$this->startServer();
|
||||||
|
|
||||||
$connection = $this->newConnection('TestOrigin', ['Origin' => 'https://google.ro']);
|
$response = $this->await($this->joinWebSocketServer(['public-channel'], 'TestOrigin', ['Origin' => 'https://google.ro']));
|
||||||
|
$this->assertSame('{"event":"pusher:error","data":{"message":"The origin is not allowed for `TestOrigin`.","code":4009}}', (string) $response);
|
||||||
$this->pusherServer->onOpen($connection);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_origin_validation_should_pass_for_the_right_origin()
|
public function test_origin_validation_should_pass_for_the_right_origin()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Test\Dashboard;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Apps\SQLiteAppManager;
|
||||||
|
use BeyondCode\LaravelWebSockets\Test\Models\User;
|
||||||
|
use BeyondCode\LaravelWebSockets\Test\TestCase;
|
||||||
|
|
||||||
|
class AppsTest extends TestCase
|
||||||
|
{
|
||||||
|
public function getEnvironmentSetUp($app)
|
||||||
|
{
|
||||||
|
parent::getEnvironmentSetUp($app);
|
||||||
|
|
||||||
|
$app['config']->set('websockets.managers.app', SQLiteAppManager::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_can_list_all_apps()
|
||||||
|
{
|
||||||
|
$this->actingAs(factory(User::class)->create())
|
||||||
|
->get(route('laravel-websockets.apps'))
|
||||||
|
->assertViewHas('apps', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_can_create_app()
|
||||||
|
{
|
||||||
|
$this->actingAs(factory(User::class)->create())
|
||||||
|
->post(route('laravel-websockets.apps.store', [
|
||||||
|
'name' => 'New App',
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->actingAs(factory(User::class)->create())
|
||||||
|
->get(route('laravel-websockets.apps'))
|
||||||
|
->assertViewHas('apps', function ($apps) {
|
||||||
|
return count($apps) === 1 && $apps[0]['name'] === 'New App';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,7 +17,6 @@ class DashboardTest extends TestCase
|
||||||
{
|
{
|
||||||
$this->actingAs(factory(User::class)->create())
|
$this->actingAs(factory(User::class)->create())
|
||||||
->get(route('laravel-websockets.dashboard'))
|
->get(route('laravel-websockets.dashboard'))
|
||||||
->assertResponseOk()
|
->assertResponseOk();
|
||||||
->see('WebSockets Dashboard');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,33 +5,26 @@ namespace BeyondCode\LaravelWebSockets\Test;
|
||||||
use BeyondCode\LaravelWebSockets\API\FetchChannel;
|
use BeyondCode\LaravelWebSockets\API\FetchChannel;
|
||||||
use GuzzleHttp\Psr7\Request;
|
use GuzzleHttp\Psr7\Request;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
use Pusher\Pusher;
|
||||||
|
|
||||||
class FetchChannelTest extends TestCase
|
class FetchChannelTest extends TestCase
|
||||||
{
|
{
|
||||||
public function test_invalid_signatures_can_not_access_the_api()
|
public function test_invalid_signatures_can_not_access_the_api()
|
||||||
{
|
{
|
||||||
$this->expectException(HttpException::class);
|
$this->startServer();
|
||||||
$this->expectExceptionMessage('Invalid auth signature provided.');
|
|
||||||
|
|
||||||
$connection = new Mocks\Connection;
|
$requestPath = '/apps/1234/channels/my-channel';
|
||||||
|
|
||||||
$requestPath = '/apps/1234/channel/my-channel';
|
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||||
|
|
||||||
$routeParams = [
|
|
||||||
'appId' => '1234',
|
|
||||||
'channelName' => 'my-channel',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string(
|
|
||||||
'TestKey', 'InvalidSecret', 'GET', $requestPath
|
'TestKey', 'InvalidSecret', 'GET', $requestPath
|
||||||
);
|
));
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
$request = new Request('GET', "{$requestPath}?{$queryString}");
|
||||||
|
|
||||||
$controller = app(FetchChannel::class);
|
$response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
||||||
|
|
||||||
$controller->onOpen($connection, $request);
|
$this->assertSame(401, $response->getStatusCode());
|
||||||
|
$this->assertSame('{"error":"Invalid auth signature provided."}', $response->getBody()->getContents());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_it_returns_the_channel_information()
|
public function test_it_returns_the_channel_information()
|
||||||
|
|
@ -47,7 +40,7 @@ class FetchChannelTest extends TestCase
|
||||||
'channelName' => 'my-channel',
|
'channelName' => 'my-channel',
|
||||||
];
|
];
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
|
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath));
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||||
|
|
||||||
|
|
@ -78,7 +71,7 @@ class FetchChannelTest extends TestCase
|
||||||
'channelName' => 'presence-channel',
|
'channelName' => 'presence-channel',
|
||||||
];
|
];
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
|
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath));
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||||
|
|
||||||
|
|
@ -100,26 +93,17 @@ class FetchChannelTest extends TestCase
|
||||||
{
|
{
|
||||||
$this->skipOnRedisReplication();
|
$this->skipOnRedisReplication();
|
||||||
|
|
||||||
$this->expectException(HttpException::class);
|
$this->startServer();
|
||||||
$this->expectExceptionMessage('Unknown channel');
|
|
||||||
|
|
||||||
$this->newActiveConnection(['my-channel']);
|
$this->newActiveConnection(['my-channel']);
|
||||||
|
|
||||||
$connection = new Mocks\Connection;
|
$requestPath = '/apps/1234/channels/invalid-channel';
|
||||||
|
|
||||||
$requestPath = '/apps/1234/channel/invalid-channel';
|
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath));
|
||||||
|
|
||||||
$routeParams = [
|
$response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
||||||
'appId' => '1234',
|
|
||||||
'channelName' => 'invalid-channel',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
|
$this->assertSame(404, $response->getStatusCode());
|
||||||
|
$this->assertSame('{"error":"Unknown channel `invalid-channel`."}', $response->getBody()->getContents());
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
|
||||||
|
|
||||||
$controller = app(FetchChannel::class);
|
|
||||||
|
|
||||||
$controller->onOpen($connection, $request);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,32 +5,26 @@ namespace BeyondCode\LaravelWebSockets\Test;
|
||||||
use BeyondCode\LaravelWebSockets\API\FetchChannels;
|
use BeyondCode\LaravelWebSockets\API\FetchChannels;
|
||||||
use GuzzleHttp\Psr7\Request;
|
use GuzzleHttp\Psr7\Request;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
use Pusher\Pusher;
|
||||||
|
|
||||||
class FetchChannelsTest extends TestCase
|
class FetchChannelsTest extends TestCase
|
||||||
{
|
{
|
||||||
public function test_invalid_signatures_can_not_access_the_api()
|
public function test_invalid_signatures_can_not_access_the_api()
|
||||||
{
|
{
|
||||||
$this->expectException(HttpException::class);
|
$this->startServer();
|
||||||
$this->expectExceptionMessage('Invalid auth signature provided.');
|
|
||||||
|
|
||||||
$connection = new Mocks\Connection;
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/channels';
|
$requestPath = '/apps/1234/channels';
|
||||||
|
|
||||||
$routeParams = [
|
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||||
'appId' => '1234',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string(
|
|
||||||
'TestKey', 'InvalidSecret', 'GET', $requestPath
|
'TestKey', 'InvalidSecret', 'GET', $requestPath
|
||||||
);
|
));
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
$request = new Request('GET', "{$requestPath}?{$queryString}");
|
||||||
|
|
||||||
$controller = app(FetchChannels::class);
|
$response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
||||||
|
|
||||||
$controller->onOpen($connection, $request);
|
$this->assertSame(401, $response->getStatusCode());
|
||||||
|
$this->assertSame('{"error":"Invalid auth signature provided."}', $response->getBody()->getContents());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_it_returns_the_channel_information()
|
public function test_it_returns_the_channel_information()
|
||||||
|
|
@ -45,9 +39,9 @@ class FetchChannelsTest extends TestCase
|
||||||
'appId' => '1234',
|
'appId' => '1234',
|
||||||
];
|
];
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string(
|
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||||
'TestKey', 'TestSecret', 'GET', $requestPath
|
'TestKey', 'TestSecret', 'GET', $requestPath
|
||||||
);
|
));
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||||
|
|
||||||
|
|
@ -80,9 +74,9 @@ class FetchChannelsTest extends TestCase
|
||||||
'appId' => '1234',
|
'appId' => '1234',
|
||||||
];
|
];
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath, [
|
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath, [
|
||||||
'filter_by_prefix' => 'presence-global',
|
'filter_by_prefix' => 'presence-global',
|
||||||
]);
|
]));
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||||
|
|
||||||
|
|
@ -116,10 +110,10 @@ class FetchChannelsTest extends TestCase
|
||||||
'appId' => '1234',
|
'appId' => '1234',
|
||||||
];
|
];
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath, [
|
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath, [
|
||||||
'filter_by_prefix' => 'presence-global',
|
'filter_by_prefix' => 'presence-global',
|
||||||
'info' => 'user_count',
|
'info' => 'user_count',
|
||||||
]);
|
]));
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||||
|
|
||||||
|
|
@ -144,29 +138,18 @@ class FetchChannelsTest extends TestCase
|
||||||
|
|
||||||
public function test_can_not_get_non_presence_channel_user_count()
|
public function test_can_not_get_non_presence_channel_user_count()
|
||||||
{
|
{
|
||||||
$this->expectException(HttpException::class);
|
$this->startServer();
|
||||||
$this->expectExceptionMessage('Request must be limited to presence channels in order to fetch user_count');
|
|
||||||
|
|
||||||
$connection = new Mocks\Connection;
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/channels';
|
$requestPath = '/apps/1234/channels';
|
||||||
|
|
||||||
$routeParams = [
|
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath, [
|
||||||
'appId' => '1234',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath, [
|
|
||||||
'info' => 'user_count',
|
'info' => 'user_count',
|
||||||
]);
|
]));
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
$response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
||||||
|
|
||||||
$controller = app(FetchChannels::class);
|
$this->assertSame(400, $response->getStatusCode());
|
||||||
|
$this->assertSame('{"error":"Request must be limited to presence channels in order to fetch user_count"}', $response->getBody()->getContents());
|
||||||
$controller->onOpen($connection, $request);
|
|
||||||
|
|
||||||
/** @var JsonResponse $response */
|
|
||||||
$response = array_pop($connection->sentRawData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_it_returns_empty_object_for_no_channels_found()
|
public function test_it_returns_empty_object_for_no_channels_found()
|
||||||
|
|
@ -179,7 +162,7 @@ class FetchChannelsTest extends TestCase
|
||||||
'appId' => '1234',
|
'appId' => '1234',
|
||||||
];
|
];
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
|
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath));
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,87 +4,56 @@ namespace BeyondCode\LaravelWebSockets\Test;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\API\FetchUsers;
|
use BeyondCode\LaravelWebSockets\API\FetchUsers;
|
||||||
use GuzzleHttp\Psr7\Request;
|
use GuzzleHttp\Psr7\Request;
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
use Pusher\Pusher;
|
||||||
|
|
||||||
class FetchUsersTest extends TestCase
|
class FetchUsersTest extends TestCase
|
||||||
{
|
{
|
||||||
public function test_invalid_signatures_can_not_access_the_api()
|
public function test_invalid_signatures_can_not_access_the_api()
|
||||||
{
|
{
|
||||||
$this->expectException(HttpException::class);
|
$this->startServer();
|
||||||
$this->expectExceptionMessage('Invalid auth signature provided.');
|
|
||||||
|
|
||||||
$connection = new Mocks\Connection;
|
$requestPath = '/apps/1234/channels/my-channel/users';
|
||||||
|
|
||||||
$requestPath = '/apps/1234/channel/my-channel';
|
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||||
|
|
||||||
$routeParams = [
|
|
||||||
'appId' => '1234',
|
|
||||||
'channelName' => 'my-channel',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string(
|
|
||||||
'TestKey', 'InvalidSecret', 'GET', $requestPath
|
'TestKey', 'InvalidSecret', 'GET', $requestPath
|
||||||
);
|
));
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
$response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
||||||
|
|
||||||
$controller = app(FetchUsers::class);
|
$this->assertSame(401, $response->getStatusCode());
|
||||||
|
$this->assertSame('{"error":"Invalid auth signature provided."}', $response->getBody()->getContents());
|
||||||
$controller->onOpen($connection, $request);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_it_only_returns_data_for_presence_channels()
|
public function test_it_only_returns_data_for_presence_channels()
|
||||||
{
|
{
|
||||||
$this->expectException(HttpException::class);
|
$this->startServer();
|
||||||
$this->expectExceptionMessage('Invalid presence channel');
|
|
||||||
|
|
||||||
$this->newActiveConnection(['my-channel']);
|
$requestPath = '/apps/1234/channels/my-channel/users';
|
||||||
|
|
||||||
$connection = new Mocks\Connection;
|
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||||
|
|
||||||
$requestPath = '/apps/1234/channel/my-channel/users';
|
|
||||||
|
|
||||||
$routeParams = [
|
|
||||||
'appId' => '1234',
|
|
||||||
'channelName' => 'my-channel',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string(
|
|
||||||
'TestKey', 'TestSecret', 'GET', $requestPath
|
'TestKey', 'TestSecret', 'GET', $requestPath
|
||||||
);
|
));
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
$response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
||||||
|
|
||||||
$controller = app(FetchUsers::class);
|
$this->assertSame(400, $response->getStatusCode());
|
||||||
|
$this->assertSame('{"error":"Invalid presence channel `my-channel`"}', $response->getBody()->getContents());
|
||||||
$controller->onOpen($connection, $request);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_it_returns_404_for_invalid_channels()
|
public function test_it_returns_400_for_invalid_channels()
|
||||||
{
|
{
|
||||||
$this->expectException(HttpException::class);
|
$this->startServer();
|
||||||
$this->expectExceptionMessage('Invalid presence channel');
|
|
||||||
|
|
||||||
$this->newActiveConnection(['my-channel']);
|
$requestPath = '/apps/1234/channels/invalid-channel/users';
|
||||||
|
|
||||||
$connection = new Mocks\Connection;
|
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||||
|
|
||||||
$requestPath = '/apps/1234/channel/invalid-channel/users';
|
|
||||||
|
|
||||||
$routeParams = [
|
|
||||||
'appId' => '1234',
|
|
||||||
'channelName' => 'invalid-channel',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string(
|
|
||||||
'TestKey', 'TestSecret', 'GET', $requestPath
|
'TestKey', 'TestSecret', 'GET', $requestPath
|
||||||
);
|
));
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
$response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
||||||
|
|
||||||
$controller = app(FetchUsers::class);
|
$this->assertSame(400, $response->getStatusCode());
|
||||||
|
$this->assertSame('{"error":"Invalid presence channel `invalid-channel`"}', $response->getBody()->getContents());
|
||||||
$controller->onOpen($connection, $request);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_it_returns_connected_user_information()
|
public function test_it_returns_connected_user_information()
|
||||||
|
|
@ -100,7 +69,7 @@ class FetchUsersTest extends TestCase
|
||||||
'channelName' => 'presence-channel',
|
'channelName' => 'presence-channel',
|
||||||
];
|
];
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
|
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath));
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||||
|
|
||||||
|
|
@ -130,7 +99,7 @@ class FetchUsersTest extends TestCase
|
||||||
'channelName' => 'presence-channel',
|
'channelName' => 'presence-channel',
|
||||||
];
|
];
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
|
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath));
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use BeyondCode\LaravelWebSockets\API\TriggerEvent;
|
||||||
use BeyondCode\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
use BeyondCode\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
||||||
use GuzzleHttp\Psr7\Request;
|
use GuzzleHttp\Psr7\Request;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Pusher\Pusher;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
|
|
||||||
class PresenceChannelTest extends TestCase
|
class PresenceChannelTest extends TestCase
|
||||||
|
|
@ -418,13 +419,13 @@ class PresenceChannelTest extends TestCase
|
||||||
'appId' => '1234',
|
'appId' => '1234',
|
||||||
];
|
];
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string(
|
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
||||||
'name' => 'some-event',
|
'name' => 'some-event',
|
||||||
'channels' => ['presence-channel'],
|
'channels' => ['presence-channel'],
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
'data' => json_encode(['some-data' => 'yes']),
|
||||||
],
|
],
|
||||||
);
|
));
|
||||||
|
|
||||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||||
|
|
||||||
|
|
@ -459,13 +460,13 @@ class PresenceChannelTest extends TestCase
|
||||||
'appId' => '1234',
|
'appId' => '1234',
|
||||||
];
|
];
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string(
|
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
||||||
'name' => 'some-event',
|
'name' => 'some-event',
|
||||||
'channels' => ['presence-channel'],
|
'channels' => ['presence-channel'],
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
'data' => json_encode(['some-data' => 'yes']),
|
||||||
],
|
],
|
||||||
);
|
));
|
||||||
|
|
||||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||||
|
|
||||||
|
|
@ -507,13 +508,13 @@ class PresenceChannelTest extends TestCase
|
||||||
'appId' => '1234',
|
'appId' => '1234',
|
||||||
];
|
];
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string(
|
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
||||||
'name' => 'some-event',
|
'name' => 'some-event',
|
||||||
'channels' => ['presence-channel'],
|
'channels' => ['presence-channel'],
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
'data' => json_encode(['some-data' => 'yes']),
|
||||||
],
|
],
|
||||||
);
|
));
|
||||||
|
|
||||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use BeyondCode\LaravelWebSockets\API\TriggerEvent;
|
||||||
use BeyondCode\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
use BeyondCode\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
||||||
use GuzzleHttp\Psr7\Request;
|
use GuzzleHttp\Psr7\Request;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Pusher\Pusher;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
|
|
||||||
class PrivateChannelTest extends TestCase
|
class PrivateChannelTest extends TestCase
|
||||||
|
|
@ -238,13 +239,13 @@ class PrivateChannelTest extends TestCase
|
||||||
'appId' => '1234',
|
'appId' => '1234',
|
||||||
];
|
];
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string(
|
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
||||||
'name' => 'some-event',
|
'name' => 'some-event',
|
||||||
'channels' => ['private-channel'],
|
'channels' => ['private-channel'],
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
'data' => json_encode(['some-data' => 'yes']),
|
||||||
],
|
],
|
||||||
);
|
));
|
||||||
|
|
||||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||||
|
|
||||||
|
|
@ -279,13 +280,13 @@ class PrivateChannelTest extends TestCase
|
||||||
'appId' => '1234',
|
'appId' => '1234',
|
||||||
];
|
];
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string(
|
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
||||||
'name' => 'some-event',
|
'name' => 'some-event',
|
||||||
'channels' => ['private-channel'],
|
'channels' => ['private-channel'],
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
'data' => json_encode(['some-data' => 'yes']),
|
||||||
],
|
],
|
||||||
);
|
));
|
||||||
|
|
||||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||||
|
|
||||||
|
|
@ -327,13 +328,13 @@ class PrivateChannelTest extends TestCase
|
||||||
'appId' => '1234',
|
'appId' => '1234',
|
||||||
];
|
];
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string(
|
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
||||||
'name' => 'some-event',
|
'name' => 'some-event',
|
||||||
'channels' => ['private-channel'],
|
'channels' => ['private-channel'],
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
'data' => json_encode(['some-data' => 'yes']),
|
||||||
],
|
],
|
||||||
);
|
));
|
||||||
|
|
||||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ namespace BeyondCode\LaravelWebSockets\Test;
|
||||||
use BeyondCode\LaravelWebSockets\API\TriggerEvent;
|
use BeyondCode\LaravelWebSockets\API\TriggerEvent;
|
||||||
use GuzzleHttp\Psr7\Request;
|
use GuzzleHttp\Psr7\Request;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Pusher\Pusher;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
|
|
||||||
class PublicChannelTest extends TestCase
|
class PublicChannelTest extends TestCase
|
||||||
|
|
@ -209,41 +210,28 @@ class PublicChannelTest extends TestCase
|
||||||
|
|
||||||
public function test_it_fires_the_event_to_public_channel()
|
public function test_it_fires_the_event_to_public_channel()
|
||||||
{
|
{
|
||||||
$this->newActiveConnection(['public-channel']);
|
$this->startServer();
|
||||||
|
|
||||||
$connection = new Mocks\Connection;
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/events';
|
$requestPath = '/apps/1234/events';
|
||||||
|
|
||||||
$routeParams = [
|
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||||
'appId' => '1234',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string(
|
|
||||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
||||||
'name' => 'some-event',
|
'name' => 'some-event',
|
||||||
'channels' => ['public-channel'],
|
'channels' => ['public-channel'],
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
'data' => json_encode(['some-data' => 'yes']),
|
||||||
],
|
],
|
||||||
);
|
));
|
||||||
|
|
||||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
$response = $this->await($this->browser->post('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
||||||
|
|
||||||
$controller = app(TriggerEvent::class);
|
$this->assertSame([], json_decode((string) $response->getBody(), true));
|
||||||
|
|
||||||
$controller->onOpen($connection, $request);
|
|
||||||
|
|
||||||
/** @var JsonResponse $response */
|
|
||||||
$response = array_pop($connection->sentRawData);
|
|
||||||
|
|
||||||
$this->assertSame([], json_decode($response->getContent(), true));
|
|
||||||
|
|
||||||
$this->statisticsCollector
|
$this->statisticsCollector
|
||||||
->getAppStatistics('1234')
|
->getAppStatistics('1234')
|
||||||
->then(function ($statistic) {
|
->then(function ($statistic) {
|
||||||
$this->assertEquals([
|
$this->assertEquals([
|
||||||
'peak_connections_count' => 1,
|
'peak_connections_count' => 0,
|
||||||
'websocket_messages_count' => 1,
|
'websocket_messages_count' => 0,
|
||||||
'api_messages_count' => 1,
|
'api_messages_count' => 1,
|
||||||
'app_id' => '1234',
|
'app_id' => '1234',
|
||||||
], $statistic->toArray());
|
], $statistic->toArray());
|
||||||
|
|
@ -260,13 +248,13 @@ class PublicChannelTest extends TestCase
|
||||||
'appId' => '1234',
|
'appId' => '1234',
|
||||||
];
|
];
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string(
|
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
||||||
'name' => 'some-event',
|
'name' => 'some-event',
|
||||||
'channels' => ['public-channel'],
|
'channels' => ['public-channel'],
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
'data' => json_encode(['some-data' => 'yes']),
|
||||||
],
|
],
|
||||||
);
|
));
|
||||||
|
|
||||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||||
|
|
||||||
|
|
@ -308,13 +296,13 @@ class PublicChannelTest extends TestCase
|
||||||
'appId' => '1234',
|
'appId' => '1234',
|
||||||
];
|
];
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string(
|
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
||||||
'name' => 'some-event',
|
'name' => 'some-event',
|
||||||
'channels' => ['public-channel'],
|
'channels' => ['public-channel'],
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
'data' => json_encode(['some-data' => 'yes']),
|
||||||
],
|
],
|
||||||
);
|
));
|
||||||
|
|
||||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,39 @@ use BeyondCode\LaravelWebSockets\Contracts\StatisticsCollector;
|
||||||
use BeyondCode\LaravelWebSockets\Contracts\StatisticsStore;
|
use BeyondCode\LaravelWebSockets\Contracts\StatisticsStore;
|
||||||
use BeyondCode\LaravelWebSockets\Facades\WebSocketRouter;
|
use BeyondCode\LaravelWebSockets\Facades\WebSocketRouter;
|
||||||
use BeyondCode\LaravelWebSockets\Helpers;
|
use BeyondCode\LaravelWebSockets\Helpers;
|
||||||
|
use BeyondCode\LaravelWebSockets\Server\Loggers\HttpLogger;
|
||||||
|
use BeyondCode\LaravelWebSockets\Server\Loggers\WebSocketsLogger;
|
||||||
|
use BeyondCode\LaravelWebSockets\ServerFactory;
|
||||||
|
use function Clue\React\Block\await;
|
||||||
|
use Clue\React\Buzz\Browser;
|
||||||
use GuzzleHttp\Psr7\Request;
|
use GuzzleHttp\Psr7\Request;
|
||||||
use Illuminate\Support\Facades\Redis;
|
use Illuminate\Support\Facades\Redis;
|
||||||
use Orchestra\Testbench\BrowserKit\TestCase as Orchestra;
|
use Orchestra\Testbench\BrowserKit\TestCase as Orchestra;
|
||||||
use Pusher\Pusher;
|
use Ratchet\Server\IoServer;
|
||||||
use React\EventLoop\Factory as LoopFactory;
|
use React\EventLoop\Factory as LoopFactory;
|
||||||
|
use React\EventLoop\LoopInterface;
|
||||||
|
use React\Promise\Deferred;
|
||||||
|
use React\Promise\PromiseInterface;
|
||||||
|
use Symfony\Component\Console\Output\BufferedOutput;
|
||||||
|
|
||||||
abstract class TestCase extends Orchestra
|
abstract class TestCase extends Orchestra
|
||||||
{
|
{
|
||||||
|
const AWAIT_TIMEOUT = 5.0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The test Browser.
|
||||||
|
*
|
||||||
|
* @var \Clue\React\Buzz\Browser
|
||||||
|
*/
|
||||||
|
protected $browser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The test WebSocket server.
|
||||||
|
*
|
||||||
|
* @var IoServer
|
||||||
|
*/
|
||||||
|
protected $server;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A test Pusher server.
|
* A test Pusher server.
|
||||||
*
|
*
|
||||||
|
|
@ -73,11 +98,31 @@ abstract class TestCase extends Orchestra
|
||||||
|
|
||||||
$this->loop = LoopFactory::create();
|
$this->loop = LoopFactory::create();
|
||||||
|
|
||||||
|
$this->app->singleton(LoopInterface::class, function () {
|
||||||
|
return $this->loop;
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->browser = (new Browser($this->loop))
|
||||||
|
->withFollowRedirects(false)
|
||||||
|
->withRejectErrorResponse(false);
|
||||||
|
|
||||||
|
$this->app->singleton(HttpLogger::class, function () {
|
||||||
|
return (new HttpLogger(new BufferedOutput()))
|
||||||
|
->enable(false)
|
||||||
|
->verbose(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->app->singleton(WebSocketsLogger::class, function () {
|
||||||
|
return (new WebSocketsLogger(new BufferedOutput()))
|
||||||
|
->enable(false)
|
||||||
|
->verbose(false);
|
||||||
|
});
|
||||||
|
|
||||||
$this->replicationMode = getenv('REPLICATION_MODE') ?: 'local';
|
$this->replicationMode = getenv('REPLICATION_MODE') ?: 'local';
|
||||||
|
|
||||||
$this->resetDatabase();
|
$this->resetDatabase();
|
||||||
$this->loadLaravelMigrations(['--database' => 'sqlite']);
|
$this->loadLaravelMigrations(['--database' => 'sqlite']);
|
||||||
$this->loadMigrationsFrom(__DIR__.'/database/migrations');
|
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
|
||||||
$this->withFactories(__DIR__.'/database/factories');
|
$this->withFactories(__DIR__.'/database/factories');
|
||||||
|
|
||||||
$this->registerCustomPath();
|
$this->registerCustomPath();
|
||||||
|
|
@ -102,6 +147,15 @@ abstract class TestCase extends Orchestra
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
parent::tearDown();
|
||||||
|
|
||||||
|
if ($this->server) {
|
||||||
|
$this->server->socket->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
|
|
@ -270,6 +324,11 @@ abstract class TestCase extends Orchestra
|
||||||
$this->channelManager = $this->app->make(ChannelManager::class);
|
$this->channelManager = $this->app->make(ChannelManager::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function await(PromiseInterface $promise, LoopInterface $loop = null, $timeout = null)
|
||||||
|
{
|
||||||
|
return await($promise, $loop ?? $this->loop, $timeout ?? static::AWAIT_TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unregister the managers for testing purposes.
|
* Unregister the managers for testing purposes.
|
||||||
*
|
*
|
||||||
|
|
@ -338,6 +397,19 @@ abstract class TestCase extends Orchestra
|
||||||
return $connection;
|
return $connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function joinWebSocketServer(array $channelsToJoin = [], string $appKey = 'TestKey', array $headers = [])
|
||||||
|
{
|
||||||
|
$promise = new Deferred();
|
||||||
|
|
||||||
|
\Ratchet\Client\connect("ws://localhost:4000/app/{$appKey}", [], [], $this->loop)->then(function ($conn) use ($promise) {
|
||||||
|
$conn->on('message', function ($msg) use ($promise) {
|
||||||
|
$promise->resolve($msg);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return $promise->promise();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a connected websocket connection.
|
* Get a connected websocket connection.
|
||||||
*
|
*
|
||||||
|
|
@ -485,27 +557,16 @@ abstract class TestCase extends Orchestra
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static function build_auth_query_string(
|
protected function startServer()
|
||||||
$auth_key,
|
{
|
||||||
$auth_secret,
|
$server = new ServerFactory('0.0.0.0', 4000);
|
||||||
$request_method,
|
|
||||||
$request_path,
|
|
||||||
$query_params = [],
|
|
||||||
$auth_version = '1.0',
|
|
||||||
$auth_timestamp = null
|
|
||||||
) {
|
|
||||||
$method = method_exists(Pusher::class, 'build_auth_query_params') ? 'build_auth_query_params' : 'build_auth_query_string';
|
|
||||||
|
|
||||||
$params = Pusher::$method(
|
WebSocketRouter::registerRoutes();
|
||||||
$auth_key, $auth_secret, $request_method, $request_path, $query_params, $auth_version, $auth_timestamp
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($method == 'build_auth_query_string') {
|
$this->server = $server
|
||||||
return $params;
|
->setLoop($this->loop)
|
||||||
}
|
->withRoutes(WebSocketRouter::getRoutes())
|
||||||
|
->setConsoleOutput(new BufferedOutput())
|
||||||
ksort($params);
|
->createServer();
|
||||||
|
|
||||||
return http_build_query($params);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,33 +2,25 @@
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Test;
|
namespace BeyondCode\LaravelWebSockets\Test;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\API\TriggerEvent;
|
use Pusher\Pusher;
|
||||||
use GuzzleHttp\Psr7\Request;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
|
||||||
|
|
||||||
class TriggerEventTest extends TestCase
|
class TriggerEventTest extends TestCase
|
||||||
{
|
{
|
||||||
public function test_invalid_signatures_can_not_fire_the_event()
|
public function test_invalid_signatures_can_not_fire_the_event()
|
||||||
{
|
{
|
||||||
$this->expectException(HttpException::class);
|
$this->startServer();
|
||||||
$this->expectExceptionMessage('Invalid auth signature provided.');
|
|
||||||
|
|
||||||
$connection = new Mocks\Connection;
|
$connection = new Mocks\Connection;
|
||||||
|
|
||||||
$requestPath = '/apps/1234/events';
|
$requestPath = '/apps/1234/events';
|
||||||
|
|
||||||
$routeParams = [
|
$queryString = http_build_query(Pusher::build_auth_query_params(
|
||||||
'appId' => '1234',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = self::build_auth_query_string(
|
|
||||||
'TestKey', 'InvalidSecret', 'GET', $requestPath
|
'TestKey', 'InvalidSecret', 'GET', $requestPath
|
||||||
);
|
));
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
$response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
||||||
|
|
||||||
$controller = app(TriggerEvent::class);
|
$this->assertSame(405, $response->getStatusCode());
|
||||||
|
$this->assertSame('', $response->getBody()->getContents());
|
||||||
$controller->onOpen($connection, $request);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
class CreateWebSocketsStatisticsEntriesTable extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function up()
|
|
||||||
{
|
|
||||||
Schema::create('websockets_statistics_entries', function (Blueprint $table) {
|
|
||||||
$table->increments('id');
|
|
||||||
$table->string('app_id');
|
|
||||||
$table->integer('peak_connections_count');
|
|
||||||
$table->integer('websocket_messages_count');
|
|
||||||
$table->integer('api_messages_count');
|
|
||||||
$table->nullableTimestamps();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function down()
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('websockets_statistics_entries');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue