diff --git a/app/Client/Client.php b/app/Client/Client.php index 0aef2aa..6380539 100644 --- a/app/Client/Client.php +++ b/app/Client/Client.php @@ -197,6 +197,14 @@ class Client protected function attachCommonConnectionListeners(ControlConnection $connection, Deferred $deferred) { + $connection->on('info', function ($data) { + $this->logger->info($data->message); + }); + + $connection->on('error', function ($data) { + $this->logger->error($data->message); + }); + $connection->on('authenticationFailed', function ($data) use ($deferred) { $this->logger->error($data->message); diff --git a/app/Contracts/SubdomainRepository.php b/app/Contracts/SubdomainRepository.php new file mode 100644 index 0000000..a32158b --- /dev/null +++ b/app/Contracts/SubdomainRepository.php @@ -0,0 +1,22 @@ +router->get('/api/users', GetUsersController::class, $adminCondition); $this->router->post('/api/users', StoreUsersController::class, $adminCondition); $this->router->get('/api/users/{id}', GetUserDetailsController::class, $adminCondition); + $this->router->post('/api/subdomains', StoreSubdomainController::class, $adminCondition); + $this->router->delete('/api/subdomains/{subdomain}', DeleteSubdomainController::class, $adminCondition); $this->router->delete('/api/users/{id}', DeleteUsersController::class, $adminCondition); $this->router->get('/api/sites', GetSitesController::class, $adminCondition); $this->router->delete('/api/sites/{id}', DisconnectSiteController::class, $adminCondition); @@ -171,6 +176,7 @@ class Factory $this->bindConfiguration() ->bindSubdomainGenerator() ->bindUserRepository() + ->bindSubdomainRepository() ->bindDatabase() ->ensureDatabaseIsInitialized() ->bindConnectionManager() @@ -207,6 +213,15 @@ class Factory return $this; } + protected function bindSubdomainRepository() + { + app()->singleton(SubdomainRepository::class, function () { + return app(config('expose.admin.subdomain_repository')); + }); + + return $this; + } + protected function bindDatabase() { app()->singleton(DatabaseInterface::class, function () { diff --git a/app/Server/Http/Controllers/Admin/DeleteSubdomainController.php b/app/Server/Http/Controllers/Admin/DeleteSubdomainController.php new file mode 100644 index 0000000..9cad085 --- /dev/null +++ b/app/Server/Http/Controllers/Admin/DeleteSubdomainController.php @@ -0,0 +1,44 @@ +userRepository = $userRepository; + $this->subdomainRepository = $subdomainRepository; + } + + public function handle(Request $request, ConnectionInterface $httpConnection) + { + $this->userRepository->getUserByToken($request->get('auth_token', '')) + ->then(function ($user) use ($request, $httpConnection) { + if (is_null($user)) { + $httpConnection->send(respond_json(['error' => 'The user does not exist'], 404)); + $httpConnection->close(); + + return; + } + + $this->subdomainRepository->deleteSubdomainForUserId($user['id'], $request->get('subdomain')) + ->then(function ($deleted) use ($httpConnection) { + $httpConnection->send(respond_json(['deleted' => $deleted], 200)); + $httpConnection->close(); + }); + }); + } +} diff --git a/app/Server/Http/Controllers/Admin/GetUserDetailsController.php b/app/Server/Http/Controllers/Admin/GetUserDetailsController.php index 2cb3a52..6fe8b0d 100644 --- a/app/Server/Http/Controllers/Admin/GetUserDetailsController.php +++ b/app/Server/Http/Controllers/Admin/GetUserDetailsController.php @@ -2,6 +2,7 @@ namespace App\Server\Http\Controllers\Admin; +use App\Contracts\SubdomainRepository; use App\Contracts\UserRepository; use Illuminate\Http\Request; use Ratchet\ConnectionInterface; @@ -13,21 +14,31 @@ class GetUserDetailsController extends AdminController /** @var UserRepository */ protected $userRepository; - public function __construct(UserRepository $userRepository) + /** @var SubdomainRepository */ + protected $subdomainRepository; + + public function __construct(UserRepository $userRepository, SubdomainRepository $subdomainRepository) { $this->userRepository = $userRepository; + $this->subdomainRepository = $subdomainRepository; } public function handle(Request $request, ConnectionInterface $httpConnection) { $this->userRepository ->getUserById($request->get('id')) - ->then(function ($user) use ($httpConnection) { - $httpConnection->send( - respond_json(['user' => $user]) - ); + ->then(function ($user) use ($httpConnection, $request) { + $this->subdomainRepository->getSubdomainsByUserId($request->get('id')) + ->then(function ($subdomains) use ($httpConnection, $user) { + $httpConnection->send( + respond_json([ + 'user' => $user, + 'subdomains' => $subdomains, + ]) + ); - $httpConnection->close(); + $httpConnection->close(); + }); }); } } diff --git a/app/Server/Http/Controllers/Admin/StoreSubdomainController.php b/app/Server/Http/Controllers/Admin/StoreSubdomainController.php new file mode 100644 index 0000000..b216b96 --- /dev/null +++ b/app/Server/Http/Controllers/Admin/StoreSubdomainController.php @@ -0,0 +1,77 @@ +userRepository = $userRepository; + $this->subdomainRepository = $subdomainRepository; + } + + public function handle(Request $request, ConnectionInterface $httpConnection) + { + $validator = Validator::make($request->all(), [ + 'subdomain' => 'required', + ], [ + 'required' => 'The :attribute field is required.', + ]); + + if ($validator->fails()) { + $httpConnection->send(respond_json(['errors' => $validator->getMessageBag()], 401)); + $httpConnection->close(); + + return; + } + + $this->userRepository->getUserByToken($request->get('auth_token', '')) + ->then(function ($user) use ($httpConnection, $request) { + if (is_null($user)) { + $httpConnection->send(respond_json(['error' => 'The user does not exist'], 404)); + $httpConnection->close(); + + return; + } + + if ($user['can_specify_subdomains'] === 0) { + $httpConnection->send(respond_json(['error' => 'The user is not allowed to reserve subdomains.'], 401)); + $httpConnection->close(); + + return; + } + + $insertData = [ + 'user_id' => $user['id'], + 'subdomain' => $request->get('subdomain'), + ]; + + $this->subdomainRepository + ->storeSubdomain($insertData) + ->then(function ($subdomain) use ($httpConnection) { + if (is_null($subdomain)) { + $httpConnection->send(respond_json(['error' => 'The subdomain is already taken.'], 422)); + $httpConnection->close(); + + return; + } + $httpConnection->send(respond_json(['subdomain' => $subdomain], 200)); + $httpConnection->close(); + }); + }); + } +} diff --git a/app/Server/Http/Controllers/ControlMessageController.php b/app/Server/Http/Controllers/ControlMessageController.php index d1bccc2..dc8b34b 100644 --- a/app/Server/Http/Controllers/ControlMessageController.php +++ b/app/Server/Http/Controllers/ControlMessageController.php @@ -3,6 +3,7 @@ namespace App\Server\Http\Controllers; use App\Contracts\ConnectionManager; +use App\Contracts\SubdomainRepository; use App\Contracts\UserRepository; use App\Http\QueryParameters; use App\Server\Exceptions\NoFreePortAvailable; @@ -20,10 +21,14 @@ class ControlMessageController implements MessageComponentInterface /** @var UserRepository */ protected $userRepository; - public function __construct(ConnectionManager $connectionManager, UserRepository $userRepository) + /** @var SubdomainRepository */ + protected $subdomainRepository; + + public function __construct(ConnectionManager $connectionManager, UserRepository $userRepository, SubdomainRepository $subdomainRepository) { $this->connectionManager = $connectionManager; $this->userRepository = $userRepository; + $this->subdomainRepository = $subdomainRepository; } /** @@ -100,22 +105,26 @@ class ControlMessageController implements MessageComponentInterface protected function handleHttpConnection(ConnectionInterface $connection, $data, $user = null) { - if (! $this->hasValidSubdomain($connection, $data->subdomain, $user)) { - return; - } + $this->hasValidSubdomain($connection, $data->subdomain, $user)->then(function ($subdomain) use ($data, $connection) { + if ($subdomain === false) { + return; + } - $connectionInfo = $this->connectionManager->storeConnection($data->host, $data->subdomain, $connection); + $data->subdomain = $subdomain; - $this->connectionManager->limitConnectionLength($connectionInfo, config('expose.admin.maximum_connection_length')); + $connectionInfo = $this->connectionManager->storeConnection($data->host, $data->subdomain, $connection); - $connection->send(json_encode([ - 'event' => 'authenticated', - 'data' => [ - 'message' => config('expose.admin.messages.message_of_the_day'), - 'subdomain' => $connectionInfo->subdomain, - 'client_id' => $connectionInfo->client_id, - ], - ])); + $this->connectionManager->limitConnectionLength($connectionInfo, config('expose.admin.maximum_connection_length')); + + $connection->send(json_encode([ + 'event' => 'authenticated', + 'data' => [ + 'message' => config('expose.admin.messages.message_of_the_day'), + 'subdomain' => $connectionInfo->subdomain, + 'client_id' => $connectionInfo->client_id, + ], + ])); + }); } protected function handleTcpConnection(ConnectionInterface $connection, $data, $user = null) @@ -203,39 +212,65 @@ class ControlMessageController implements MessageComponentInterface return $deferred->promise(); } - protected function hasValidSubdomain(ConnectionInterface $connection, ?string $subdomain, ?array $user): bool + protected function hasValidSubdomain(ConnectionInterface $connection, ?string $subdomain, ?array $user): PromiseInterface { + /** + * Check if the user can specify a custom subdomain in the first place. + */ if (! is_null($user) && $user['can_specify_subdomains'] === 0 && ! is_null($subdomain)) { $connection->send(json_encode([ - 'event' => 'subdomainTaken', + 'event' => 'info', 'data' => [ - 'message' => config('expose.admin.messages.custom_subdomain_unauthorized'), + 'message' => config('expose.admin.messages.custom_subdomain_unauthorized').PHP_EOL, ], ])); - $connection->close(); - return false; + return \React\Promise\resolve(null); } + /** + * Check if the given subdomain is reserved for a different user. + */ if (! is_null($subdomain)) { - $controlConnection = $this->connectionManager->findControlConnectionForSubdomain($subdomain); - if (! is_null($controlConnection) || $subdomain === config('expose.admin.subdomain')) { - $message = config('expose.admin.messages.subdomain_taken'); - $message = str_replace(':subdomain', $subdomain, $message); + return $this->subdomainRepository->getSubdomainByName($subdomain) + ->then(function ($foundSubdomain) use ($connection, $subdomain, $user) { + if (! is_null($foundSubdomain) && ! is_null($user) && $foundSubdomain['user_id'] !== $user['id']) { + $message = config('expose.admin.messages.subdomain_reserved'); + $message = str_replace(':subdomain', $subdomain, $message); - $connection->send(json_encode([ - 'event' => 'subdomainTaken', - 'data' => [ - 'message' => $message, - ], - ])); - $connection->close(); + $connection->send(json_encode([ + 'event' => 'subdomainTaken', + 'data' => [ + 'message' => $message, + ], + ])); + $connection->close(); - return false; - } + return \React\Promise\resolve(false); + } + + $controlConnection = $this->connectionManager->findControlConnectionForSubdomain($subdomain); + + if (! is_null($controlConnection) || $subdomain === config('expose.admin.subdomain')) { + $message = config('expose.admin.messages.subdomain_taken'); + $message = str_replace(':subdomain', $subdomain, $message); + + $connection->send(json_encode([ + 'event' => 'subdomainTaken', + 'data' => [ + 'message' => $message, + ], + ])); + $connection->close(); + + return \React\Promise\resolve(false); + } + + return \React\Promise\resolve($subdomain); + }); } - return true; + return \React\Promise\resolve($subdomain); } protected function canShareTcpPorts(ConnectionInterface $connection, $data, $user) diff --git a/app/Server/SubdomainRepository/DatabaseSubdomainRepository.php b/app/Server/SubdomainRepository/DatabaseSubdomainRepository.php new file mode 100644 index 0000000..b6b0de8 --- /dev/null +++ b/app/Server/SubdomainRepository/DatabaseSubdomainRepository.php @@ -0,0 +1,132 @@ +database = $database; + } + + public function getSubdomains(): PromiseInterface + { + $deferred = new Deferred(); + + $this->database + ->query('SELECT * FROM subdomains ORDER by created_at DESC') + ->then(function (Result $result) use ($deferred) { + $deferred->resolve($result->rows); + }); + + return $deferred->promise(); + } + + public function getSubdomainById($id): PromiseInterface + { + $deferred = new Deferred(); + + $this->database + ->query('SELECT * FROM subdomains WHERE id = :id', ['id' => $id]) + ->then(function (Result $result) use ($deferred) { + $deferred->resolve($result->rows[0] ?? null); + }); + + return $deferred->promise(); + } + + public function getSubdomainByName(string $name): PromiseInterface + { + $deferred = new Deferred(); + + $this->database + ->query('SELECT * FROM subdomains WHERE subdomain = :name', ['name' => $name]) + ->then(function (Result $result) use ($deferred) { + $deferred->resolve($result->rows[0] ?? null); + }); + + return $deferred->promise(); + } + + public function getSubdomainsByUserId($id): PromiseInterface + { + $deferred = new Deferred(); + + $this->database + ->query('SELECT * FROM subdomains WHERE user_id = :user_id ORDER by created_at DESC', [ + 'user_id' => $id, + ]) + ->then(function (Result $result) use ($deferred) { + $deferred->resolve($result->rows); + }); + + return $deferred->promise(); + } + + public function storeSubdomain(array $data): PromiseInterface + { + $deferred = new Deferred(); + + $this->getSubdomainByName($data['subdomain']) + ->then(function ($registeredSubdomain) use ($data, $deferred) { + if (! is_null($registeredSubdomain)) { + $deferred->resolve(null); + + return; + } + + $this->database->query(" + INSERT INTO subdomains (user_id, subdomain, created_at) + VALUES (:user_id, :subdomain, DATETIME('now')) + ", $data) + ->then(function (Result $result) use ($deferred) { + $this->database->query('SELECT * FROM subdomains WHERE id = :id', ['id' => $result->insertId]) + ->then(function (Result $result) use ($deferred) { + $deferred->resolve($result->rows[0]); + }); + }); + }); + + return $deferred->promise(); + } + + public function getSubdomainsByUserIdAndName($id, $name): PromiseInterface + { + $deferred = new Deferred(); + + $this->database + ->query('SELECT * FROM subdomains WHERE user_id = :user_id AND subdomain = :name ORDER by created_at DESC', [ + 'user_id' => $id, + 'name' => $name, + ]) + ->then(function (Result $result) use ($deferred) { + $deferred->resolve($result->rows); + }); + + return $deferred->promise(); + } + + public function deleteSubdomainForUserId($userId, $subdomainId): PromiseInterface + { + $deferred = new Deferred(); + + $this->database->query('DELETE FROM subdomains WHERE id = :id AND user_id = :user_id', [ + 'id' => $subdomainId, + 'user_id' => $userId, + ]) + ->then(function (Result $result) use ($deferred) { + $deferred->resolve($result); + }); + + return $deferred->promise(); + } +} diff --git a/config/expose.php b/config/expose.php index a48fa6f..f1a754f 100644 --- a/config/expose.php +++ b/config/expose.php @@ -232,6 +232,8 @@ return [ */ 'user_repository' => \App\Server\UserRepository\DatabaseUserRepository::class, + 'subdomain_repository' => \App\Server\SubdomainRepository\DatabaseSubdomainRepository::class, + /* |-------------------------------------------------------------------------- | Messages @@ -249,7 +251,7 @@ return [ '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.', + 'custom_subdomain_unauthorized' => 'You are not allowed to specify custom subdomains. Please upgrade to Expose Pro. Assigning a random subdomain instead.', 'no_free_tcp_port_available' => 'There are no free TCP ports available on this server. Please try again later.', ], diff --git a/database/migrations/04_create_subdomains_table.sql b/database/migrations/04_create_subdomains_table.sql new file mode 100644 index 0000000..adedb65 --- /dev/null +++ b/database/migrations/04_create_subdomains_table.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS subdomains ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + subdomain STRING NOT NULL, + created_at DATETIME, + updated_at DATETIME +) diff --git a/tests/Feature/Server/ApiTest.php b/tests/Feature/Server/ApiTest.php index 620a91e..3bdde90 100644 --- a/tests/Feature/Server/ApiTest.php +++ b/tests/Feature/Server/ApiTest.php @@ -5,6 +5,7 @@ namespace Tests\Feature\Server; use App\Contracts\ConnectionManager; use App\Server\Factory; use Clue\React\Buzz\Browser; +use Clue\React\Buzz\Message\ResponseException; use GuzzleHttp\Psr7\Response; use Nyholm\Psr7\Request; use Ratchet\Server\IoConnection; @@ -65,10 +66,10 @@ class ApiTest extends TestCase } /** @test */ - public function it_can_get_user_details() + public function it_does_not_allow_subdomain_reservation_for_users_without_the_right_flag() { /** @var Response $response */ - $this->await($this->browser->post('http://127.0.0.1:8080/api/users', [ + $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', @@ -76,6 +77,72 @@ class ApiTest extends TestCase 'name' => 'Marcel', ]))); + $user = json_decode($response->getBody()->getContents())->user; + + $this->expectException(ResponseException::class); + $this->expectExceptionMessage('HTTP status code 401'); + + $this->await($this->browser->post('http://127.0.0.1:8080/api/subdomains', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode('username:secret'), + 'Content-Type' => 'application/json', + ], json_encode([ + 'auth_token' => $user->auth_token, + 'subdomain' => 'reserved', + ]))); + } + + /** @test */ + public function it_allows_subdomain_reservation_for_users_with_the_right_flag() + { + /** @var Response $response */ + $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, + ]))); + + $user = json_decode($response->getBody()->getContents())->user; + + $response = $this->await($this->browser->post('http://127.0.0.1:8080/api/subdomains', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode('username:secret'), + 'Content-Type' => 'application/json', + ], json_encode([ + 'auth_token' => $user->auth_token, + 'subdomain' => 'reserved', + ]))); + + $this->assertSame(200, $response->getStatusCode()); + } + + /** @test */ + public function it_can_get_user_details() + { + /** @var Response $response */ + $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, + ]))); + + $user = json_decode($response->getBody()->getContents())->user; + + $this->await($this->browser->post('http://127.0.0.1:8080/api/subdomains', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode('username:secret'), + 'Content-Type' => 'application/json', + ], json_encode([ + 'auth_token' => $user->auth_token, + 'subdomain' => 'reserved', + ]))); + /** @var Response $response */ $response = $this->await($this->browser->get('http://127.0.0.1:8080/api/users/1', [ 'Host' => 'expose.localhost', @@ -85,10 +152,117 @@ class ApiTest extends TestCase $body = json_decode($response->getBody()->getContents()); $user = $body->user; + $subdomains = $body->subdomains; $this->assertSame('Marcel', $user->name); $this->assertSame([], $user->sites); $this->assertSame([], $user->tcp_connections); + + $this->assertCount(1, $subdomains); + } + + /** @test */ + public function it_can_delete_subdomains() + { + /** @var Response $response */ + $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, + ]))); + + $user = json_decode($response->getBody()->getContents())->user; + + $response = $this->await($this->browser->post('http://127.0.0.1:8080/api/subdomains', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode('username:secret'), + 'Content-Type' => 'application/json', + ], json_encode([ + 'subdomain' => 'reserved', + 'auth_token' => $user->auth_token, + ]))); + + $this->await($this->browser->delete('http://127.0.0.1:8080/api/subdomains/1', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode('username:secret'), + 'Content-Type' => 'application/json', + ], json_encode([ + 'auth_token' => $user->auth_token, + ]))); + + /** @var Response $response */ + $response = $this->await($this->browser->get('http://127.0.0.1:8080/api/users/1', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode('username:secret'), + 'Content-Type' => 'application/json', + ])); + + $body = json_decode($response->getBody()->getContents()); + $subdomains = $body->subdomains; + + $this->assertCount(0, $subdomains); + } + + /** @test */ + public function it_can_not_reserve_an_already_reserved_subdomain() + { + /** @var Response $response */ + $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, + ]))); + + $user = json_decode($response->getBody()->getContents())->user; + + $this->await($this->browser->post('http://127.0.0.1:8080/api/subdomains', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode('username:secret'), + 'Content-Type' => 'application/json', + ], json_encode([ + 'subdomain' => 'reserved', + 'auth_token' => $user->auth_token, + ]))); + + $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' => 'Sebastian', + 'can_specify_subdomains' => 1, + ]))); + + $user = json_decode($response->getBody()->getContents())->user; + + $this->expectException(ResponseException::class); + $this->expectExceptionMessage('HTTP status code 422'); + + $this->await($this->browser->post('http://127.0.0.1:8080/api/subdomains', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode('username:secret'), + 'Content-Type' => 'application/json', + ], json_encode([ + 'subdomain' => 'reserved', + 'auth_token' => $user->auth_token, + ]))); + + $response = $this->await($this->browser->get('http://127.0.0.1:8080/api/users/2', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode('username:secret'), + 'Content-Type' => 'application/json', + ])); + + $body = json_decode($response->getBody()->getContents()); + $subdomains = $body->subdomains; + + $this->assertCount(0, $subdomains); } /** @test */ diff --git a/tests/Feature/Server/TunnelTest.php b/tests/Feature/Server/TunnelTest.php index 73f543c..3474501 100644 --- a/tests/Feature/Server/TunnelTest.php +++ b/tests/Feature/Server/TunnelTest.php @@ -235,8 +235,6 @@ class TunnelTest extends TestCase 'can_specify_subdomains' => 0, ]))); - $this->expectException(\UnexpectedValueException::class); - $user = json_decode($response->getBody()->getContents())->user; $this->createTestHttpServer(); @@ -248,7 +246,92 @@ class TunnelTest extends TestCase $client = $this->createClient(); $response = $this->await($client->connectToServer('127.0.0.1:8085', 'tunnel', $user->auth_token)); - $this->assertSame('tunnel', $response->subdomain); + $this->assertNotSame('tunnel', $response->subdomain); + } + + /** @test */ + public function it_rejects_users_that_want_to_use_a_reserved_subdomain() + { + $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' => 1, + ]))); + + $user = json_decode($response->getBody()->getContents())->user; + + $response = $this->await($this->browser->post('http://127.0.0.1:8080/api/subdomains', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode('username:secret'), + 'Content-Type' => 'application/json', + ], json_encode([ + 'subdomain' => 'reserved', + 'auth_token' => $user->auth_token, + ]))); + + $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' => 'Test-User', + 'can_specify_subdomains' => 1, + ]))); + + $user = json_decode($response->getBody()->getContents())->user; + + $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(); + $response = $this->await($client->connectToServer('127.0.0.1:8085', 'reserved', $user->auth_token)); + + $this->assertSame('reserved', $response->subdomain); + } + + /** @test */ + public function it_allows_users_to_use_their_own_reserved_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' => 1, + ]))); + + $user = json_decode($response->getBody()->getContents())->user; + + $response = $this->await($this->browser->post('http://127.0.0.1:8080/api/subdomains', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode('username:secret'), + 'Content-Type' => 'application/json', + ], json_encode([ + 'subdomain' => 'reserved', + 'auth_token' => $user->auth_token, + ]))); + + $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', 'reserved', $user->auth_token)); + + $this->assertSame('reserved', $response->subdomain); } /** @test */