368 lines
14 KiB
Rust
368 lines
14 KiB
Rust
|
|
use async_session::{Result, Session, SessionStore};
|
|
use anyhow::anyhow;
|
|
use async_trait::async_trait;
|
|
use std::{
|
|
fs,
|
|
path::Path,
|
|
};
|
|
use walkdir::{DirEntry, WalkDir};
|
|
use binascii::bin2hex;
|
|
|
|
#[allow(unused)]
|
|
fn is_hidden(entry: &DirEntry) -> bool {
|
|
entry.file_name()
|
|
.to_str()
|
|
.map(|s| s.starts_with("."))
|
|
.unwrap_or(false)
|
|
}
|
|
#[derive(Debug, Clone)]
|
|
pub struct FileStore {
|
|
pub sess_path: String,
|
|
pub ses_file: String,
|
|
}
|
|
#[async_trait]
|
|
impl SessionStore for FileStore {
|
|
async fn load_session(&self, cookie_value: String) -> Result<Option<Session>> {
|
|
let id = Session::id_from_cookie_value(&cookie_value)?;
|
|
log::trace!("loading session by id `{}`", &id);
|
|
//dbg!("loading session by id `{}`", &id);
|
|
self.load_session_file(&id).await
|
|
}
|
|
async fn store_session(&self, session: Session) -> Result<Option<String>> {
|
|
log::trace!("storing session by id `{}`", session.id());
|
|
let id_filename = match self.get_path(session.id()) {
|
|
Ok(res) => res,
|
|
Err(e) => {
|
|
return Err(e);
|
|
}
|
|
};
|
|
// let mut out_buffer = [0u8; 100];
|
|
// let id_filename = if let Ok(res) = bin2hex(session.id().as_bytes(),&mut out_buffer) {
|
|
// std::str::from_utf8(res)?.to_owned()
|
|
// } else {
|
|
// return Ok(None);
|
|
// };
|
|
let sess_id_path = format!("{}/{}", self.sess_path, &id_filename);
|
|
if ! Path::new(&sess_id_path).exists() {
|
|
fs::create_dir(&sess_id_path)?;
|
|
}
|
|
let content_session = serde_json::to_string(&session)?;
|
|
fs::write(&format!("{}/{}",&sess_id_path, self.ses_file), content_session)?;
|
|
//session.reset_data_changed(); // do not need is it is serialized in file write
|
|
Ok(session.into_cookie_value())
|
|
}
|
|
async fn destroy_session(&self, session: Session) -> Result {
|
|
log::trace!("destroying session by id `{}`", session.id());
|
|
match self.get_path(session.id()) {
|
|
Ok(res) => match self.get_session_id_path(&res) {
|
|
Ok(session_id_path) => Ok(fs::remove_file(&session_id_path)?),
|
|
Err(e) => Err(e),
|
|
},
|
|
Err(e) => Err(e)
|
|
}
|
|
// let mut out_buffer = [0u8; 100];
|
|
// if let Ok(res) = bin2hex(session.id().as_bytes(),&mut out_buffer) {
|
|
// let id_filename = std::str::from_utf8(res)?.to_owned();
|
|
// Ok(fs::remove_file(
|
|
// &format!("{}/{}/{}",self.sess_path, &id_filename, self.ses_file)
|
|
// )?)
|
|
// } else {
|
|
// Ok(())
|
|
// }
|
|
}
|
|
async fn clear_store(&self) -> Result {
|
|
log::trace!("clearing memory store");
|
|
let sess_path = format!("{}", self.sess_path);
|
|
fs::remove_dir_all(&sess_path)?;
|
|
fs::create_dir(&sess_path)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
impl FileStore {
|
|
/// Create a new instance of FilesStore
|
|
pub fn check_paths(&self) -> Result {
|
|
if ! Path::new(&self.sess_path).exists() {
|
|
fs::create_dir(&self.sess_path)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
pub fn get_path(&self,id: &str) -> Result<String> {
|
|
let mut out_buffer = [0u8; 100];
|
|
match bin2hex(&id.as_bytes(),&mut out_buffer) {
|
|
Ok(res) => Ok(std::str::from_utf8(res)?.to_owned()),
|
|
Err(e) => {
|
|
Err(anyhow!("Filename path {} not generated: {:?}", &id, &e))
|
|
}
|
|
}
|
|
}
|
|
pub fn get_session_id_path(&self,id_filename: &str) -> Result<String> {
|
|
let session_id_path = format!("{}/{}/{}",self.sess_path, id_filename, &self.ses_file);
|
|
if ! Path::new(&session_id_path).exists() {
|
|
Err(anyhow!("Filename path {} not found: {}", id_filename, &session_id_path ))
|
|
} else {
|
|
Ok(session_id_path)
|
|
}
|
|
}
|
|
/// As session Id from `async_session` comes in base64 it will be not valid for OS filename
|
|
/// `bin2hex` pass id to hex as bytes and from there to string or viceversa
|
|
pub async fn load_session_file(&self, id: &str) -> Result<Option<Session>> {
|
|
let session_id_path = match self.get_path(id) {
|
|
Ok(res) => match self.get_session_id_path(&res) {
|
|
Ok(path) => path,
|
|
Err(e) => return Err(e),
|
|
},
|
|
Err(e) => {
|
|
return Err(e);
|
|
}
|
|
};
|
|
// let mut out_buffer = [0u8; 100];
|
|
// let id_filename = if let Ok(res) = bin2hex(&id.as_bytes(),&mut out_buffer) {
|
|
// std::str::from_utf8(res)?.to_owned()
|
|
// } else {
|
|
// return Ok(None);
|
|
// };
|
|
// let session_id_path = format!("{}/{}/{}",self.sess_path, &id_filename, &self.ses_file);
|
|
// dbg!(&session_id_path);
|
|
if ! Path::new(&session_id_path).exists() {
|
|
dbg!("No path: {}", &session_id_path);
|
|
// let sess_id_path = format!("{}/{}", self.sess_path, &id_filename);
|
|
// if ! Path::new(&sess_id_path).exists() {
|
|
// fs::create_dir(&sess_id_path)?;
|
|
// }
|
|
// create
|
|
}
|
|
if let Ok(session_content) = fs::read_to_string(&session_id_path) {
|
|
// match serde_json::from_str::<serde_json::Value>(&session_content) {
|
|
match serde_json::from_str::<Session>(&session_content) {
|
|
Ok(session) => {
|
|
Ok(session.validate())
|
|
},
|
|
Err(e) => {
|
|
dbg!("Error loading session content from {}: {}",&session_id_path, e);
|
|
//log::error!("Error loading session content from {}: {}",&session_id_path, e);
|
|
Ok(None)
|
|
}
|
|
}
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
#[allow(dead_code)]
|
|
pub async fn cleanup(&self) -> Result {
|
|
log::trace!("cleaning up file store...");
|
|
let mut count: usize = 0;
|
|
let sess_path = format!("{}", self.sess_path);
|
|
let walker = WalkDir::new(&sess_path).into_iter();
|
|
for entry in walker.filter_entry(|e| !is_hidden(e)) {
|
|
match entry {
|
|
Ok(dir_entry) => {
|
|
// println!("{}", &dir_entry.path().display());
|
|
if ! Path::new(&dir_entry.path()).is_dir() {
|
|
continue;
|
|
}
|
|
let session_file = format!("{}/{}",&dir_entry.path().display(), &self.ses_file);
|
|
let id_path = format!("{}",&dir_entry.path().display());
|
|
let id = id_path.replace(&sess_path,"");
|
|
if let Some(session) = self.load_session_file(&id).await.unwrap_or_default() {
|
|
if session.is_expired() {
|
|
let _ = fs::remove_file(&session_file);
|
|
log::trace!("found {} expired session",&id_path);
|
|
count +=1;
|
|
}
|
|
}
|
|
},
|
|
Err(e) => println!("Error on {}: {}", &sess_path, e)
|
|
}
|
|
}
|
|
log::trace!("found {} expired session {} cleaned",&sess_path, count);
|
|
Ok(())
|
|
}
|
|
#[allow(dead_code)]
|
|
pub async fn count(&self) -> usize {
|
|
let mut count: usize = 0;
|
|
let sess_path = format!("{}", self.sess_path);
|
|
let walker = WalkDir::new(&sess_path).into_iter();
|
|
for entry in walker.filter_entry(|e| !is_hidden(e)) {
|
|
match entry {
|
|
Ok(dir_entry) => {
|
|
// println!("{}", &dir_entry.path().display());
|
|
if ! Path::new(&dir_entry.path()).is_dir() {
|
|
continue;
|
|
}
|
|
let session_file = format!("{}/{}",&dir_entry.path().display(), &self.ses_file);
|
|
if Path::new(&session_file).exists() {
|
|
count +=1;
|
|
}
|
|
},
|
|
Err(e) => println!("Error on {}: {}", &sess_path, e)
|
|
}
|
|
}
|
|
count
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use async_std::task;
|
|
use std::time::Duration;
|
|
const TEST_SESS_FILESTORE: &str = "/tmp/test_sess_filestore";
|
|
const TEST_IDS_FILESTORE: &str = "/tmp/test_ids_filestore";
|
|
|
|
#[async_std::test]
|
|
async fn creating_a_new_session_with_no_expiry() -> Result {
|
|
let sess_path_store = format!("{}_0", TEST_SESS_FILESTORE);
|
|
let ids_path_store = format!("{}_0", TEST_IDS_FILESTORE);
|
|
let _ = fs::remove_dir_all(&sess_path_store);
|
|
let _ = fs::remove_dir_all(&ids_path_store);
|
|
let store = FileStore {
|
|
sess_path: sess_path_store.to_owned(),
|
|
ses_file: String::from("session"),
|
|
};
|
|
store.check_paths()?;
|
|
let mut session = Session::new();
|
|
session.insert("key", "Hello")?;
|
|
let cloned = session.clone();
|
|
let cookie_value = store.store_session(session).await?.unwrap();
|
|
assert!(true);
|
|
let loaded_session = store.load_session(cookie_value).await?.unwrap();
|
|
assert_eq!(cloned.id(), loaded_session.id());
|
|
assert_eq!("Hello", &loaded_session.get::<String>("key").unwrap());
|
|
assert!(!loaded_session.is_expired());
|
|
assert!(loaded_session.validate().is_some());
|
|
let _ = fs::remove_dir_all(&sess_path_store);
|
|
let _ = fs::remove_dir_all(&ids_path_store);
|
|
Ok(())
|
|
}
|
|
#[async_std::test]
|
|
async fn updating_a_session() -> Result {
|
|
let sess_path_store = format!("{}_1", TEST_SESS_FILESTORE);
|
|
let _ = fs::remove_dir_all(&sess_path_store);
|
|
let store = FileStore {
|
|
sess_path: sess_path_store.to_owned(),
|
|
ses_file: String::from("session"),
|
|
};
|
|
store.check_paths()?;
|
|
|
|
let mut session = Session::new();
|
|
|
|
session.insert("key", "value")?;
|
|
let cookie_value = store.store_session(session).await?.unwrap();
|
|
|
|
let mut session = store.load_session(cookie_value.clone()).await?.unwrap();
|
|
session.insert("key", "other value")?;
|
|
|
|
assert_eq!(store.store_session(session).await?, None);
|
|
let session = store.load_session(cookie_value).await?.unwrap();
|
|
assert_eq!(&session.get::<String>("key").unwrap(), "other value");
|
|
fs::remove_dir_all(&sess_path_store)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[async_std::test]
|
|
async fn updating_a_session_extending_expiry() -> Result {
|
|
let sess_path_store = format!("{}_2", TEST_SESS_FILESTORE);
|
|
let _ = fs::remove_dir_all(&sess_path_store);
|
|
let store = FileStore {
|
|
sess_path: sess_path_store.to_owned(),
|
|
ses_file: String::from("session"),
|
|
};
|
|
store.check_paths()?;
|
|
|
|
let mut session = Session::new();
|
|
session.expire_in(Duration::from_secs(1));
|
|
let original_expires = session.expiry().unwrap().clone();
|
|
let cookie_value = store.store_session(session).await?.unwrap();
|
|
|
|
let mut session = store.load_session(cookie_value.clone()).await?.unwrap();
|
|
|
|
assert_eq!(session.expiry().unwrap(), &original_expires);
|
|
session.expire_in(Duration::from_secs(3));
|
|
let new_expires = session.expiry().unwrap().clone();
|
|
assert_eq!(None, store.store_session(session).await?);
|
|
|
|
let session = store.load_session(cookie_value.clone()).await?.unwrap();
|
|
assert_eq!(session.expiry().unwrap(), &new_expires);
|
|
|
|
task::sleep(Duration::from_secs(3)).await;
|
|
assert_eq!(None, store.load_session(cookie_value).await?);
|
|
fs::remove_dir_all(&sess_path_store)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[async_std::test]
|
|
async fn creating_a_new_session_with_expiry() -> Result {
|
|
let sess_path_store = format!("{}_3", TEST_SESS_FILESTORE);
|
|
let _ = fs::remove_dir_all(&sess_path_store);
|
|
let store = FileStore {
|
|
sess_path: sess_path_store.to_owned(),
|
|
ses_file: String::from("session"),
|
|
};
|
|
store.check_paths()?;
|
|
|
|
let mut session = Session::new();
|
|
session.expire_in(Duration::from_secs(3));
|
|
session.insert("key", "value")?;
|
|
let cloned = session.clone();
|
|
|
|
let cookie_value = store.store_session(session).await?.unwrap();
|
|
|
|
let loaded_session = store.load_session(cookie_value.clone()).await?.unwrap();
|
|
assert_eq!(cloned.id(), loaded_session.id());
|
|
assert_eq!("value", &*loaded_session.get::<String>("key").unwrap());
|
|
|
|
assert!(!loaded_session.is_expired());
|
|
|
|
task::sleep(Duration::from_secs(3)).await;
|
|
assert_eq!(None, store.load_session(cookie_value).await?);
|
|
fs::remove_dir_all(&sess_path_store)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[async_std::test]
|
|
async fn destroying_a_single_session() -> Result {
|
|
let sess_path_store = format!("{}_4", TEST_SESS_FILESTORE);
|
|
let _ = fs::remove_dir_all(&sess_path_store);
|
|
let store = FileStore {
|
|
sess_path: sess_path_store.to_owned(),
|
|
ses_file: String::from("session"),
|
|
};
|
|
store.check_paths()?;
|
|
|
|
for _ in 0..3i8 {
|
|
store.store_session(Session::new()).await?;
|
|
}
|
|
let cookie = store.store_session(Session::new()).await?.unwrap();
|
|
assert_eq!(4, store.count().await);
|
|
let session = store.load_session(cookie.clone()).await?.unwrap();
|
|
store.destroy_session(session.clone()).await?;
|
|
assert!(store.load_session(cookie).await.is_err());
|
|
assert_eq!(3, store.count().await);
|
|
|
|
// attempting to destroy the session again IS an ERROR, file should be deleted before
|
|
assert!(store.destroy_session(session).await.is_err());
|
|
fs::remove_dir_all(&sess_path_store)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[async_std::test]
|
|
async fn clearing_the_whole_store() -> Result {
|
|
let sess_path_store = format!("{}_5", TEST_SESS_FILESTORE);
|
|
let _ = fs::remove_dir_all(&sess_path_store);
|
|
let store = FileStore {
|
|
sess_path: sess_path_store.to_owned(),
|
|
ses_file: String::from("session"),
|
|
};
|
|
store.check_paths()?;
|
|
|
|
for _ in 0..3i8 {
|
|
store.store_session(Session::new()).await?;
|
|
}
|
|
assert_eq!(3, store.count().await);
|
|
store.clear_store().await.unwrap();
|
|
assert_eq!(0, store.count().await);
|
|
fs::remove_dir_all(&sess_path_store)?;
|
|
Ok(())
|
|
}
|
|
} |