1
0
mirror of https://github.com/actix/actix-extras.git synced 2024-11-30 10:32:55 +01:00

add sled session backend

This commit is contained in:
Rob Ede 2022-07-31 05:14:59 +01:00
parent 6e79465362
commit 25f8a4cfae
No known key found for this signature in database
GPG Key ID: 97C636207D3EF933
10 changed files with 265 additions and 40 deletions

1
.gitignore vendored
View File

@ -10,5 +10,6 @@ guide/build/
*.pyc *.pyc
*.pid *.pid
*.sock *.sock
*.db
*~ *~
.DS_Store .DS_Store

View File

@ -21,8 +21,9 @@ name = "actix_session"
path = "src/lib.rs" path = "src/lib.rs"
[features] [features]
default = [] default = ["sled-session"]
cookie-session = [] cookie-session = []
sled-session = ["sled"]
redis-actor-session = ["actix-redis", "actix", "futures-core", "rand"] redis-actor-session = ["actix-redis", "actix", "futures-core", "rand"]
redis-rs-session = ["redis", "rand"] redis-rs-session = ["redis", "rand"]
redis-rs-tls-session = ["redis-rs-session", "redis/tokio-native-tls-comp"] redis-rs-tls-session = ["redis-rs-session", "redis/tokio-native-tls-comp"]
@ -40,6 +41,9 @@ serde = { version = "1" }
serde_json = { version = "1" } serde_json = { version = "1" }
tracing = { version = "0.1.30", default-features = false, features = ["log"] } tracing = { version = "0.1.30", default-features = false, features = ["log"] }
# sled-session
sled = { version = "0.34", optional = true }
# redis-actor-session # redis-actor-session
actix = { version = "0.13", default-features = false, optional = true } actix = { version = "0.13", default-features = false, optional = true }
actix-redis = { version = "0.12", optional = true } actix-redis = { version = "0.12", optional = true }
@ -57,7 +61,7 @@ log = "0.4"
[[example]] [[example]]
name = "basic" name = "basic"
required-features = ["redis-actor-session"] required-features = ["sled-session"]
[[example]] [[example]]
name = "authentication" name = "authentication"

View File

@ -1,4 +1,4 @@
use actix_session::{storage::RedisActorSessionStore, Session, SessionMiddleware}; use actix_session::{storage::SledSessionStore, Session, SessionMiddleware};
use actix_web::{cookie::Key, middleware, web, App, Error, HttpRequest, HttpServer, Responder}; use actix_web::{cookie::Key, middleware, web, App, Error, HttpRequest, HttpServer, Responder};
/// simple handler /// simple handler
@ -21,7 +21,9 @@ async fn main() -> std::io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
// The signing key would usually be read from a configuration file/environment variables. // The signing key would usually be read from a configuration file/environment variables.
let signing_key = Key::generate(); let signing_key = Key::from(&[0; 64]);
let sled_session_store = SledSessionStore::new("./session.db").unwrap();
log::info!("starting HTTP server at http://localhost:8080"); log::info!("starting HTTP server at http://localhost:8080");
@ -31,7 +33,7 @@ async fn main() -> std::io::Result<()> {
.wrap(middleware::Logger::default()) .wrap(middleware::Logger::default())
// cookie session middleware // cookie session middleware
.wrap(SessionMiddleware::new( .wrap(SessionMiddleware::new(
RedisActorSessionStore::new("127.0.0.1:6379"), sled_session_store.clone(),
signing_key.clone(), signing_key.clone(),
)) ))
// register simple route, handle all methods // register simple route, handle all methods

View File

@ -156,6 +156,23 @@ pub mod test_helpers {
use crate::{config::CookieContentSecurity, storage::SessionStore}; use crate::{config::CookieContentSecurity, storage::SessionStore};
/// Prints name of function it is called in.
///
/// Taken from: https://docs.rs/stdext/0.3.1/src/stdext/macros.rs.html
macro_rules! function_name {
() => {{
// Okay, this is ugly, I get it. However, this is the best we can get on a stable rust.
fn f() {}
fn type_name_of<T>(_: T) -> &'static str {
std::any::type_name::<T>()
}
let name = type_name_of(f);
// `3` is the length of the `::f`.
&name[..name.len() - 3]
}};
}
pub(crate) use function_name;
/// Generate a random cookie signing/encryption key. /// Generate a random cookie signing/encryption key.
pub fn key() -> Key { pub fn key() -> Key {
Key::generate() Key::generate()

View File

@ -1,13 +1,9 @@
use std::convert::TryInto; use std::convert::TryInto as _;
use actix_web::cookie::time::Duration; use actix_web::cookie::time::Duration;
use anyhow::Error;
use super::SessionKey; use super::{LoadError, SaveError, SessionKey, SessionState, UpdateError};
use crate::storage::{ use crate::storage::SessionStore;
interface::{LoadError, SaveError, SessionState, UpdateError},
SessionStore,
};
/// Use the session key, stored in the session cookie, as storage backend for the session state. /// Use the session key, stored in the session cookie, as storage backend for the session state.
/// ///
@ -67,7 +63,7 @@ impl SessionStore for CookieSessionStore {
_ttl: &Duration, _ttl: &Duration,
) -> Result<SessionKey, SaveError> { ) -> Result<SessionKey, SaveError> {
let session_key = serde_json::to_string(&session_state) let session_key = serde_json::to_string(&session_state)
.map_err(anyhow::Error::new) .map_err(Into::into)
.map_err(SaveError::Serialization)?; .map_err(SaveError::Serialization)?;
Ok(session_key Ok(session_key
@ -90,7 +86,11 @@ impl SessionStore for CookieSessionStore {
}) })
} }
async fn update_ttl(&self, _session_key: &SessionKey, _ttl: &Duration) -> Result<(), Error> { async fn update_ttl(
&self,
_session_key: &SessionKey,
_ttl: &Duration,
) -> Result<(), anyhow::Error> {
Ok(()) Ok(())
} }

View File

@ -101,7 +101,7 @@ pub enum UpdateError {
Serialization(anyhow::Error), Serialization(anyhow::Error),
/// Something went wrong when updating the session state. /// Something went wrong when updating the session state.
#[display(fmt = "Something went wrong when updating the session state.")] #[display(fmt = "Something went wrong when updating the session state")]
Other(anyhow::Error), Other(anyhow::Error),
} }

View File

@ -3,12 +3,16 @@
mod interface; mod interface;
mod session_key; mod session_key;
pub(crate) use self::interface::SessionState;
pub use self::interface::{LoadError, SaveError, SessionStore, UpdateError}; pub use self::interface::{LoadError, SaveError, SessionStore, UpdateError};
pub use self::session_key::SessionKey; pub use self::session_key::SessionKey;
#[cfg(feature = "cookie-session")] #[cfg(feature = "cookie-session")]
mod cookie; mod cookie;
#[cfg(feature = "sled-session")]
mod sled;
#[cfg(feature = "redis-actor-session")] #[cfg(feature = "redis-actor-session")]
mod redis_actor; mod redis_actor;
@ -19,8 +23,10 @@ mod redis_rs;
mod utils; mod utils;
#[cfg(feature = "cookie-session")] #[cfg(feature = "cookie-session")]
pub use cookie::CookieSessionStore; pub use self::cookie::CookieSessionStore;
#[cfg(feature = "redis-actor-session")] #[cfg(feature = "redis-actor-session")]
pub use redis_actor::{RedisActorSessionStore, RedisActorSessionStoreBuilder}; pub use self::redis_actor::{RedisActorSessionStore, RedisActorSessionStoreBuilder};
#[cfg(feature = "redis-rs-session")] #[cfg(feature = "redis-rs-session")]
pub use redis_rs::{RedisSessionStore, RedisSessionStoreBuilder}; pub use self::redis_rs::{RedisSessionStore, RedisSessionStoreBuilder};
#[cfg(feature = "sled-session")]
pub use self::sled::SledSessionStore;

View File

@ -1,14 +1,9 @@
use actix::Addr; use actix::Addr;
use actix_redis::{resp_array, Command, RedisActor, RespValue}; use actix_redis::{resp_array, Command, RedisActor, RespValue};
use actix_web::cookie::time::Duration; use actix_web::cookie::time::Duration;
use anyhow::Error;
use super::SessionKey; use super::{LoadError, SaveError, SessionKey, SessionState, UpdateError};
use crate::storage::{ use crate::storage::{utils::generate_session_key, SessionStore};
interface::{LoadError, SaveError, SessionState, UpdateError},
utils::generate_session_key,
SessionStore,
};
/// Use Redis as session storage backend. /// Use Redis as session storage backend.
/// ///
@ -156,6 +151,7 @@ impl SessionStore for RedisActorSessionStore {
let body = serde_json::to_string(&session_state) let body = serde_json::to_string(&session_state)
.map_err(Into::into) .map_err(Into::into)
.map_err(SaveError::Serialization)?; .map_err(SaveError::Serialization)?;
let session_key = generate_session_key(); let session_key = generate_session_key();
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref()); let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
@ -239,7 +235,11 @@ impl SessionStore for RedisActorSessionStore {
} }
} }
async fn update_ttl(&self, session_key: &SessionKey, ttl: &Duration) -> Result<(), Error> { async fn update_ttl(
&self,
session_key: &SessionKey,
ttl: &Duration,
) -> Result<(), anyhow::Error> {
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref()); let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
let cmd = Command(resp_array![ let cmd = Command(resp_array![

View File

@ -1,15 +1,11 @@
use std::{convert::TryInto, sync::Arc}; use std::{convert::TryInto as _, sync::Arc};
use actix_web::cookie::time::Duration; use actix_web::cookie::time::Duration;
use anyhow::{Context, Error}; use anyhow::Context as _;
use redis::{aio::ConnectionManager, AsyncCommands, Cmd, FromRedisValue, RedisResult, Value}; use redis::{aio::ConnectionManager, AsyncCommands, Cmd, FromRedisValue, RedisResult, Value};
use super::SessionKey; use super::{LoadError, SaveError, SessionKey, SessionState, UpdateError};
use crate::storage::{ use crate::storage::{utils::generate_session_key, SessionStore};
interface::{LoadError, SaveError, SessionState, UpdateError},
utils::generate_session_key,
SessionStore,
};
/// Use Redis as session storage backend. /// Use Redis as session storage backend.
/// ///
@ -139,8 +135,8 @@ impl SessionStore for RedisSessionStore {
async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError> { async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError> {
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref()); let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
let value: Option<String> = self let value = self
.execute_command(redis::cmd("GET").arg(&[&cache_key])) .execute_command::<Option<String>>(redis::cmd("GET").arg(&[&cache_key]))
.await .await
.map_err(Into::into) .map_err(Into::into)
.map_err(LoadError::Other)?; .map_err(LoadError::Other)?;
@ -161,6 +157,7 @@ impl SessionStore for RedisSessionStore {
let body = serde_json::to_string(&session_state) let body = serde_json::to_string(&session_state)
.map_err(Into::into) .map_err(Into::into)
.map_err(SaveError::Serialization)?; .map_err(SaveError::Serialization)?;
let session_key = generate_session_key(); let session_key = generate_session_key();
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref()); let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
@ -169,7 +166,7 @@ impl SessionStore for RedisSessionStore {
&body, &body,
"NX", // NX: only set the key if it does not already exist "NX", // NX: only set the key if it does not already exist
"EX", // EX: set expiry "EX", // EX: set expiry
&format!("{}", ttl.whole_seconds()), ttl.whole_seconds().to_string().as_str(),
])) ]))
.await .await
.map_err(Into::into) .map_err(Into::into)
@ -194,9 +191,9 @@ impl SessionStore for RedisSessionStore {
.execute_command(redis::cmd("SET").arg(&[ .execute_command(redis::cmd("SET").arg(&[
&cache_key, &cache_key,
&body, &body,
"XX", // XX: Only set the key if it already exist. "XX", // XX: only set the key if it already exists
"EX", // EX: set expiry "EX", // EX: set expiry
&format!("{}", ttl.whole_seconds()), ttl.whole_seconds().to_string().as_str(),
])) ]))
.await .await
.map_err(Into::into) .map_err(Into::into)
@ -223,7 +220,11 @@ impl SessionStore for RedisSessionStore {
} }
} }
async fn update_ttl(&self, session_key: &SessionKey, ttl: &Duration) -> Result<(), Error> { async fn update_ttl(
&self,
session_key: &SessionKey,
ttl: &Duration,
) -> Result<(), anyhow::Error> {
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref()); let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
self.client self.client
@ -235,6 +236,7 @@ impl SessionStore for RedisSessionStore {
)?, )?,
) )
.await?; .await?;
Ok(()) Ok(())
} }
@ -321,12 +323,14 @@ mod tests {
async fn loading_an_invalid_session_state_returns_deserialization_error() { async fn loading_an_invalid_session_state_returns_deserialization_error() {
let store = redis_store().await; let store = redis_store().await;
let session_key = generate_session_key(); let session_key = generate_session_key();
store store
.client .client
.clone() .clone()
.set::<_, _, ()>(session_key.as_ref(), "random-thing-which-is-not-json") .set::<_, _, ()>(session_key.as_ref(), "random-thing-which-is-not-json")
.await .await
.unwrap(); .unwrap();
assert!(matches!( assert!(matches!(
store.load(&session_key).await.unwrap_err(), store.load(&session_key).await.unwrap_err(),
LoadError::Deserialization(_), LoadError::Deserialization(_),
@ -338,10 +342,12 @@ mod tests {
let store = redis_store().await; let store = redis_store().await;
let session_key = generate_session_key(); let session_key = generate_session_key();
let initial_session_key = session_key.as_ref().to_owned(); let initial_session_key = session_key.as_ref().to_owned();
let updated_session_key = store let updated_session_key = store
.update(session_key, HashMap::new(), &time::Duration::seconds(1)) .update(session_key, HashMap::new(), &time::Duration::seconds(1))
.await .await
.unwrap(); .unwrap();
assert_ne!(initial_session_key, updated_session_key.as_ref()); assert_ne!(initial_session_key, updated_session_key.as_ref());
} }
} }

View File

@ -0,0 +1,189 @@
use std::{path::Path, sync::Arc};
use actix_web::cookie::time::Duration;
use async_trait::async_trait;
use super::{
utils::generate_session_key, LoadError, SaveError, SessionKey, SessionState, SessionStore,
UpdateError,
};
#[derive(Clone)]
struct CacheConfiguration {
cache_keygen: Arc<dyn Fn(&str) -> String + Send + Sync>,
}
impl Default for CacheConfiguration {
fn default() -> Self {
Self {
cache_keygen: Arc::new(str::to_owned),
}
}
}
/// TODO
#[cfg_attr(docsrs, doc(cfg(feature = "sled-session")))]
#[derive(Clone)]
pub struct SledSessionStore {
configuration: CacheConfiguration,
db: sled::Db,
}
impl SledSessionStore {
/// TODO
pub fn new(db_path: impl AsRef<Path>) -> Result<Self, anyhow::Error> {
Ok(Self {
configuration: CacheConfiguration::default(),
db: sled::open(db_path)?,
})
}
}
#[async_trait(?Send)]
impl SessionStore for SledSessionStore {
async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError> {
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
let value = self
.db
.get(cache_key)
.map_err(Into::into)
.map_err(LoadError::Other)?;
match value {
None => Ok(None),
Some(value) => Ok(serde_json::from_slice(&value)
.map_err(Into::into)
.map_err(LoadError::Deserialization)?),
}
}
async fn save(
&self,
session_state: SessionState,
ttl: &Duration,
) -> Result<SessionKey, SaveError> {
let session_key = generate_session_key();
self.update(session_key, session_state, ttl)
.await
.map_err(|err| match err {
UpdateError::Serialization(err) => SaveError::Serialization(err),
UpdateError::Other(err) => SaveError::Other(err),
})
}
async fn update(
&self,
session_key: SessionKey,
session_state: SessionState,
_ttl: &Duration,
) -> Result<SessionKey, UpdateError> {
let body = serde_json::to_vec(&session_state)
.map_err(Into::into)
.map_err(UpdateError::Serialization)?;
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
self.db
.insert(cache_key, body)
.map_err(Into::into)
.map_err(UpdateError::Other)?;
Ok(session_key)
}
async fn update_ttl(
&self,
session_key: &SessionKey,
_ttl: &Duration,
) -> Result<(), anyhow::Error> {
let _cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
Ok(())
}
async fn delete(&self, session_key: &SessionKey) -> Result<(), anyhow::Error> {
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
self.db
.drop_tree(&cache_key)
.map_err(Into::into)
.map_err(UpdateError::Other)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use actix_web::cookie::time;
use super::*;
use crate::test_helpers::{acceptance_test_suite, function_name};
fn sled_db(fn_name: &str) -> SledSessionStore {
let db_name = fn_name.replace("::", ".").replace(".{{closure}}", "");
SledSessionStore::new(
[
env!("CARGO_TARGET_DIR"),
"/tmp-actix-session-tests/",
&db_name,
"-",
rand::random::<u16>().to_string().as_str(),
".db",
]
.concat(),
)
.unwrap()
}
#[actix_web::test]
async fn session_workflow() {
let store = sled_db(function_name!());
// TODO: use invalidation_supported = true
acceptance_test_suite(move || store.clone(), false).await;
}
#[actix_web::test]
async fn loading_a_missing_session_returns_none() {
let store = sled_db(function_name!());
let session_key = generate_session_key();
assert!(store.load(&session_key).await.unwrap().is_none());
}
#[actix_web::test]
async fn loading_an_invalid_session_state_returns_deserialization_error() {
let store = sled_db(function_name!());
let session_key = generate_session_key();
store
.db
.insert(session_key.as_ref(), "random-thing-which-is-not-json")
.unwrap();
assert!(matches!(
store.load(&session_key).await.unwrap_err(),
LoadError::Deserialization(_),
));
}
// ignored until TTL handling is implemented
#[ignore]
#[actix_web::test]
async fn updating_of_an_expired_state_is_handled_gracefully() {
let store = sled_db(function_name!());
let session_key = generate_session_key();
let initial_session_key = session_key.as_ref().to_owned();
let updated_session_key = store
.update(session_key, HashMap::new(), &time::Duration::seconds(1))
.await
.unwrap();
assert_ne!(initial_session_key, updated_session_key.as_ref());
}
}