diff --git a/app/Server/Factory.php b/app/Server/Factory.php index 35e3e3b..5c56987 100644 --- a/app/Server/Factory.php +++ b/app/Server/Factory.php @@ -38,6 +38,7 @@ use App\Server\Http\Controllers\Admin\StoreUsersController; use App\Server\Http\Controllers\ControlMessageController; use App\Server\Http\Controllers\TunnelMessageController; +use App\Server\Http\Controllers\ValidateTunnelController; use App\Server\Http\Router; use App\Server\LoggerRepository\NullLogger; use App\Server\StatisticsCollector\DatabaseStatisticsCollector; @@ -166,6 +167,17 @@ protected function addAdminRoutes() $this->router->get('/api/tcp', GetTcpConnectionsController::class, $adminCondition); $this->router->delete('/api/tcp/{id}', DisconnectTcpConnectionController::class, $adminCondition); + + return $this; + } + + protected function addValidateTunnel() + { + $localCondition = 'request.headers.get("Host") matches "/^'.$this->host.':'.$this->port.'$/i"'; + + $this->router->get('/validate-tunnel', ValidateTunnelController::class, $localCondition); + + return $this; } protected function bindConfiguration() @@ -209,7 +221,8 @@ public function createServer() ->ensureDatabaseIsInitialized() ->registerStatisticsCollector() ->bindConnectionManager() - ->addAdminRoutes(); + ->addAdminRoutes() + ->addValidateTunnel(); $controlConnection = $this->addControlConnectionRoute(); diff --git a/app/Server/Http/Controllers/ValidateTunnelController.php b/app/Server/Http/Controllers/ValidateTunnelController.php new file mode 100644 index 0000000..91107a9 --- /dev/null +++ b/app/Server/Http/Controllers/ValidateTunnelController.php @@ -0,0 +1,109 @@ +connectionManager = $connectionManager; + $this->configuration = $configuration; + } + + public function handle(Request $request, ConnectionInterface $httpConnection) + { + $key = $request->get('key'); + + // Only allow requests with the correct key + if ($key !== $this->getAuthorizedKey()) { + $httpConnection->send( + respond_json(['exists' => false], 401), + ); + $httpConnection->close(); + + return; + } + + $domain = $request->get('domain'); + if ($domain === null) { + $httpConnection->send( + respond_json(['exists' => false, 'error' => 'invalid_domain'], 404), + ); + $httpConnection->close(); + + return; + } + + // If the domain is the same as the hostname, then it requested the main domain + $hostname = $this->configuration->hostname(); + if ($hostname === $domain) { + $this->isSuccessful($httpConnection); + + return; + } + + // Also allow the admin dashboard + $adminSubdomain = config('expose.admin.subdomain'); + if ($domain === $adminSubdomain.'.'.$hostname) { + $this->isSuccessful($httpConnection); + + return; + } + + // Check if the domain is a tunnel + $sites = collect($this->connectionManager->getConnections()) + ->filter(function ($site) use ($domain) { + $isControlConnection = get_class($site) === ControlConnection::class; + if (! $isControlConnection) { + return false; + } + + $fqdn = sprintf( + '%s.%s', + $site->subdomain, + $site->serverHost, + ); + + return $fqdn === $domain; + }) + ->map(function (ControlConnection $site) { + return sprintf( + '%s.%s', + $site->host, + $site->subdomain, + ); + }); + + if ($sites->count() > 0) { + $this->isSuccessful($httpConnection); + + return; + } + + $httpConnection->send(respond_json(['exists' => false, 'error' => 'no_tunnel_found'], 404)); + $httpConnection->close(); + } + + private function isSuccessful(ConnectionInterface $connection): void + { + $connection->send(respond_json(['exists' => true])); + $connection->close(); + } + + private function getAuthorizedKey(): string + { + return config('expose.validate_tunnel.authorized_key'); + } +} diff --git a/config/expose.php b/config/expose.php index 8ca4f57..4249fcc 100644 --- a/config/expose.php +++ b/config/expose.php @@ -390,4 +390,8 @@ 'repository' => \App\Server\StatisticsRepository\DatabaseStatisticsRepository::class, ], ], + + 'validate_tunnel' => [ + 'authorized_key' => 'asHzMGp4y4fYmNzWAUmgsZZbcjSM5e', + ], ];