diff --git a/app/Client/Client.php b/app/Client/Client.php index b1a9068..913e186 100644 --- a/app/Client/Client.php +++ b/app/Client/Client.php @@ -107,7 +107,7 @@ class Client $connection->on('authenticated', function ($data) use ($deferred, $sharedUrl) { $httpProtocol = $this->configuration->port() === 443 ? 'https' : 'http'; - $host = $data->server_host; + $host = $data->server_host ?? $this->configuration->host(); $this->logger->info($data->message); $this->logger->info("Local-URL:\t\t{$sharedUrl}"); @@ -116,7 +116,7 @@ class Client $this->logger->info("Expose-URL:\t\thttps://{$data->subdomain}.{$host}"); $this->logger->line(''); - static::$subdomains[] = "{$httpProtocol}://{$data->subdomain}.{$data->server_host}"; + static::$subdomains[] = "{$httpProtocol}://{$data->subdomain}.{$host}"; $deferred->resolve($data); }); diff --git a/app/Client/Http/HttpClient.php b/app/Client/Http/HttpClient.php index 0227eea..77d8099 100644 --- a/app/Client/Http/HttpClient.php +++ b/app/Client/Http/HttpClient.php @@ -8,7 +8,6 @@ use App\Logger\RequestLogger; use Clue\React\Buzz\Browser; use GuzzleHttp\Psr7\Message; use function GuzzleHttp\Psr7\parse_request; -use function GuzzleHttp\Psr7\str; use Laminas\Http\Request; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; @@ -86,7 +85,7 @@ class HttpClient protected function sendRequestToApplication(RequestInterface $request, $proxyConnection = null) { (new Browser($this->loop, $this->createConnector())) - ->withFollowRedirects(true) + ->withFollowRedirects(false) ->withRejectErrorResponse(false) ->requestStreaming( $request->getMethod(), @@ -106,7 +105,7 @@ class HttpClient /* @var $body \React\Stream\ReadableStreamInterface */ $body = $response->getBody(); - $this->logResponse(str($response)); + $this->logResponse(Message::toString($response)); $body->on('data', function ($chunk) use ($proxyConnection, $response) { $response->buffer .= $chunk; diff --git a/app/Contracts/SubdomainRepository.php b/app/Contracts/SubdomainRepository.php index e1c46a4..f55339f 100644 --- a/app/Contracts/SubdomainRepository.php +++ b/app/Contracts/SubdomainRepository.php @@ -14,6 +14,8 @@ interface SubdomainRepository public function getSubdomainByNameAndDomain(string $name, string $domain): PromiseInterface; + public function getSubdomainsByNameAndDomain(string $name, string $domain): PromiseInterface; + public function getSubdomainsByUserId($id): PromiseInterface; public function getSubdomainsByUserIdAndName($id, $name): PromiseInterface; diff --git a/app/Server/Http/Controllers/Admin/StoreSubdomainController.php b/app/Server/Http/Controllers/Admin/StoreSubdomainController.php index a590b55..8cac94c 100644 --- a/app/Server/Http/Controllers/Admin/StoreSubdomainController.php +++ b/app/Server/Http/Controllers/Admin/StoreSubdomainController.php @@ -77,12 +77,6 @@ class StoreSubdomainController extends AdminController $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 69102ae..caf11ab 100644 --- a/app/Server/Http/Controllers/ControlMessageController.php +++ b/app/Server/Http/Controllers/ControlMessageController.php @@ -275,7 +275,7 @@ class ControlMessageController implements MessageComponentInterface $this->domainRepository ->getDomainsByUserId($user['id']) - ->then(function ($domains) use ($connection, $deferred , $serverHost) { + ->then(function ($domains) use ($connection, $deferred, $serverHost) { $userDomain = collect($domains)->first(function ($domain) use ($serverHost) { return strtolower($domain['domain']) === strtolower($serverHost); }); @@ -323,9 +323,13 @@ class ControlMessageController implements MessageComponentInterface * Check if the given subdomain is reserved for a different user. */ if (! is_null($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']) { + return $this->subdomainRepository->getSubdomainsByNameAndDomain($subdomain, $serverHost) + ->then(function ($foundSubdomains) use ($connection, $subdomain, $user, $serverHost) { + $ownSubdomain = collect($foundSubdomains)->first(function ($subdomain) use ($user) { + return $subdomain['user_id'] === $user['id']; + }); + + if (count($foundSubdomains) > 0 && ! is_null($user) && is_null($ownSubdomain)) { $message = config('expose.admin.messages.subdomain_reserved'); $message = str_replace(':subdomain', $subdomain, $message); diff --git a/app/Server/Http/Controllers/TunnelMessageController.php b/app/Server/Http/Controllers/TunnelMessageController.php index f3e107f..c06648f 100644 --- a/app/Server/Http/Controllers/TunnelMessageController.php +++ b/app/Server/Http/Controllers/TunnelMessageController.php @@ -79,7 +79,7 @@ class TunnelMessageController extends Controller protected function detectServerHost(Request $request): ?string { - return Str::after($request->header('Host'), '.'); + return Str::before(Str::after($request->header('Host'), '.'), ':'); } protected function sendRequestToClient(Request $request, ControlConnection $controlConnection, ConnectionInterface $httpConnection) diff --git a/app/Server/SubdomainRepository/DatabaseSubdomainRepository.php b/app/Server/SubdomainRepository/DatabaseSubdomainRepository.php index eaf3d00..f599204 100644 --- a/app/Server/SubdomainRepository/DatabaseSubdomainRepository.php +++ b/app/Server/SubdomainRepository/DatabaseSubdomainRepository.php @@ -73,6 +73,22 @@ class DatabaseSubdomainRepository implements SubdomainRepository return $deferred->promise(); } + public function getSubdomainsByNameAndDomain(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); + }); + + return $deferred->promise(); + } + public function getSubdomainsByUserId($id): PromiseInterface { $deferred = new Deferred(); @@ -92,23 +108,14 @@ class DatabaseSubdomainRepository implements SubdomainRepository { $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, domain, created_at) - VALUES (:user_id, :subdomain, :domain, DATETIME('now')) - ", $data) + $this->database->query(" + 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]) ->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]); - }); + $deferred->resolve($result->rows[0]); }); }); diff --git a/builds/expose b/builds/expose index 5ca5876..3ee298d 100755 Binary files a/builds/expose and b/builds/expose differ diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index dcfd29d..38e4cf8 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -13,6 +13,31 @@ composer global require beyondcode/expose After that, you are ready to go and can [share your first site](/docs/expose/getting-started/sharing-your-first-site). +## As a docker container + +Expose has a `Dockerfile` already in the source root. +You can build and use it without requiring any extra effort. + +```bash +docker build -t expose . +``` + +Usage: + +```bash +docker run expose +``` + +Examples: + +```bash +docker run expose share http://192.168.2.100 # share a local site +docker run expose serve my-domain.com # start a server +``` + +Now you're ready to go and can [share your first site](/docs/expose/getting-started/sharing-your-first-site). + + ### Extending Expose By default, Expose comes as an executable PHAR file. This allows you to use all Expose features out of the box – without any additional setup required. diff --git a/tests/Feature/Server/ApiTest.php b/tests/Feature/Server/ApiTest.php index 9026bc2..b53111a 100644 --- a/tests/Feature/Server/ApiTest.php +++ b/tests/Feature/Server/ApiTest.php @@ -424,65 +424,6 @@ class ApiTest extends TestCase $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 */ public function it_can_list_all_currently_connected_sites_from_all_users() { diff --git a/tests/Feature/Server/TunnelTest.php b/tests/Feature/Server/TunnelTest.php index 20b2fdc..9e5faa1 100644 --- a/tests/Feature/Server/TunnelTest.php +++ b/tests/Feature/Server/TunnelTest.php @@ -315,6 +315,50 @@ class TunnelTest extends TestCase $this->assertSame('reserved', $response->subdomain); } + /** @test */ + public function it_allows_users_that_both_have_the_same_reserved_subdomain() + { + $this->app['config']['expose.admin.validate_auth_tokens'] = true; + + $user = $this->createUser([ + 'name' => 'Marcel', + 'can_specify_subdomains' => 1, + ]); + + $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, + ]))); + + $user = $this->createUser([ + 'name' => 'Test-User', + 'can_specify_subdomains' => 1, + ]); + + $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', null, $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() {