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> { 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> { 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 { 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 { 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> { 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::(&session_content) { match serde_json::from_str::(&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::("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::("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::("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(()) } }