You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

293 lines
12 KiB

//! Handles sessions and maintaining them
//!
//! Each active and authed client has a `Session` object associated with it. Clients can auth as multiple users within these sessions. Sessions expire after being inactive for their ttl
use super::*;
use tokio::{
time::{
self,
DelayQueue,
delay_queue,
Duration,
},
sync::{
RwLock,
RwLockReadGuard,
RwLockWriteGuard,
watch,
},
};
use std::collections::{
HashMap,
};
use std::{
task::{Context,Poll},
pin::Pin,
sync::Weak,
marker::PhantomData,
};
use server::user::UserID;
id_type!(pub SessionID: "A unique session id");
impl SessionID
{
/// Create a new random session ID.
#[inline] pub fn new() -> Self
{
Self::id_new()
}
}
#[derive(Debug)]
struct Inner
{
id: SessionID,
ttl_send: watch::Sender<Duration>,
ttl: Duration,
users: RwLock<Vec<UserID>>,
}
/// A lock to a `Session` object, temporarily preventing it's expiring TTL from destroying the underlying session object.
///
/// While this object is alive, its TTL expiring will not cause the session to be destroyed; however, it will still be removed from the `Sessions` container that created it, which will cause lookups for this `Session` to fail.
/// If the TTL expires while the object(s) of this lock are alive, the underlying session data will be destroyed once all `SessionLock`s refering to it have been dropped.
///
/// It is still possible to check if the TTL has expired using methods on the lock.
///
/// # Remarks
/// You should refrain from keeping `SessionLock`s alive longer than they need to be, as they cause the object to outlive its TTL, it could result in a potential security vulerability and/or memory leak (a leaked `SessionLock` will cause the session to valid forever, which is a huge security risk despite it being unable to be looked up).
///
/// However, if a lock is needed withing a request's context, it should be acquired as soon as possible to prevent any kind of data race that destroys the object while it's still technically find to use. This is a logical issue, not a memory safety one. It is still safe to defer the creation of locks to later on in the request's handler.
///
/// # Notes
/// This 'lock' does not cause any exclusion on waiting threads. It's not really a lock. It's (essentially) free to acquire and hold locks as long as you like, however if you keep a persistent `SessionLock` object, it may outlive its original TTL causing a potential security vulerability as well as a potential memory leak.
///
/// # Warnings
/// It is assumed that when session or session lock objects interact with a container that *it is the same container that was used to create the object*. It is the responsibility of the consumer of these APIs to ensure that a session does not access an unrelated container and the result is *logically undefined behaviour*.
#[derive(Debug)]
pub struct SessionLock<'a>(Arc<Inner>, PhantomData<&'a Session>);
impl<'a, 'b> AsRef<SessionID> for &'b SessionLock<'a>
where 'a: 'b
{
#[inline] fn as_ref(&self) -> &SessionID
{
&self.0.id
}
}
impl AsRef<SessionID> for SessionID
{
#[inline] fn as_ref(&self) -> &SessionID
{
self
}
}
//impl session (`Inner`) methods on `SessionLock`
impl<'a> SessionLock<'a>
{
pub fn id(&self) -> &SessionID
{
&self.0.id
}
pub fn ttl(&self) -> &Duration
{
&self.0.ttl
}
pub fn users(&self) -> &RwLock<Vec<UserID>>
{
&self.0.users
}
pub async fn add_user(&mut self, id: UserID)
{
self.0.users.write().await.push(id)
}
pub async fn with_users<F>(&self, mut clo: F)
where F: FnMut(&UserID)
{
for x in self.0.users.read().await.iter()
{
clo(x);
}
}
pub async fn has_user(&self, id: impl AsRef<UserID>) -> bool
{
self.0.users.read().await.contains(id.as_ref())
}
pub async fn remove_user(&mut self, id: impl AsRef<UserID>)
{
self.0.users.write().await.retain(move |x| x!= id.as_ref());
}
}
/// A `Session` object.
#[derive(Debug, Clone)]
pub struct Session(Weak<Inner>);
impl Session
{
/// Acquire a lock of this session, preventing it from being destroyed while the lock is active.
///
/// This should be used to batch transations, as it insures the subsequent sessions cannot fail mid-processing.
/// If the session is flagged for destruction while the lock is held, it will still be removed from it's `Sessions` object container, and the actual session object will be destroyed when the lock is released.
///
/// A locked session that has been destroyed is able to re-add itself to a `Sessions` container.
///
/// # Notes
/// This 'lock' does not cause any exclusion on waiting threads. It's not really a lock. It's (essentially) free to acquire and hold locks as long as you like, however if you keep a persistent `SessionLock` object, it may outlive its original TTL causing a potential security vulerability as well as a potential memory leak.
///
/// # Warnings
/// It is assumed that when session or session lock objects interact with a container that *it is the same container that was used to create the object*. It is the responsibility of the consumer of these APIs to ensure that a session does not access an unrelated container and the result is *logically undefined behaviour*.
#[inline] pub fn lock(&self) -> Option<SessionLock<'_>>
{
self.0.upgrade().map(|x| SessionLock(x, PhantomData))
}
/// Check to see if this session has not yet been destroyed.
#[inline] pub fn is_alive(&self) -> bool
{
self.0.strong_count()>0
}
/// Check to see if this session is still alive, but has been removed from its pool and is awaiting destruction.
pub async fn is_zombie(&self, cont: &Sessions) -> bool
{
OptionFuture::from(self.lock().map(|ses| async move {cont.sessions.read().await.contains_key(&ses.0.id)})).await.unwrap_or(false)
}
}
/// A container of `Session` objects.
#[derive(Debug)]
pub struct Sessions
{
sessions: Arc<RwLock<HashMap<SessionID, Arc<Inner>>>>,
}
impl Sessions
{
/// Create a new, empty, container.
#[inline] pub fn new() -> Self
{
Self {
sessions: Arc::new(RwLock::new(HashMap::new()))
}
}
/// Consume a strong session reference and start its ttl timer to remove itself from the container.
/// This spawns a new detached task that owns a `Weak` reference to the inner sessions map. If the `Sessions` container is dropped before this task completes, then nothing happens after the TTL expires.
/// The detached task does *not* prevent the `Sessions` object from being destroyed on drop.
///
/// # Locking
/// When the TTL of this session expires, and the `Sessions` container has not been dropped, then the container's write lock is acquired to remove the session from the container. The task completes immediately after, releasing the lock after the single remove operation.
///
/// # Timeing-out & cancelling
/// The client's session can refresh this expiration timer task by sending a new ttl to its `ttl_send` watch sender.
/// Usually this should be the same ttl which is set within the session's inner, but it can really be anything.
#[inline] fn detach_ttl_timer(&self, ses: Arc<Inner>, mut ttl: watch::Receiver<Duration>)
{
let intern = Arc::downgrade(&self.sessions);
let ses = Arc::downgrade(&ses);
tokio::spawn(async move {
let timed_out = if let Some(mut tm) = ttl.recv().await {
loop {
tokio::select! {
_ = time::delay_for(tm) => {
break true; // We timed out
}
nttl = ttl.recv() => {
tm = match nttl {
Some(nttl) => nttl, // Client session refreshed it's ttl
_ => return, // Client session was dropped. Return here because there's no reason to try to upgrade `ses` for removal now, it has been dropped and therefor must have been removed already.
};
}
}
}
} else {
return // Client session was dropped before we could even spawn this task. No reason to try to upgrade `ses` for removal now, it has been dropped and therefor must have been removed already.
};
if let Some(ses) = ses.upgrade() {
if timed_out {
// We timed out
info!("Session {} timed out, removing", ses.id);
} else {
// There was an error / somehow the session was dropped?
error!("Impossible error: TTL timer for session {} failed to communicate with session. Attempting removal anyway", ses.id);
}
if let Some(intern) = intern.upgrade() {
if intern.write().await.remove(&ses.id).is_some() {
trace!("Removed session {} from container, now dropping upgraded reference.", ses.id);
} else{
warn!("Failed to remove valid and alive session {} from alive container, this shouldn't happen and indicates a bug that we're working on the wrong container", ses.id);
}
} else {
// Any still-alive sessions are zombies after we free our upgraded session reference here
trace!("Failed to upgrade reference to container, it has been dropped. Exiting");
}
} else {
trace!("Session was dropped as we were about to remove it.");
}
});
}
/// Create and insert a new session with a new ID, tuned by `cfg`, and inserted into the collection. Return a session object containing this.
#[inline]
#[deprecated = "Useless without immediately calling `session.lock()` to retrieve the newly generated ID, which is a performance hit and `session.lock()` may fail causing the new session to be unindexable. Use `SessionID::new()` and `insert_new_with_id` instead."]
pub async fn insert_new(&mut self, cfg: &Settings) -> Session
{
self.insert_new_with_id(cfg, SessionID::id_new()).await
}
/// Create and insert a new `Session` object with the provided session ID using the session control options provided by `cfg`.
///
/// After inserting into the container, the new `Session` object is returned.
///
/// # Locking
/// This method acquires the container's *write* lock when inserting the session into the container. It is release immediately after the single insert operation.
///
/// It is not guaranteed that this method will complete without yielding.
/// As no concurrent calls to this function (or any others on this object) are possible due to Rust's single mutable reference rule, the only contributors to causing this call to yield are detached TTL expiration tasks which will also acquire the *write* lock when the TTL expires.
///
/// # Notes
/// It is not guaranteed that the *first* call to `session.lock()` will succeed. The only case that will cause this to happen is with improporly configured session TTLs.
/// Upon the consumer of this API retrieving this return value, if operations on the object are needed at any point the caller's context, she should immediately call `session.lock()` to prevent improporly configured TTLs destroying the session before it's enabled to be used.
///
/// If this happens, the session creation attempt should be considered to have failed, the request should return an error, and a log should be outputted informing the user that she configured the Session control TTL incorrectly; this is a configuration error.
pub async fn insert_new_with_id(&mut self, cfg: &Settings, id: SessionID) -> Session
{
let ttl = Duration::from_millis(cfg.auth_token_ttl_millis.jitter());
let (ttl_send, rx) = watch::channel(ttl);
let ses = Arc::new(Inner {
id: id.clone(),
ttl, ttl_send,
users: RwLock::new(Vec::new()),
});
let output = Session(Arc::downgrade(&ses));
self.sessions.write().await.insert(id.clone(), Arc::clone(&ses));
self.detach_ttl_timer(ses, rx);
output
}
/// Attempt to get a `Session` object of `id`.
///
/// If this method returns `None` for a contextually valid session ID, then the session is invalid to create any *new* accessors to.
/// However, if the session object has been removed due to expired TTL or another kind of invalidation, it doesn't nessisarily mean the session object has been destroyed.
/// It may still be being used by another task or request.
pub async fn get(&self, id: &SessionID) -> Option<Session>
{
self.sessions.read().await.get(id)
.map(Arc::downgrade)
.map(Session)
}
/// Remove a session with this ID from the container.
pub async fn remove(&mut self, ses: impl AsRef<SessionID>)
{
self.sessions.write().await.remove(ses.as_ref());
}
}