diff --git a/app/Client/Client.php b/app/Client/Client.php index fccb13e..88a3ad4 100644 --- a/app/Client/Client.php +++ b/app/Client/Client.php @@ -40,12 +40,12 @@ class Client $this->logger = $logger; } - public function share(string $sharedUrl, array $subdomains = []) + public function share(string $sharedUrl, array $subdomains = [], $serverHost = null) { $sharedUrl = $this->prepareSharedUrl($sharedUrl); foreach ($subdomains as $subdomain) { - $this->connectToServer($sharedUrl, $subdomain, $this->configuration->auth()); + $this->connectToServer($sharedUrl, $subdomain, $serverHost, $this->configuration->auth()); } } @@ -72,7 +72,7 @@ class Client return $url; } - public function connectToServer(string $sharedUrl, $subdomain, $authToken = ''): PromiseInterface + public function connectToServer(string $sharedUrl, $subdomain, $serverHost = null, $authToken = ''): PromiseInterface { $deferred = new Deferred(); $promise = $deferred->promise(); @@ -82,18 +82,18 @@ class Client connect($wsProtocol."://{$this->configuration->host()}:{$this->configuration->port()}/expose/control?authToken={$authToken}", [], [ 'X-Expose-Control' => 'enabled', ], $this->loop) - ->then(function (WebSocket $clientConnection) use ($sharedUrl, $subdomain, $deferred, $authToken) { + ->then(function (WebSocket $clientConnection) use ($sharedUrl, $subdomain, $serverHost, $deferred, $authToken) { $this->connectionRetries = 0; $connection = ControlConnection::create($clientConnection); - $connection->authenticate($sharedUrl, $subdomain); + $connection->authenticate($sharedUrl, $subdomain, $serverHost); - $clientConnection->on('close', function () use ($sharedUrl, $subdomain, $authToken) { + $clientConnection->on('close', function () use ($sharedUrl, $subdomain, $serverHost, $authToken) { $this->logger->error('Connection to server closed.'); - $this->retryConnectionOrExit(function () use ($sharedUrl, $subdomain, $authToken) { - $this->connectToServer($sharedUrl, $subdomain, $authToken); + $this->retryConnectionOrExit(function () use ($sharedUrl, $subdomain, $serverHost, $authToken) { + $this->connectToServer($sharedUrl, $subdomain, $serverHost, $authToken); }); }); @@ -107,7 +107,7 @@ class Client $connection->on('authenticated', function ($data) use ($deferred, $sharedUrl) { $httpProtocol = $this->configuration->port() === 443 ? 'https' : 'http'; - $host = $this->configuration->host(); + $host = $data->server_host; if ($httpProtocol !== 'https') { $host .= ":{$this->configuration->port()}"; @@ -119,7 +119,7 @@ class Client $this->logger->info("Expose-URL:\t\t{$httpProtocol}://{$data->subdomain}.{$host}"); $this->logger->line(''); - static::$subdomains[] = "{$httpProtocol}://{$data->subdomain}.{$host}"; + static::$subdomains[] = "{$httpProtocol}://{$data->subdomain}.{$data->server_host}"; $deferred->resolve($data); }); diff --git a/app/Client/Connections/ControlConnection.php b/app/Client/Connections/ControlConnection.php index 02bc31b..2d4540e 100644 --- a/app/Client/Connections/ControlConnection.php +++ b/app/Client/Connections/ControlConnection.php @@ -57,13 +57,14 @@ class ControlConnection $this->proxyManager->createTcpProxy($this->clientId, $data); } - public function authenticate(string $sharedHost, string $subdomain) + public function authenticate(string $sharedHost, string $subdomain, $serverHost = null) { $this->socket->send(json_encode([ 'event' => 'authenticate', 'data' => [ 'type' => 'http', 'host' => $sharedHost, + 'server_host' => $serverHost, 'subdomain' => empty($subdomain) ? null : $subdomain, ], ])); diff --git a/app/Client/Factory.php b/app/Client/Factory.php index 467a61d..59ab54e 100644 --- a/app/Client/Factory.php +++ b/app/Client/Factory.php @@ -106,9 +106,9 @@ class Factory return $this; } - public function share($sharedUrl, $subdomain = null) + public function share($sharedUrl, $subdomain = null, $serverHost = null) { - app('expose.client')->share($sharedUrl, $subdomain); + app('expose.client')->share($sharedUrl, $subdomain, $serverHost); return $this; } @@ -120,11 +120,11 @@ class Factory return $this; } - public function shareFolder(string $folder, string $name, $subdomain = null) + public function shareFolder(string $folder, string $name, $subdomain = null, $serverHost = null) { $host = $this->createFileServer($folder, $name); - $this->share($host, $subdomain); + $this->share($host, $subdomain, $serverHost); return $this; } diff --git a/app/Commands/ShareCommand.php b/app/Commands/ShareCommand.php index 452f809..c668e1d 100644 --- a/app/Commands/ShareCommand.php +++ b/app/Commands/ShareCommand.php @@ -7,7 +7,7 @@ use React\EventLoop\LoopInterface; class ShareCommand extends ServerAwareCommand { - protected $signature = 'share {host} {--subdomain=} {--auth=} {--dns=}'; + protected $signature = 'share {host} {--subdomain=} {--auth=} {--dns=} {--domain=}'; protected $description = 'Share a local url with a remote expose server'; @@ -29,7 +29,11 @@ class ShareCommand extends ServerAwareCommand ->setPort($this->getServerPort()) ->setAuth($auth) ->createClient() - ->share($this->argument('host'), explode(',', $this->option('subdomain'))) + ->share( + $this->argument('host'), + explode(',', $this->option('subdomain')), + $this->option('domain') + ) ->createHttpServer() ->run(); } diff --git a/app/Commands/ShareCurrentWorkingDirectoryCommand.php b/app/Commands/ShareCurrentWorkingDirectoryCommand.php index a901495..eed099a 100644 --- a/app/Commands/ShareCurrentWorkingDirectoryCommand.php +++ b/app/Commands/ShareCurrentWorkingDirectoryCommand.php @@ -4,7 +4,7 @@ namespace App\Commands; class ShareCurrentWorkingDirectoryCommand extends ShareCommand { - protected $signature = 'share-cwd {host?} {--subdomain=} {--auth=} {--dns=}'; + protected $signature = 'share-cwd {host?} {--subdomain=} {--auth=} {--dns=} {--domain=}'; public function handle() { diff --git a/app/Commands/ShareFilesCommand.php b/app/Commands/ShareFilesCommand.php index f29781f..52c5995 100644 --- a/app/Commands/ShareFilesCommand.php +++ b/app/Commands/ShareFilesCommand.php @@ -7,7 +7,7 @@ use React\EventLoop\LoopInterface; class ShareFilesCommand extends ServerAwareCommand { - protected $signature = 'share-files {folder=.} {--name=} {--subdomain=} {--auth=}'; + protected $signature = 'share-files {folder=.} {--name=} {--subdomain=} {--auth=} {--domain=}'; protected $description = 'Share a local folder with a remote expose server'; @@ -28,7 +28,8 @@ class ShareFilesCommand extends ServerAwareCommand ->shareFolder( $this->argument('folder'), $this->option('name') ?? '', - explode(',', $this->option('subdomain')) + explode(',', $this->option('subdomain')), + $this->option('domain') ) ->createHttpServer() ->run(); diff --git a/app/Contracts/ConnectionManager.php b/app/Contracts/ConnectionManager.php index 111c3b7..2898ded 100644 --- a/app/Contracts/ConnectionManager.php +++ b/app/Contracts/ConnectionManager.php @@ -8,7 +8,7 @@ use Ratchet\ConnectionInterface; interface ConnectionManager { - public function storeConnection(string $host, ?string $subdomain, ConnectionInterface $connection): ControlConnection; + public function storeConnection(string $host, ?string $subdomain, ?string $serverHost, ConnectionInterface $connection): ControlConnection; public function storeTcpConnection(int $port, ConnectionInterface $connection): ControlConnection; @@ -20,7 +20,7 @@ interface ConnectionManager public function removeControlConnection($connection); - public function findControlConnectionForSubdomain($subdomain): ?ControlConnection; + public function findControlConnectionForSubdomainAndServerHost($subdomain, $serverHost): ?ControlConnection; public function findControlConnectionForClientId(string $clientId): ?ControlConnection; diff --git a/app/Server/Connections/ConnectionManager.php b/app/Server/Connections/ConnectionManager.php index 07c5d75..e2e93ab 100644 --- a/app/Server/Connections/ConnectionManager.php +++ b/app/Server/Connections/ConnectionManager.php @@ -6,6 +6,7 @@ use App\Contracts\ConnectionManager as ConnectionManagerContract; use App\Contracts\StatisticsCollector; use App\Contracts\SubdomainGenerator; use App\Http\QueryParameters; +use App\Server\Configuration; use App\Server\Exceptions\NoFreePortAvailable; use Ratchet\ConnectionInterface; use React\EventLoop\LoopInterface; @@ -48,7 +49,7 @@ class ConnectionManager implements ConnectionManagerContract }); } - public function storeConnection(string $host, ?string $subdomain, ConnectionInterface $connection): ControlConnection + public function storeConnection(string $host, ?string $subdomain, ?string $serverHost, ConnectionInterface $connection): ControlConnection { $clientId = (string) uniqid(); @@ -59,6 +60,7 @@ class ConnectionManager implements ConnectionManagerContract $host, $subdomain ?? $this->subdomainGenerator->generateSubdomain(), $clientId, + $serverHost, $this->getAuthTokenFromConnection($connection) ); @@ -152,10 +154,10 @@ class ConnectionManager implements ConnectionManagerContract } } - public function findControlConnectionForSubdomain($subdomain): ?ControlConnection + public function findControlConnectionForSubdomainAndServerHost($subdomain, $serverHost): ?ControlConnection { - return collect($this->connections)->last(function ($connection) use ($subdomain) { - return $connection->subdomain == $subdomain; + return collect($this->connections)->last(function ($connection) use ($subdomain, $serverHost) { + return $connection->subdomain == $subdomain && $connection->serverHost === $serverHost; }); } diff --git a/app/Server/Connections/ControlConnection.php b/app/Server/Connections/ControlConnection.php index 80b1d31..a85fa59 100644 --- a/app/Server/Connections/ControlConnection.php +++ b/app/Server/Connections/ControlConnection.php @@ -12,19 +12,21 @@ class ControlConnection /** @var ConnectionInterface */ public $socket; public $host; + public $serverHost; public $authToken; public $subdomain; public $client_id; public $proxies = []; protected $shared_at; - public function __construct(ConnectionInterface $socket, string $host, string $subdomain, string $clientId, string $authToken = '') + public function __construct(ConnectionInterface $socket, string $host, string $subdomain, string $clientId, string $serverHost, string $authToken = '') { $this->socket = $socket; $this->host = $host; $this->subdomain = $subdomain; $this->client_id = $clientId; $this->authToken = $authToken; + $this->serverHost = $serverHost; $this->shared_at = now()->toDateTimeString(); } @@ -61,6 +63,7 @@ class ControlConnection return [ 'type' => 'http', 'host' => $this->host, + 'server_host' => $this->serverHost, 'client_id' => $this->client_id, 'auth_token' => $this->authToken, 'subdomain' => $this->subdomain, diff --git a/app/Server/Http/Controllers/ControlMessageController.php b/app/Server/Http/Controllers/ControlMessageController.php index 851f747..3b32389 100644 --- a/app/Server/Http/Controllers/ControlMessageController.php +++ b/app/Server/Http/Controllers/ControlMessageController.php @@ -6,6 +6,7 @@ use App\Contracts\ConnectionManager; use App\Contracts\SubdomainRepository; use App\Contracts\UserRepository; use App\Http\QueryParameters; +use App\Server\Configuration; use App\Server\Exceptions\NoFreePortAvailable; use Illuminate\Support\Arr; use Ratchet\ConnectionInterface; @@ -26,11 +27,15 @@ class ControlMessageController implements MessageComponentInterface /** @var SubdomainRepository */ protected $subdomainRepository; - public function __construct(ConnectionManager $connectionManager, UserRepository $userRepository, SubdomainRepository $subdomainRepository) + /** @var Configuration */ + protected $configuration; + + public function __construct(ConnectionManager $connectionManager, UserRepository $userRepository, SubdomainRepository $subdomainRepository, Configuration $configuration) { $this->connectionManager = $connectionManager; $this->userRepository = $userRepository; $this->subdomainRepository = $subdomainRepository; + $this->configuration = $configuration; } /** @@ -93,6 +98,9 @@ class ControlMessageController implements MessageComponentInterface if (! isset($data->type)) { $data->type = 'http'; } + if (! isset($data->server_host) || is_null($data->server_host)) { + $data->server_host = $this->configuration->hostname(); + } $this->verifyAuthToken($connection) ->then(function ($user) use ($connection) { @@ -139,14 +147,14 @@ class ControlMessageController implements MessageComponentInterface protected function handleHttpConnection(ConnectionInterface $connection, $data, $user = null) { - $this->hasValidSubdomain($connection, $data->subdomain, $user)->then(function ($subdomain) use ($data, $connection) { + $this->hasValidSubdomain($connection, $data->subdomain, $user, $data->server_host)->then(function ($subdomain) use ($data, $connection) { if ($subdomain === false) { return; } $data->subdomain = $subdomain; - $connectionInfo = $this->connectionManager->storeConnection($data->host, $data->subdomain, $connection); + $connectionInfo = $this->connectionManager->storeConnection($data->host, $data->subdomain, $data->server_host, $connection); $this->connectionManager->limitConnectionLength($connectionInfo, config('expose.admin.maximum_connection_length')); @@ -155,6 +163,7 @@ class ControlMessageController implements MessageComponentInterface 'data' => [ 'message' => config('expose.admin.messages.message_of_the_day'), 'subdomain' => $connectionInfo->subdomain, + 'server_host' => $connectionInfo->serverHost, 'client_id' => $connectionInfo->client_id, ], ])); @@ -250,7 +259,7 @@ class ControlMessageController implements MessageComponentInterface return $deferred->promise(); } - protected function hasValidSubdomain(ConnectionInterface $connection, ?string $subdomain, ?array $user): PromiseInterface + protected function hasValidSubdomain(ConnectionInterface $connection, ?string $subdomain, ?array $user, string $serverHost): PromiseInterface { /** * Check if the user can specify a custom subdomain in the first place. @@ -271,7 +280,7 @@ class ControlMessageController implements MessageComponentInterface */ if (! is_null($subdomain)) { return $this->subdomainRepository->getSubdomainByName($subdomain) - ->then(function ($foundSubdomain) use ($connection, $subdomain, $user) { + ->then(function ($foundSubdomain) use ($connection, $subdomain, $user, $serverHost) { 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); @@ -287,7 +296,7 @@ class ControlMessageController implements MessageComponentInterface return \React\Promise\resolve(false); } - $controlConnection = $this->connectionManager->findControlConnectionForSubdomain($subdomain); + $controlConnection = $this->connectionManager->findControlConnectionForSubdomainAndServerHost($subdomain, $serverHost); if (! is_null($controlConnection) || $subdomain === config('expose.admin.subdomain') || in_array($subdomain, config('expose.admin.reserved_subdomains', []))) { $message = config('expose.admin.messages.subdomain_taken'); diff --git a/app/Server/Http/Controllers/TunnelMessageController.php b/app/Server/Http/Controllers/TunnelMessageController.php index 3b58ae0..adbe324 100644 --- a/app/Server/Http/Controllers/TunnelMessageController.php +++ b/app/Server/Http/Controllers/TunnelMessageController.php @@ -41,6 +41,7 @@ class TunnelMessageController extends Controller public function handle(Request $request, ConnectionInterface $httpConnection) { $subdomain = $this->detectSubdomain($request); + $serverHost = $this->detectServerHost($request); if (is_null($subdomain)) { $httpConnection->send( @@ -51,7 +52,7 @@ class TunnelMessageController extends Controller return; } - $controlConnection = $this->connectionManager->findControlConnectionForSubdomain($subdomain); + $controlConnection = $this->connectionManager->findControlConnectionForSubdomainAndServerHost($subdomain, $serverHost); if (is_null($controlConnection)) { $httpConnection->send( @@ -69,11 +70,16 @@ class TunnelMessageController extends Controller protected function detectSubdomain(Request $request): ?string { - $subdomain = Str::before($request->getHost(), '.'.$this->configuration->hostname()); + $subdomain = Str::before($request->getHost(), '.'); return $subdomain === $request->getHost() ? null : $subdomain; } + protected function detectServerHost(Request $request): ?string + { + return Str::after($request->getHost(), '.'); + } + protected function sendRequestToClient(Request $request, ControlConnection $controlConnection, ConnectionInterface $httpConnection) { $request = $this->prepareRequest($request, $controlConnection); diff --git a/tests/Feature/Server/ApiTest.php b/tests/Feature/Server/ApiTest.php index 3bdde90..b3abd60 100644 --- a/tests/Feature/Server/ApiTest.php +++ b/tests/Feature/Server/ApiTest.php @@ -284,11 +284,11 @@ class ApiTest extends TestCase $connection = \Mockery::mock(IoConnection::class); $connection->httpRequest = new Request('GET', '/?authToken='.$createdUser->auth_token); - $connectionManager->storeConnection('some-host.test', 'fixed-subdomain', $connection); + $connectionManager->storeConnection('some-host.test', 'fixed-subdomain', 'localhost', $connection); $connection = \Mockery::mock(IoConnection::class); $connection->httpRequest = new Request('GET', '/?authToken=some-other-token'); - $connectionManager->storeConnection('some-different-host.test', 'different-subdomain', $connection); + $connectionManager->storeConnection('some-different-host.test', 'different-subdomain', 'localhost', $connection); $connection = \Mockery::mock(IoConnection::class); $connection->httpRequest = new Request('GET', '/?authToken='.$createdUser->auth_token); @@ -307,6 +307,7 @@ class ApiTest extends TestCase $this->assertCount(1, $users[0]->sites); $this->assertCount(1, $users[0]->tcp_connections); $this->assertSame('some-host.test', $users[0]->sites[0]->host); + $this->assertSame('localhost', $users[0]->sites[0]->server_host); $this->assertSame('fixed-subdomain', $users[0]->sites[0]->subdomain); } @@ -319,7 +320,7 @@ class ApiTest extends TestCase $connection = \Mockery::mock(IoConnection::class); $connection->httpRequest = new Request('GET', '/?authToken=some-token'); - $connectionManager->storeConnection('some-host.test', 'fixed-subdomain', $connection); + $connectionManager->storeConnection('some-host.test', 'fixed-subdomain', 'localhost', $connection); /** @var Response $response */ $response = $this->await($this->browser->get('http://127.0.0.1:8080/api/sites', [ @@ -346,7 +347,7 @@ class ApiTest extends TestCase $connection = \Mockery::mock(IoConnection::class); $connection->httpRequest = new Request('GET', '/'); - $connectionManager->storeConnection('some-host.test', 'fixed-subdomain', $connection); + $connectionManager->storeConnection('some-host.test', 'fixed-subdomain', 'localhost', $connection); /** @var Response $response */ $response = $this->await($this->browser->get('http://127.0.0.1:8080/api/sites', [ diff --git a/tests/Feature/Server/TunnelTest.php b/tests/Feature/Server/TunnelTest.php index e050b1b..d849f33 100644 --- a/tests/Feature/Server/TunnelTest.php +++ b/tests/Feature/Server/TunnelTest.php @@ -213,7 +213,7 @@ class TunnelTest extends TestCase * the created test HTTP server. */ $client = $this->createClient(); - $response = $this->await($client->connectToServer('127.0.0.1:8085', 'tunnel', $user->auth_token)); + $response = $this->await($client->connectToServer('127.0.0.1:8085', 'tunnel', null, $user->auth_token)); $this->assertSame('tunnel', $response->subdomain); } @@ -235,7 +235,7 @@ class TunnelTest extends TestCase * the created test HTTP server. */ $client = $this->createClient(); - $response = $this->await($client->connectToServer('127.0.0.1:8085', 'tunnel', $user->auth_token)); + $response = $this->await($client->connectToServer('127.0.0.1:8085', 'tunnel', null, $user->auth_token)); $this->assertNotSame('tunnel', $response->subdomain); } @@ -272,11 +272,56 @@ class TunnelTest extends TestCase * the created test HTTP server. */ $client = $this->createClient(); - $response = $this->await($client->connectToServer('127.0.0.1:8085', 'reserved', $user->auth_token)); + $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() + { + $this->app['config']['expose.admin.validate_auth_tokens'] = true; + + $user = $this->createUser([ + 'name' => 'Marcel', + 'can_specify_subdomains' => 1, + ]); + + $this->createTestHttpServer(); + + $this->expectException(\UnexpectedValueException::class); + $client = $this->createClient(); + + $response = $this->await($client->connectToServer('127.0.0.1:8085', 'taken', null, $user->auth_token)); + $this->assertSame('taken', $response->subdomain); + + $response = $this->await($client->connectToServer('127.0.0.1:8085', 'taken', null, $user->auth_token)); + $this->assertSame('taken', $response->subdomain); + } + + /** @test */ + public function it_allows_users_to_use_a_subdomain_that_is_already_in_use_on_a_different_shared_host() + { + $this->app['config']['expose.admin.validate_auth_tokens'] = true; + + $user = $this->createUser([ + 'name' => 'Marcel', + 'can_specify_subdomains' => 1, + ]); + + $this->createTestHttpServer(); + + $client = $this->createClient(); + + $response = $this->await($client->connectToServer('127.0.0.1:8085', 'taken', null, $user->auth_token)); + $this->assertSame('localhost', $response->server_host); + $this->assertSame('taken', $response->subdomain); + + $response = $this->await($client->connectToServer('127.0.0.1:8085', 'taken', 'share.beyondco.de', $user->auth_token)); + $this->assertSame('share.beyondco.de', $response->server_host); + $this->assertSame('taken', $response->subdomain); + } + /** @test */ public function it_allows_users_to_use_their_own_reserved_subdomains() { @@ -302,7 +347,7 @@ class TunnelTest extends TestCase * the created test HTTP server. */ $client = $this->createClient(); - $response = $this->await($client->connectToServer('127.0.0.1:8085', 'reserved', $user->auth_token)); + $response = $this->await($client->connectToServer('127.0.0.1:8085', 'reserved', null, $user->auth_token)); $this->assertSame('reserved', $response->subdomain); } @@ -363,7 +408,7 @@ class TunnelTest extends TestCase $this->createTestHttpServer(); $client = $this->createClient(); - $response = $this->await($client->connectToServer('127.0.0.1:8085', 'foo', $user->auth_token)); + $response = $this->await($client->connectToServer('127.0.0.1:8085', 'foo', null, $user->auth_token)); $this->assertNotSame('foo', $response->subdomain); }