diff --git a/src/.env.example b/src/.env.example index 69ee05d9..d084b377 100644 --- a/src/.env.example +++ b/src/.env.example @@ -1,147 +1,148 @@ APP_NAME=Kolab APP_ENV=local APP_KEY= APP_DEBUG=true APP_URL=http://127.0.0.1:8000 APP_PUBLIC_URL= APP_DOMAIN=kolabnow.com APP_THEME=default APP_LOCALE=en APP_LOCALES=en,de ASSET_URL=http://127.0.0.1:8000 WEBMAIL_URL=/apps SUPPORT_URL=/support SUPPORT_EMAIL= LOG_CHANNEL=stack +LOG_SLOW_REQUESTS=5 DB_CONNECTION=mysql DB_DATABASE=kolabdev DB_HOST=127.0.0.1 DB_PASSWORD=kolab DB_PORT=3306 DB_USERNAME=kolabdev BROADCAST_DRIVER=redis CACHE_DRIVER=redis QUEUE_CONNECTION=redis SESSION_DRIVER=file SESSION_LIFETIME=120 MFA_DSN=mysql://roundcube:Welcome2KolabSystems@127.0.0.1/roundcube MFA_TOTP_DIGITS=6 MFA_TOTP_INTERVAL=30 MFA_TOTP_DIGEST=sha1 IMAP_URI=ssl://127.0.0.1:993 IMAP_ADMIN_LOGIN=cyrus-admin IMAP_ADMIN_PASSWORD=Welcome2KolabSystems IMAP_VERIFY_HOST=false IMAP_VERIFY_PEER=false LDAP_BASE_DN="dc=mgmt,dc=com" LDAP_DOMAIN_BASE_DN="ou=Domains,dc=mgmt,dc=com" LDAP_HOSTS=127.0.0.1 LDAP_PORT=389 LDAP_SERVICE_BIND_DN="uid=kolab-service,ou=Special Users,dc=mgmt,dc=com" LDAP_SERVICE_BIND_PW="Welcome2KolabSystems" LDAP_USE_SSL=false LDAP_USE_TLS=false # Administrative LDAP_ADMIN_BIND_DN="cn=Directory Manager" LDAP_ADMIN_BIND_PW="Welcome2KolabSystems" LDAP_ADMIN_ROOT_DN="dc=mgmt,dc=com" # Hosted (public registration) LDAP_HOSTED_BIND_DN="uid=hosted-kolab-service,ou=Special Users,dc=mgmt,dc=com" LDAP_HOSTED_BIND_PW="Welcome2KolabSystems" LDAP_HOSTED_ROOT_DN="dc=hosted,dc=com" OPENVIDU_API_PASSWORD=MY_SECRET OPENVIDU_API_URL=http://localhost:8080/api/ OPENVIDU_API_USERNAME=OPENVIDUAPP OPENVIDU_API_VERIFY_TLS=true OPENVIDU_COTURN_IP=127.0.0.1 OPENVIDU_COTURN_REDIS_DATABASE=2 OPENVIDU_COTURN_REDIS_IP=127.0.0.1 OPENVIDU_COTURN_REDIS_PASSWORD=turn # Used as COTURN_IP, TURN_PUBLIC_IP, for KMS_TURN_URL OPENVIDU_PUBLIC_IP=127.0.0.1 OPENVIDU_PUBLIC_PORT=3478 OPENVIDU_SERVER_PORT=8080 OPENVIDU_WEBHOOK=true OPENVIDU_WEBHOOK_ENDPOINT=http://127.0.0.1:8000/webhooks/meet/openvidu # "CDR" events, see https://docs.openvidu.io/en/2.13.0/reference-docs/openvidu-server-cdr/ #OPENVIDU_WEBHOOK_EVENTS=[sessionCreated,sessionDestroyed,participantJoined,participantLeft,webrtcConnectionCreated,webrtcConnectionDestroyed,recordingStatusChanged,filterEventDispatched,mediaNodeStatusChanged] #OPENVIDU_WEBHOOK_HEADERS=[\"Authorization:\ Basic\ SOMETHING\"] REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null REDIS_PORT=6379 SWOOLE_HOT_RELOAD_ENABLE=true SWOOLE_HTTP_ACCESS_LOG=true SWOOLE_HTTP_HOST=127.0.0.1 SWOOLE_HTTP_PORT=8000 SWOOLE_HTTP_REACTOR_NUM=1 SWOOLE_HTTP_WEBSOCKET=true SWOOLE_HTTP_WORKER_NUM=1 SWOOLE_OB_OUTPUT=true PAYMENT_PROVIDER= MOLLIE_KEY= STRIPE_KEY= STRIPE_PUBLIC_KEY= STRIPE_WEBHOOK_SECRET= MAIL_DRIVER=smtp MAIL_HOST=smtp.mailtrap.io MAIL_PORT=2525 MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=null MAIL_FROM_ADDRESS="noreply@example.com" MAIL_FROM_NAME="Example.com" MAIL_REPLYTO_ADDRESS=null MAIL_REPLYTO_NAME=null DNS_TTL=3600 DNS_SPF="v=spf1 mx -all" DNS_STATIC="%s. MX 10 ext-mx01.mykolab.com." DNS_COPY_FROM=null AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_DEFAULT_REGION=us-east-1 AWS_BUCKET= PUSHER_APP_ID= PUSHER_APP_KEY= PUSHER_APP_SECRET= PUSHER_APP_CLUSTER=mt1 MIX_ASSET_PATH='/' MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" JWT_SECRET= JWT_TTL=60 COMPANY_NAME= COMPANY_ADDRESS= COMPANY_DETAILS= COMPANY_EMAIL= COMPANY_LOGO= COMPANY_FOOTER= VAT_COUNTRIES=CH,LI VAT_RATE=7.7 KB_ACCOUNT_DELETE= KB_ACCOUNT_SUSPENDED= diff --git a/src/app/Http/Middleware/RequestLogger.php b/src/app/Http/Middleware/RequestLogger.php index 59d30556..37904d61 100644 --- a/src/app/Http/Middleware/RequestLogger.php +++ b/src/app/Http/Middleware/RequestLogger.php @@ -1,31 +1,38 @@ fullUrl(); $method = $request->getMethod(); $mem = round(memory_get_peak_usage() / 1024 / 1024, 1); $time = microtime(true) - self::$start; \Log::debug(sprintf("C: %s %s [%sM]: %.4f sec.", $method, $url, $mem, $time)); + } else { + $threshold = \config('logging.slow_log'); + if ($threshold && ($time = microtime(true) - self::$start) > $threshold) { + $url = $request->fullUrl(); + $method = $request->getMethod(); + \Log::warning(sprintf("[STATS] %s %s: %.4f sec.", $method, $url, $time)); + } } } } diff --git a/src/app/OpenVidu/Room.php b/src/app/OpenVidu/Room.php index 572061d0..5a483390 100644 --- a/src/app/OpenVidu/Room.php +++ b/src/app/OpenVidu/Room.php @@ -1,419 +1,427 @@ false, // No exceptions from Guzzle 'base_uri' => \config('openvidu.api_url'), 'verify' => \config('openvidu.api_verify_tls'), 'auth' => [ \config('openvidu.api_username'), \config('openvidu.api_password') - ] + ], + '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 self::$client; } /** * Destroy a OpenVidu connection * * @param string $conn Connection identifier * * @return bool True on success, False otherwise * @throws \Exception if session does not exist */ public function closeOVConnection($conn): bool { if (!$this->session_id) { throw new \Exception("The room session does not exist"); } $url = 'sessions/' . $this->session_id . '/connection/' . urlencode($conn); $response = $this->client()->request('DELETE', $url); return $response->getStatusCode() == 204; } /** * Fetch a OpenVidu connection information. * * @param string $conn Connection identifier * * @return ?array Connection data on success, Null otherwise * @throws \Exception if session does not exist */ public function getOVConnection($conn): ?array { // Note: getOVConnection() not getConnection() because Eloquent\Model::getConnection() exists // TODO: Maybe use some other name? getParticipant? if (!$this->session_id) { throw new \Exception("The room session does not exist"); } $url = 'sessions/' . $this->session_id . '/connection/' . urlencode($conn); $response = $this->client()->request('GET', $url); if ($response->getStatusCode() == 200) { return json_decode($response->getBody(), true); } return null; } /** * Create a OpenVidu session * * @return array|null Session data on success, NULL otherwise */ public function createSession(): ?array { $response = $this->client()->request( 'POST', "sessions", [ 'json' => [ 'mediaMode' => 'ROUTED', 'recordingMode' => 'MANUAL' ] ] ); if ($response->getStatusCode() !== 200) { $this->session_id = null; $this->save(); return null; } $session = json_decode($response->getBody(), true); $this->session_id = $session['id']; $this->save(); return $session; } /** * Delete a OpenVidu session * * @return bool */ public function deleteSession(): bool { if (!$this->session_id) { return true; } $response = $this->client()->request( 'DELETE', "sessions/" . $this->session_id, ); if ($response->getStatusCode() == 204) { $this->session_id = null; $this->save(); return true; } return false; } /** * Returns metadata for every connection in a session. * * @return array Connections metadata, indexed by connection identifier * @throws \Exception if session does not exist */ public function getSessionConnections(): array { if (!$this->session_id) { throw new \Exception("The room session does not exist"); } return Connection::where('session_id', $this->session_id) // Ignore screen sharing connection for now ->whereRaw("(role & " . self::ROLE_SCREEN . ") = 0") ->get() ->keyBy('id') ->map(function ($item) { // Warning: Make sure to not return all metadata here as it might contain sensitive data. return [ 'role' => $item->role, 'hand' => $item->metadata['hand'] ?? 0, 'language' => $item->metadata['language'] ?? null, ]; }) // Sort by order in the queue, so UI can re-build the existing queue in order ->sort(function ($a, $b) { return $a['hand'] <=> $b['hand']; }) ->all(); } /** * Create a OpenVidu 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"); } // FIXME: Looks like passing the role in 'data' param is the only way // to make it visible for everyone in a room. So, for example we can // handle/style subscribers/publishers/moderators differently on the // client-side. Is this a security issue? $data = ['role' => $role]; $url = 'sessions/' . $this->session_id . '/connection'; $post = [ 'json' => [ 'role' => self::OV_ROLE_PUBLISHER, 'data' => json_encode($data) ] ]; $response = $this->client()->request('POST', $url, $post); if ($response->getStatusCode() == 200) { $json = json_decode($response->getBody(), true); $authToken = base64_encode($json['id'] . ':' . \random_bytes(16)); // Extract the 'token' part of the token, it will be used to authenticate the connection. // It will be needed in next iterations e.g. to authenticate moderators that aren't // Kolab4 users (or are just not logged in to Kolab4). // FIXME: we could as well generate our own token for auth purposes parse_str(parse_url($json['token'], PHP_URL_QUERY), $url); // Create the connection reference in our database $conn = new Connection(); $conn->id = $json['id']; $conn->session_id = $this->session_id; $conn->room_id = $this->id; $conn->role = $role; $conn->metadata = ['token' => $url['token'], 'authToken' => $authToken]; $conn->save(); return [ 'session' => $this->session_id, 'token' => $json['token'], 'authToken' => $authToken, 'connectionId' => $json['id'], 'role' => $role, ]; } 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}"); return $response->getStatusCode() == 200; } /** * The room owner. * * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ public function owner() { return $this->belongsTo('\App\User', 'user_id', 'id'); } /** * 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)); } /** * Any (additional) properties of this room. * * @return \Illuminate\Database\Eloquent\Relations\HasMany */ public function settings() { return $this->hasMany('App\OpenVidu\RoomSetting', 'room_id'); } /** * Send a OpenVidu signal to the session participants (connections) * * @param string $name Signal name (type) * @param array $data Signal data array * @param null|int|string[] $target List of target connections, Null for all connections. * It can be also a 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 = [ 'session' => $this->session_id, 'type' => $name, 'data' => $data ? json_encode($data) : '', ]; // Get connection IDs by participant role if (is_int($target)) { $connections = Connection::where('session_id', $this->session_id) ->whereRaw("(role & $target)") ->pluck('id') ->all(); if (empty($connections)) { return false; } $target = $connections; } if (!empty($target)) { $post['to'] = $target; } $response = $this->client()->request('POST', 'signal', ['json' => $post]); return $response->getStatusCode() == 200; } } diff --git a/src/config/logging.php b/src/config/logging.php index d09cd7d2..0dcdb218 100644 --- a/src/config/logging.php +++ b/src/config/logging.php @@ -1,94 +1,96 @@ env('LOG_CHANNEL', 'stack'), /* |-------------------------------------------------------------------------- | Log Channels |-------------------------------------------------------------------------- | | Here you may configure the log channels for your application. Out of | the box, Laravel uses the Monolog PHP logging library. This gives | you a variety of powerful log handlers / formatters to utilize. | | Available Drivers: "single", "daily", "slack", "syslog", | "errorlog", "monolog", | "custom", "stack" | */ 'channels' => [ 'stack' => [ 'driver' => 'stack', 'channels' => ['daily'], 'ignore_exceptions' => false, ], 'single' => [ 'driver' => 'single', 'path' => storage_path('logs/laravel.log'), 'level' => 'debug', ], 'daily' => [ 'driver' => 'daily', 'path' => storage_path('logs/laravel.log'), 'level' => 'debug', 'days' => 14, ], 'slack' => [ 'driver' => 'slack', 'url' => env('LOG_SLACK_WEBHOOK_URL'), 'username' => 'Laravel Log', 'emoji' => ':boom:', 'level' => 'critical', ], 'papertrail' => [ 'driver' => 'monolog', 'level' => 'debug', 'handler' => SyslogUdpHandler::class, 'handler_with' => [ 'host' => env('PAPERTRAIL_URL'), 'port' => env('PAPERTRAIL_PORT'), ], ], 'stderr' => [ 'driver' => 'monolog', 'handler' => StreamHandler::class, 'formatter' => env('LOG_STDERR_FORMATTER'), 'with' => [ 'stream' => 'php://stderr', ], ], 'syslog' => [ 'driver' => 'syslog', 'level' => 'debug', ], 'errorlog' => [ 'driver' => 'errorlog', 'level' => 'debug', ], ], + 'slow_log' => (float) env('LOG_SLOW_REQUESTS', 5), + ];