From cc5ae043265b798cd3edd41ec36306dbe0c258ca Mon Sep 17 00:00:00 2001 From: euzu Date: Thu, 17 Apr 2025 15:43:26 +0200 Subject: [PATCH] renamed config log `active_client`to `log_active_user` --- CHANGELOG.md | 1 + src/api/main_api.rs | 2 +- src/api/model/active_user_manager.rs | 62 ++++++++++++++++++++-------- src/model/config.rs | 2 +- 4 files changed, 47 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20ee518e7..250e13565 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ url: ['http://localhost:3001/xmltv.php?epg_id=1', 'http://localhost:3001/xmltv.p When no matching epg_id is found, the display name is used to match a channel name. The given strings are stripped to get a better match. - fixed chno assignment issue - Added `method` attribute to input config. It can be set to `GET` or `POST`. +- !BREAKING_CHANGE! `log` config `active_clients` renamed to `log_active_user` # 2.2.5 (2025-03-27) - fixed web ui playlist regexp search diff --git a/src/api/main_api.rs b/src/api/main_api.rs index c07b12f79..3b841f237 100644 --- a/src/api/main_api.rs +++ b/src/api/main_api.rs @@ -122,7 +122,7 @@ async fn create_shared_data(cfg: &Arc) -> AppState { } }); - let active_users = Arc::new(ActiveUserManager::new(cfg.log.as_ref().is_some_and(|l| l.active_clients))); + let active_users = Arc::new(ActiveUserManager::new(cfg.log.as_ref().is_some_and(|l| l.log_active_user))); let active_provider = Arc::new(ActiveProviderManager::new(cfg).await); let client = if cfg.connect_timeout_secs > 0 { diff --git a/src/api/model/active_user_manager.rs b/src/api/model/active_user_manager.rs index db5cb3c45..bfef93ed6 100644 --- a/src/api/model/active_user_manager.rs +++ b/src/api/model/active_user_manager.rs @@ -1,10 +1,13 @@ use std::collections::HashMap; use std::sync::Arc; -use std::sync::atomic::{AtomicU32, Ordering}; +use jsonwebtoken::get_current_timestamp; use log::{info, trace}; use tokio::sync::RwLock; use crate::model::api_proxy::UserConnectionPermission; +// TODO this should be bound to grace_period, because grace_period can be more than 2 secs +const GRACE_TIME_CHECK: u64 = 2; + pub struct UserConnectionGuard { manager: Arc, username: String, @@ -19,9 +22,25 @@ impl Drop for UserConnectionGuard { } } +struct UserConnectionData { + connections: u32, + granted_grace: u32, + grace_ts: u64, +} + +impl UserConnectionData { + fn new() -> Self { + Self { + connections: 1, + granted_grace: 0, + grace_ts: 0, + } + } +} + pub struct ActiveUserManager { - log_active_clients: bool, - pub user: Arc>>, + log_active_user: bool, + user: Arc>>, } impl Default for ActiveUserManager { @@ -31,38 +50,45 @@ impl Default for ActiveUserManager { } impl ActiveUserManager { - pub fn new(log_active_clients: bool) -> Self { + pub fn new(log_active_user: bool) -> Self { Self { - log_active_clients, + log_active_user, user: Arc::new(RwLock::new(HashMap::new())), } } fn clone_inner(&self) -> Self { Self { - log_active_clients: self.log_active_clients, + log_active_user: self.log_active_user, user: Arc::clone(&self.user), } } pub async fn user_connections(&self, username: &str) -> u32 { - if let Some(counter) = self.user.read().await.get(username) { - return counter.load(Ordering::SeqCst); + if let Some(connection_data) = self.user.read().await.get(username) { + return connection_data.connections; } 0 } pub async fn connection_permission(&self, username: &str, max_connections: u32, grace_period: bool) -> UserConnectionPermission { - if let Some(counter) = self.user.read().await.get(username) { - let current_connections = counter.load(Ordering::SeqCst); + if let Some(connection_data) = self.user.write().await.get_mut(username) { + let current_connections = connection_data.connections; + if connection_data.granted_grace > 0 && (get_current_timestamp() - connection_data.grace_ts) > GRACE_TIME_CHECK { + trace!("User access denied, grace exhausted, too many connections: {username}"); + return UserConnectionPermission::Exhausted; + } let extra_con = u32::from(grace_period); if current_connections < max_connections + extra_con { + connection_data.granted_grace += 1; + connection_data.grace_ts = get_current_timestamp(); return UserConnectionPermission::GracePeriod; } if current_connections >= max_connections { trace!("User access denied, too many connections: {username}"); return UserConnectionPermission::Exhausted; } + connection_data.granted_grace = 0; } UserConnectionPermission::Allowed } @@ -72,15 +98,15 @@ impl ActiveUserManager { } pub async fn active_connections(&self) -> usize { - self.user.read().await.values().map(|c| c.load(Ordering::SeqCst) as usize).sum() + self.user.read().await.values().map(|c| c.connections as usize).sum() } pub async fn add_connection(&self, username: &str) -> UserConnectionGuard { let mut lock = self.user.write().await; - if let Some(counter) = lock.get(username) { - counter.fetch_add(1, Ordering::SeqCst); + if let Some(connection_data) = lock.get_mut(username) { + connection_data.connections += 1; } else { - lock.insert(username.to_string(), AtomicU32::new(1)); + lock.insert(username.to_string(), UserConnectionData::new()); } drop(lock); @@ -94,9 +120,9 @@ impl ActiveUserManager { async fn remove_connection(&self, username: &str) { let mut lock = self.user.write().await; - if let Some(counter) = lock.get(username) { - let new_count = counter.fetch_sub(1, Ordering::SeqCst) - 1; - if new_count == 0 { + if let Some(connection_data) = lock.get_mut(username) { + connection_data.connections -= 1; + if connection_data.connections == 0 { lock.remove(username); } } @@ -106,7 +132,7 @@ impl ActiveUserManager { } async fn log_active_user(&self) { - if self.log_active_clients { + if self.log_active_user { let user_count = self.active_users().await; let user_connection_count = self.active_connections().await; info!("Active Users: {user_count}, Active User Connections: {user_connection_count}"); diff --git a/src/model/config.rs b/src/model/config.rs index fd5602801..790e156b2 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -1120,7 +1120,7 @@ pub struct LogConfig { #[serde(default = "default_as_true")] pub sanitize_sensitive_info: bool, #[serde(default)] - pub active_clients: bool, + pub log_active_user: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub log_level: Option, }