diff --git a/app/DomainRepository/DatabaseDomainRepository.php b/app/DomainRepository/DatabaseDomainRepository.php index 24474af..43ae237 100644 --- a/app/DomainRepository/DatabaseDomainRepository.php +++ b/app/DomainRepository/DatabaseDomainRepository.php @@ -79,8 +79,8 @@ public function storeDomain(array $data): PromiseInterface $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')) + INSERT INTO domains (user_id, domain, error_page, created_at) + VALUES (:user_id, :domain, :error_page, DATETIME('now')) ", $data) ->then(function (Result $result) use ($deferred) { $this->database->query('SELECT * FROM domains WHERE id = :id', ['id' => $result->insertId]) @@ -128,7 +128,15 @@ public function updateDomain($id, array $data): PromiseInterface { $deferred = new Deferred(); - // TODO + $this->database->query('UPDATE domains SET error_page = :error_page WHERE (id = :id OR domain = :id)', array_merge($data, [ + 'id' => $id, + ])) + ->then(function (Result $result) use ($deferred, $id) { + $this->getDomainById($id) + ->then(function ($domain) use ($deferred) { + $deferred->resolve($domain); + }); + }); return $deferred->promise(); } diff --git a/app/Factory.php b/app/Factory.php index 175213a..58bddb0 100644 --- a/app/Factory.php +++ b/app/Factory.php @@ -32,6 +32,7 @@ use Expose\Server\Http\Controllers\Admin\StoreSettingsController; use Expose\Server\Http\Controllers\Admin\StoreSubdomainController; use Expose\Server\Http\Controllers\Admin\StoreUsersController; +use Expose\Server\Http\Controllers\Admin\UpdateDomainController; use Expose\Server\Http\Controllers\ControlMessageController; use Expose\Server\Http\Controllers\HealthController; use Expose\Server\Http\Controllers\TunnelMessageController; @@ -160,6 +161,7 @@ protected function addAdminRoutes() $this->router->get('/api/logs/{subdomain}', GetLogsForSubdomainController::class, $adminCondition); $this->router->post('/api/domains', StoreDomainController::class, $adminCondition); + $this->router->put('/api/domains/{id}', UpdateDomainController::class, $adminCondition); $this->router->delete('/api/domains/{domain}', DeleteSubdomainController::class, $adminCondition); $this->router->post('/api/subdomains', StoreSubdomainController::class, $adminCondition); diff --git a/app/Http/Controllers/Admin/StoreDomainController.php b/app/Http/Controllers/Admin/StoreDomainController.php index 5c5b2c1..f900191 100644 --- a/app/Http/Controllers/Admin/StoreDomainController.php +++ b/app/Http/Controllers/Admin/StoreDomainController.php @@ -28,6 +28,7 @@ public function handle(Request $request, ConnectionInterface $httpConnection) { $validator = Validator::make($request->all(), [ 'domain' => 'required', + 'error_page' => 'nullable|string' ], [ 'required' => 'The :attribute field is required.', ]); @@ -59,6 +60,7 @@ public function handle(Request $request, ConnectionInterface $httpConnection) $insertData = [ 'user_id' => $user['id'], 'domain' => $request->get('domain'), + 'error_page' => $request->get('error_page'), ]; $this->domainRepository diff --git a/app/Http/Controllers/Admin/UpdateDomainController.php b/app/Http/Controllers/Admin/UpdateDomainController.php new file mode 100644 index 0000000..eb06344 --- /dev/null +++ b/app/Http/Controllers/Admin/UpdateDomainController.php @@ -0,0 +1,71 @@ +userRepository = $userRepository; + $this->domainRepository = $domainRepository; + } + + public function handle(Request $request, ConnectionInterface $httpConnection) + { + $id = $request->get('id'); + + $validator = Validator::make($request->all(), [ + 'error_page' => 'nullable|string' + ]); + + 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, $id) { + 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; + } + + $updateData = [ + 'error_page' => $request->get('error_page'), + ]; + + $this->domainRepository + ->updateDomain($id, $updateData) + ->then(function ($domain) use ($httpConnection) { + $httpConnection->send(respond_json(['domain' => $domain], 200)); + $httpConnection->close(); + }); + }); + } +} diff --git a/app/Http/Controllers/TunnelMessageController.php b/app/Http/Controllers/TunnelMessageController.php index cdf2c21..34d597e 100644 --- a/app/Http/Controllers/TunnelMessageController.php +++ b/app/Http/Controllers/TunnelMessageController.php @@ -6,6 +6,7 @@ use Expose\Server\Connections\ControlConnection; use Expose\Server\Connections\HttpConnection; use Expose\Server\Contracts\ConnectionManager; +use Expose\Server\Contracts\DomainRepository; use Expose\Server\Contracts\StatisticsCollector; use Expose\Common\Http\Controllers\Controller; use GuzzleHttp\Psr7\Message; @@ -31,11 +32,15 @@ class TunnelMessageController extends Controller /** @var StatisticsCollector */ protected $statisticsCollector; - public function __construct(ConnectionManager $connectionManager, StatisticsCollector $statisticsCollector, Configuration $configuration) + /** @var DomainRepository */ + protected $domainRepository; + + public function __construct(ConnectionManager $connectionManager, StatisticsCollector $statisticsCollector, Configuration $configuration, DomainRepository $domainRepository) { $this->connectionManager = $connectionManager; $this->configuration = $configuration; $this->statisticsCollector = $statisticsCollector; + $this->domainRepository = $domainRepository; } public function handle(Request $request, ConnectionInterface $httpConnection) @@ -55,11 +60,24 @@ public function handle(Request $request, ConnectionInterface $httpConnection) $controlConnection = $this->connectionManager->findControlConnectionForSubdomainAndServerHost($subdomain, $serverHost); if (is_null($controlConnection)) { - $httpConnection->send( - respond_html($this->getBlade($httpConnection, 'server.errors.404', ['subdomain' => $subdomain]), 404) - ); - $httpConnection->close(); - + $this->domainRepository + ->getDomainByName(strtolower($serverHost)) + ->then(function ($domain) use ($subdomain, $httpConnection) { + if (is_null($domain) || is_null($domain['error_page'])) { + $errorPageContent = $this->getBlade($httpConnection, 'server.errors.404', ['subdomain' => $subdomain]); + } else { + $errorPageContent = str_replace( + ['%%subdomain%%'], + [$subdomain], + $domain['error_page'] + ); + } + + $httpConnection->send( + respond_html($errorPageContent, 404) + ); + $httpConnection->close(); + }); return; } diff --git a/database/migrations/12_add_404_to_domains_table.sql b/database/migrations/12_add_404_to_domains_table.sql new file mode 100644 index 0000000..d27a0f7 --- /dev/null +++ b/database/migrations/12_add_404_to_domains_table.sql @@ -0,0 +1 @@ +ALTER TABLE domains ADD COLUMN error_page TEXT DEFAULT NULL; \ No newline at end of file diff --git a/tests/Feature/Server/TunnelTest.php b/tests/Feature/Server/TunnelTest.php index 0513c8c..6650ad1 100644 --- a/tests/Feature/Server/TunnelTest.php +++ b/tests/Feature/Server/TunnelTest.php @@ -78,6 +78,109 @@ public function it_returns_404_for_non_existing_clients_on_custom_hosts() ])); } + /** @test */ + public function it_returns_default_404_pages_for_custom_domains_when_no_custom_error_page_is_specified() + { + $this->app['config']['expose-server.validate_auth_tokens'] = true; + + $user = $this->createUser([ + 'name' => 'Marcel', + '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' => 'share.beyondco.de', + 'auth_token' => $user->auth_token, + ]))); + + try { + $this->await($this->browser->get('http://127.0.0.1:8080/', [ + 'Host' => 'tunnel.share.beyondco.de', + ])); + } catch (ResponseException $e) { + $response = $e->getResponse(); + + $this->assertStringContainsString('Expose', $response->getBody()->getContents()); + } + } + + /** @test */ + public function it_returns_custom_404_pages_for_custom_domains_when_specified() + { + $this->app['config']['expose-server.validate_auth_tokens'] = true; + + $user = $this->createUser([ + 'name' => 'Marcel', + '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' => 'share.beyondco.de', + 'error_page' => '

Custom 404 for %%subdomain%%

', + 'auth_token' => $user->auth_token, + ]))); + + try { + $this->await($this->browser->get('http://127.0.0.1:8080/', [ + 'Host' => 'tunnel.share.beyondco.de', + ])); + } catch (ResponseException $e) { + $response = $e->getResponse(); + + $this->assertStringContainsString('

Custom 404 for tunnel

', $response->getBody()->getContents()); + } + } + + /** @test */ + public function it_can_update_404_pages_for_custom_domains() + { + $this->app['config']['expose-server.validate_auth_tokens'] = true; + + $user = $this->createUser([ + 'name' => 'Marcel', + 'can_specify_domains' => 1, + ]); + + $domainResponse = $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, + ]))); + + $domain = json_decode($domainResponse->getBody()->getContents())->domain; + + $this->await($this->browser->put('http://127.0.0.1:8080/api/domains/'.$domain->id, [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode('username:secret'), + 'Content-Type' => 'application/json', + ], json_encode([ + 'domain' => 'share.beyondco.de', + 'error_page' => '

Custom 404 for %%subdomain%%

', + 'auth_token' => $user->auth_token, + ]))); + + try { + $this->await($this->browser->get('http://127.0.0.1:8080/', [ + 'Host' => 'tunnel.share.beyondco.de', + ])); + } catch (ResponseException $e) { + $response = $e->getResponse(); + + $this->assertStringContainsString('

Custom 404 for tunnel

', $response->getBody()->getContents()); + } + } + /** @test */ public function it_sends_incoming_requests_to_the_connected_client() {