Merge branch 'master' of github.com:beyondcode/expose into master

This commit is contained in:
Sebastian Schlein
2021-06-14 16:49:35 +02:00
11 changed files with 107 additions and 91 deletions

View File

@@ -107,7 +107,7 @@ class Client
$connection->on('authenticated', function ($data) use ($deferred, $sharedUrl) { $connection->on('authenticated', function ($data) use ($deferred, $sharedUrl) {
$httpProtocol = $this->configuration->port() === 443 ? 'https' : 'http'; $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($data->message);
$this->logger->info("Local-URL:\t\t{$sharedUrl}"); $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->info("Expose-URL:\t\thttps://{$data->subdomain}.{$host}");
$this->logger->line(''); $this->logger->line('');
static::$subdomains[] = "{$httpProtocol}://{$data->subdomain}.{$data->server_host}"; static::$subdomains[] = "{$httpProtocol}://{$data->subdomain}.{$host}";
$deferred->resolve($data); $deferred->resolve($data);
}); });

View File

@@ -8,7 +8,6 @@ use App\Logger\RequestLogger;
use Clue\React\Buzz\Browser; use Clue\React\Buzz\Browser;
use GuzzleHttp\Psr7\Message; use GuzzleHttp\Psr7\Message;
use function GuzzleHttp\Psr7\parse_request; use function GuzzleHttp\Psr7\parse_request;
use function GuzzleHttp\Psr7\str;
use Laminas\Http\Request; use Laminas\Http\Request;
use Psr\Http\Message\RequestInterface; use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
@@ -86,7 +85,7 @@ class HttpClient
protected function sendRequestToApplication(RequestInterface $request, $proxyConnection = null) protected function sendRequestToApplication(RequestInterface $request, $proxyConnection = null)
{ {
(new Browser($this->loop, $this->createConnector())) (new Browser($this->loop, $this->createConnector()))
->withFollowRedirects(true) ->withFollowRedirects(false)
->withRejectErrorResponse(false) ->withRejectErrorResponse(false)
->requestStreaming( ->requestStreaming(
$request->getMethod(), $request->getMethod(),
@@ -106,7 +105,7 @@ class HttpClient
/* @var $body \React\Stream\ReadableStreamInterface */ /* @var $body \React\Stream\ReadableStreamInterface */
$body = $response->getBody(); $body = $response->getBody();
$this->logResponse(str($response)); $this->logResponse(Message::toString($response));
$body->on('data', function ($chunk) use ($proxyConnection, $response) { $body->on('data', function ($chunk) use ($proxyConnection, $response) {
$response->buffer .= $chunk; $response->buffer .= $chunk;

View File

@@ -14,6 +14,8 @@ interface SubdomainRepository
public function getSubdomainByNameAndDomain(string $name, string $domain): PromiseInterface; public function getSubdomainByNameAndDomain(string $name, string $domain): PromiseInterface;
public function getSubdomainsByNameAndDomain(string $name, string $domain): PromiseInterface;
public function getSubdomainsByUserId($id): PromiseInterface; public function getSubdomainsByUserId($id): PromiseInterface;
public function getSubdomainsByUserIdAndName($id, $name): PromiseInterface; public function getSubdomainsByUserIdAndName($id, $name): PromiseInterface;

View File

@@ -77,12 +77,6 @@ class StoreSubdomainController extends AdminController
$this->subdomainRepository $this->subdomainRepository
->storeSubdomain($insertData) ->storeSubdomain($insertData)
->then(function ($subdomain) use ($httpConnection) { ->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->send(respond_json(['subdomain' => $subdomain], 200));
$httpConnection->close(); $httpConnection->close();
}); });

View File

@@ -275,7 +275,7 @@ class ControlMessageController implements MessageComponentInterface
$this->domainRepository $this->domainRepository
->getDomainsByUserId($user['id']) ->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) { $userDomain = collect($domains)->first(function ($domain) use ($serverHost) {
return strtolower($domain['domain']) === strtolower($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. * Check if the given subdomain is reserved for a different user.
*/ */
if (! is_null($subdomain)) { if (! is_null($subdomain)) {
return $this->subdomainRepository->getSubdomainByNameAndDomain($subdomain, $serverHost) return $this->subdomainRepository->getSubdomainsByNameAndDomain($subdomain, $serverHost)
->then(function ($foundSubdomain) use ($connection, $subdomain, $user, $serverHost) { ->then(function ($foundSubdomains) use ($connection, $subdomain, $user, $serverHost) {
if (! is_null($foundSubdomain) && ! is_null($user) && $foundSubdomain['user_id'] !== $user['id']) { $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 = config('expose.admin.messages.subdomain_reserved');
$message = str_replace(':subdomain', $subdomain, $message); $message = str_replace(':subdomain', $subdomain, $message);

View File

@@ -79,7 +79,7 @@ class TunnelMessageController extends Controller
protected function detectServerHost(Request $request): ?string 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) protected function sendRequestToClient(Request $request, ControlConnection $controlConnection, ConnectionInterface $httpConnection)

View File

@@ -73,6 +73,22 @@ class DatabaseSubdomainRepository implements SubdomainRepository
return $deferred->promise(); 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 public function getSubdomainsByUserId($id): PromiseInterface
{ {
$deferred = new Deferred(); $deferred = new Deferred();
@@ -92,23 +108,14 @@ class DatabaseSubdomainRepository implements SubdomainRepository
{ {
$deferred = new Deferred(); $deferred = new Deferred();
$this->getSubdomainByName($data['subdomain']) $this->database->query("
->then(function ($registeredSubdomain) use ($data, $deferred) { INSERT INTO subdomains (user_id, subdomain, domain, created_at)
if (! is_null($registeredSubdomain)) { VALUES (:user_id, :subdomain, :domain, DATETIME('now'))
$deferred->resolve(null); ", $data)
->then(function (Result $result) use ($deferred) {
return; $this->database->query('SELECT * FROM subdomains WHERE id = :id', ['id' => $result->insertId])
}
$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) { ->then(function (Result $result) use ($deferred) {
$this->database->query('SELECT * FROM subdomains WHERE id = :id', ['id' => $result->insertId]) $deferred->resolve($result->rows[0]);
->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows[0]);
});
}); });
}); });

Binary file not shown.

View File

@@ -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). 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 <expose command>
```
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 ### 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. 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.

View File

@@ -424,65 +424,6 @@ class ApiTest extends TestCase
$this->assertCount(0, $subdomains); $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 */ /** @test */
public function it_can_list_all_currently_connected_sites_from_all_users() public function it_can_list_all_currently_connected_sites_from_all_users()
{ {

View File

@@ -315,6 +315,50 @@ class TunnelTest extends TestCase
$this->assertSame('reserved', $response->subdomain); $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 */ /** @test */
public function it_rejects_users_that_want_to_use_a_reserved_subdomain_on_a_custom_domain() public function it_rejects_users_that_want_to_use_a_reserved_subdomain_on_a_custom_domain()
{ {