mirror of
https://github.com/actix/actix-extras.git
synced 2024-11-27 17:22:57 +01:00
add sled session backend
This commit is contained in:
parent
6e79465362
commit
25f8a4cfae
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,5 +10,6 @@ guide/build/
|
||||
*.pyc
|
||||
*.pid
|
||||
*.sock
|
||||
*.db
|
||||
*~
|
||||
.DS_Store
|
||||
|
@ -21,8 +21,9 @@ name = "actix_session"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["sled-session"]
|
||||
cookie-session = []
|
||||
sled-session = ["sled"]
|
||||
redis-actor-session = ["actix-redis", "actix", "futures-core", "rand"]
|
||||
redis-rs-session = ["redis", "rand"]
|
||||
redis-rs-tls-session = ["redis-rs-session", "redis/tokio-native-tls-comp"]
|
||||
@ -40,6 +41,9 @@ serde = { version = "1" }
|
||||
serde_json = { version = "1" }
|
||||
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||
|
||||
# sled-session
|
||||
sled = { version = "0.34", optional = true }
|
||||
|
||||
# redis-actor-session
|
||||
actix = { version = "0.13", default-features = false, optional = true }
|
||||
actix-redis = { version = "0.12", optional = true }
|
||||
@ -57,7 +61,7 @@ log = "0.4"
|
||||
|
||||
[[example]]
|
||||
name = "basic"
|
||||
required-features = ["redis-actor-session"]
|
||||
required-features = ["sled-session"]
|
||||
|
||||
[[example]]
|
||||
name = "authentication"
|
||||
|
@ -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};
|
||||
|
||||
/// 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"));
|
||||
|
||||
// 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");
|
||||
|
||||
@ -31,7 +33,7 @@ async fn main() -> std::io::Result<()> {
|
||||
.wrap(middleware::Logger::default())
|
||||
// cookie session middleware
|
||||
.wrap(SessionMiddleware::new(
|
||||
RedisActorSessionStore::new("127.0.0.1:6379"),
|
||||
sled_session_store.clone(),
|
||||
signing_key.clone(),
|
||||
))
|
||||
// register simple route, handle all methods
|
||||
|
@ -156,6 +156,23 @@ pub mod test_helpers {
|
||||
|
||||
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.
|
||||
pub fn key() -> Key {
|
||||
Key::generate()
|
||||
|
@ -1,13 +1,9 @@
|
||||
use std::convert::TryInto;
|
||||
use std::convert::TryInto as _;
|
||||
|
||||
use actix_web::cookie::time::Duration;
|
||||
use anyhow::Error;
|
||||
|
||||
use super::SessionKey;
|
||||
use crate::storage::{
|
||||
interface::{LoadError, SaveError, SessionState, UpdateError},
|
||||
SessionStore,
|
||||
};
|
||||
use super::{LoadError, SaveError, SessionKey, SessionState, UpdateError};
|
||||
use crate::storage::SessionStore;
|
||||
|
||||
/// 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,
|
||||
) -> Result<SessionKey, SaveError> {
|
||||
let session_key = serde_json::to_string(&session_state)
|
||||
.map_err(anyhow::Error::new)
|
||||
.map_err(Into::into)
|
||||
.map_err(SaveError::Serialization)?;
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
|
@ -101,7 +101,7 @@ pub enum UpdateError {
|
||||
Serialization(anyhow::Error),
|
||||
|
||||
/// 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),
|
||||
}
|
||||
|
||||
|
@ -3,12 +3,16 @@
|
||||
mod interface;
|
||||
mod session_key;
|
||||
|
||||
pub(crate) use self::interface::SessionState;
|
||||
pub use self::interface::{LoadError, SaveError, SessionStore, UpdateError};
|
||||
pub use self::session_key::SessionKey;
|
||||
|
||||
#[cfg(feature = "cookie-session")]
|
||||
mod cookie;
|
||||
|
||||
#[cfg(feature = "sled-session")]
|
||||
mod sled;
|
||||
|
||||
#[cfg(feature = "redis-actor-session")]
|
||||
mod redis_actor;
|
||||
|
||||
@ -19,8 +23,10 @@ mod redis_rs;
|
||||
mod utils;
|
||||
|
||||
#[cfg(feature = "cookie-session")]
|
||||
pub use cookie::CookieSessionStore;
|
||||
pub use self::cookie::CookieSessionStore;
|
||||
#[cfg(feature = "redis-actor-session")]
|
||||
pub use redis_actor::{RedisActorSessionStore, RedisActorSessionStoreBuilder};
|
||||
pub use self::redis_actor::{RedisActorSessionStore, RedisActorSessionStoreBuilder};
|
||||
#[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;
|
||||
|
@ -1,14 +1,9 @@
|
||||
use actix::Addr;
|
||||
use actix_redis::{resp_array, Command, RedisActor, RespValue};
|
||||
use actix_web::cookie::time::Duration;
|
||||
use anyhow::Error;
|
||||
|
||||
use super::SessionKey;
|
||||
use crate::storage::{
|
||||
interface::{LoadError, SaveError, SessionState, UpdateError},
|
||||
utils::generate_session_key,
|
||||
SessionStore,
|
||||
};
|
||||
use super::{LoadError, SaveError, SessionKey, SessionState, UpdateError};
|
||||
use crate::storage::{utils::generate_session_key, SessionStore};
|
||||
|
||||
/// Use Redis as session storage backend.
|
||||
///
|
||||
@ -156,6 +151,7 @@ impl SessionStore for RedisActorSessionStore {
|
||||
let body = serde_json::to_string(&session_state)
|
||||
.map_err(Into::into)
|
||||
.map_err(SaveError::Serialization)?;
|
||||
|
||||
let session_key = generate_session_key();
|
||||
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 cmd = Command(resp_array![
|
||||
|
@ -1,15 +1,11 @@
|
||||
use std::{convert::TryInto, sync::Arc};
|
||||
use std::{convert::TryInto as _, sync::Arc};
|
||||
|
||||
use actix_web::cookie::time::Duration;
|
||||
use anyhow::{Context, Error};
|
||||
use anyhow::Context as _;
|
||||
use redis::{aio::ConnectionManager, AsyncCommands, Cmd, FromRedisValue, RedisResult, Value};
|
||||
|
||||
use super::SessionKey;
|
||||
use crate::storage::{
|
||||
interface::{LoadError, SaveError, SessionState, UpdateError},
|
||||
utils::generate_session_key,
|
||||
SessionStore,
|
||||
};
|
||||
use super::{LoadError, SaveError, SessionKey, SessionState, UpdateError};
|
||||
use crate::storage::{utils::generate_session_key, SessionStore};
|
||||
|
||||
/// 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> {
|
||||
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
|
||||
|
||||
let value: Option<String> = self
|
||||
.execute_command(redis::cmd("GET").arg(&[&cache_key]))
|
||||
let value = self
|
||||
.execute_command::<Option<String>>(redis::cmd("GET").arg(&[&cache_key]))
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.map_err(LoadError::Other)?;
|
||||
@ -161,6 +157,7 @@ impl SessionStore for RedisSessionStore {
|
||||
let body = serde_json::to_string(&session_state)
|
||||
.map_err(Into::into)
|
||||
.map_err(SaveError::Serialization)?;
|
||||
|
||||
let session_key = generate_session_key();
|
||||
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
|
||||
|
||||
@ -169,7 +166,7 @@ impl SessionStore for RedisSessionStore {
|
||||
&body,
|
||||
"NX", // NX: only set the key if it does not already exist
|
||||
"EX", // EX: set expiry
|
||||
&format!("{}", ttl.whole_seconds()),
|
||||
ttl.whole_seconds().to_string().as_str(),
|
||||
]))
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
@ -194,9 +191,9 @@ impl SessionStore for RedisSessionStore {
|
||||
.execute_command(redis::cmd("SET").arg(&[
|
||||
&cache_key,
|
||||
&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
|
||||
&format!("{}", ttl.whole_seconds()),
|
||||
ttl.whole_seconds().to_string().as_str(),
|
||||
]))
|
||||
.await
|
||||
.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());
|
||||
|
||||
self.client
|
||||
@ -235,6 +236,7 @@ impl SessionStore for RedisSessionStore {
|
||||
)?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -321,12 +323,14 @@ mod tests {
|
||||
async fn loading_an_invalid_session_state_returns_deserialization_error() {
|
||||
let store = redis_store().await;
|
||||
let session_key = generate_session_key();
|
||||
|
||||
store
|
||||
.client
|
||||
.clone()
|
||||
.set::<_, _, ()>(session_key.as_ref(), "random-thing-which-is-not-json")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
store.load(&session_key).await.unwrap_err(),
|
||||
LoadError::Deserialization(_),
|
||||
@ -338,10 +342,12 @@ mod tests {
|
||||
let store = redis_store().await;
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
189
actix-session/src/storage/sled.rs
Normal file
189
actix-session/src/storage/sled.rs
Normal 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());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user