From 0ebe6a4ce4256a23a30e0c200a311a76120a3c82 Mon Sep 17 00:00:00 2001 From: Marcel Pociot Date: Mon, 7 Sep 2020 20:19:56 +0200 Subject: [PATCH] Add a new flag to users to allow the specification of custom subdomains --- .../Admin/StoreUsersController.php | 1 + .../Controllers/ControlMessageController.php | 19 ++++- .../UserRepository/DatabaseUserRepository.php | 4 +- config/expose.php | 2 + .../02_add_feature_flags_to_users_table.sql | 1 + resources/views/server/users/index.twig | 34 +++++++- tests/Feature/Server/TunnelTest.php | 85 ++++++++++++++++++- 7 files changed, 137 insertions(+), 9 deletions(-) create mode 100644 database/migrations/02_add_feature_flags_to_users_table.sql diff --git a/app/Server/Http/Controllers/Admin/StoreUsersController.php b/app/Server/Http/Controllers/Admin/StoreUsersController.php index 3b78fd9..8a0cb61 100644 --- a/app/Server/Http/Controllers/Admin/StoreUsersController.php +++ b/app/Server/Http/Controllers/Admin/StoreUsersController.php @@ -39,6 +39,7 @@ class StoreUsersController extends AdminController $insertData = [ 'name' => $request->get('name'), 'auth_token' => (string) Str::uuid(), + 'can_specify_subdomains' => (int) $request->get('can_specify_subdomains') ]; $this->userRepository diff --git a/app/Server/Http/Controllers/ControlMessageController.php b/app/Server/Http/Controllers/ControlMessageController.php index 77c6550..d2d4f40 100644 --- a/app/Server/Http/Controllers/ControlMessageController.php +++ b/app/Server/Http/Controllers/ControlMessageController.php @@ -5,6 +5,7 @@ namespace App\Server\Http\Controllers; use App\Contracts\ConnectionManager; use App\Contracts\UserRepository; use App\Http\QueryParameters; +use Illuminate\Support\Arr; use Ratchet\ConnectionInterface; use Ratchet\WebSocket\MessageComponentInterface; use React\Promise\Deferred; @@ -77,8 +78,8 @@ class ControlMessageController implements MessageComponentInterface protected function authenticate(ConnectionInterface $connection, $data) { $this->verifyAuthToken($connection) - ->then(function () use ($connection, $data) { - if (! $this->hasValidSubdomain($connection, $data->subdomain)) { + ->then(function ($user) use ($connection, $data) { + if (! $this->hasValidSubdomain($connection, $data->subdomain, $user)) { return; } @@ -147,8 +148,20 @@ class ControlMessageController implements MessageComponentInterface return $deferred->promise(); } - protected function hasValidSubdomain(ConnectionInterface $connection, ?string $subdomain): bool + protected function hasValidSubdomain(ConnectionInterface $connection, ?string $subdomain, ?array $user): bool { + if (! is_null($user) && $user['can_specify_subdomains'] === 0 && ! is_null($subdomain)) { + $connection->send(json_encode([ + 'event' => 'subdomainTaken', + 'data' => [ + 'message' => config('expose.admin.messages.custom_subdomain_unauthorized'), + ], + ])); + $connection->close(); + + return false; + } + if (! is_null($subdomain)) { $controlConnection = $this->connectionManager->findControlConnectionForSubdomain($subdomain); if (! is_null($controlConnection) || $subdomain === config('expose.admin.subdomain')) { diff --git a/app/Server/UserRepository/DatabaseUserRepository.php b/app/Server/UserRepository/DatabaseUserRepository.php index 5f61cb0..43b15b1 100644 --- a/app/Server/UserRepository/DatabaseUserRepository.php +++ b/app/Server/UserRepository/DatabaseUserRepository.php @@ -113,8 +113,8 @@ class DatabaseUserRepository implements UserRepository $deferred = new Deferred(); $this->database->query(" - INSERT INTO users (name, auth_token, created_at) - VALUES (:name, :auth_token, DATETIME('now')) + INSERT INTO users (name, auth_token, can_specify_subdomains, created_at) + VALUES (:name, :auth_token, :can_specify_subdomains, DATETIME('now')) ", $data) ->then(function (Result $result) use ($deferred) { $this->database->query('SELECT * FROM users WHERE id = :id', ['id' => $result->insertId]) diff --git a/config/expose.php b/config/expose.php index e08b519..7835ceb 100644 --- a/config/expose.php +++ b/config/expose.php @@ -230,6 +230,8 @@ return [ 'invalid_auth_token' => 'Authentication failed. Please check your authentication token and try again.', 'subdomain_taken' => 'The chosen subdomain :subdomain is already taken. Please choose a different subdomain.', + + 'custom_subdomain_unauthorized' => 'You are not allowed to specify custom subdomains. Please upgrade to Expose Pro.', ], ], ]; diff --git a/database/migrations/02_add_feature_flags_to_users_table.sql b/database/migrations/02_add_feature_flags_to_users_table.sql new file mode 100644 index 0000000..520d8c3 --- /dev/null +++ b/database/migrations/02_add_feature_flags_to_users_table.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD can_specify_subdomains BOOLEAN DEFAULT 1; diff --git a/resources/views/server/users/index.twig b/resources/views/server/users/index.twig index 6d2ec62..fbdd6e7 100644 --- a/resources/views/server/users/index.twig +++ b/resources/views/server/users/index.twig @@ -24,6 +24,25 @@ +
+ +
+
+
+ + +
+
+
+
@@ -51,6 +70,9 @@ Auth-Token + + Custom Subdomains + Created At @@ -65,6 +87,14 @@ @{ user.auth_token } + + + No + + + Yes + + @{ user.created_at } @@ -113,6 +143,7 @@ data: { userForm: { name: '', + can_specify_subdomains: true, errors: {}, }, paginated: {{ paginated|json_encode|raw }} @@ -140,7 +171,7 @@ }).then((response) => { return response.json(); }).then((data) => { - this.users = this.users.filter(u => u.id !== user.id); + this.getUsers(1) }); }, saveUser() { @@ -155,6 +186,7 @@ }).then((data) => { if (data.user) { this.userForm.name = ''; + this.userForm.can_specify_subdomains = 0; this.userForm.errors = {}; this.users.unshift(data.user); } diff --git a/tests/Feature/Server/TunnelTest.php b/tests/Feature/Server/TunnelTest.php index ed60749..cc612f6 100644 --- a/tests/Feature/Server/TunnelTest.php +++ b/tests/Feature/Server/TunnelTest.php @@ -27,6 +27,9 @@ class TunnelTest extends TestCase parent::setUp(); $this->browser = new Browser($this->loop); + $this->browser = $this->browser->withOptions([ + 'followRedirects' => false, + ]); $this->startServer(); } @@ -58,6 +61,8 @@ class TunnelTest extends TestCase { $this->createTestHttpServer(); + $this->app['config']['expose.admin.validate_auth_tokens'] = false; + /** * We create an expose client that connects to our server and shares * the created test HTTP server. @@ -98,22 +103,96 @@ class TunnelTest extends TestCase { $this->app['config']['expose.admin.validate_auth_tokens'] = true; - $this->createTestHttpServer(); + $response = $this->await($this->browser->post('http://127.0.0.1:8080/api/users', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode('username:secret'), + 'Content-Type' => 'application/json', + ], json_encode([ + 'name' => 'Marcel', + 'can_specify_subdomains' => 1, + ]))); - $this->expectException(\UnexpectedValueException::class); + $user = json_decode($response->getBody()->getContents())->user; + + $this->createTestHttpServer(); /** * 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')); + $response = $this->await($client->connectToServer('127.0.0.1:8085', 'tunnel', $user->auth_token)); + + $this->assertSame('tunnel', $response->subdomain); + } + + /** @test */ + public function it_rejects_clients_to_specify_custom_subdomains() + { + $this->app['config']['expose.admin.validate_auth_tokens'] = true; + + $response = $this->await($this->browser->post('http://127.0.0.1:8080/api/users', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode('username:secret'), + 'Content-Type' => 'application/json', + ], json_encode([ + 'name' => 'Marcel', + 'can_specify_subdomains' => 0, + ]))); + + $this->expectException(\UnexpectedValueException::class); + + $user = json_decode($response->getBody()->getContents())->user; + + $this->createTestHttpServer(); + + /** + * We create an expose client that connects to our server and shares + * the created test HTTP server. + */ + $client = $this->createClient(); + $response = $this->await($client->connectToServer('127.0.0.1:8085', 'tunnel', $user->auth_token)); + + $this->assertSame('tunnel', $response->subdomain); + } + + /** @test */ + public function it_allows_clients_to_use_random_subdomains_if_custom_subdomains_are_forbidden() + { + $this->app['config']['expose.admin.validate_auth_tokens'] = true; + + $response = $this->await($this->browser->post('http://127.0.0.1:8080/api/users', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode('username:secret'), + 'Content-Type' => 'application/json', + ], json_encode([ + 'name' => 'Marcel', + 'can_specify_subdomains' => 0, + ]))); + + $user = json_decode($response->getBody()->getContents())->user; + + $this->createTestHttpServer(); + + /** + * We create an expose client that connects to our server and shares + * the created test HTTP server. + */ + $client = $this->createClient(); + $response = $this->await($client->connectToServer('127.0.0.1:8085', '', $user->auth_token)); + + $this->assertInstanceOf(\stdClass::class, $response); } protected function startServer() { + $this->app['config']['expose.admin.subdomain'] = 'expose'; $this->app['config']['expose.admin.database'] = ':memory:'; + $this->app['config']['expose.admin.users'] = [ + 'username' => 'secret', + ]; + $this->serverFactory = new Factory(); $this->serverFactory->setLoop($this->loop)