diff --git a/src/app/Console/Commands/Meet/SessionsCommand.php b/src/app/Console/Commands/Meet/SessionsCommand.php index 518aef14..470ff1b3 100644 --- a/src/app/Console/Commands/Meet/SessionsCommand.php +++ b/src/app/Console/Commands/Meet/SessionsCommand.php @@ -1,70 +1,55 @@ false, // No exceptions from Guzzle - 'base_uri' => \config('meet.api_url'), - 'verify' => \config('meet.api_verify_tls'), - 'headers' => [ - 'X-Auth-Token' => \config('meet.api_token'), - ], - 'connect_timeout' => 10, - 'timeout' => 10, - ]); + $response = Service::client()->get('sessions')->throwUnlessStatus(200); - $response = $client->request('GET', 'sessions'); - - if ($response->getStatusCode() !== 200) { - return 1; - } - - $sessions = json_decode($response->getBody(), true); - - foreach ($sessions as $session) { - $room = \App\Meet\Room::where('session_id', $session['roomId'])->first(); + foreach ($response->json() as $session) { + $room = Room::where('session_id', $session['roomId'])->first(); if ($room) { $owner = $room->wallet()->owner->email; $roomName = $room->name; } else { $owner = '(none)'; $roomName = '(none)'; } $this->info( sprintf( "Session: %s for %s since %s (by %s)", $session['roomId'], $roomName, \Carbon\Carbon::parse($session['createdAt'], 'UTC'), $owner ) ); } } } diff --git a/src/app/Console/Commands/Status/Health.php b/src/app/Console/Commands/Status/Health.php index c62ce3be..abadff43 100644 --- a/src/app/Console/Commands/Status/Health.php +++ b/src/app/Console/Commands/Status/Health.php @@ -1,219 +1,197 @@ line($exception); return false; } } private function checkOpenExchangeRates() { try { OpenExchangeRates::healthcheck(); return true; } catch (\Exception $exception) { $this->line($exception); return false; } } private function checkMollie() { try { return Mollie::healthcheck(); } catch (\Exception $exception) { $this->line($exception); return false; } } private function checkDAV() { try { DAV::healthcheck(); return true; } catch (\Exception $exception) { $this->line($exception); return false; } } private function checkLDAP() { try { LDAP::healthcheck(); return true; } catch (\Exception $exception) { $this->line($exception); return false; } } private function checkIMAP() { try { IMAP::healthcheck(); return true; } catch (\Exception $exception) { $this->line($exception); return false; } } private function checkRoundcube() { try { //TODO maybe run a select? Roundcube::dbh(); return true; } catch (\Exception $exception) { $this->line($exception); return false; } } private function checkRedis() { try { Redis::connection(); return true; } catch (\Exception $exception) { $this->line($exception); return false; } } private function checkStorage() { try { Storage::healthcheck(); return true; } catch (\Exception $exception) { $this->line($exception); return false; } } private function checkMeet() { $urls = \config('meet.api_urls'); $success = true; foreach ($urls as $url) { $this->line("Checking $url"); try { - $client = new \GuzzleHttp\Client( - [ - 'http_errors' => false, // No exceptions from Guzzle - 'base_uri' => $url, - 'verify' => \config('meet.api_verify_tls'), - 'headers' => [ - 'X-Auth-Token' => \config('meet.api_token'), - ], - 'connect_timeout' => 10, - 'timeout' => 10, - 'on_stats' => function (\GuzzleHttp\TransferStats $stats) { - $threshold = \config('logging.slow_log'); - if ($threshold && ($sec = $stats->getTransferTime()) > $threshold) { - $url = $stats->getEffectiveUri(); - $method = $stats->getRequest()->getMethod(); - \Log::warning(sprintf("[STATS] %s %s: %.4f sec.", $method, $url, $sec)); - } - }, - ] - ); - - $response = $client->request('GET', "ping"); - if ($response->getStatusCode() != 200) { - $code = $response->getStatusCode(); - $reason = $response->getReasonPhrase(); + $response = \App\Meet\Service::client($url)->get('ping'); + if (!$response->ok()) { $success = false; - $this->line("Backend {$url} not available. Status: {$code} Reason: {$reason}"); + $this->line("Backend {$url} not available. Status: " . $response->status()); } } catch (\Exception $exception) { $success = false; - $this->line("Backend {$url} not available. Error: {$exception}"); + $this->line("Backend {$url} not available. Error: " . $exception->getMessage()); } } + return $success; } /** * Execute the console command. * * @return mixed */ public function handle() { $result = 0; $steps = $this->option('check'); if (empty($steps)) { $steps = [ 'DB', 'Redis', 'IMAP', 'Roundcube', 'Meet', 'DAV', 'Mollie', 'OpenExchangeRates' ]; if (\config('app.with_ldap')) { array_unshift($steps, 'LDAP'); } if (\config('app.with_imap')) { array_unshift($steps, 'IMAP'); } if (\config('app.with_files')) { array_unshift($steps, 'Storage'); } } foreach ($steps as $step) { $func = "check{$step}"; $this->line("Checking {$step}..."); if ($this->{$func}()) { $this->info("OK"); } else { $this->error("Not found"); $result = 1; } } return $result; } } diff --git a/src/app/Meet/Room.php b/src/app/Meet/Room.php index 722408c3..6313fa0d 100644 --- a/src/app/Meet/Room.php +++ b/src/app/Meet/Room.php @@ -1,340 +1,279 @@ The attributes that should be cast */ protected $casts = [ 'created_at' => 'datetime:Y-m-d H:i:s', 'deleted_at' => 'datetime:Y-m-d H:i:s', 'updated_at' => 'datetime:Y-m-d H:i:s', ]; /** @var array The attributes that are mass assignable */ protected $fillable = ['name', 'description']; /** @var array The attributes that can be not set */ protected $nullable = ['description']; /** @var string Database table name */ protected $table = 'openvidu_rooms'; - /** @var \GuzzleHttp\Client|null HTTP client instance */ - private $client = null; - - /** - * Select a Meet server for this room - * - * This needs to always result in the same server for the same room, - * so all participants end up on the same server. - * - * @return string The server url - */ - private function selectMeetServer() - { - $urls = \config('meet.api_urls'); - - $count = count($urls); - - if ($count == 0) { - \Log::error("No meet server configured."); - return ""; - } - - //Select a random backend. - //If the names are evenly distributed this should theoretically result in an even distribution. - $index = abs(intval(hash("crc32b", $this->name), 16) % $count); - - return $urls[$index]; - } - /** * Creates HTTP client for connections to Meet server * - * @return \GuzzleHttp\Client HTTP client instance + * @return HTTP client instance */ private function client() { - if (!$this->client) { - $url = $this->selectMeetServer(); - - $this->client = new \GuzzleHttp\Client( - [ - 'http_errors' => false, // No exceptions from Guzzle - 'base_uri' => $url, - 'verify' => \config('meet.api_verify_tls'), - 'headers' => [ - 'X-Auth-Token' => \config('meet.api_token'), - ], - 'connect_timeout' => 10, - 'timeout' => 10, - 'on_stats' => function (\GuzzleHttp\TransferStats $stats) { - $threshold = \config('logging.slow_log'); - if ($threshold && ($sec = $stats->getTransferTime()) > $threshold) { - $url = $stats->getEffectiveUri(); - $method = $stats->getRequest()->getMethod(); - \Log::warning(sprintf("[STATS] %s %s: %.4f sec.", $method, $url, $sec)); - } - }, - ] - ); - } - - return $this->client; + return Service::clientForRoom($this->name); } /** * Create a Meet session * * @return array|null Session data on success, NULL otherwise */ public function createSession(): ?array { - $params = [ - 'json' => [ /* request params here */ ] - ]; - - $response = $this->client()->request('POST', "sessions", $params); + $response = $this->client()->post('sessions'); - if ($response->getStatusCode() !== 200) { + if ($response->status() !== 200) { $this->logError("Failed to create the meet session", $response); $this->session_id = null; $this->save(); return null; } - $session = json_decode($response->getBody(), true); + $session = $response->json(); $this->session_id = $session['id']; $this->save(); return $session; } /** * Create a Meet session (connection) token * * @param int $role User role (see self::ROLE_* constants) * * @return array|null Token data on success, NULL otherwise * @throws \Exception if session does not exist */ public function getSessionToken($role = self::ROLE_SUBSCRIBER): ?array { if (!$this->session_id) { throw new \Exception("The room session does not exist"); } $url = 'sessions/' . $this->session_id . '/connection'; $post = [ - 'json' => [ - 'role' => $role, - ] + 'role' => $role, ]; - $response = $this->client()->request('POST', $url, $post); - - if ($response->getStatusCode() == 200) { - $json = json_decode($response->getBody(), true); + $response = $this->client()->post($url, $post); + if ($response->status() == 200) { return [ - 'token' => $json['token'], + 'token' => $response->json('token'), 'role' => $role, ]; } $this->logError("Failed to create the meet peer connection", $response); return null; } /** * Check if the room has an active session * * @return bool True when the session exists, False otherwise */ public function hasSession(): bool { if (!$this->session_id) { return false; } - $response = $this->client()->request('GET', "sessions/{$this->session_id}"); + $response = $this->client()->get("sessions/{$this->session_id}"); $this->logError("Failed to check that a meet session exists", $response); - return $response->getStatusCode() == 200; + return $response->status() == 200; } /** * Accept the join request. * * @param string $id Request identifier * * @return bool True on success, False on failure */ public function requestAccept(string $id): bool { $request = Cache::get($this->session_id . '-' . $id); if ($request) { $request['status'] = self::REQUEST_ACCEPTED; return Cache::put($this->session_id . '-' . $id, $request, now()->addHours(1)); } return false; } /** * Deny the join request. * * @param string $id Request identifier * * @return bool True on success, False on failure */ public function requestDeny(string $id): bool { $request = Cache::get($this->session_id . '-' . $id); if ($request) { $request['status'] = self::REQUEST_DENIED; return Cache::put($this->session_id . '-' . $id, $request, now()->addHours(1)); } return false; } /** * Get the join request data. * * @param string $id Request identifier * * @return array|null Request data (e.g. nickname, status, picture?) */ public function requestGet(string $id): ?array { return Cache::get($this->session_id . '-' . $id); } /** * Save the join request. * * @param string $id Request identifier * @param array $request Request data * * @return bool True on success, False on failure */ public function requestSave(string $id, array $request): bool { // We don't really need the picture in the cache // As we use this cache for the request status only unset($request['picture']); return Cache::put($this->session_id . '-' . $id, $request, now()->addHours(1)); } /** * Send a signal to the Meet session participants (peers) * * @param string $name Signal name (type) * @param array $data Signal data array * @param int $target Limit targets by their participant role * * @return bool True on success, False on failure * @throws \Exception if session does not exist */ public function signal(string $name, array $data = [], $target = null): bool { if (!$this->session_id) { throw new \Exception("The room session does not exist"); } $post = [ 'roomId' => $this->session_id, 'type' => $name, 'role' => $target, 'data' => $data, ]; - $response = $this->client()->request('POST', 'signal', ['json' => $post]); + $response = $this->client()->post('signal', $post); $this->logError("Failed to send a signal to the meet session", $response); - return $response->getStatusCode() == 200; + return $response->status() == 200; } /** * Returns a map of supported ACL labels. * * @return array Map of supported permission rights/ACL labels */ protected function supportedACL(): array { return [ 'full' => \App\Permission::READ | \App\Permission::WRITE | \App\Permission::ADMIN, ]; } /** * Returns room name (required by the EntitleableTrait). * * @return string|null Room name */ public function toString(): ?string { return $this->name; } /** * Log an error for a failed request to the meet server * * @param string $str The error string * @param object $response Guzzle client response */ private function logError(string $str, $response) { - $code = $response->getStatusCode(); + $code = $response->status(); if ($code != 200) { \Log::error("$str [$code]"); } } } diff --git a/src/app/Meet/Service.php b/src/app/Meet/Service.php new file mode 100644 index 00000000..c01c100f --- /dev/null +++ b/src/app/Meet/Service.php @@ -0,0 +1,86 @@ + 1 && $roomName) { + $index = abs(intval(hash('crc32b', $roomName), 16) % $count); + } + + return $urls[$index]; + } + + /** + * Creates HTTP client for connection to the Meet server + * + * @param ?string $roomName Room name + * + * @return HTTP client instance + */ + public static function clientForRoom($roomName = null) + { + $url = self::selectMeetServer($roomName); + + return Http::withSlowLog() + ->withOptions([ + 'verify' => \config('meet.api_verify_tls'), + ]) + ->withHeaders([ + 'X-Auth-Token' => \config('meet.api_token'), + ]) + ->baseUrl($url) + ->timeout(10) + ->connectTimeout(10); + } + + /** + * Creates HTTP client for connection to the Meet server. + * Server location can be provided, otherwise first server on the list is used. + * + * @param ?string $baseUrl Server location + * + * @return HTTP client instance + */ + public static function client($baseUrl = null) + { + if (empty($baseUrl)) { + $baseUrl = self::selectMeetServer(); + } + + return Http::withSlowLog() + ->withOptions([ + 'verify' => \config('meet.api_verify_tls'), + ]) + ->withHeaders([ + 'X-Auth-Token' => \config('meet.api_token'), + ]) + ->baseUrl($baseUrl) + ->timeout(10) + ->connectTimeout(10); + } +} diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php index cad0c62d..00e11703 100644 --- a/src/app/Providers/AppServiceProvider.php +++ b/src/app/Providers/AppServiceProvider.php @@ -1,154 +1,168 @@ 'overrideValue1', * 'queue.connections.database.table' => 'overrideValue2', * ]; */ private function applyOverrideConfig(): void { $overrideConfig = (array) \config('override'); foreach (array_keys($overrideConfig) as $key) { \config([$key => $overrideConfig[$key]]); } } /** * Bootstrap any application services. */ public function boot(): void { \App\Domain::observe(\App\Observers\DomainObserver::class); \App\Entitlement::observe(\App\Observers\EntitlementObserver::class); \App\EventLog::observe(\App\Observers\EventLogObserver::class); \App\Group::observe(\App\Observers\GroupObserver::class); \App\GroupSetting::observe(\App\Observers\GroupSettingObserver::class); \App\Meet\Room::observe(\App\Observers\Meet\RoomObserver::class); \App\PackageSku::observe(\App\Observers\PackageSkuObserver::class); \App\PlanPackage::observe(\App\Observers\PlanPackageObserver::class); \App\Resource::observe(\App\Observers\ResourceObserver::class); \App\ResourceSetting::observe(\App\Observers\ResourceSettingObserver::class); \App\SharedFolder::observe(\App\Observers\SharedFolderObserver::class); \App\SharedFolderAlias::observe(\App\Observers\SharedFolderAliasObserver::class); \App\SharedFolderSetting::observe(\App\Observers\SharedFolderSettingObserver::class); \App\SignupCode::observe(\App\Observers\SignupCodeObserver::class); \App\SignupInvitation::observe(\App\Observers\SignupInvitationObserver::class); \App\SignupToken::observe(\App\Observers\SignupTokenObserver::class); \App\Transaction::observe(\App\Observers\TransactionObserver::class); \App\User::observe(\App\Observers\UserObserver::class); \App\UserAlias::observe(\App\Observers\UserAliasObserver::class); \App\UserSetting::observe(\App\Observers\UserSettingObserver::class); \App\VerificationCode::observe(\App\Observers\VerificationCodeObserver::class); \App\Wallet::observe(\App\Observers\WalletObserver::class); \App\PowerDNS\Domain::observe(\App\Observers\PowerDNS\DomainObserver::class); \App\PowerDNS\Record::observe(\App\Observers\PowerDNS\RecordObserver::class); Schema::defaultStringLength(191); // Register some template helpers Blade::directive( 'theme_asset', function ($path) { $path = trim($path, '/\'"'); return ""; } ); Builder::macro( 'withEnvTenantContext', function (string $table = null) { $tenantId = \config('app.tenant_id'); if ($tenantId) { /** @var Builder $this */ return $this->where(($table ? "$table." : "") . "tenant_id", $tenantId); } /** @var Builder $this */ return $this->whereNull(($table ? "$table." : "") . "tenant_id"); } ); Builder::macro( 'withObjectTenantContext', function (object $object, string $table = null) { $tenantId = $object->tenant_id; if ($tenantId) { /** @var Builder $this */ return $this->where(($table ? "$table." : "") . "tenant_id", $tenantId); } /** @var Builder $this */ return $this->whereNull(($table ? "$table." : "") . "tenant_id"); } ); Builder::macro( 'withSubjectTenantContext', function (string $table = null) { if ($user = auth()->user()) { $tenantId = $user->tenant_id; } else { $tenantId = \config('app.tenant_id'); } if ($tenantId) { /** @var Builder $this */ return $this->where(($table ? "$table." : "") . "tenant_id", $tenantId); } /** @var Builder $this */ return $this->whereNull(($table ? "$table." : "") . "tenant_id"); } ); // Query builder 'whereLike' mocro Builder::macro( 'whereLike', function (string $column, string $search, int $mode = 0) { $search = addcslashes($search, '%_'); switch ($mode) { case 2: $search .= '%'; break; case 1: $search = '%' . $search; break; default: $search = '%' . $search . '%'; } /** @var Builder $this */ return $this->where($column, 'like', $search); } ); + Http::macro('withSlowLog', function () { + return Http::withOptions([ + 'on_stats' => function (\GuzzleHttp\TransferStats $stats) { + $threshold = \config('logging.slow_log'); + if ($threshold && ($sec = $stats->getTransferTime()) > $threshold) { + $url = $stats->getEffectiveUri(); + $method = $stats->getRequest()->getMethod(); + \Log::warning(sprintf("[STATS] %s %s: %.4f sec.", $method, $url, $sec)); + } + }, + ]); + }); + $this->applyOverrideConfig(); } }