begin rework of web session api

master
Avril 4 years ago
parent 916bc5ed0e
commit c326a63259
Signed by: flanchan
GPG Key ID: 284488987C31F630

@ -5,6 +5,7 @@ use tokio::{
self, self,
DelayQueue, DelayQueue,
delay_queue, delay_queue,
Duration,
}, },
sync::{ sync::{
RwLock, RwLock,
@ -18,116 +19,153 @@ use std::collections::{
use std::{ use std::{
task::{Context,Poll}, task::{Context,Poll},
pin::Pin, pin::Pin,
sync::Weak,
marker::PhantomData,
}; };
use server::user::UserID; use server::user::UserID;
id_type!(pub SessionID: "A unique session id"); id_type!(pub SessionID: "A unique session id");
#[derive(Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] impl SessionID
pub struct Session
{ {
id: Option<SessionID>, // `None` for uncoupled /// Create a new random session ID.
#[inline] pub fn new() -> Self
users: Vec<UserID>, {
Self::id_new()
}
} }
impl Session #[derive(Debug)]
struct Inner
{ {
fn couple_to_new(self) -> Self id: SessionID,
{
self.couple_to(SessionID::id_new())
}
#[inline] fn couple_to(self, id: SessionID) -> Self
{
Self {
id: Some(id),
..self
}
}
/// See if this session object is coupled. If it is, return the session's ID. ttl: Duration,
#[inline] pub fn coupled(&self) -> Option<&SessionID> users: RwLock<Vec<UserID>>,
{
self.id.as_ref()
}
} }
/// 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>);
pub struct SessionPurge<'a, F = fn(Session)>(&'a mut Sessions, F); /// A `Session` object.
#[derive(Debug)]
pub struct Session(Weak<Inner>);
impl<'a, F> Future for SessionPurge<'a, F> impl Session
where F: FnMut(Session) + 'a + Unpin
{ {
type Output = Result<(), time::Error>; /// Acquire a lock of this session, preventing it from being destroyed while the lock is active.
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { ///
let this = self.get_mut(); /// This should be used to batch transations, as it insures the subsequent sessions cannot fail mid-processing.
while let Some(res) = futures::ready!(this.0.expire.poll_expired(cx)) { /// 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.
let ent = res?; ///
let _ = this.0.ids.remove(ent.get_ref()).map(|x| x.0).map(&mut this.1); /// A locked session that has been destroyed is able to re-add itself to a `Sessions` container.
} ///
/// # Notes
Poll::Ready(Ok(())) /// 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))
} }
} }
/// A container of `Session` objects.
#[derive(Debug)] #[derive(Debug)]
pub struct Sessions pub struct Sessions
{ {
ids: HashMap<SessionID, (Session, delay_queue::Key)>, sessions: Arc<RwLock<HashMap<SessionID, Arc<Inner>>>>,
expire: DelayQueue<SessionID>,
ttl_range: (u64, u64),
rewrite_needed: !,
} }
impl Sessions impl Sessions
{ {
pub fn purge(&mut self) -> SessionPurge<'_> /// Create a new, empty, container.
#[inline] pub fn new() -> Self
{ {
SessionPurge(self, std::mem::drop) Self {
sessions: Arc::new(RwLock::new(HashMap::new()))
}
} }
pub fn purge_now(&mut self) /// 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.
#[inline] fn detach_ttl_timer(&self, ses: Arc<Inner>)
{ {
self.purge().now_or_never(); let intern = Arc::downgrade(&self.sessions);
tokio::spawn(async move {
time::delay_for(ses.ttl).await;
if let Some(intern) = intern.upgrade() {
intern.write().await.remove(&ses.id);
}
});
} }
pub fn new_session_id(&mut self, ses: Session) -> SessionID
{
self.purge_now();
let ses = ses.couple_to_new(); /// Create and insert a new session with a new ID, tuned by `cfg`, and inserted into the collection. Return a session object containing this.
let id = ses.coupled().unwrap().clone(); #[inline]
let k = self.expire.insert(id.clone(), time::Duration::from_millis(self.ttl_range.jitter())); #[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."]
self.ids.insert(id.clone(), (ses, k)); pub async fn insert_new(&mut self, cfg: &Settings) -> Session
id {
self.insert_new_with_id(cfg, SessionID::id_new()).await
} }
pub fn new_session(&mut self, ses: Session) -> &mut Session /// 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
{ {
self.purge_now(); let ses = Arc::new(Inner {
id: id.clone(),
ttl: Duration::from_millis(cfg.auth_token_ttl_millis.jitter()),
users: RwLock::new(Vec::new()),
});
let output = Session(Arc::downgrade(&ses));
self.sessions.write().await.insert(id.clone(), Arc::clone(&ses));
let ses = ses.couple_to_new(); self.detach_ttl_timer(ses);
let id = ses.coupled().unwrap().clone(); output
let k = self.expire.insert(id.clone(), time::Duration::from_millis(self.ttl_range.jitter()));
&mut self.ids.entry(id).insert((ses, k)).into_mut().0
} }
pub fn session(&mut self, id: &SessionID) -> Option<&mut Session> /// 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>
{ {
/////////TODO: `Session` and `Sessions` need a whole rework. We can't be returning references that borrow this container, it will block all other consumers wanting to get their own session reference. Rewrite using `Arc<Session>`s instead of `Session` s. self.sessions.read().await.get(id)
self.purge_now(); .map(Arc::downgrade)
self.ids.get_mut(id).map(|(ses, _)| ses) .map(Session)
}
pub fn new(cfg: &settings::Settings) -> Sessions
{
Self{
ids: HashMap::new(),
expire: DelayQueue::new(),
ttl_range: cfg.auth_token_ttl_millis,
}
} }
} }

@ -142,7 +142,7 @@ pub struct State
auth_tokens: RwLock<AuthContainer>, auth_tokens: RwLock<AuthContainer>,
//TODO: user auths, public keys, hashed passwords, etc. //TODO: user auths, public keys, hashed passwords, etc.
logged_in: Arc<(RwLock<Box<Sessions>>, Notify)>, sessions: Sessions,
settings: Settings, settings: Settings,
} }
@ -153,27 +153,12 @@ impl State
{ {
Self { Self {
auth_tokens: RwLock::new(AuthContainer::new()), auth_tokens: RwLock::new(AuthContainer::new()),
logged_in: Arc::new((RwLock::new(Box::new(Sessions::new(&settings))), Notify::new())), sessions: Sessions::new(),
backend: RwLock::new(backend), backend: RwLock::new(backend),
settings, settings,
}.with_detatched_purger() }
}
fn with_detatched_purger(self) -> Self
{
let logged_in = Arc::clone(&self.logged_in);
tokio::spawn(async move {
while Arc::strong_count(&logged_in) > 1 {
logged_in.1.notified().await;
// We're now allowed to perform cleanup.
{
let mut sessions = logged_in.0.write().await;
sessions.purge_now();
}
}
});
self
} }
/// The web server settings /// The web server settings
pub fn cfg(&self) -> &Settings pub fn cfg(&self) -> &Settings
{ {

Loading…
Cancel
Save