#![allow(non_snake_case)]
use serde::{Deserialize, Serialize};
use std::collections::HashMap as FxHashMap;
use std::time::{SystemTime, UNIX_EPOCH};
use uuid::Uuid;
use crate::core::concurrent::RiShardedLock;
#[cfg(feature = "pyo3")]
use pyo3::PyResult;
const MAX_SESSION_DATA_ENTRIES: usize = 100;
const MAX_SESSION_DATA_KEY_LENGTH: usize = 256;
const MAX_SESSION_DATA_VALUE_LENGTH: usize = 4096;
#[allow(dead_code)]
const MAX_USER_AGENT_LENGTH: usize = 1024;
#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass(get_all, set_all))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RiSession {
pub id: String,
pub user_id: String,
pub created_at: u64,
pub last_accessed: u64,
pub expires_at: u64,
pub data: FxHashMap<String, String>,
pub ip_address: Option<String>,
pub user_agent: Option<String>,
}
#[cfg(feature = "pyo3")]
#[pyo3::prelude::pymethods]
impl RiSession {
#[new]
fn py_new(
id: Option<String>,
user_id: String,
created_at: Option<u64>,
last_accessed: Option<u64>,
expires_at: Option<u64>,
data: Option<FxHashMap<String, String>>,
ip_address: Option<String>,
user_agent: Option<String>,
) -> Self {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
Self {
id: id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
user_id,
created_at: created_at.unwrap_or(now),
last_accessed: last_accessed.unwrap_or(now),
expires_at: expires_at.unwrap_or(now + 28800),
data: data.unwrap_or_default(),
ip_address,
user_agent,
}
}
}
impl RiSession {
pub fn new(user_id: String, timeout_secs: u64, ip_address: Option<String>, user_agent: Option<String>) -> Self {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
Self {
id: Uuid::new_v4().to_string(),
user_id,
created_at: now,
last_accessed: now,
expires_at: now + timeout_secs,
data: FxHashMap::default(),
ip_address,
user_agent,
}
}
pub fn is_expired(&self) -> bool {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
now > self.expires_at
}
pub fn touch(&mut self) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
self.last_accessed = now;
}
pub fn extend(&mut self, timeout_secs: u64) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
self.expires_at = now + timeout_secs;
}
pub fn get_data(&self, key: &str) -> Option<&String> {
self.data.get(key)
}
pub fn set_data(&mut self, key: String, value: String) -> bool {
if key.len() > MAX_SESSION_DATA_KEY_LENGTH {
log::warn!(
"[Ri.Session] Data key too long: {} chars (max {})",
key.len(), MAX_SESSION_DATA_KEY_LENGTH
);
return false;
}
let safe_value = if value.len() > MAX_SESSION_DATA_VALUE_LENGTH {
log::warn!(
"[Ri.Session] Data value truncated: {} chars (max {})",
value.len(), MAX_SESSION_DATA_VALUE_LENGTH
);
value[..MAX_SESSION_DATA_VALUE_LENGTH].to_string()
} else {
value
};
if !self.data.contains_key(&key) && self.data.len() >= MAX_SESSION_DATA_ENTRIES {
log::warn!(
"[Ri.Session] Maximum data entries reached: {} (max {})",
self.data.len(), MAX_SESSION_DATA_ENTRIES
);
return false;
}
self.data.insert(key, safe_value);
true
}
pub fn remove_data(&mut self, key: &str) -> Option<String> {
self.data.remove(key)
}
}
#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
pub struct RiSessionManager {
sessions: RiShardedLock<String, RiSession>,
timeout_secs: u64,
max_sessions_per_user: usize,
}
impl RiSessionManager {
pub fn new(timeout_secs: u64) -> Self {
Self {
sessions: RiShardedLock::with_default_shards(),
timeout_secs,
max_sessions_per_user: 5,
}
}
pub async fn create_session(&self, user_id: String, ip_address: Option<String>, user_agent: Option<String>) -> crate::core::RiResult<String> {
let user_sessions: Vec<(String, u64)> = self.sessions.collect_where(|_, s| s.user_id == user_id && !s.is_expired()).await
.into_iter()
.map(|s| (s.id.clone(), s.created_at))
.collect();
if user_sessions.len() >= self.max_sessions_per_user {
let mut sessions_with_time = user_sessions;
sessions_with_time.sort_by_key(|(_, t)| *t);
if let Some((oldest_id, _)) = sessions_with_time.first() {
self.sessions.remove(oldest_id).await;
}
}
let session = RiSession::new(user_id, self.timeout_secs, ip_address, user_agent);
let session_id = session.id.clone();
self.sessions.insert(session_id.clone(), session).await;
Ok(session_id)
}
pub async fn validate_session_security(
&self,
session_id: &str,
current_ip: Option<&str>,
current_user_agent: Option<&str>,
) -> crate::core::RiResult<bool> {
let session = self.sessions.get(session_id).await;
match session {
Some(s) => {
if s.is_expired() {
self.sessions.remove(session_id).await;
return Ok(false);
}
if let (Some(stored_ip), Some(current)) = (&s.ip_address, current_ip) {
if stored_ip != current {
log::warn!(
"[Ri.Session] IP address mismatch for session {}: expected {}, got {}",
session_id, stored_ip, current
);
return Ok(false);
}
}
if let (Some(stored_ua), Some(current)) = (&s.user_agent, current_user_agent) {
if stored_ua != current {
log::warn!(
"[Ri.Session] User-Agent mismatch for session {}: expected {}, got {}",
session_id, stored_ua, current
);
return Ok(false);
}
}
Ok(true)
}
None => Ok(false),
}
}
pub async fn regenerate_session_id(&self, old_session_id: &str) -> crate::core::RiResult<Option<String>> {
let old_session = self.sessions.get(old_session_id).await;
match old_session {
Some(mut s) => {
if s.is_expired() {
self.sessions.remove(old_session_id).await;
return Ok(None);
}
self.sessions.remove(old_session_id).await;
let new_session_id = Uuid::new_v4().to_string();
s.id = new_session_id.clone();
s.touch();
self.sessions.insert(new_session_id.clone(), s).await;
log::info!(
"[Ri.Session] Regenerated session ID: {} -> {}",
old_session_id, new_session_id
);
Ok(Some(new_session_id))
}
None => Ok(None),
}
}
pub async fn get_session(&self, session_id: &str) -> crate::core::RiResult<Option<RiSession>> {
let session = self.sessions.get(session_id).await;
match session {
Some(mut s) => {
if s.is_expired() {
self.sessions.remove(session_id).await;
Ok(None)
} else {
s.touch();
self.sessions.insert(session_id.to_string(), s.clone()).await;
Ok(Some(s))
}
}
None => Ok(None),
}
}
pub async fn update_session(&self, session_id: &str, data: FxHashMap<String, String>) -> crate::core::RiResult<bool> {
let session = self.sessions.get(session_id).await;
match session {
Some(mut s) => {
if s.is_expired() {
self.sessions.remove(session_id).await;
Ok(false)
} else {
for (key, value) in data {
s.set_data(key, value);
}
s.touch();
self.sessions.insert(session_id.to_string(), s).await;
Ok(true)
}
}
None => Ok(false),
}
}
pub async fn extend_session(&self, session_id: &str) -> crate::core::RiResult<bool> {
let session = self.sessions.get(session_id).await;
match session {
Some(mut s) => {
if s.is_expired() {
self.sessions.remove(session_id).await;
Ok(false)
} else {
s.extend(self.timeout_secs);
self.sessions.insert(session_id.to_string(), s).await;
Ok(true)
}
}
None => Ok(false),
}
}
pub async fn destroy_session(&self, session_id: &str) -> crate::core::RiResult<bool> {
Ok(self.sessions.remove(session_id).await.is_some())
}
pub async fn destroy_user_sessions(&self, user_id: &str) -> crate::core::RiResult<usize> {
let count = self.sessions.remove_where(|_, s| s.user_id == user_id).await;
Ok(count)
}
pub async fn get_user_sessions(&self, user_id: &str) -> crate::core::RiResult<Vec<RiSession>> {
let user_sessions = self.sessions.collect_where(|_, s| s.user_id == user_id && !s.is_expired()).await;
Ok(user_sessions)
}
pub async fn cleanup_expired(&self) -> crate::core::RiResult<usize> {
let count = self.sessions.remove_where(|_, s| s.is_expired()).await;
Ok(count)
}
pub async fn cleanup_all(&self) -> crate::core::RiResult<()> {
self.sessions.clear().await;
Ok(())
}
pub fn get_timeout(&self) -> u64 {
self.timeout_secs
}
pub fn set_timeout(&mut self, timeout_secs: u64) {
self.timeout_secs = timeout_secs;
}
}
#[cfg(feature = "pyo3")]
#[pyo3::prelude::pymethods]
impl RiSessionManager {
#[new]
fn py_new(timeout_secs: u64) -> PyResult<Self> {
Ok(Self::new(timeout_secs))
}
#[pyo3(name = "create_session")]
fn create_session_impl(&self, user_id: String, ip_address: Option<String>, user_agent: Option<String>) -> PyResult<String> {
let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
rt.block_on(async {
self.create_session(user_id, ip_address, user_agent).await
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
})
}
#[pyo3(name = "get_session")]
fn get_session_impl(&self, session_id: String) -> PyResult<Option<RiSession>> {
let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
rt.block_on(async {
self.get_session(&session_id).await
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
})
}
#[pyo3(name = "update_session")]
fn update_session_impl(&self, session_id: String, data: FxHashMap<String, String>) -> PyResult<bool> {
let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
rt.block_on(async {
self.update_session(&session_id, data).await
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
})
}
#[pyo3(name = "destroy_session")]
fn destroy_session_impl(&self, session_id: String) -> PyResult<bool> {
let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
rt.block_on(async {
self.destroy_session(&session_id).await
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
})
}
#[pyo3(name = "extend_session")]
fn extend_session_impl(&self, session_id: String) -> PyResult<bool> {
let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
rt.block_on(async {
self.extend_session(&session_id).await
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
})
}
#[pyo3(name = "cleanup_expired")]
fn cleanup_expired_impl(&self) -> PyResult<usize> {
let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
rt.block_on(async {
self.cleanup_expired().await
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
})
}
#[pyo3(name = "get_timeout")]
fn get_timeout_impl(&self) -> u64 {
self.get_timeout()
}
}