diff --git a/app/Client/Exceptions/InvalidServerProvided.php b/app/Client/Exceptions/InvalidServerProvided.php new file mode 100644 index 0000000..0fb075e --- /dev/null +++ b/app/Client/Exceptions/InvalidServerProvided.php @@ -0,0 +1,13 @@ +option('server-host') ?? config('expose.servers.'.$this->option('server').'.host', 'localhost'); + if ($this->option('server-host')) { + return $this->option('server-host'); + } + + /** + * Try to find the server in the servers array. + * If no array exists at all (when upgrading from v1), + * always return sharedwithexpose.com + */ + if (config('expose.servers') === null) { + return static::DEFAULT_HOSTNAME; + } + + $server = $this->option('server'); + $host = config('expose.servers.'.$server.'.host'); + + if (! is_null($host)) { + return $host; + } + + return $this->lookupRemoteServerHost($server); } protected function getServerPort() { - return $this->option('server-port') ?? config('expose.servers.'.$this->option('server').'.port', 8080); + if ($this->option('server-port')) { + return $this->option('server-port'); + } + + /** + * Try to find the server in the servers array. + * If no array exists at all (when upgrading from v1), + * always return sharedwithexpose.com + */ + if (config('expose.servers') === null) { + return static::DEFAULT_PORT; + } + + + $server = $this->option('server'); + $host = config('expose.servers.'.$server.'.port'); + + if (! is_null($host)) { + return $host; + } + + return $this->lookupRemoteServerPort($server); + } + + protected function lookupRemoteServers() + { + try { + return Http::get(config('expose.server_endpoint', static::DEFAULT_SERVER_ENDPOINT))->json(); + } catch (\Throwable $e) { + return []; + } + } + + protected function lookupRemoteServerHost($server) + { + $servers = $this->lookupRemoteServers(); + $host = Arr::get($servers, $server.'.host'); + + throw_if(is_null($host), new InvalidServerProvided($server)); + + return $host; + } + + protected function lookupRemoteServerPort($server) + { + $servers = $this->lookupRemoteServers(); + $port = Arr::get($servers, $server.'.port'); + + throw_if(is_null($port), new InvalidServerProvided($server)); + + return $port; } } diff --git a/app/Commands/ShareCurrentWorkingDirectoryCommand.php b/app/Commands/ShareCurrentWorkingDirectoryCommand.php index b682c3d..9c26afc 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=}'; + protected $signature = 'share-cwd {host?} {--subdomain=} {--auth=} {--dns=}'; public function handle() { diff --git a/app/Contracts/UserRepository.php b/app/Contracts/UserRepository.php index 66c0f74..5c08baf 100644 --- a/app/Contracts/UserRepository.php +++ b/app/Contracts/UserRepository.php @@ -10,11 +10,13 @@ interface UserRepository public function getUserById($id): PromiseInterface; - public function paginateUsers(int $perPage, int $currentPage): PromiseInterface; + public function paginateUsers(string $searchQuery, int $perPage, int $currentPage): PromiseInterface; public function getUserByToken(string $authToken): PromiseInterface; public function storeUser(array $data): PromiseInterface; public function deleteUser($id): PromiseInterface; + + public function getUsersByTokens(array $authTokens): PromiseInterface; } diff --git a/app/Server/Connections/TcpControlConnection.php b/app/Server/Connections/TcpControlConnection.php index db5c284..9803d4c 100644 --- a/app/Server/Connections/TcpControlConnection.php +++ b/app/Server/Connections/TcpControlConnection.php @@ -76,6 +76,7 @@ class TcpControlConnection extends ControlConnection return [ 'type' => 'tcp', 'port' => $this->port, + 'auth_token' => $this->authToken, 'client_id' => $this->client_id, 'shared_port' => $this->shared_port, 'shared_at' => $this->shared_at, diff --git a/app/Server/Http/Controllers/Admin/GetSitesController.php b/app/Server/Http/Controllers/Admin/GetSitesController.php index e7df0f7..9c23563 100644 --- a/app/Server/Http/Controllers/Admin/GetSitesController.php +++ b/app/Server/Http/Controllers/Admin/GetSitesController.php @@ -3,6 +3,7 @@ namespace App\Server\Http\Controllers\Admin; use App\Contracts\ConnectionManager; +use App\Contracts\UserRepository; use App\Server\Configuration; use App\Server\Connections\ControlConnection; use Illuminate\Http\Request; @@ -10,31 +11,54 @@ use Ratchet\ConnectionInterface; class GetSitesController extends AdminController { + protected $keepConnectionOpen = true; + /** @var ConnectionManager */ protected $connectionManager; + /** @var Configuration */ protected $configuration; - public function __construct(ConnectionManager $connectionManager, Configuration $configuration) + /** @var UserRepository */ + protected $userRepository; + + public function __construct(ConnectionManager $connectionManager, Configuration $configuration, UserRepository $userRepository) { $this->connectionManager = $connectionManager; + $this->userRepository = $userRepository; } public function handle(Request $request, ConnectionInterface $httpConnection) { - $httpConnection->send( - respond_json([ - 'sites' => collect($this->connectionManager->getConnections()) - ->filter(function ($connection) { - return get_class($connection) === ControlConnection::class; - }) - ->map(function ($site, $siteId) { - $site = $site->toArray(); - $site['id'] = $siteId; + $authTokens = []; - return $site; - })->values(), - ]) - ); + $sites = collect($this->connectionManager->getConnections()) + ->filter(function ($connection) { + return get_class($connection) === ControlConnection::class; + }) + ->map(function ($site, $siteId) use (&$authTokens) { + $site = $site->toArray(); + $site['id'] = $siteId; + $authTokens[] = $site['auth_token']; + + return $site; + })->values(); + + $this->userRepository->getUsersByTokens($authTokens) + ->then(function ($users) use ($httpConnection, $sites) { + $users = collect($users); + $sites = collect($sites)->map(function ($site) use ($users) { + $site['user'] = $users->firstWhere('auth_token', $site['auth_token']); + return $site; + })->toArray(); + + $httpConnection->send( + respond_json([ + 'sites' => $sites, + ]) + ); + + $httpConnection->close(); + }); } } diff --git a/app/Server/Http/Controllers/Admin/GetTcpConnectionsController.php b/app/Server/Http/Controllers/Admin/GetTcpConnectionsController.php index 5f98546..53dba22 100644 --- a/app/Server/Http/Controllers/Admin/GetTcpConnectionsController.php +++ b/app/Server/Http/Controllers/Admin/GetTcpConnectionsController.php @@ -3,6 +3,7 @@ namespace App\Server\Http\Controllers\Admin; use App\Contracts\ConnectionManager; +use App\Contracts\UserRepository; use App\Server\Configuration; use App\Server\Connections\TcpControlConnection; use Illuminate\Http\Request; @@ -10,32 +11,54 @@ use Ratchet\ConnectionInterface; class GetTcpConnectionsController extends AdminController { + protected $keepConnectionOpen = true; + /** @var ConnectionManager */ protected $connectionManager; + /** @var Configuration */ protected $configuration; - public function __construct(ConnectionManager $connectionManager, Configuration $configuration) + /** @var UserRepository */ + protected $userRepository; + + public function __construct(ConnectionManager $connectionManager, Configuration $configuration, UserRepository $userRepository) { $this->connectionManager = $connectionManager; + $this->userRepository = $userRepository; } public function handle(Request $request, ConnectionInterface $httpConnection) { - $httpConnection->send( - respond_json([ - 'tcp_connections' => collect($this->connectionManager->getConnections()) - ->filter(function ($connection) { - return get_class($connection) === TcpControlConnection::class; - }) - ->map(function ($site, $siteId) { - $site = $site->toArray(); - $site['id'] = $siteId; + $authTokens = []; + $connections = collect($this->connectionManager->getConnections()) + ->filter(function ($connection) { + return get_class($connection) === TcpControlConnection::class; + }) + ->map(function ($site, $siteId) use (&$authTokens) { + $site = $site->toArray(); + $site['id'] = $siteId; + $authTokens[] = $site['auth_token']; - return $site; - }) - ->values(), - ]) - ); + return $site; + }) + ->values(); + + $this->userRepository->getUsersByTokens($authTokens) + ->then(function ($users) use ($httpConnection, $connections) { + $users = collect($users); + $connections = collect($connections)->map(function ($connection) use ($users) { + $connection['user'] = $users->firstWhere('auth_token', $connection['auth_token']); + return $connection; + })->toArray(); + + $httpConnection->send( + respond_json([ + 'tcp_connections' => $connections, + ]) + ); + + $httpConnection->close(); + }); } } diff --git a/app/Server/Http/Controllers/Admin/GetUsersController.php b/app/Server/Http/Controllers/Admin/GetUsersController.php index 933f38e..4c82864 100644 --- a/app/Server/Http/Controllers/Admin/GetUsersController.php +++ b/app/Server/Http/Controllers/Admin/GetUsersController.php @@ -21,7 +21,7 @@ class GetUsersController extends AdminController public function handle(Request $request, ConnectionInterface $httpConnection) { $this->userRepository - ->paginateUsers(20, (int) $request->get('page', 1)) + ->paginateUsers($request->get('search', ''), (int) $request->get('perPage', 20), (int) $request->get('page', 1)) ->then(function ($paginated) use ($httpConnection) { $httpConnection->send( respond_json(['paginated' => $paginated]) diff --git a/app/Server/Http/Controllers/Admin/ListSitesController.php b/app/Server/Http/Controllers/Admin/ListSitesController.php index 3ca6bcc..bbc05a9 100644 --- a/app/Server/Http/Controllers/Admin/ListSitesController.php +++ b/app/Server/Http/Controllers/Admin/ListSitesController.php @@ -26,17 +26,6 @@ class ListSitesController extends AdminController $sites = $this->getView($httpConnection, 'server.sites.index', [ 'scheme' => $this->configuration->port() === 443 ? 'https' : 'http', 'configuration' => $this->configuration, - 'sites' => collect($this->connectionManager->getConnections()) - ->filter(function ($connection) { - return get_class($connection) === ControlConnection::class; - }) - ->map(function ($site, $siteId) { - $site = $site->toArray(); - $site['id'] = $siteId; - - return $site; - }) - ->values(), ]); $httpConnection->send( diff --git a/app/Server/Http/Controllers/Admin/ListTcpConnectionsController.php b/app/Server/Http/Controllers/Admin/ListTcpConnectionsController.php index 2d219fe..be755b0 100644 --- a/app/Server/Http/Controllers/Admin/ListTcpConnectionsController.php +++ b/app/Server/Http/Controllers/Admin/ListTcpConnectionsController.php @@ -26,17 +26,6 @@ class ListTcpConnectionsController extends AdminController $sites = $this->getView($httpConnection, 'server.tcp.index', [ 'scheme' => $this->configuration->port() === 443 ? 'https' : 'http', 'configuration' => $this->configuration, - 'connections' => collect($this->connectionManager->getConnections()) - ->filter(function ($connection) { - return get_class($connection) === TcpControlConnection::class; - }) - ->map(function ($connection, $connectionId) { - $connection = $connection->toArray(); - $connection['id'] = $connectionId; - - return $connection; - }) - ->values(), ]); $httpConnection->send( diff --git a/app/Server/Http/Controllers/Admin/ListUsersController.php b/app/Server/Http/Controllers/Admin/ListUsersController.php index 2fff138..c2adc64 100644 --- a/app/Server/Http/Controllers/Admin/ListUsersController.php +++ b/app/Server/Http/Controllers/Admin/ListUsersController.php @@ -21,7 +21,7 @@ class ListUsersController extends AdminController public function handle(Request $request, ConnectionInterface $httpConnection) { $this->userRepository - ->paginateUsers(20, (int) $request->get('page', 1)) + ->paginateUsers($request->get('search', ''), 20, (int) $request->get('page', 1)) ->then(function ($paginated) use ($httpConnection) { $httpConnection->send( respond_html($this->getView($httpConnection, 'server.users.index', ['paginated' => $paginated])) diff --git a/app/Server/Http/Controllers/Admin/StoreSettingsController.php b/app/Server/Http/Controllers/Admin/StoreSettingsController.php index 04aa289..7abb5fd 100644 --- a/app/Server/Http/Controllers/Admin/StoreSettingsController.php +++ b/app/Server/Http/Controllers/Admin/StoreSettingsController.php @@ -31,6 +31,14 @@ class StoreSettingsController extends AdminController config()->set('expose.admin.messages.message_of_the_day', Arr::get($messages, 'message_of_the_day')); + config()->set('expose.admin.messages.custom_subdomain_unauthorized', Arr::get($messages, 'custom_subdomain_unauthorized')); + + config()->set('expose.admin.messages.no_free_tcp_port_available', Arr::get($messages, 'no_free_tcp_port_available')); + + config()->set('expose.admin.messages.tcp_port_sharing_unauthorized', Arr::get($messages, 'tcp_port_sharing_unauthorized')); + + config()->set('expose.admin.messages.tcp_port_sharing_disabled', Arr::get($messages, 'tcp_port_sharing_disabled')); + $httpConnection->send( respond_json([ 'configuration' => $this->configuration, diff --git a/app/Server/Http/Controllers/ControlMessageController.php b/app/Server/Http/Controllers/ControlMessageController.php index cfc2f85..95e2b22 100644 --- a/app/Server/Http/Controllers/ControlMessageController.php +++ b/app/Server/Http/Controllers/ControlMessageController.php @@ -275,6 +275,18 @@ class ControlMessageController implements MessageComponentInterface protected function canShareTcpPorts(ConnectionInterface $connection, $data, $user) { + if (! config('expose.admin.allow_tcp_port_sharing', true)) { + $connection->send(json_encode([ + 'event' => 'authenticationFailed', + 'data' => [ + 'message' => config('expose.admin.messages.tcp_port_sharing_disabled'), + ], + ])); + $connection->close(); + + return false; + } + if (! is_null($user) && $user['can_share_tcp_ports'] === 0) { $connection->send(json_encode([ 'event' => 'authenticationFailed', diff --git a/app/Server/UserRepository/DatabaseUserRepository.php b/app/Server/UserRepository/DatabaseUserRepository.php index d572f17..06536f6 100644 --- a/app/Server/UserRepository/DatabaseUserRepository.php +++ b/app/Server/UserRepository/DatabaseUserRepository.php @@ -36,34 +36,52 @@ class DatabaseUserRepository implements UserRepository return $deferred->promise(); } - public function paginateUsers(int $perPage, int $currentPage): PromiseInterface + public function paginateUsers(string $searchQuery, int $perPage, int $currentPage): PromiseInterface { $deferred = new Deferred(); $this->database - ->query('SELECT * FROM users ORDER by created_at DESC LIMIT :limit OFFSET :offset', [ - 'limit' => $perPage + 1, - 'offset' => $currentPage < 2 ? 0 : ($currentPage - 1) * $perPage, - ]) - ->then(function (Result $result) use ($deferred, $perPage, $currentPage) { - if (count($result->rows) == $perPage + 1) { - array_pop($result->rows); - $nextPage = $currentPage + 1; - } + ->query('SELECT COUNT(*) AS count FROM users') + ->then(function (Result $result) use ($searchQuery, $deferred, $perPage, $currentPage) { + $totalUsers = $result->rows[0]['count']; - $users = collect($result->rows)->map(function ($user) { - return $this->getUserDetails($user); - })->toArray(); + $query = 'SELECT * FROM users '; - $paginated = [ - 'users' => $users, - 'current_page' => $currentPage, - 'per_page' => $perPage, - 'next_page' => $nextPage ?? null, - 'previous_page' => $currentPage > 1 ? $currentPage - 1 : null, + $bindings = [ + 'limit' => $perPage + 1, + 'offset' => $currentPage < 2 ? 0 : ($currentPage - 1) * $perPage, ]; - $deferred->resolve($paginated); + if ($searchQuery !== '') { + $query .= "WHERE name LIKE '%".$searchQuery."%' "; + $bindings['search'] = $searchQuery; + } + + $query .= ' ORDER by created_at DESC LIMIT :limit OFFSET :offset'; + + $this->database + ->query($query, $bindings) + ->then(function (Result $result) use ($deferred, $perPage, $currentPage, $totalUsers) { + if (count($result->rows) == $perPage + 1) { + array_pop($result->rows); + $nextPage = $currentPage + 1; + } + + $users = collect($result->rows)->map(function ($user) { + return $this->getUserDetails($user); + })->toArray(); + + $paginated = [ + 'total' => $totalUsers, + 'users' => $users, + 'current_page' => $currentPage, + 'per_page' => $perPage, + 'next_page' => $nextPage ?? null, + 'previous_page' => $currentPage > 1 ? $currentPage - 1 : null, + ]; + + $deferred->resolve($paginated); + }); }); return $deferred->promise(); @@ -138,4 +156,25 @@ class DatabaseUserRepository implements UserRepository return $deferred->promise(); } + + public function getUsersByTokens(array $authTokens): PromiseInterface + { + $deferred = new Deferred(); + + $authTokenString = collect($authTokens)->map(function ($token) { + return '"'.$token.'"'; + })->join(','); + + $this->database->query('SELECT * FROM users WHERE auth_token IN ('.$authTokenString.')') + ->then(function (Result $result) use ($deferred) { + + $users = collect($result->rows)->map(function ($user) { + return $this->getUserDetails($user); + })->toArray(); + + $deferred->resolve($users); + }); + + return $deferred->promise(); + } } diff --git a/builds/expose b/builds/expose index 36a972a..d2caf2c 100755 Binary files a/builds/expose and b/builds/expose differ diff --git a/config/app.php b/config/app.php index 82aff24..7fba342 100644 --- a/config/app.php +++ b/config/app.php @@ -26,7 +26,7 @@ return [ | */ - 'version' => '1.4.2', + 'version' => '2.0.0-beta', /* |-------------------------------------------------------------------------- diff --git a/config/expose.php b/config/expose.php index 20fdeaa..32bf286 100644 --- a/config/expose.php +++ b/config/expose.php @@ -19,6 +19,21 @@ return [ ], ], + /* + |-------------------------------------------------------------------------- + | Server Endpoint + |-------------------------------------------------------------------------- + | + | When you specify a server that does not exist in above static array, + | Expose will perform a GET request to this URL and tries to retrieve + | a JSON payload that looks like the configurations servers array. + | + | Expose then tries to load the configuration for the given server + | if available. + | + */ + 'server_endpoint' => 'https://beyondco.de/api/expose/servers', + /* |-------------------------------------------------------------------------- | DNS @@ -165,6 +180,19 @@ return [ */ 'validate_auth_tokens' => false, + /* + |-------------------------------------------------------------------------- + | TCP Port Sharing + |-------------------------------------------------------------------------- + | + | Control if you want to allow users to share TCP ports with your Expose + | server. You can add fine-grained control per authentication token, + | but if you want to disable TCP port sharing in general, set this + | value to false. + | + */ + 'allow_tcp_port_sharing' => true, + /* |-------------------------------------------------------------------------- | TCP Port Range @@ -281,6 +309,8 @@ return [ 'tcp_port_sharing_unauthorized' => 'You are not allowed to share TCP ports. Please upgrade to Expose Pro.', 'no_free_tcp_port_available' => 'There are no free TCP ports available on this server. Please try again later.', + + 'tcp_port_sharing_disabled' => 'TCP port sharing is not available on this Expose server.', ], ], ]; diff --git a/resources/views/client/dashboard.twig b/resources/views/client/dashboard.twig index a6cf14c..b2a3ac6 100644 --- a/resources/views/client/dashboard.twig +++ b/resources/views/client/dashboard.twig @@ -5,7 +5,7 @@ - + + + +
-