Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade v #19

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [0.5.3] - 2024-02-08

### Added
- New `prepare_regenerate()` method for atomic session ID regeneration with updates
- Support for atomic (insert/update with ID regeneration) operations in both Redis and Memory stores

## [0.5.2] - 2024-02-07

### Added
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "ruts"
description = "A middleware for tower sessions"
version = "0.5.1"
version = "0.5.3"
edition = "2021"
rust-version = "1.75.0"
authors = ["Jimmie Lovell <jimmieomlovell@gmail.com>"]
Expand Down
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Add the following to your `Cargo.toml`:

```toml
[dependencies]
ruts = "0.4.2"
ruts = "0.5.3"
```

## Quick Start
Expand Down Expand Up @@ -87,13 +87,21 @@ async fn handler(session: Session<RedisStore<Client>>) -> String {

```rust
// Get session data
let value = session.get::<ValueType>("key").await?;
let value: ValueType = session.get("key").await?;

// Insert new data
session.insert::<ValueType>("key", &value).await?;
session.insert("key", &value, optional_field_expiration).await?;

// Prepare a new session ID for the next insert
let new_id = session.prepare_regenerate();
session.insert("key", &value, optional_field_expiration).await?;

// Update existing data
session.update::<ValueType>("key", &new_value).await?;
session.update("key", &new_value, optional_field_expiration).await?;

// Prepare a new session ID for the next update
let new_id = session.prepare_regenerate();
session.update("key", &value, optional_field_expiration).await?;

// Remove data
session.remove("key").await?;
Expand Down
6 changes: 3 additions & 3 deletions src/extract/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,21 @@ where
tracing::error!("session layer not found in the request extensions");
(
StatusCode::INTERNAL_SERVER_ERROR,
"session not found in the request",
"Session not found in the request",
)
})?;

// Cookies are only used if the SessionLayer has a cookie_options set.
let cookie_name = session_inner.cookie_name.ok_or_else(|| {
tracing::error!("missing cookie options");
(StatusCode::INTERNAL_SERVER_ERROR, "missing cookie options")
(StatusCode::INTERNAL_SERVER_ERROR, "Missing cookie options")
})?;

let cookies_ext = parts.extensions.get::<Cookies>().ok_or_else(|| {
tracing::error!("cookies not found in the request extensions");
(
StatusCode::INTERNAL_SERVER_ERROR,
"cookies not found in the request",
"Cookies not found in the request",
)
})?;

Expand Down
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@ pub use session::*;
mod extract;

mod service;

pub use service::*;

// Reexport external crates
pub use cookie;
pub use tower_cookies;
113 changes: 93 additions & 20 deletions src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,16 +165,35 @@ where
where
T: Send + Sync + Serialize,
{
let id = self.inner.get_or_set_id();
let inserted = self
.inner
.store
.insert(&id, field, value, self.max_age(), field_expire)
.await
.map_err(|err| {
tracing::error!(err = %err, "failed to insert field-value to session store");
err
})?;
let current_id = self.inner.get_or_set_id();
let pending_id = self.inner.take_pending_id();

let inserted = match pending_id {
Some(new_id) => {
let inserted = self.inner
.store
.insert_with_rename(&current_id, &new_id, field, value, self.max_age(), field_expire)
.await
.map_err(|err| {
tracing::error!(err = %err, "failed to insert field-value with rename to session store");
err
})?;
if inserted {
*self.inner.id.write() = Some(new_id);
}
inserted
}
None => {
self.inner
.store
.insert(&current_id, field, value, self.max_age(), field_expire)
.await
.map_err(|err| {
tracing::error!(err = %err, "failed to insert field-value to session store");
err
})?
}
};

if inserted {
self.inner.set_changed();
Expand Down Expand Up @@ -232,16 +251,37 @@ where
where
T: Send + Sync + Serialize,
{
let id = self.inner.get_or_set_id();
let updated = self
.inner
.store
.update(&id, field, value, self.max_age(), field_expire)
.await
.map_err(|err| {
tracing::error!(err = %err, "failed to update field in session store");
err
})?;
let current_id = self.inner.get_or_set_id();
let pending_id = self.inner.take_pending_id();

let updated = match pending_id {
Some(new_id) => {
let updated = self.inner
.store
.update_with_rename(&current_id, &new_id, field, value, self.max_age(), field_expire)
.await
.map_err(|err| {
tracing::error!(err = %err, "failed to update field-value with rename in session store");
err
})?;

if updated {
*self.inner.id.write() = Some(new_id);
}

updated
}
None => {
self.inner
.store
.update(&current_id, field, value, self.max_age(), field_expire)
.await
.map_err(|err| {
tracing::error!(err = %err, "failed to update field in session store");
err
})?
}
};

if updated {
self.inner.set_changed();
Expand Down Expand Up @@ -415,6 +455,29 @@ where
Ok(None)
}

/// Prepares a new session ID to be used in the next store operation.
/// The new ID will be used to rename the current session when the next
/// insert or update operation is performed.
///
/// # Example
///
/// ```rust
/// use ruts::Session;
/// use fred::clients::Client;
/// use ruts::store::redis::RedisStore;
///
/// async fn some_handler_could_be_axum(session: Session<RedisStore<Client>>) {
/// let new_id = session.prepare_regenerate();
/// // The next update/insert operation will use this new ID
/// session.update("field", &"value", None).await.unwrap();
/// }
/// ```
pub fn prepare_regenerate(&self) -> Id {
let new_id = Id::default();
self.inner.set_pending_id(Some(new_id));
new_id
}

/// Returns the session ID, if it exists.
pub fn id(&self) -> Option<Id> {
self.inner.get_id()
Expand All @@ -433,6 +496,7 @@ const DEFAULT_COOKIE_MAX_AGE: i64 = 10 * 60;
pub struct Inner<T: SessionStore> {
pub state: AtomicU8,
pub id: RwLock<Option<Id>>,
pub pending_id: RwLock<Option<Id>>,
pub cookie_max_age: AtomicI64,
pub cookie_name: Option<&'static str>,
pub cookies: OnceLock<Cookies>,
Expand All @@ -448,6 +512,7 @@ impl<T: SessionStore> Inner<T> {
Self {
state: AtomicU8::new(0),
id: RwLock::new(None),
pending_id: RwLock::new(None),
cookie_max_age: AtomicI64::new(cookie_max_age.unwrap_or(DEFAULT_COOKIE_MAX_AGE)),
cookie_name,
cookies: OnceLock::new(),
Expand Down Expand Up @@ -475,6 +540,14 @@ impl<T: SessionStore> Inner<T> {
*self.id.write() = id;
}

pub fn set_pending_id(&self, id: Option<Id>) {
*self.pending_id.write() = id;
}

pub fn take_pending_id(&self) -> Option<Id> {
self.pending_id.write().take()
}

pub fn set_changed(&self) {
self.state
.fetch_or(SESSION_STATE_CHANGED, Ordering::Relaxed);
Expand Down
95 changes: 95 additions & 0 deletions src/store/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,101 @@ impl SessionStore for MemoryStore {
Ok(true)
}

async fn insert_with_rename<T>(
&self,
old_session_id: &Id,
new_session_id: &Id,
field: &str,
value: &T,
key_seconds: i64,
_field_seconds: Option<i64>,
) -> Result<bool, Error>
where
T: Send + Sync + Serialize,
{
self.cleanup_expired();

let mut data = self.data.write();

// Check if old session exists and new session doesn't
if !data.contains_key(&old_session_id.to_string()) || data.contains_key(&new_session_id.to_string()) {
return Ok(false);
}

// Get the fields map, return false if field exists
let fields = data.get_mut(&old_session_id.to_string()).unwrap();
if fields.contains_key(field) {
return Ok(false);
}

// Calculate expiration
let expires_at = if key_seconds > 0 {
Some(Instant::now() + Duration::from_secs(key_seconds as u64))
} else {
None
};

// Insert the new field
fields.insert(
field.to_string(),
StoredValue {
data: serialize_value(value)?,
expires_at,
},
);

// Move the map to the new session ID
let fields = data.remove(&old_session_id.to_string()).unwrap();
data.insert(new_session_id.to_string(), fields);

Ok(true)
}

async fn update_with_rename<T>(
&self,
old_session_id: &Id,
new_session_id: &Id,
field: &str,
value: &T,
key_seconds: i64,
_field_seconds: Option<i64>,
) -> Result<bool, Error>
where
T: Send + Sync + Serialize,
{
self.cleanup_expired();

let mut data = self.data.write();

// Check if old session exists and new session doesn't
if !data.contains_key(&old_session_id.to_string()) || data.contains_key(&new_session_id.to_string()) {
return Ok(false);
}

// Calculate expiration
let expires_at = if key_seconds > 0 {
Some(Instant::now() + Duration::from_secs(key_seconds as u64))
} else {
None
};

// Update the field
let fields = data.get_mut(&old_session_id.to_string()).unwrap();
fields.insert(
field.to_string(),
StoredValue {
data: serialize_value(value)?,
expires_at,
},
);

// Move the map to the new session ID
let fields = data.remove(&old_session_id.to_string()).unwrap();
data.insert(new_session_id.to_string(), fields);

Ok(true)
}

async fn rename_session_id(
&self,
old_session_id: &Id,
Expand Down
Loading