11 Commits

Author SHA1 Message Date
Marcel Pociot
a83349e6b9 Merge pull request #118 from beyondcode/analysis-gOEJOG
Apply fixes from StyleCI
2020-09-07 13:34:13 +02:00
Marcel Pociot
9363e97d81 Apply fixes from StyleCI 2020-09-07 11:34:05 +00:00
Marcel Pociot
47b2350631 Associate shared sites with auth tokens 2020-09-07 13:33:40 +02:00
Marcel Pociot
13f184a955 Merge pull request #108 from beyondcode/analysis-J2x92V
Apply fixes from StyleCI
2020-08-13 00:20:14 +02:00
Marcel Pociot
55a456d5e1 Apply fixes from StyleCI 2020-08-12 22:20:06 +00:00
Marcel Pociot
f9084c3c31 Merge pull request #83 from ahmedash95/detect-valet-links
Auto detect valet links
2020-08-13 00:19:47 +02:00
Marcel Pociot
730b8457a6 Merge pull request #106 from CDRO/patch-1
[DOCS] Describe apache proxy configuration example
2020-08-07 14:34:10 +02:00
Tizian Schmidlin
188e1efe57 [DOCS] Describe apache proxy configuration exampl 2020-08-03 09:00:55 +02:00
Marcel Pociot
eaf04a8eae Don't try to log requests as curl when the requests are bigger than 256kb 2020-07-31 10:55:32 +02:00
Ahmed Ashraf
8db13e70af set path only if exists 2020-07-03 12:53:31 +02:00
Ahmed Ashraf
dfe889692b auto detect valet links 2020-07-03 12:50:08 +02:00
12 changed files with 377 additions and 10 deletions

View File

@@ -8,12 +8,12 @@ class ShareCurrentWorkingDirectoryCommand extends ShareCommand
public function handle()
{
$host = $this->prepareSharedHost(basename(getcwd()).'.'.$this->detectTld());
$subdomain = $this->detectName();
$host = $this->prepareSharedHost($subdomain.'.'.$this->detectTld());
$this->input->setArgument('host', $host);
if (! $this->option('subdomain')) {
$subdomain = str_replace('.', '-', basename(getcwd()));
$this->input->setOption('subdomain', $subdomain);
}
@@ -33,6 +33,32 @@ class ShareCurrentWorkingDirectoryCommand extends ShareCommand
return config('expose.default_tld', 'test');
}
protected function detectName(): string
{
$projectPath = getcwd();
$valetSitesPath = ($_SERVER['HOME'] ?? $_SERVER['USERPROFILE']).DIRECTORY_SEPARATOR.'.config'.DIRECTORY_SEPARATOR.'valet'.DIRECTORY_SEPARATOR.'Sites';
if (is_dir($valetSitesPath)) {
$site = collect(scandir($valetSitesPath))
->skip(2)
->map(function ($site) use ($valetSitesPath) {
return $valetSitesPath.DIRECTORY_SEPARATOR.$site;
})->mapWithKeys(function ($site) {
return [$site => readlink($site)];
})->filter(function ($sourcePath) use ($projectPath) {
return $sourcePath === $projectPath;
})
->keys()
->first();
if ($site) {
$projectPath = $site;
}
}
return str_replace('.', '-', basename($projectPath));
}
protected function prepareSharedHost($host): string
{
$certificateFile = ($_SERVER['HOME'] ?? $_SERVER['USERPROFILE']).DIRECTORY_SEPARATOR.'.config'.DIRECTORY_SEPARATOR.'valet'.DIRECTORY_SEPARATOR.'Certificates'.DIRECTORY_SEPARATOR.$host.'.crt';

View File

@@ -23,4 +23,6 @@ interface ConnectionManager
public function findControlConnectionForClientId(string $clientId): ?ControlConnection;
public function getConnections(): array;
public function getConnectionsForAuthToken(string $authToken): array;
}

View File

@@ -308,6 +308,12 @@ class LoggedRequest implements \JsonSerializable
protected function getRequestAsCurl(): string
{
$maxRequestLength = 256000;
if (strlen($this->rawRequest) > $maxRequestLength) {
return '';
}
try {
return (new CurlFormatter())->format(parse_request($this->rawRequest));
} catch (\Throwable $e) {

View File

@@ -4,6 +4,7 @@ namespace App\Server\Connections;
use App\Contracts\ConnectionManager as ConnectionManagerContract;
use App\Contracts\SubdomainGenerator;
use App\Http\QueryParameters;
use Ratchet\ConnectionInterface;
use React\EventLoop\LoopInterface;
@@ -46,7 +47,13 @@ class ConnectionManager implements ConnectionManagerContract
$connection->client_id = $clientId;
$storedConnection = new ControlConnection($connection, $host, $subdomain ?? $this->subdomainGenerator->generateSubdomain(), $clientId);
$storedConnection = new ControlConnection(
$connection,
$host,
$subdomain ?? $this->subdomainGenerator->generateSubdomain(),
$clientId,
$this->getAuthTokenFromConnection($connection)
);
$this->connections[] = $storedConnection;
@@ -99,4 +106,21 @@ class ConnectionManager implements ConnectionManagerContract
{
return $this->connections;
}
protected function getAuthTokenFromConnection(ConnectionInterface $connection): string
{
return QueryParameters::create($connection->httpRequest)->get('authToken');
}
public function getConnectionsForAuthToken(string $authToken): array
{
return collect($this->connections)
->filter(function ($connection) use ($authToken) {
return $connection->authToken === $authToken;
})
->map(function ($connection) {
return $connection->toArray();
})
->toArray();
}
}

View File

@@ -12,17 +12,19 @@ class ControlConnection
/** @var ConnectionInterface */
public $socket;
public $host;
public $authToken;
public $subdomain;
public $client_id;
public $proxies = [];
protected $shared_at;
public function __construct(ConnectionInterface $socket, string $host, string $subdomain, string $clientId)
public function __construct(ConnectionInterface $socket, string $host, string $subdomain, string $clientId, string $authToken = '')
{
$this->socket = $socket;
$this->host = $host;
$this->subdomain = $subdomain;
$this->client_id = $clientId;
$this->authToken = $authToken;
$this->shared_at = now()->toDateTimeString();
}
@@ -57,6 +59,7 @@ class ControlConnection
return [
'host' => $this->host,
'client_id' => $this->client_id,
'auth_token' => $this->authToken,
'subdomain' => $this->subdomain,
'shared_at' => $this->shared_at,
];

View File

@@ -12,6 +12,7 @@ use App\Server\Http\Controllers\Admin\DeleteUsersController;
use App\Server\Http\Controllers\Admin\DisconnectSiteController;
use App\Server\Http\Controllers\Admin\GetSettingsController;
use App\Server\Http\Controllers\Admin\GetSitesController;
use App\Server\Http\Controllers\Admin\GetUserDetailsController;
use App\Server\Http\Controllers\Admin\GetUsersController;
use App\Server\Http\Controllers\Admin\ListSitesController;
use App\Server\Http\Controllers\Admin\ListUsersController;
@@ -124,6 +125,7 @@ class Factory
$this->router->post('/api/settings', StoreSettingsController::class, $adminCondition);
$this->router->get('/api/users', GetUsersController::class, $adminCondition);
$this->router->post('/api/users', StoreUsersController::class, $adminCondition);
$this->router->get('/api/users/{id}', GetUserDetailsController::class, $adminCondition);
$this->router->delete('/api/users/{id}', DeleteUsersController::class, $adminCondition);
$this->router->get('/api/sites', GetSitesController::class, $adminCondition);
$this->router->delete('/api/sites/{id}', DisconnectSiteController::class, $adminCondition);

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Server\Http\Controllers\Admin;
use App\Contracts\UserRepository;
use Illuminate\Http\Request;
use Ratchet\ConnectionInterface;
class GetUserDetailsController extends AdminController
{
protected $keepConnectionOpen = true;
/** @var UserRepository */
protected $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function handle(Request $request, ConnectionInterface $httpConnection)
{
$this->userRepository
->getUserById($request->get('id'))
->then(function ($user) use ($httpConnection) {
$httpConnection->send(
respond_json(['user' => $user])
);
$httpConnection->close();
});
}
}

View File

@@ -8,7 +8,6 @@ use App\Http\QueryParameters;
use Ratchet\ConnectionInterface;
use Ratchet\WebSocket\MessageComponentInterface;
use React\Promise\Deferred;
use React\Promise\FulfilledPromise;
use React\Promise\PromiseInterface;
use stdClass;
@@ -127,7 +126,7 @@ class ControlMessageController implements MessageComponentInterface
protected function verifyAuthToken(ConnectionInterface $connection): PromiseInterface
{
if (config('expose.admin.validate_auth_tokens') !== true) {
return new FulfilledPromise();
return \React\Promise\resolve(null);
}
$deferred = new Deferred();

View File

@@ -2,6 +2,7 @@
namespace App\Server\UserRepository;
use App\Contracts\ConnectionManager;
use App\Contracts\UserRepository;
use Clue\React\SQLite\DatabaseInterface;
use Clue\React\SQLite\Result;
@@ -13,9 +14,13 @@ class DatabaseUserRepository implements UserRepository
/** @var DatabaseInterface */
protected $database;
public function __construct(DatabaseInterface $database)
/** @var ConnectionManager */
protected $connectionManager;
public function __construct(DatabaseInterface $database, ConnectionManager $connectionManager)
{
$this->database = $database;
$this->connectionManager = $connectionManager;
}
public function getUsers(): PromiseInterface
@@ -46,8 +51,12 @@ class DatabaseUserRepository implements UserRepository
$nextPage = $currentPage + 1;
}
$users = collect($result->rows)->map(function ($user) {
return $this->getUserDetails($user);
})->toArray();
$paginated = [
'users' => $result->rows,
'users' => $users,
'current_page' => $currentPage,
'per_page' => $perPage,
'next_page' => $nextPage ?? null,
@@ -60,6 +69,13 @@ class DatabaseUserRepository implements UserRepository
return $deferred->promise();
}
protected function getUserDetails(array $user)
{
$user['sites'] = $user['auth_token'] !== '' ? $this->connectionManager->getConnectionsForAuthToken($user['auth_token']) : [];
return $user;
}
public function getUserById($id): PromiseInterface
{
$deferred = new Deferred();
@@ -67,7 +83,13 @@ class DatabaseUserRepository implements UserRepository
$this->database
->query('SELECT * FROM users WHERE id = :id', ['id' => $id])
->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows[0] ?? null);
$user = $result->rows[0] ?? null;
if (! is_null($user)) {
$user = $this->getUserDetails($user);
}
$deferred->resolve($user);
});
return $deferred->promise();

View File

@@ -7,7 +7,9 @@ order: 2
Once your Expose server is running, you can only access it over the port that you configure when the server gets started.
If you want to enable SSL support, you will need to use a proxy service - like Nginx, HAProxy or Caddy - to handle the SSL configurations and proxy all non-SSL requests to your expose server.
If you want to enable SSL support, you will need to use a proxy service - like Nginx, HAProxy, Apache2 or Caddy - to handle the SSL configurations and proxy all non-SSL requests to your expose server.
## Nginx configuration
A basic Nginx configuration would look like this, but you might want to tweak the SSL parameters to your liking.
@@ -40,3 +42,47 @@ server {
}
}
```
## Apache2 configuration
A basic Apache configuration would look like this, but you might want to tweak the SSL parameters to your liking.
```
Listen 80
Listen 443
<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerName expose.domain.tld
ServerAlias *.expose.domain.tld
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so
ServerAdmin admin@domain.tld
ProxyPass "/" "http://localhost:8080/"
ProxyPassReverse "/" "http://localhost:8080/"
ProxyPreserveHost On
# Needed for websocket support
RewriteCond %{HTTP:UPGRADE} ^WebSocket$ [NC,OR]
RewriteCond %{HTTP:CONNECTION} ^Upgrade$ [NC]
RewriteRule .* ws://127.0.0.1:8080%{REQUEST_URI} [P,QSA,L]
<Proxy http://localhost:8080>
Require all granted
Options none
</Proxy>
ErrorLog ${APACHE_LOG_DIR}/expose.domain.tld-error.log
CustomLog ${APACHE_LOG_DIR}/expose.domain.tld-access.log combined
SSLCertificateFile /etc/letsencrypt/live/expose.domain.tld-0001/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/expose.domain.tld-0001/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>
</IfModule>
```

View File

@@ -8,6 +8,7 @@ use Clue\React\Buzz\Browser;
use Clue\React\Buzz\Message\ResponseException;
use GuzzleHttp\Psr7\Response;
use Illuminate\Support\Str;
use Nyholm\Psr7\Request;
use Psr\Http\Message\ResponseInterface;
use Ratchet\Server\IoConnection;
use Tests\Feature\TestCase;
@@ -149,6 +150,8 @@ class AdminTest extends TestCase
$connectionManager = app(ConnectionManager::class);
$connection = \Mockery::mock(IoConnection::class);
$connection->httpRequest = new Request('GET', '/?authToken=some-token');
$connectionManager->storeConnection('some-host.text', 'fixed-subdomain', $connection);
/** @var Response $response */

View File

@@ -0,0 +1,201 @@
<?php
namespace Tests\Feature\Server;
use App\Contracts\ConnectionManager;
use App\Server\Factory;
use Clue\React\Buzz\Browser;
use GuzzleHttp\Psr7\Response;
use Nyholm\Psr7\Request;
use Ratchet\Server\IoConnection;
use Tests\Feature\TestCase;
class ApiTest extends TestCase
{
/** @var Browser */
protected $browser;
/** @var Factory */
protected $serverFactory;
public function setUp(): void
{
parent::setUp();
$this->browser = new Browser($this->loop);
$this->browser = $this->browser->withOptions([
'followRedirects' => false,
]);
$this->startServer();
}
public function tearDown(): void
{
$this->serverFactory->getSocket()->close();
parent::tearDown();
}
/** @test */
public function it_can_list_all_registered_users()
{
/** @var 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',
])));
/** @var Response $response */
$response = $this->await($this->browser->get('http://127.0.0.1:8080/api/users', [
'Host' => 'expose.localhost',
'Authorization' => base64_encode('username:secret'),
'Content-Type' => 'application/json',
]));
$body = json_decode($response->getBody()->getContents());
$users = $body->paginated->users;
$this->assertCount(1, $users);
$this->assertSame('Marcel', $users[0]->name);
$this->assertSame([], $users[0]->sites);
}
/** @test */
public function it_can_get_user_details()
{
/** @var 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',
])));
/** @var Response $response */
$response = $this->await($this->browser->get('http://127.0.0.1:8080/api/users/1', [
'Host' => 'expose.localhost',
'Authorization' => base64_encode('username:secret'),
'Content-Type' => 'application/json',
]));
$body = json_decode($response->getBody()->getContents());
$user = $body->user;
$this->assertSame('Marcel', $user->name);
$this->assertSame([], $user->sites);
}
/** @test */
public function it_can_list_all_currently_connected_sites_from_all_users()
{
/** @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',
])));
$createdUser = json_decode($response->getBody()->getContents())->user;
/** @var ConnectionManager $connectionManager */
$connectionManager = app(ConnectionManager::class);
$connection = \Mockery::mock(IoConnection::class);
$connection->httpRequest = new Request('GET', '/?authToken='.$createdUser->auth_token);
$connectionManager->storeConnection('some-host.test', 'fixed-subdomain', $connection);
$connection = \Mockery::mock(IoConnection::class);
$connection->httpRequest = new Request('GET', '/?authToken=some-other-token');
$connectionManager->storeConnection('some-different-host.test', 'different-subdomain', $connection);
/** @var Response $response */
$response = $this->await($this->browser->get('http://127.0.0.1:8080/api/users', [
'Host' => 'expose.localhost',
'Authorization' => base64_encode('username:secret'),
'Content-Type' => 'application/json',
]));
$body = json_decode($response->getBody()->getContents());
$users = $body->paginated->users;
$this->assertCount(1, $users[0]->sites);
$this->assertSame('some-host.test', $users[0]->sites[0]->host);
$this->assertSame('fixed-subdomain', $users[0]->sites[0]->subdomain);
}
/** @test */
public function it_can_list_all_currently_connected_sites()
{
/** @var ConnectionManager $connectionManager */
$connectionManager = app(ConnectionManager::class);
$connection = \Mockery::mock(IoConnection::class);
$connection->httpRequest = new Request('GET', '/?authToken=some-token');
$connectionManager->storeConnection('some-host.test', 'fixed-subdomain', $connection);
/** @var Response $response */
$response = $this->await($this->browser->get('http://127.0.0.1:8080/api/sites', [
'Host' => 'expose.localhost',
'Authorization' => base64_encode('username:secret'),
'Content-Type' => 'application/json',
]));
$body = json_decode($response->getBody()->getContents());
$sites = $body->sites;
$this->assertCount(1, $sites);
$this->assertSame('some-host.test', $sites[0]->host);
$this->assertSame('some-token', $sites[0]->auth_token);
$this->assertSame('fixed-subdomain', $sites[0]->subdomain);
}
/** @test */
public function it_can_list_all_currently_connected_sites_without_auth_tokens()
{
/** @var ConnectionManager $connectionManager */
$connectionManager = app(ConnectionManager::class);
$connection = \Mockery::mock(IoConnection::class);
$connection->httpRequest = new Request('GET', '/');
$connectionManager->storeConnection('some-host.test', 'fixed-subdomain', $connection);
/** @var Response $response */
$response = $this->await($this->browser->get('http://127.0.0.1:8080/api/sites', [
'Host' => 'expose.localhost',
'Authorization' => base64_encode('username:secret'),
'Content-Type' => 'application/json',
]));
$body = json_decode($response->getBody()->getContents());
$sites = $body->sites;
$this->assertCount(1, $sites);
$this->assertSame('some-host.test', $sites[0]->host);
$this->assertSame('', $sites[0]->auth_token);
$this->assertSame('fixed-subdomain', $sites[0]->subdomain);
}
protected function startServer()
{
$this->app['config']['expose.admin.subdomain'] = 'expose';
$this->app['config']['expose.admin.database'] = ':memory:';
$this->app['config']['expose.admin.users'] = [
'username' => 'secret',
];
$this->serverFactory = new Factory();
$this->serverFactory->setLoop($this->loop)
->createServer();
}
}