From 72d33b1b70460134a1f7efc428643f430768a00e Mon Sep 17 00:00:00 2001 From: Marcel Pociot Date: Sun, 3 May 2020 00:21:16 +0200 Subject: [PATCH] wip --- app/Client/Client.php | 51 ++++++++--- app/Contracts/ConnectionManager.php | 2 + app/Contracts/UserRepository.php | 18 ++++ app/Server/Connections/ConnectionManager.php | 20 ++++- app/Server/Connections/ControlConnection.php | 8 ++ app/Server/Factory.php | 15 +++- .../Admin/DeleteUsersController.php | 21 +++-- .../Controllers/Admin/ListSitesController.php | 3 - .../Controllers/Admin/ListUsersController.php | 28 +++--- .../Admin/SaveSettingsController.php | 1 - .../Admin/ShowSettingsController.php | 4 - .../Admin/StoreUsersController.php | 27 +++--- .../Controllers/ControlMessageController.php | 77 +++++++++------- .../UserRepository/DatabaseUserRepository.php | 89 +++++++++++++++++++ config/expose.php | 40 ++++++++- resources/views/server/sites/index.twig | 2 +- tests/Feature/Server/AdminTest.php | 52 ++++++++++- tests/Feature/Server/TunnelTest.php | 44 ++++++++- tests/Feature/TestCase.php | 9 ++ 19 files changed, 408 insertions(+), 103 deletions(-) create mode 100644 app/Contracts/UserRepository.php create mode 100644 app/Server/UserRepository/DatabaseUserRepository.php diff --git a/app/Client/Client.php b/app/Client/Client.php index 9ff44fa..1df84b7 100644 --- a/app/Client/Client.php +++ b/app/Client/Client.php @@ -4,8 +4,10 @@ namespace App\Client; use App\Client\Connections\ControlConnection; use App\Logger\CliRequestLogger; +use Carbon\Carbon; use Ratchet\Client\WebSocket; use React\EventLoop\LoopInterface; +use React\Promise\Deferred; use React\Promise\PromiseInterface; use function Ratchet\Client\connect; @@ -20,6 +22,9 @@ class Client /** @var CliRequestLogger */ protected $logger; + /** @var int */ + protected $timeConnected = 0; + public static $subdomains = []; public function __construct(LoopInterface $loop, Configuration $configuration, CliRequestLogger $logger) @@ -34,20 +39,18 @@ class Client $this->logger->info("Sharing http://{$sharedUrl}"); foreach ($subdomains as $subdomain) { - $this->connectToServer($sharedUrl, $subdomain); + $this->connectToServer($sharedUrl, $subdomain, config('expose.auth_token')); } } - public function connectToServer(string $sharedUrl, $subdomain): PromiseInterface + public function connectToServer(string $sharedUrl, $subdomain, $authToken = ''): PromiseInterface { $deferred = new \React\Promise\Deferred(); $promise = $deferred->promise(); - $token = config('expose.auth_token'); - $wsProtocol = $this->configuration->port() === 443 ? "wss" : "ws"; - connect($wsProtocol."://{$this->configuration->host()}:{$this->configuration->port()}/expose/control?authToken={$token}", [], [ + connect($wsProtocol."://{$this->configuration->host()}:{$this->configuration->port()}/expose/control?authToken={$authToken}", [], [ 'X-Expose-Control' => 'enabled', ], $this->loop) ->then(function (WebSocket $clientConnection) use ($sharedUrl, $subdomain, $deferred) { @@ -55,19 +58,32 @@ class Client $connection->authenticate($sharedUrl, $subdomain); - $clientConnection->on('close', function() { + $clientConnection->on('close', function() use ($deferred) { $this->logger->error('Connection to server closed.'); - exit(1); + + $this->exit($deferred); }); - $connection->on('authenticationFailed', function ($data) { + $connection->on('authenticationFailed', function ($data) use ($deferred) { $this->logger->error("Authentication failed. Please check your authentication token and try again."); - exit(1); + + $this->exit($deferred); }); - $connection->on('subdomainTaken', function ($data) { + $connection->on('subdomainTaken', function ($data) use ($deferred) { $this->logger->error("The chosen subdomain \"{$data->data->subdomain}\" is already taken. Please choose a different subdomain."); - exit(1); + + $this->exit($deferred); + }); + + $connection->on('setMaximumConnectionLength', function ($data) { + $this->loop->addPeriodicTimer(1, function() use ($data) { + $this->timeConnected++; + + $carbon = Carbon::createFromFormat('s', str_pad($data->length * 60 - $this->timeConnected, 2, 0, STR_PAD_LEFT)); + + $this->logger->info('Remaining time: '.$carbon->format('H:i:s')); + }); }); $connection->on('authenticated', function ($data) use ($deferred) { @@ -89,11 +105,18 @@ class Client $this->logger->error("Could not connect to the server."); $this->logger->error($e->getMessage()); - $deferred->reject(); - - exit(1); + $this->exit($deferred); }); return $promise; } + + protected function exit(Deferred $deferred) + { + $deferred->reject(); + + $this->loop->futureTick(function(){ + exit(1); + }); + } } diff --git a/app/Contracts/ConnectionManager.php b/app/Contracts/ConnectionManager.php index e3064f0..a208ef6 100644 --- a/app/Contracts/ConnectionManager.php +++ b/app/Contracts/ConnectionManager.php @@ -10,6 +10,8 @@ interface ConnectionManager { public function storeConnection(string $host, ?string $subdomain, ConnectionInterface $connection): ControlConnection; + public function limitConnectionLength(ControlConnection $connection, int $maximumConnectionLength); + public function storeHttpConnection(ConnectionInterface $httpConnection, $requestId): HttpConnection; public function getHttpConnectionForRequestId(string $requestId): ?HttpConnection; diff --git a/app/Contracts/UserRepository.php b/app/Contracts/UserRepository.php new file mode 100644 index 0000000..53bab8a --- /dev/null +++ b/app/Contracts/UserRepository.php @@ -0,0 +1,18 @@ +subdomainGenerator = $subdomainGenerator; + $this->loop = $loop; + } + + public function limitConnectionLength(ControlConnection $connection, int $maximumConnectionLength) + { + if ($maximumConnectionLength === 0) { + return; + } + + $connection->setMaximumConnectionLength($maximumConnectionLength); + + $this->loop->addTimer($maximumConnectionLength * 60, function() use ($connection) { + $connection->socket->close(); + }); } public function storeConnection(string $host, ?string $subdomain, ConnectionInterface $connection): ControlConnection diff --git a/app/Server/Connections/ControlConnection.php b/app/Server/Connections/ControlConnection.php index abf145f..310f8fe 100644 --- a/app/Server/Connections/ControlConnection.php +++ b/app/Server/Connections/ControlConnection.php @@ -33,6 +33,14 @@ class ControlConnection $this->shared_at = now()->toDateTimeString(); } + public function setMaximumConnectionLength(int $maximumConnectionLength) + { + $this->socket->send(json_encode([ + 'event' => 'setMaximumConnectionLength', + 'length' => $maximumConnectionLength, + ])); + } + public function registerProxy($requestId) { $this->socket->send(json_encode([ diff --git a/app/Server/Factory.php b/app/Server/Factory.php index 841e940..ddd3209 100644 --- a/app/Server/Factory.php +++ b/app/Server/Factory.php @@ -4,6 +4,7 @@ namespace App\Server; use App\Contracts\ConnectionManager as ConnectionManagerContract; use App\Contracts\SubdomainGenerator; +use App\Contracts\UserRepository; use App\Http\Server as HttpServer; use App\Server\Connections\ConnectionManager; use App\Server\Http\Controllers\Admin\DeleteUsersController; @@ -117,7 +118,7 @@ class Factory $this->router->get('/settings', ShowSettingsController::class, $adminCondition); $this->router->post('/settings', SaveSettingsController::class, $adminCondition); $this->router->post('/users', StoreUsersController::class, $adminCondition); - $this->router->delete('/users/delete/{id}', DeleteUsersController::class, $adminCondition); + $this->router->delete('/users/{id}', DeleteUsersController::class, $adminCondition); $this->router->get('/sites', ListSitesController::class, $adminCondition); } @@ -133,7 +134,7 @@ class Factory protected function bindSubdomainGenerator() { app()->singleton(SubdomainGenerator::class, function ($app) { - return $app->make(RandomSubdomainGenerator::class); + return $app->make(config('expose.admin.subdomain_generator')); }); return $this; @@ -154,6 +155,7 @@ class Factory $this->bindConfiguration() ->bindSubdomainGenerator() + ->bindUserRepository() ->bindDatabase() ->ensureDatabaseIsInitialized() ->bindConnectionManager() @@ -181,6 +183,15 @@ class Factory return $this->socket; } + protected function bindUserRepository() + { + app()->singleton(UserRepository::class, function() { + return app(config('expose.admin.user_repository')); + }); + + return $this; + } + protected function bindDatabase() { app()->singleton(DatabaseInterface::class, function() { diff --git a/app/Server/Http/Controllers/Admin/DeleteUsersController.php b/app/Server/Http/Controllers/Admin/DeleteUsersController.php index b90934a..129baf6 100644 --- a/app/Server/Http/Controllers/Admin/DeleteUsersController.php +++ b/app/Server/Http/Controllers/Admin/DeleteUsersController.php @@ -2,9 +2,8 @@ namespace App\Server\Http\Controllers\Admin; +use App\Contracts\UserRepository; use App\Http\Controllers\Controller; -use Clue\React\SQLite\DatabaseInterface; -use Clue\React\SQLite\Result; use GuzzleHttp\Psr7\Response; use Illuminate\Http\Request; use Illuminate\Support\Facades\Validator; @@ -19,20 +18,20 @@ class DeleteUsersController extends AdminController { protected $keepConnectionOpen = true; - /** @var DatabaseInterface */ - protected $database; + /** @var UserRepository */ + protected $userRepository; - public function __construct(DatabaseInterface $database) + public function __construct(UserRepository $userRepository) { - $this->database = $database; + $this->userRepository = $userRepository; } public function handle(Request $request, ConnectionInterface $httpConnection) { - $this->database->query("DELETE FROM users WHERE id = :id", ['id' => $request->id]) - ->then(function (Result $result) use ($httpConnection) { - $httpConnection->send(respond_json(['deleted' => true], 200)); - $httpConnection->close(); - }); + $this->userRepository->deleteUser($request->get('id')) + ->then(function() use ($httpConnection) { + $httpConnection->send(respond_json(['deleted' => true], 200)); + $httpConnection->close(); + }); } } diff --git a/app/Server/Http/Controllers/Admin/ListSitesController.php b/app/Server/Http/Controllers/Admin/ListSitesController.php index 6cabe7f..a093d54 100644 --- a/app/Server/Http/Controllers/Admin/ListSitesController.php +++ b/app/Server/Http/Controllers/Admin/ListSitesController.php @@ -5,9 +5,6 @@ namespace App\Server\Http\Controllers\Admin; use App\Contracts\ConnectionManager; use App\Http\Controllers\Controller; use App\Server\Configuration; -use Clue\React\SQLite\DatabaseInterface; -use Clue\React\SQLite\Result; -use GuzzleHttp\Psr7\Response; use Illuminate\Http\Request; use Ratchet\ConnectionInterface; use Twig\Environment; diff --git a/app/Server/Http/Controllers/Admin/ListUsersController.php b/app/Server/Http/Controllers/Admin/ListUsersController.php index b171053..4f02c4b 100644 --- a/app/Server/Http/Controllers/Admin/ListUsersController.php +++ b/app/Server/Http/Controllers/Admin/ListUsersController.php @@ -2,8 +2,8 @@ namespace App\Server\Http\Controllers\Admin; +use App\Contracts\UserRepository; use App\Http\Controllers\Controller; -use Clue\React\SQLite\DatabaseInterface; use Clue\React\SQLite\Result; use GuzzleHttp\Psr7\Response; use Illuminate\Http\Request; @@ -17,26 +17,24 @@ class ListUsersController extends AdminController { protected $keepConnectionOpen = true; - /** @var DatabaseInterface */ - protected $database; + /** @var UserRepository */ + protected $userRepository; - public function __construct(DatabaseInterface $database) + public function __construct(UserRepository $userRepository) { - $this->database = $database; + $this->userRepository = $userRepository; } public function handle(Request $request, ConnectionInterface $httpConnection) { - $this->database->query('SELECT * FROM users ORDER by created_at DESC')->then(function (Result $result) use ($httpConnection) { - $httpConnection->send( - respond_html($this->getView($httpConnection, 'server.users.index', ['users' => $result->rows])) - ); + $this->userRepository + ->getUsers() + ->then(function ($users) use ($httpConnection) { + $httpConnection->send( + respond_html($this->getView($httpConnection, 'server.users.index', ['users' => $users])) + ); - $httpConnection->close(); - }, function (\Exception $exception) use ($httpConnection) { - $httpConnection->send(respond_html('Something went wrong: '.$exception->getMessage(), 500)); - - $httpConnection->close(); - }); + $httpConnection->close(); + }); } } diff --git a/app/Server/Http/Controllers/Admin/SaveSettingsController.php b/app/Server/Http/Controllers/Admin/SaveSettingsController.php index a46dc8f..1335179 100644 --- a/app/Server/Http/Controllers/Admin/SaveSettingsController.php +++ b/app/Server/Http/Controllers/Admin/SaveSettingsController.php @@ -5,7 +5,6 @@ namespace App\Server\Http\Controllers\Admin; use App\Contracts\ConnectionManager; use App\Http\Controllers\Controller; use App\Server\Configuration; -use Clue\React\SQLite\DatabaseInterface; use Clue\React\SQLite\Result; use GuzzleHttp\Psr7\Response; use Illuminate\Http\Request; diff --git a/app/Server/Http/Controllers/Admin/ShowSettingsController.php b/app/Server/Http/Controllers/Admin/ShowSettingsController.php index 6e9741f..519b337 100644 --- a/app/Server/Http/Controllers/Admin/ShowSettingsController.php +++ b/app/Server/Http/Controllers/Admin/ShowSettingsController.php @@ -3,11 +3,7 @@ namespace App\Server\Http\Controllers\Admin; use App\Contracts\ConnectionManager; -use App\Http\Controllers\Controller; use App\Server\Configuration; -use Clue\React\SQLite\DatabaseInterface; -use Clue\React\SQLite\Result; -use GuzzleHttp\Psr7\Response; use Illuminate\Http\Request; use Ratchet\ConnectionInterface; use Twig\Environment; diff --git a/app/Server/Http/Controllers/Admin/StoreUsersController.php b/app/Server/Http/Controllers/Admin/StoreUsersController.php index 21beb66..944a765 100644 --- a/app/Server/Http/Controllers/Admin/StoreUsersController.php +++ b/app/Server/Http/Controllers/Admin/StoreUsersController.php @@ -2,8 +2,8 @@ namespace App\Server\Http\Controllers\Admin; +use App\Contracts\UserRepository; use App\Http\Controllers\Controller; -use Clue\React\SQLite\DatabaseInterface; use Clue\React\SQLite\Result; use GuzzleHttp\Psr7\Response; use Illuminate\Http\Request; @@ -19,12 +19,12 @@ class StoreUsersController extends AdminController { protected $keepConnectionOpen = true; - /** @var DatabaseInterface */ - protected $database; + /** @var UserRepository */ + protected $userRepository; - public function __construct(DatabaseInterface $database) + public function __construct(UserRepository $userRepository) { - $this->database = $database; + $this->userRepository = $userRepository; } public function handle(Request $request, ConnectionInterface $httpConnection) @@ -47,16 +47,11 @@ class StoreUsersController extends AdminController 'auth_token' => (string)Str::uuid() ]; - $this->database->query(" - INSERT INTO users (name, auth_token, created_at) - VALUES (:name, :auth_token, DATETIME('now')) - ", $insertData) - ->then(function (Result $result) use ($httpConnection) { - $this->database->query("SELECT * FROM users WHERE id = :id", ['id' => $result->insertId]) - ->then(function (Result $result) use ($httpConnection) { - $httpConnection->send(respond_json(['user' => $result->rows[0]], 200)); - $httpConnection->close(); - }); - }); + $this->userRepository + ->storeUser($insertData) + ->then(function ($user) use ($httpConnection) { + $httpConnection->send(respond_json(['user' => $user], 200)); + $httpConnection->close(); + }); } } diff --git a/app/Server/Http/Controllers/ControlMessageController.php b/app/Server/Http/Controllers/ControlMessageController.php index 7557ad3..315c6d0 100644 --- a/app/Server/Http/Controllers/ControlMessageController.php +++ b/app/Server/Http/Controllers/ControlMessageController.php @@ -3,10 +3,12 @@ namespace App\Server\Http\Controllers; use App\Contracts\ConnectionManager; +use App\Contracts\UserRepository; use App\Http\QueryParameters; -use Clue\React\SQLite\DatabaseInterface; -use Clue\React\SQLite\Result; use Ratchet\WebSocket\MessageComponentInterface; +use React\Promise\Deferred; +use React\Promise\FulfilledPromise; +use React\Promise\PromiseInterface; use stdClass; use Ratchet\ConnectionInterface; @@ -16,13 +18,13 @@ class ControlMessageController implements MessageComponentInterface /** @var ConnectionManager */ protected $connectionManager; - /** @var DatabaseInterface */ - protected $database; + /** @var UserRepository */ + protected $userRepository; - public function __construct(ConnectionManager $connectionManager, DatabaseInterface $database) + public function __construct(ConnectionManager $connectionManager, UserRepository $userRepository) { $this->connectionManager = $connectionManager; - $this->database = $database; + $this->userRepository = $userRepository; } /** @@ -75,21 +77,28 @@ class ControlMessageController implements MessageComponentInterface protected function authenticate(ConnectionInterface $connection, $data) { - if (config('expose.admin.validate_auth_tokens') === true) { - $this->verifyAuthToken($connection); - } + $this->verifyAuthToken($connection) + ->then(function () use ($connection, $data) { + if (! $this->hasValidSubdomain($connection, $data->subdomain)) { + return; + } - if (! $this->hasValidSubdomain($connection, $data->subdomain)) { - return; - } + $connectionInfo = $this->connectionManager->storeConnection($data->host, $data->subdomain, $connection); - $connectionInfo = $this->connectionManager->storeConnection($data->host, $data->subdomain, $connection); + $this->connectionManager->limitConnectionLength($connectionInfo, config('expose.admin.maximum_session_length')); - $connection->send(json_encode([ - 'event' => 'authenticated', - 'subdomain' => $connectionInfo->subdomain, - 'client_id' => $connectionInfo->client_id - ])); + $connection->send(json_encode([ + 'event' => 'authenticated', + 'subdomain' => $connectionInfo->subdomain, + 'client_id' => $connectionInfo->client_id + ])); + }, function () use ($connection) { + $connection->send(json_encode([ + 'event' => 'authenticationFailed', + 'data' => [] + ])); + $connection->close(); + }); } protected function registerProxy(ConnectionInterface $connection, $data) @@ -111,28 +120,34 @@ class ControlMessageController implements MessageComponentInterface // } - protected function verifyAuthToken(ConnectionInterface $connection) + protected function verifyAuthToken(ConnectionInterface $connection): PromiseInterface { + if (config('expose.admin.validate_auth_tokens') !== true) { + return new FulfilledPromise(); + } + + $deferred = new Deferred(); + $authToken = QueryParameters::create($connection->httpRequest)->get('authToken'); - $this->database - ->query("SELECT * FROM users WHERE auth_token = :token", ['token' => $authToken]) - ->then(function (Result $result) use ($connection) { - if (count($result->rows) === 0) { - $connection->send(json_encode([ - 'event' => 'authenticationFailed', - 'data' => [] - ])); - $connection->close(); + $this->userRepository + ->getUserByToken($authToken) + ->then(function ($user) use ($connection, $deferred) { + if (is_null($user)) { + $deferred->reject(); + } else { + $deferred->resolve($user); } - }); + }); + + return $deferred->promise(); } protected function hasValidSubdomain(ConnectionInterface $connection, ?string $subdomain): bool { - if (! is_null($subdomain)) { + if (!is_null($subdomain)) { $controlConnection = $this->connectionManager->findControlConnectionForSubdomain($subdomain); - if (! is_null($controlConnection) || $subdomain === config('expose.admin.subdomain')) { + if (!is_null($controlConnection) || $subdomain === config('expose.admin.subdomain')) { $connection->send(json_encode([ 'event' => 'subdomainTaken', 'data' => [ diff --git a/app/Server/UserRepository/DatabaseUserRepository.php b/app/Server/UserRepository/DatabaseUserRepository.php new file mode 100644 index 0000000..551c989 --- /dev/null +++ b/app/Server/UserRepository/DatabaseUserRepository.php @@ -0,0 +1,89 @@ +database = $database; + } + + public function getUsers(): PromiseInterface + { + $deferred = new Deferred(); + + $this->database + ->query("SELECT * FROM users ORDER by created_at DESC") + ->then(function (Result $result) use ($deferred) { + $deferred->resolve($result->rows); + }); + + return $deferred->promise(); + } + + public function getUserById($id): PromiseInterface + { + $deferred = new Deferred(); + + $this->database + ->query("SELECT * FROM users WHERE id = :id", ['id' => $id]) + ->then(function (Result $result) use ($deferred) { + $deferred->resolve($result->rows[0] ?? null); + }); + + return $deferred->promise(); + } + + public function getUserByToken(string $authToken): PromiseInterface + { + $deferred = new Deferred(); + + $this->database + ->query("SELECT * FROM users WHERE auth_token = :token", ['token' => $authToken]) + ->then(function (Result $result) use ($deferred) { + $deferred->resolve($result->rows[0] ?? null); + }); + + return $deferred->promise(); + } + + public function storeUser(array $data): PromiseInterface + { + $deferred = new Deferred(); + + $this->database->query(" + INSERT INTO users (name, auth_token, created_at) + VALUES (:name, :auth_token, DATETIME('now')) + ", $data) + ->then(function (Result $result) use ($deferred) { + $this->database->query("SELECT * FROM users WHERE id = :id", ['id' => $result->insertId]) + ->then(function (Result $result) use ($deferred) { + $deferred->resolve($result->rows[0]); + }); + }); + + return $deferred->promise(); + } + + public function deleteUser($id): PromiseInterface + { + $deferred = new Deferred(); + + $this->database->query("DELETE FROM users WHERE id = :id", ['id' => $id]) + ->then(function (Result $result) use ($deferred) { + $deferred->resolve($result); + }); + + return $deferred->promise(); + } +} diff --git a/config/expose.php b/config/expose.php index 0b916ca..d6ae5d2 100644 --- a/config/expose.php +++ b/config/expose.php @@ -11,6 +11,20 @@ return [ 'validate_auth_tokens' => false, + + /* + |-------------------------------------------------------------------------- + | Maximum session length + |-------------------------------------------------------------------------- + | + | If you want to limit the amount of time that a single connection can + | stay connected to the expose server, you can specify the maximum + | session length in minutes here. A maximum length of 0 means that + | clients can stay connected as long as they want. + | + */ + 'maximum_session_length' => 0, + /* |-------------------------------------------------------------------------- | Subdomain @@ -23,6 +37,18 @@ return [ */ 'subdomain' => 'expose', + /* + |-------------------------------------------------------------------------- + | Subdomain Generator + |-------------------------------------------------------------------------- + | + | This is the subdomain generator that will be used, when no specific + | subdomain was provided. The default implementation simply generates + | a random string for you. Feel free to change this. + | + */ + 'subdomain_generator' => \App\Server\SubdomainGenerator\RandomSubdomainGenerator::class, + /* |-------------------------------------------------------------------------- | Users @@ -35,6 +61,18 @@ return [ */ 'users' => [ 'username' => 'password' - ] + ], + + /* + |-------------------------------------------------------------------------- + | User Repository + |-------------------------------------------------------------------------- + | + | This is the user repository, which by default loads and saves all authorized + | users in a SQLite database. You can implement your own user repository + | if you want to store your users in a different store (Redis, MySQL, etc.) + | + */ + 'user_repository' => \App\Server\UserRepository\DatabaseUserRepository::class, ] ]; diff --git a/resources/views/server/sites/index.twig b/resources/views/server/sites/index.twig index 878f847..6870823 100644 --- a/resources/views/server/sites/index.twig +++ b/resources/views/server/sites/index.twig @@ -61,7 +61,7 @@ methods: { deleteUser(user) { - fetch('/expose/users/delete/' + user.id, { + fetch('/expose/users/' + user.id, { method: 'DELETE', }).then((response) => { return response.json(); diff --git a/tests/Feature/Server/AdminTest.php b/tests/Feature/Server/AdminTest.php index 3b6adcc..7d79805 100644 --- a/tests/Feature/Server/AdminTest.php +++ b/tests/Feature/Server/AdminTest.php @@ -84,6 +84,25 @@ class AdminTest extends TestCase /** @test */ public function it_can_create_users() { + /** @var Response $response */ + $response = $this->await($this->browser->post('http://127.0.0.1:8080/users', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode("username:secret"), + 'Content-Type' => 'application/json' + ], json_encode([ + 'name' => 'Marcel', + ]))); + + $responseData = json_decode($response->getBody()->getContents()); + $this->assertSame('Marcel', $responseData->user->name); + + $this->assertDatabaseHasResults('SELECT * FROM users WHERE name = "Marcel"'); + } + + /** @test */ + public function it_can_delete_users() + { + /** @var Response $response */ $this->await($this->browser->post('http://127.0.0.1:8080/users', [ 'Host' => 'expose.localhost', 'Authorization' => base64_encode("username:secret"), @@ -92,7 +111,38 @@ class AdminTest extends TestCase 'name' => 'Marcel', ]))); - $this->assertDatabaseHasResults('SELECT * FROM users WHERE name = "Marcel"'); + + $this->await($this->browser->delete('http://127.0.0.1:8080/users/1', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode("username:secret"), + 'Content-Type' => 'application/json' + ])); + + $this->assertDatabaseHasNoResults('SELECT * FROM users WHERE name = "Marcel"'); + } + + /** @test */ + public function it_can_list_all_users() + { + /** @var Response $response */ + $this->await($this->browser->post('http://127.0.0.1:8080/users', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode("username:secret"), + 'Content-Type' => 'application/json' + ], json_encode([ + 'name' => 'Marcel', + ]))); + + /** @var Response $response */ + $response = $this->await($this->browser->get('http://127.0.0.1:8080/users', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode("username:secret"), + 'Content-Type' => 'application/json' + ])); + + $body = $response->getBody()->getContents(); + + $this->assertTrue(Str::contains($body, 'Marcel')); } /** @test */ diff --git a/tests/Feature/Server/TunnelTest.php b/tests/Feature/Server/TunnelTest.php index e3d410b..bd99869 100644 --- a/tests/Feature/Server/TunnelTest.php +++ b/tests/Feature/Server/TunnelTest.php @@ -24,6 +24,9 @@ class TunnelTest extends TestCase /** @var Factory */ protected $serverFactory; + /** @var \React\Socket\Server */ + protected $testHttpServer; + public function setUp(): void { parent::setUp(); @@ -37,6 +40,10 @@ class TunnelTest extends TestCase { $this->serverFactory->getSocket()->close(); + if (isset($this->testHttpServer)) { + $this->testHttpServer->close(); + } + parent::tearDown(); } @@ -74,6 +81,39 @@ class TunnelTest extends TestCase $this->assertSame('Hello World!', $response->getBody()->getContents()); } + /** @test */ + public function it_rejects_clients_with_invalid_auth_tokens() + { + $this->app['config']['expose.admin.validate_auth_tokens'] = true; + + $this->createTestHttpServer(); + + $this->expectException(\UnexpectedValueException::class); + + /** + * We create an expose client that connects to our server and shares + * the created test HTTP server + */ + $client = $this->createClient(); + $result = $this->await($client->connectToServer('127.0.0.1:8085', 'tunnel')); + } + + /** @test */ + public function it_allows_clients_with_valid_auth_tokens() + { + $this->app['config']['expose.admin.validate_auth_tokens'] = true; + + $this->createTestHttpServer(); + + $this->expectException(\UnexpectedValueException::class); + + /** + * We create an expose client that connects to our server and shares + * the created test HTTP server + */ + $client = $this->createClient(); + $this->await($client->connectToServer('127.0.0.1:8085', 'tunnel')); + } protected function startServer() { @@ -104,7 +144,7 @@ class TunnelTest extends TestCase return new Response(200, ['Content-Type' => 'text/plain'], "Hello World!"); }); - $socket = new \React\Socket\Server(8085, $this->loop); - $server->listen($socket); + $this->testHttpServer = new \React\Socket\Server(8085, $this->loop); + $server->listen($this->testHttpServer); } } diff --git a/tests/Feature/TestCase.php b/tests/Feature/TestCase.php index 10f32e1..257b08a 100644 --- a/tests/Feature/TestCase.php +++ b/tests/Feature/TestCase.php @@ -46,4 +46,13 @@ abstract class TestCase extends \Tests\TestCase $this->assertGreaterThanOrEqual(1, count($result->rows)); } + + protected function assertDatabaseHasNoResults($query) + { + $database = app(DatabaseInterface::class); + + $result = $this->await($database->query($query)); + + $this->assertEmpty($result->rows); + } }