Skip to content

Bump bumpalo from 3.11.0 to 3.12.0 #2

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target
2,118 changes: 2,118 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "actix-session-sqlx"
version = "0.1.0"
edition = "2021"
authors = ["Christopher Kolstad <git@chriswk.no>", "Simon Hornby <liquidwicked64@gmail.com>"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
actix-session = "0.7.1"
anyhow = "1.0.62"
async-trait = "0.1.57"
chrono = { version = "0.4.19", features = ["serde"] }
rand = "0.8.5"
serde = { version = "1.0.144", features = ["derive"]}
serde_json = { version = "1.0.85" }
sqlx = { version = "0.6.0", features = ["json", "chrono", "runtime-actix-rustls", "time", "postgres"] }
time = "0.3.14"

[dev-dependencies]
testcontainers = { version ="0.14.0"}
actix-test = "0.1.0"
actix-web = { version = "4", default_features = false, features = ["cookies", "secure-cookies", "macros"] }
actix-session = { version = "0.7.1" }
237 changes: 237 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
use std::collections::HashMap;
use std::sync::Arc;
use actix_session::storage::{LoadError, SaveError, SessionKey, SessionStore, UpdateError};
use chrono::Utc;
use sqlx::{Pool, Postgres, Row};
use sqlx::postgres::PgPoolOptions;
use rand::{distributions::Alphanumeric, rngs::OsRng, Rng as _};
use time::Duration;
use serde_json;
use crate::ConnectionData::{ConnectionPool, ConnectionString};

/// Use Postgres via Sqlx as session storage backend.
///
/// ```no_run
/// use actix_web::{web, App, HttpServer, HttpResponse, Error};
/// use actix_session_sqlx::SqlxPostgresqlSessionStore;
/// use actix_session::SessionMiddleware;
/// use actix_web::cookie::Key;
///
/// // The secret key would usually be read from a configuration file/environment variables.
/// fn get_secret_key() -> Key {
/// # todo!()
/// // [...]
/// }
///
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// let secret_key = get_secret_key();
/// let psql_connection_string = "postgres://<username>:<password>@127.0.0.1:5432/<yourdatabase>";
/// let store = SqlxPostgresqlSessionStore::new(psql_connection_string).await.unwrap();
///
/// HttpServer::new(move ||
/// App::new()
/// .wrap(SessionMiddleware::new(
/// store.clone(),
/// secret_key.clone()
/// ))
/// .default_service(web::to(|| HttpResponse::Ok())))
/// .bind(("127.0.0.1", 8080))?
/// .run()
/// .await
/// }
/// ```
/// If you already have a connection pool, you can use something like
/*/// ```no_run
/// use actix_web::{web, App, HttpServer, HttpResponse, Error};
/// use actix_session_sqlx::SqlxPostgresqlSessionStore;
/// use actix_session::SessionMiddleware;
/// use actix_web::cookie::Key;
///
/// // The secret key would usually be read from a configuration file/environment variables.
/// fn get_secret_key() -> Key {
/// # todo!()
/// // [...]
/// }
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// use sqlx::postgres::PgPoolOptions;
/// let secret_key = get_secret_key();
/// let pool = PgPoolOptions::find_some_way_to_build_your_pool(psql_connection_string);
/// let store = SqlxPostgresqlSessionStore::from_pool(pool).await.expect("Could not build session store");
///
/// HttpServer::new(move ||
/// App::new()
/// .wrap(SessionMiddleware::new(
/// store.clone(),
/// secret_key.clone()
/// ))
/// .default_service(web::to(|| HttpResponse::Ok())))
/// .bind(("127.0.0.1", 8080))?
/// .run()
/// .await
/// }
/// ```
*/
#[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),
}
}
}

#[derive(Clone)]
pub struct SqlxPostgresqlSessionStore {
client_pool: Pool<Postgres>,
configuration: CacheConfiguration,
}

fn generate_session_key() -> SessionKey {
let value = std::iter::repeat(())
.map(|()| OsRng.sample(Alphanumeric))
.take(64)
.collect::<Vec<_>>();

// These unwraps will never panic because pre-conditions are always verified
// (i.e. length and character set)
String::from_utf8(value).unwrap().try_into().unwrap()
}

impl SqlxPostgresqlSessionStore {
pub fn builder<S: Into<String>>(connection_string: S) -> SqlxPostgresqlSessionStoreBuilder {
SqlxPostgresqlSessionStoreBuilder {
connection_data: ConnectionString(connection_string.into()),
configuration: CacheConfiguration::default()
}
}

pub async fn new<S: Into<String>>(connection_string: S) -> Result<SqlxPostgresqlSessionStore, anyhow::Error> {
Self::builder(connection_string).build().await
}

pub async fn from_pool(pool: Pool<Postgres>) -> SqlxPostgresqlSessionStoreBuilder {
SqlxPostgresqlSessionStoreBuilder {
connection_data: ConnectionPool(pool.clone()),
configuration: CacheConfiguration::default()
}
}
}

pub enum ConnectionData {
ConnectionString(String),
ConnectionPool(Pool<Postgres>)
}

#[must_use]
pub struct SqlxPostgresqlSessionStoreBuilder {
connection_data: ConnectionData,
configuration: CacheConfiguration,
}

impl SqlxPostgresqlSessionStoreBuilder {
pub async fn build(self) -> Result<SqlxPostgresqlSessionStore, anyhow::Error> {
match self.connection_data {
ConnectionString(conn_string) => {
PgPoolOptions::new()
.max_connections(1)
.connect(conn_string.as_str())
.await
.map_err(Into::into)
.map(|pool| {
SqlxPostgresqlSessionStore {
client_pool: pool,
configuration: self.configuration
}
})
},
ConnectionPool(pool) => Ok(SqlxPostgresqlSessionStore {
client_pool: pool, configuration: self.configuration
})
}
}
}
pub(crate) type SessionState = HashMap<String, String>;

#[async_trait::async_trait(?Send)]
impl SessionStore for SqlxPostgresqlSessionStore {
async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError> {
let key = (self.configuration.cache_keygen)(session_key.as_ref());
let row = sqlx::query("SELECT session_state FROM sessions WHERE key = $1 AND expires > NOW()")
.bind( key)
.fetch_optional(&self.client_pool)
.await
.map_err(Into::into)
.map_err(LoadError::Other)?;
match row {
None => Ok(None),
Some(r) => {
let data: String = r.get("session_state");
let state: SessionState = serde_json::from_str(&data).map_err(Into::into).map_err(LoadError::Deserialization)?;
Ok(Some(state))
}
}
}

async fn save(&self, session_state: SessionState, ttl: &Duration) -> Result<SessionKey, SaveError> {
let body = serde_json::to_string(&session_state)
.map_err(Into::into)
.map_err(SaveError::Serialization)?;
let key = generate_session_key();
let cache_key = (self.configuration.cache_keygen)(key.as_ref());
let expires = Utc::now() + chrono::Duration::seconds(ttl.whole_seconds() as i64);
sqlx::query("INSERT INTO sessions(key, session_state, expires) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING")
.bind(cache_key)
.bind( body)
.bind( expires)
.execute(&self.client_pool)
.await
.map_err(Into::into)
.map_err(SaveError::Other)?;
Ok(key)
}

async fn update(&self, session_key: SessionKey, session_state: SessionState, ttl: &Duration) -> Result<SessionKey, UpdateError> {
let body = serde_json::to_string(&session_state).map_err(Into::into).map_err(UpdateError::Serialization)?;
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
let new_expires = Utc::now() + chrono::Duration::seconds(ttl.whole_seconds());
sqlx::query("UPDATE sessions SET session_state = $1, expires = $2 WHERE key = $3")
.bind( body)
.bind( new_expires)
.bind( cache_key)
.execute(&self.client_pool)
.await
.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 new_expires = Utc::now() + chrono::Duration::seconds(ttl.whole_seconds() as i64);
let key = (self.configuration.cache_keygen)(session_key.as_ref());
sqlx::query("UPDATE sessions SET expires = $1 WHERE key = $2")
.bind(new_expires)
.bind( key)
.execute(&self.client_pool)
.await
.map_err(Into::into)
.map_err(UpdateError::Other)?;
Ok(())
}

async fn delete(&self, session_key: &SessionKey) -> Result<(), anyhow::Error> {
let key = (self.configuration.cache_keygen)(session_key.as_ref());
sqlx::query("DELETE FROM sessions WHERE key = $1")
.bind(key)
.execute(&self.client_pool)
.await
.map_err(Into::into)
.map_err(UpdateError::Other)?;
Ok(())
}
}
60 changes: 60 additions & 0 deletions tests/session_store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
mod test_helpers;
#[cfg(test)]
pub mod tests {
use actix_session::storage::SessionStore;
use actix_session_sqlx::SqlxPostgresqlSessionStore;
use sqlx::postgres::PgPoolOptions;
use std::collections::HashMap;
use testcontainers::clients;
use testcontainers::core::WaitFor;
use testcontainers::images::generic;

async fn postgres_store(url: String) -> SqlxPostgresqlSessionStore {
SqlxPostgresqlSessionStore::new(url).await.expect("")
}

#[actix_web::test]
async fn test_session_workflow() {
let docker = clients::Cli::default();
let postgres = docker.run(
generic::GenericImage::new("postgres", "14-alpine")
.with_wait_for(WaitFor::message_on_stderr(
"database system is ready to accept connections",
))
.with_env_var("POSTGRES_DB", "sessions")
.with_env_var("POSTGRES_PASSWORD", "example")
.with_env_var("POSTGRES_HOST_AUTH_METHOD", "trust")
.with_env_var("POSTGRES_USER", "tests"),
);
let url = format!(
"postgres://tests:example@localhost:{}/sessions",
postgres.get_host_port_ipv4(5432)
);

let postgres_store = postgres_store(url.clone()).await;
let pool = PgPoolOptions::new()
.max_connections(1)
.connect(url.as_str())
.await
.expect("Could not connect to database");
sqlx::query(
r#"CREATE TABLE sessions(
key TEXT PRIMARY KEY NOT NULL,
session_state TEXT,
expires TIMESTAMP WITH TIME ZONE NOT NULL
);"#,
)
.execute(&pool)
.await
.expect("Could not create table");
let mut session = HashMap::new();
session.insert("key".to_string(), "value".to_string());
let data = postgres_store
.save(session, &time::Duration::days(1))
.await;
println!("{:#?}", data);
assert!(data
.is_ok());
super::test_helpers::acceptance_test_suite(move || postgres_store.clone(), true).await;
}
}
579 changes: 579 additions & 0 deletions tests/test_helpers.rs

Large diffs are not rendered by default.