From 74ac9d2d1aac0ae7c7284cc65e7e56aade41e663 Mon Sep 17 00:00:00 2001 From: Marcel Pociot Date: Wed, 2 Jun 2021 10:21:36 +0200 Subject: [PATCH] Custom domain support --- app/Client/Client.php | 7 +- app/Contracts/DomainRepository.php | 24 +++ app/Contracts/SubdomainRepository.php | 2 + .../DatabaseDomainRepository.php | 135 +++++++++++++ app/Server/Factory.php | 18 +- .../Admin/StoreDomainController.php | 72 +++++++ .../Admin/StoreSubdomainController.php | 8 +- .../Admin/StoreUsersController.php | 1 + .../Controllers/ControlMessageController.php | 83 ++++++-- .../DatabaseSubdomainRepository.php | 20 +- .../UserRepository/DatabaseUserRepository.php | 4 +- config/expose.php | 2 + .../08_add_domain_to_subdomains_table.sql | 10 + tests/Feature/Server/ApiTest.php | 54 +++++ tests/Feature/Server/TunnelTest.php | 185 ++++++++++++++++++ 15 files changed, 594 insertions(+), 31 deletions(-) create mode 100644 app/Contracts/DomainRepository.php create mode 100644 app/Server/DomainRepository/DatabaseDomainRepository.php create mode 100644 app/Server/Http/Controllers/Admin/StoreDomainController.php create mode 100644 database/migrations/08_add_domain_to_subdomains_table.sql diff --git a/app/Client/Client.php b/app/Client/Client.php index 88a3ad4..b1a9068 100644 --- a/app/Client/Client.php +++ b/app/Client/Client.php @@ -109,14 +109,11 @@ class Client $httpProtocol = $this->configuration->port() === 443 ? 'https' : 'http'; $host = $data->server_host; - if ($httpProtocol !== 'https') { - $host .= ":{$this->configuration->port()}"; - } - $this->logger->info($data->message); $this->logger->info("Local-URL:\t\t{$sharedUrl}"); $this->logger->info("Dashboard-URL:\t\thttp://127.0.0.1:".config()->get('expose.dashboard_port')); - $this->logger->info("Expose-URL:\t\t{$httpProtocol}://{$data->subdomain}.{$host}"); + $this->logger->info("Expose-URL:\t\thttp://{$data->subdomain}.{$host}:{$this->configuration->port()}"); + $this->logger->info("Expose-URL:\t\thttps://{$data->subdomain}.{$host}"); $this->logger->line(''); static::$subdomains[] = "{$httpProtocol}://{$data->subdomain}.{$data->server_host}"; diff --git a/app/Contracts/DomainRepository.php b/app/Contracts/DomainRepository.php new file mode 100644 index 0000000..1e09987 --- /dev/null +++ b/app/Contracts/DomainRepository.php @@ -0,0 +1,24 @@ +database = $database; + } + + public function getDomains(): PromiseInterface + { + $deferred = new Deferred(); + + $this->database + ->query('SELECT * FROM domains ORDER by created_at DESC') + ->then(function (Result $result) use ($deferred) { + $deferred->resolve($result->rows); + }); + + return $deferred->promise(); + } + + public function getDomainById($id): PromiseInterface + { + $deferred = new Deferred(); + + $this->database + ->query('SELECT * FROM domains WHERE id = :id', ['id' => $id]) + ->then(function (Result $result) use ($deferred) { + $deferred->resolve($result->rows[0] ?? null); + }); + + return $deferred->promise(); + } + + public function getDomainByName(string $name): PromiseInterface + { + $deferred = new Deferred(); + + $this->database + ->query('SELECT * FROM domains WHERE domain = :name', ['name' => $name]) + ->then(function (Result $result) use ($deferred) { + $deferred->resolve($result->rows[0] ?? null); + }); + + return $deferred->promise(); + } + + public function getDomainsByUserId($id): PromiseInterface + { + $deferred = new Deferred(); + + $this->database + ->query('SELECT * FROM domains 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 storeDomain(array $data): PromiseInterface + { + $deferred = new Deferred(); + + $this->getDomainByName($data['domain']) + ->then(function ($registeredDomain) use ($data, $deferred) { + $this->database->query(" + INSERT INTO domains (user_id, domain, created_at) + VALUES (:user_id, :domain, DATETIME('now')) + ", $data) + ->then(function (Result $result) use ($deferred) { + $this->database->query('SELECT * FROM domains WHERE id = :id', ['id' => $result->insertId]) + ->then(function (Result $result) use ($deferred) { + $deferred->resolve($result->rows[0]); + }); + }); + }); + + return $deferred->promise(); + } + + public function getDomainsByUserIdAndName($id, $name): PromiseInterface + { + $deferred = new Deferred(); + + $this->database + ->query('SELECT * FROM domains WHERE user_id = :user_id AND domain = :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 deleteDomainForUserId($userId, $domainId): PromiseInterface + { + $deferred = new Deferred(); + + $this->database->query('DELETE FROM domains WHERE id = :id AND user_id = :user_id', [ + 'id' => $domainId, + 'user_id' => $userId, + ]) + ->then(function (Result $result) use ($deferred) { + $deferred->resolve($result); + }); + + return $deferred->promise(); + } + + public function updateDomain($id, array $data): PromiseInterface + { + $deferred = new Deferred(); + + // TODO + + return $deferred->promise(); + } +} diff --git a/app/Server/Factory.php b/app/Server/Factory.php index 9b396bb..27e03a5 100644 --- a/app/Server/Factory.php +++ b/app/Server/Factory.php @@ -3,6 +3,7 @@ namespace App\Server; use App\Contracts\ConnectionManager as ConnectionManagerContract; +use App\Contracts\DomainRepository; use App\Contracts\StatisticsCollector; use App\Contracts\StatisticsRepository; use App\Contracts\SubdomainGenerator; @@ -11,6 +12,7 @@ use App\Contracts\UserRepository; use App\Http\RouteGenerator; use App\Http\Server as HttpServer; use App\Server\Connections\ConnectionManager; +use App\Server\DomainRepository\DatabaseDomainRepository; use App\Server\Http\Controllers\Admin\DeleteSubdomainController; use App\Server\Http\Controllers\Admin\DeleteUsersController; use App\Server\Http\Controllers\Admin\DisconnectSiteController; @@ -26,6 +28,7 @@ use App\Server\Http\Controllers\Admin\ListTcpConnectionsController; use App\Server\Http\Controllers\Admin\ListUsersController; use App\Server\Http\Controllers\Admin\RedirectToUsersController; use App\Server\Http\Controllers\Admin\ShowSettingsController; +use App\Server\Http\Controllers\Admin\StoreDomainController; use App\Server\Http\Controllers\Admin\StoreSettingsController; use App\Server\Http\Controllers\Admin\StoreSubdomainController; use App\Server\Http\Controllers\Admin\StoreUsersController; @@ -34,6 +37,7 @@ use App\Server\Http\Controllers\TunnelMessageController; use App\Server\Http\Router; use App\Server\StatisticsCollector\DatabaseStatisticsCollector; use App\Server\StatisticsRepository\DatabaseStatisticsRepository; +use App\Server\SubdomainRepository\DatabaseSubdomainRepository; use Clue\React\SQLite\DatabaseInterface; use Phar; use Ratchet\Server\IoServer; @@ -139,6 +143,8 @@ class Factory $this->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/domains', StoreDomainController::class, $adminCondition); + $this->router->delete('/api/domains/{domain}', DeleteSubdomainController::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); @@ -183,6 +189,7 @@ class Factory ->bindSubdomainGenerator() ->bindUserRepository() ->bindSubdomainRepository() + ->bindDomainRepository() ->bindDatabase() ->ensureDatabaseIsInitialized() ->registerStatisticsCollector() @@ -223,7 +230,16 @@ class Factory protected function bindSubdomainRepository() { app()->singleton(SubdomainRepository::class, function () { - return app(config('expose.admin.subdomain_repository')); + return app(config('expose.admin.subdomain_repository', DatabaseSubdomainRepository::class)); + }); + + return $this; + } + + protected function bindDomainRepository() + { + app()->singleton(DomainRepository::class, function () { + return app(config('expose.admin.domain_repository', DatabaseDomainRepository::class)); }); return $this; diff --git a/app/Server/Http/Controllers/Admin/StoreDomainController.php b/app/Server/Http/Controllers/Admin/StoreDomainController.php new file mode 100644 index 0000000..be85ed5 --- /dev/null +++ b/app/Server/Http/Controllers/Admin/StoreDomainController.php @@ -0,0 +1,72 @@ +userRepository = $userRepository; + $this->domainRepository = $domainRepository; + } + + public function handle(Request $request, ConnectionInterface $httpConnection) + { + $validator = Validator::make($request->all(), [ + 'domain' => '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_domains'] === 0) { + $httpConnection->send(respond_json(['error' => 'The user is not allowed to reserve custom domains.'], 401)); + $httpConnection->close(); + + return; + } + + $insertData = [ + 'user_id' => $user['id'], + 'domain' => $request->get('domain'), + ]; + + $this->domainRepository + ->storeDomain($insertData) + ->then(function ($domain) use ($httpConnection) { + $httpConnection->send(respond_json(['domain' => $domain], 200)); + $httpConnection->close(); + }); + }); + } +} diff --git a/app/Server/Http/Controllers/Admin/StoreSubdomainController.php b/app/Server/Http/Controllers/Admin/StoreSubdomainController.php index d564267..30aaf8f 100644 --- a/app/Server/Http/Controllers/Admin/StoreSubdomainController.php +++ b/app/Server/Http/Controllers/Admin/StoreSubdomainController.php @@ -4,6 +4,7 @@ namespace App\Server\Http\Controllers\Admin; use App\Contracts\SubdomainRepository; use App\Contracts\UserRepository; +use App\Server\Configuration; use Illuminate\Http\Request; use Illuminate\Support\Facades\Validator; use Ratchet\ConnectionInterface; @@ -18,10 +19,14 @@ class StoreSubdomainController extends AdminController /** @var UserRepository */ protected $userRepository; - public function __construct(UserRepository $userRepository, SubdomainRepository $subdomainRepository) + /** @var Configuration */ + protected $configuration; + + public function __construct(UserRepository $userRepository, SubdomainRepository $subdomainRepository, Configuration $configuration) { $this->userRepository = $userRepository; $this->subdomainRepository = $subdomainRepository; + $this->configuration = $configuration; } public function handle(Request $request, ConnectionInterface $httpConnection) @@ -66,6 +71,7 @@ class StoreSubdomainController extends AdminController $insertData = [ 'user_id' => $user['id'], 'subdomain' => $request->get('subdomain'), + 'domain' => $request->get('domain', $this->configuration->hostname()), ]; $this->subdomainRepository diff --git a/app/Server/Http/Controllers/Admin/StoreUsersController.php b/app/Server/Http/Controllers/Admin/StoreUsersController.php index ada3d74..b846f92 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 'name' => $request->get('name'), 'auth_token' => (string) Str::uuid(), 'can_specify_subdomains' => (int) $request->get('can_specify_subdomains'), + 'can_specify_domains' => (int) $request->get('can_specify_domains'), 'can_share_tcp_ports' => (int) $request->get('can_share_tcp_ports'), 'max_connections' => (int) $request->get('max_connections'), ]; diff --git a/app/Server/Http/Controllers/ControlMessageController.php b/app/Server/Http/Controllers/ControlMessageController.php index 3b32389..3a99919 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\DomainRepository; use App\Contracts\SubdomainRepository; use App\Contracts\UserRepository; use App\Http\QueryParameters; @@ -27,14 +28,18 @@ class ControlMessageController implements MessageComponentInterface /** @var SubdomainRepository */ protected $subdomainRepository; + /** @var DomainRepository */ + protected $domainRepository; + /** @var Configuration */ protected $configuration; - public function __construct(ConnectionManager $connectionManager, UserRepository $userRepository, SubdomainRepository $subdomainRepository, Configuration $configuration) + public function __construct(ConnectionManager $connectionManager, UserRepository $userRepository, SubdomainRepository $subdomainRepository, Configuration $configuration, DomainRepository $domainRepository) { $this->connectionManager = $connectionManager; $this->userRepository = $userRepository; $this->subdomainRepository = $subdomainRepository; + $this->domainRepository = $domainRepository; $this->configuration = $configuration; } @@ -147,27 +152,31 @@ class ControlMessageController implements MessageComponentInterface protected function handleHttpConnection(ConnectionInterface $connection, $data, $user = null) { - $this->hasValidSubdomain($connection, $data->subdomain, $user, $data->server_host)->then(function ($subdomain) use ($data, $connection) { - if ($subdomain === false) { - return; - } + $this->hasValidDomain($connection, $data->server_host, $user) + ->then(function () use ($connection, $data, $user) { + return $this->hasValidSubdomain($connection, $data->subdomain, $user, $data->server_host); + }) + ->then(function ($subdomain) use ($data, $connection) { + if ($subdomain === false) { + return; + } - $data->subdomain = $subdomain; + $data->subdomain = $subdomain; - $connectionInfo = $this->connectionManager->storeConnection($data->host, $data->subdomain, $data->server_host, $connection); + $connectionInfo = $this->connectionManager->storeConnection($data->host, $data->subdomain, $data->server_host, $connection); - $this->connectionManager->limitConnectionLength($connectionInfo, config('expose.admin.maximum_connection_length')); + $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, - 'server_host' => $connectionInfo->serverHost, - 'client_id' => $connectionInfo->client_id, - ], - ])); - }); + $connection->send(json_encode([ + 'event' => 'authenticated', + 'data' => [ + 'message' => config('expose.admin.messages.message_of_the_day'), + 'subdomain' => $connectionInfo->subdomain, + 'server_host' => $connectionInfo->serverHost, + 'client_id' => $connectionInfo->client_id, + ], + ])); + }); } protected function handleTcpConnection(ConnectionInterface $connection, $data, $user = null) @@ -259,6 +268,40 @@ class ControlMessageController implements MessageComponentInterface return $deferred->promise(); } + protected function hasValidDomain(ConnectionInterface $connection, ?string $serverHost, ?array $user): PromiseInterface + { + if (! is_null($user) && $serverHost !== $this->configuration->hostname()) { + $deferred = new Deferred(); + + $this->domainRepository + ->getDomainsByUserId($user['id']) + ->then(function ($domains) use ($connection, $deferred, $user, $serverHost) { + $userDomain = collect($domains)->first(function ($domain) use ($serverHost) { + return strtolower($domain['domain']) === strtolower($serverHost); + }); + + if (is_null($userDomain)) { + $connection->send(json_encode([ + 'event' => 'authenticationFailed', + 'data' => [ + 'message' => config('expose.admin.messages.custom_domain_unauthorized').PHP_EOL, + ], + ])); + $connection->close(); + + $deferred->reject(null); + return; + } + + $deferred->resolve(null); + }); + + return $deferred->promise(); + } else { + return \React\Promise\resolve(null); + } + } + protected function hasValidSubdomain(ConnectionInterface $connection, ?string $subdomain, ?array $user, string $serverHost): PromiseInterface { /** @@ -279,9 +322,9 @@ class ControlMessageController implements MessageComponentInterface * Check if the given subdomain is reserved for a different user. */ if (! is_null($subdomain)) { - return $this->subdomainRepository->getSubdomainByName($subdomain) + return $this->subdomainRepository->getSubdomainByNameAndDomain($subdomain, $serverHost) ->then(function ($foundSubdomain) use ($connection, $subdomain, $user, $serverHost) { - if (! is_null($foundSubdomain) && ! is_null($user) && $foundSubdomain['user_id'] !== $user['id']) { + 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); diff --git a/app/Server/SubdomainRepository/DatabaseSubdomainRepository.php b/app/Server/SubdomainRepository/DatabaseSubdomainRepository.php index b6b0de8..ce742dd 100644 --- a/app/Server/SubdomainRepository/DatabaseSubdomainRepository.php +++ b/app/Server/SubdomainRepository/DatabaseSubdomainRepository.php @@ -57,6 +57,22 @@ class DatabaseSubdomainRepository implements SubdomainRepository return $deferred->promise(); } + public function getSubdomainByNameAndDomain(string $name, string $domain): PromiseInterface + { + $deferred = new Deferred(); + + $this->database + ->query('SELECT * FROM subdomains WHERE subdomain = :name AND domain = :domain', [ + 'name' => $name, + 'domain' => $domain + ]) + ->then(function (Result $result) use ($deferred) { + $deferred->resolve($result->rows[0] ?? null); + }); + + return $deferred->promise(); + } + public function getSubdomainsByUserId($id): PromiseInterface { $deferred = new Deferred(); @@ -85,8 +101,8 @@ class DatabaseSubdomainRepository implements SubdomainRepository } $this->database->query(" - INSERT INTO subdomains (user_id, subdomain, created_at) - VALUES (:user_id, :subdomain, DATETIME('now')) + INSERT INTO subdomains (user_id, subdomain, domain, created_at) + VALUES (:user_id, :subdomain, :domain, DATETIME('now')) ", $data) ->then(function (Result $result) use ($deferred) { $this->database->query('SELECT * FROM subdomains WHERE id = :id', ['id' => $result->insertId]) diff --git a/app/Server/UserRepository/DatabaseUserRepository.php b/app/Server/UserRepository/DatabaseUserRepository.php index cddc841..5b472bc 100644 --- a/app/Server/UserRepository/DatabaseUserRepository.php +++ b/app/Server/UserRepository/DatabaseUserRepository.php @@ -151,8 +151,8 @@ class DatabaseUserRepository implements UserRepository $deferred = new Deferred(); $this->database->query(" - INSERT INTO users (name, auth_token, can_specify_subdomains, can_share_tcp_ports, max_connections, created_at) - VALUES (:name, :auth_token, :can_specify_subdomains, :can_share_tcp_ports, :max_connections, DATETIME('now')) + INSERT INTO users (name, auth_token, can_specify_subdomains, can_specify_domains, can_share_tcp_ports, max_connections, created_at) + VALUES (:name, :auth_token, :can_specify_subdomains, :can_specify_domains, :can_share_tcp_ports, :max_connections, 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 35d731d..303438a 100644 --- a/config/expose.php +++ b/config/expose.php @@ -321,6 +321,8 @@ return [ 'custom_subdomain_unauthorized' => 'You are not allowed to specify custom subdomains. Please upgrade to Expose Pro. Assigning a random subdomain instead.', + 'custom_domain_unauthorized' => 'You are not allowed to use this custom domain.', + 'tcp_port_sharing_unauthorized' => 'You are not allowed to share TCP ports. Please upgrade to Expose Pro.', 'no_free_tcp_port_available' => 'There are no free TCP ports available on this server. Please try again later.', diff --git a/database/migrations/08_add_domain_to_subdomains_table.sql b/database/migrations/08_add_domain_to_subdomains_table.sql new file mode 100644 index 0000000..08abf8a --- /dev/null +++ b/database/migrations/08_add_domain_to_subdomains_table.sql @@ -0,0 +1,10 @@ +ALTER TABLE users ADD can_specify_domains BOOLEAN DEFAULT 1; +ALTER TABLE subdomains ADD domain STRING; + +CREATE TABLE IF NOT EXISTS domains ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + domain STRING NOT NULL, + created_at DATETIME, + updated_at DATETIME +) diff --git a/tests/Feature/Server/ApiTest.php b/tests/Feature/Server/ApiTest.php index b3abd60..b7f749f 100644 --- a/tests/Feature/Server/ApiTest.php +++ b/tests/Feature/Server/ApiTest.php @@ -65,6 +65,60 @@ class ApiTest extends TestCase $this->assertSame([], $users[0]->sites); } + /** @test */ + public function it_does_not_allow_domain_reservation_for_users_without_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', + ]))); + + $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/domains', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode('username:secret'), + 'Content-Type' => 'application/json', + ], json_encode([ + 'auth_token' => $user->auth_token, + 'domain' => 'reserved', + ]))); + } + + /** @test */ + public function it_allows_domain_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_domains' => 1, + ]))); + + $user = json_decode($response->getBody()->getContents())->user; + + $response = $this->await($this->browser->post('http://127.0.0.1:8080/api/domains', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode('username:secret'), + 'Content-Type' => 'application/json', + ], json_encode([ + 'auth_token' => $user->auth_token, + 'domain' => 'reserved', + ]))); + + $this->assertSame(200, $response->getStatusCode()); + } + /** @test */ public function it_does_not_allow_subdomain_reservation_for_users_without_the_right_flag() { diff --git a/tests/Feature/Server/TunnelTest.php b/tests/Feature/Server/TunnelTest.php index d849f33..0caefa3 100644 --- a/tests/Feature/Server/TunnelTest.php +++ b/tests/Feature/Server/TunnelTest.php @@ -277,6 +277,53 @@ class TunnelTest extends TestCase $this->assertSame('reserved', $response->subdomain); } + /** @test */ + public function it_rejects_users_that_want_to_use_a_reserved_subdomain_on_a_custom_domain() + { + $this->app['config']['expose.admin.validate_auth_tokens'] = true; + + $user = $this->createUser([ + 'name' => 'Marcel', + 'can_specify_domains' => 1, + 'can_specify_subdomains' => 1, + ]); + + $this->await($this->browser->post('http://127.0.0.1:8080/api/domains', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode('username:secret'), + 'Content-Type' => 'application/json', + ], json_encode([ + 'domain' => 'share.beyondco.de', + 'auth_token' => $user->auth_token, + ]))); + + $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', + 'domain' => 'share.beyondco.de', + 'auth_token' => $user->auth_token, + ]))); + + $user = $this->createUser([ + 'name' => 'Test-User', + 'can_specify_subdomains' => 1, + ]); + + $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', null, $user->auth_token)); + + $this->assertSame('reserved', $response->subdomain); + } + /** @test */ public function it_rejects_users_that_want_to_use_a_subdomain_that_is_already_in_use() { @@ -306,9 +353,19 @@ class TunnelTest extends TestCase $user = $this->createUser([ 'name' => 'Marcel', + 'can_specify_domains' => 1, 'can_specify_subdomains' => 1, ]); + $this->await($this->browser->post('http://127.0.0.1:8080/api/domains', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode('username:secret'), + 'Content-Type' => 'application/json', + ], json_encode([ + 'domain' => 'share.beyondco.de', + 'auth_token' => $user->auth_token, + ]))); + $this->createTestHttpServer(); $client = $this->createClient(); @@ -322,6 +379,93 @@ class TunnelTest extends TestCase $this->assertSame('taken', $response->subdomain); } + /** @test */ + public function it_allows_users_that_want_to_use_a_reserved_subdomain_on_a_custom_domain() + { + $this->app['config']['expose.admin.validate_auth_tokens'] = true; + + $user = $this->createUser([ + 'name' => 'Marcel', + 'can_specify_subdomains' => 1, + ]); + + $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, + ]))); + + $user = $this->createUser([ + 'name' => 'Test-User', + 'can_specify_subdomains' => 1, + 'can_specify_domains' => 1, + ]); + + $this->await($this->browser->post('http://127.0.0.1:8080/api/domains', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode('username:secret'), + 'Content-Type' => 'application/json', + ], json_encode([ + 'domain' => 'beyondco.de', + 'auth_token' => $user->auth_token, + ]))); + + $this->createTestHttpServer(); + + $client = $this->createClient(); + $response = $this->await($client->connectToServer('127.0.0.1:8085', 'reserved', 'beyondco.de', $user->auth_token)); + + $this->assertSame('reserved', $response->subdomain); + } + + /** @test */ + public function it_rejects_users_that_want_to_use_a_reserved_subdomain_on_a_custom_domain_that_does_not_belong_them() + { + $this->app['config']['expose.admin.validate_auth_tokens'] = true; + + $user = $this->createUser([ + 'name' => 'Marcel', + 'can_specify_subdomains' => 1, + 'can_specify_domains' => 1, + ]); + + $this->await($this->browser->post('http://127.0.0.1:8080/api/domains', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode('username:secret'), + 'Content-Type' => 'application/json', + ], json_encode([ + 'domain' => 'beyondco.de', + 'auth_token' => $user->auth_token, + ]))); + + $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', + 'domain' => 'beyondco.de', + 'auth_token' => $user->auth_token, + ]))); + + $user = $this->createUser([ + 'name' => 'Test-User', + 'can_specify_subdomains' => 1, + ]); + + $this->createTestHttpServer(); + + $this->expectException(\UnexpectedValueException::class); + + $client = $this->createClient(); + $response = $this->await($client->connectToServer('127.0.0.1:8085', 'reserved', 'beyondco.de', $user->auth_token)); + + $this->assertSame('reserved', $response->subdomain); + } + /** @test */ public function it_allows_users_to_use_their_own_reserved_subdomains() { @@ -352,6 +496,47 @@ class TunnelTest extends TestCase $this->assertSame('reserved', $response->subdomain); } + /** @test */ + public function it_allows_users_to_use_their_own_reserved_subdomains_on_custom_domains() + { + $this->app['config']['expose.admin.validate_auth_tokens'] = true; + + $user = $this->createUser([ + 'name' => 'Marcel', + 'can_specify_domains' => 1, + 'can_specify_subdomains' => 1, + ]); + + $response = $this->await($this->browser->post('http://127.0.0.1:8080/api/domains', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode('username:secret'), + 'Content-Type' => 'application/json', + ], json_encode([ + 'domain' => 'share.beyondco.de', + 'auth_token' => $user->auth_token, + ]))); + + $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', + 'domain' => 'share.beyondco.de', + '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', 'share.beyondco.de', $user->auth_token)); + + $this->assertSame('reserved', $response->subdomain); + } + /** @test */ public function it_rejects_clients_with_too_many_connections() {