1use std::collections::HashMap;
4use std::sync::Mutex;
5
6use chrono::{DateTime, Duration, Utc};
7use rainier_support::{BoxFuture, Error, Result};
8
9use crate::session::SessionData;
10
11pub trait SessionStore: Send + Sync + 'static {
20 fn name(&self) -> &str;
23
24 fn read<'a>(&'a self, id: &'a str) -> BoxFuture<'a, Result<Option<SessionData>>>;
26
27 fn write<'a>(&'a self, id: &'a str, data: &'a SessionData) -> BoxFuture<'a, Result<()>>;
29
30 fn destroy<'a>(&'a self, id: &'a str) -> BoxFuture<'a, Result<()>>;
32
33 fn gc(&self) -> BoxFuture<'_, Result<u64>> {
35 Box::pin(async { Ok(0) })
36 }
37
38 fn is_client_side(&self) -> bool {
43 false
44 }
45
46 fn encode(&self, id: &str, data: &SessionData) -> Result<String> {
50 let _ = data;
51 Ok(id.to_string())
52 }
53
54 fn decode(&self, value: &str) -> Result<(String, Option<SessionData>)> {
62 if is_well_formed(value) {
63 Ok((value.to_string(), None))
64 } else {
65 Err(Error::bad_request("that is not a session id this application issued"))
66 }
67 }
68}
69
70pub fn is_well_formed(id: &str) -> bool {
73 id.len() == 64 && id.bytes().all(|byte| byte.is_ascii_hexdigit())
74}
75
76pub struct MemorySessionStore {
83 entries: Mutex<HashMap<String, Entry>>,
84 lifetime: Duration,
85}
86
87#[derive(Debug, Clone)]
88struct Entry {
89 data: SessionData,
90 expires_at: DateTime<Utc>,
91}
92
93impl Default for MemorySessionStore {
94 fn default() -> Self {
95 Self::new(Duration::hours(2))
96 }
97}
98
99impl MemorySessionStore {
100 pub fn new(lifetime: Duration) -> Self {
102 Self { entries: Mutex::new(HashMap::new()), lifetime }
103 }
104
105 pub fn len(&self) -> usize {
107 self.entries.lock().unwrap_or_else(|e| e.into_inner()).len()
108 }
109
110 pub fn is_empty(&self) -> bool {
112 self.len() == 0
113 }
114}
115
116impl SessionStore for MemorySessionStore {
117 fn name(&self) -> &str {
118 "memory"
119 }
120
121 fn read<'a>(&'a self, id: &'a str) -> BoxFuture<'a, Result<Option<SessionData>>> {
122 Box::pin(async move {
123 let mut entries = self.entries.lock().unwrap_or_else(|e| e.into_inner());
124 match entries.get(id) {
125 Some(entry) if entry.expires_at > Utc::now() => Ok(Some(entry.data.clone())),
126 Some(_) => {
127 entries.remove(id);
129 Ok(None)
130 }
131 None => Ok(None),
132 }
133 })
134 }
135
136 fn write<'a>(&'a self, id: &'a str, data: &'a SessionData) -> BoxFuture<'a, Result<()>> {
137 Box::pin(async move {
138 self.entries.lock().unwrap_or_else(|e| e.into_inner()).insert(
139 id.to_string(),
140 Entry { data: data.clone(), expires_at: Utc::now() + self.lifetime },
141 );
142 Ok(())
143 })
144 }
145
146 fn destroy<'a>(&'a self, id: &'a str) -> BoxFuture<'a, Result<()>> {
147 Box::pin(async move {
148 self.entries.lock().unwrap_or_else(|e| e.into_inner()).remove(id);
149 Ok(())
150 })
151 }
152
153 fn gc(&self) -> BoxFuture<'_, Result<u64>> {
154 Box::pin(async move {
155 let mut entries = self.entries.lock().unwrap_or_else(|e| e.into_inner());
156 let now = Utc::now();
157 let before = entries.len();
158 entries.retain(|_, entry| entry.expires_at > now);
159 Ok((before - entries.len()) as u64)
160 })
161 }
162}
163
164impl std::fmt::Debug for MemorySessionStore {
165 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166 f.debug_struct("MemorySessionStore").field("sessions", &self.len()).finish()
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 fn data(user: u64) -> SessionData {
175 let mut values = serde_json::Map::new();
176 values.insert("user_id".into(), user.into());
177 SessionData { values, flash: Vec::new() }
178 }
179
180 #[tokio::test]
181 async fn a_written_session_reads_back() {
182 let store = MemorySessionStore::default();
183 store.write("abc", &data(42)).await.unwrap();
184
185 let read = store.read("abc").await.unwrap().expect("present");
186 assert_eq!(read.values["user_id"], 42);
187 }
188
189 #[tokio::test]
190 async fn an_unknown_session_reads_as_none() {
191 assert!(MemorySessionStore::default().read("nope").await.unwrap().is_none());
192 }
193
194 #[tokio::test]
195 async fn destroying_a_session_removes_it() {
196 let store = MemorySessionStore::default();
197 store.write("abc", &data(42)).await.unwrap();
198
199 store.destroy("abc").await.unwrap();
200 assert!(store.read("abc").await.unwrap().is_none());
201 }
202
203 #[tokio::test]
204 async fn an_expired_session_reads_as_none_and_is_dropped() {
205 let store = MemorySessionStore::new(Duration::seconds(-1));
206 store.write("abc", &data(42)).await.unwrap();
207
208 assert!(store.read("abc").await.unwrap().is_none());
209 assert!(store.is_empty(), "reading an expired session should clean it up");
210 }
211
212 #[tokio::test]
213 async fn writing_again_refreshes_the_expiry() {
214 let store = MemorySessionStore::new(Duration::seconds(60));
215 store.write("abc", &data(42)).await.unwrap();
216 store.write("abc", &data(43)).await.unwrap();
217
218 assert_eq!(store.read("abc").await.unwrap().unwrap().values["user_id"], 43);
219 }
220
221 #[tokio::test]
222 async fn gc_removes_only_the_expired() {
223 let store = MemorySessionStore::new(Duration::seconds(-1));
224 store.write("a", &data(1)).await.unwrap();
225 store.write("b", &data(2)).await.unwrap();
226
227 assert_eq!(store.gc().await.unwrap(), 2);
228 assert!(store.is_empty());
229
230 let live = MemorySessionStore::default();
231 live.write("a", &data(1)).await.unwrap();
232 assert_eq!(live.gc().await.unwrap(), 0);
233 assert_eq!(live.len(), 1);
234 }
235
236 #[tokio::test]
237 async fn two_sessions_are_independent() {
238 let store = MemorySessionStore::default();
239 store.write("first", &data(42)).await.unwrap();
240 store.write("second", &data(42)).await.unwrap();
241
242 store.destroy("first").await.unwrap();
243
244 assert!(store.read("first").await.unwrap().is_none());
245 assert!(
246 store.read("second").await.unwrap().is_some(),
247 "logging out one device must not log out the others"
248 );
249 }
250}
251
252#[cfg(test)]
253mod port_tests {
254 use super::*;
255
256 #[test]
257 fn a_well_formed_id_is_sixty_four_hex_characters() {
258 assert!(is_well_formed(&crate::session::generate_session_id()));
259 assert!(is_well_formed(&"a".repeat(64)));
260 assert!(is_well_formed(&"F".repeat(64)));
261 }
262
263 #[test]
264 fn anything_else_is_not() {
265 for bad in ["", "short", &"z".repeat(64), &"a".repeat(63), &"a".repeat(65), "../../etc"] {
266 assert!(!is_well_formed(bad), "{bad:?}");
267 }
268 }
269
270 #[test]
271 fn the_default_encoding_is_the_id_itself() {
272 let store = MemorySessionStore::default();
273 let id = crate::session::generate_session_id();
274
275 assert_eq!(store.encode(&id, &SessionData::default()).unwrap(), id);
276 assert!(!store.is_client_side());
277 }
278
279 #[test]
280 fn the_default_decoding_refuses_a_client_chosen_id() {
281 let store = MemorySessionStore::default();
283
284 assert!(store.decode("chosen-by-the-client").is_err());
285
286 let id = crate::session::generate_session_id();
287 assert_eq!(store.decode(&id).unwrap(), (id, None));
288 }
289}