| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | |
|
| 5 | |
|
| 6 | |
|
| 7 | | use rand::RngCore; |
| 8 | | use std::collections::HashMap; |
| 9 | | use std::sync::Mutex; |
| 10 | |
|
| 11 | |
|
| 12 | | pub const LINK_MS: u64 = 60_000; |
| 13 | | pub const SESSION_MS: u64 = 12 * 3_600_000; |
| 14 | |
|
| 15 | |
|
| 16 | | #[derive(Clone, Debug)] |
| 17 | | pub struct Session { |
| 18 | | pub key: [u8; 32], |
| 19 | | pub proof: Vec<u8>, |
| 20 | | pub csrf: String, |
| 21 | | pub expires_ms: u64, |
| 22 | | } |
| 23 | |
|
| 24 | | #[derive(Debug, Default)] |
| 25 | | pub struct Sessions { |
| 26 | | codes: Mutex<HashMap<String, (Session, u64)>>, |
| 27 | | live: Mutex<HashMap<String, Session>>, |
| 28 | | } |
| 29 | |
|
| 30 | 8× | fn random_hex() -> String { |
| 31 | 8× | let mut b = [0u8; 32]; |
| 32 | 8× | rand::rngs::OsRng.fill_bytes(&mut b); |
| 33 | 8× | hex::encode(b) |
| 34 | 8× | } |
| 35 | |
|
| 36 | | impl Sessions { |
| 37 | | |
| 38 | 3× | pub fn link(&self, key: [u8; 32], proof: Vec<u8>, now: u64) -> String { |
| 39 | 3× | let code = random_hex(); |
| 40 | 3× | let s = Session { key, proof, csrf: random_hex(), expires_ms: now + SESSION_MS }; |
| 41 | 3× | let mut codes = self.codes.lock().unwrap(); |
| 42 | 3× | codes.retain(|_, (_, until)| *until > now); |
| 43 | 3× | codes.insert(code.clone(), (s, now + LINK_MS)); |
| 44 | 3× | code |
| 45 | 3× | } |
| 46 | |
|
| 47 | | |
| 48 | 5× | pub fn open(&self, code: &str, now: u64) -> Option<String> { |
| 49 | 5× | let (s, until) = self.codes.lock().unwrap().remove(code)?; |
| 50 | 3× | if until < now { |
| 51 | 1× | return None; |
| 52 | 2× | } |
| 53 | 2× | let id = random_hex(); |
| 54 | 2× | let mut live = self.live.lock().unwrap(); |
| 55 | 2× | live.retain(|_, s| s.expires_ms > now); |
| 56 | 2× | live.insert(id.clone(), s); |
| 57 | 2× | Some(id) |
| 58 | 5× | } |
| 59 | |
|
| 60 | 14× | pub fn get(&self, id: &str, now: u64) -> Option<Session> { |
| 61 | 14× | self.live.lock().unwrap().get(id).filter(|s| s.expires_ms > now).cloned() |
| 62 | 14× | } |
| 63 | |
|
| 64 | 1× | pub fn close(&self, id: &str) { |
| 65 | 1× | self.live.lock().unwrap().remove(id); |
| 66 | 1× | } |
| 67 | | } |
| 68 | |
|
| 69 | | #[cfg(test)] |
| 70 | | mod tests { |
| 71 | | use super::*; |
| 72 | |
|
| 73 | | #[test] |
| 74 | 1× | fn a_link_opens_one_session_once_and_only_while_fresh() { |
| 75 | 1× | let s = Sessions::default(); |
| 76 | 1× | let code = s.link([1; 32], vec![9], 1_000); |
| 77 | 1× | let id = s.open(&code, 2_000).expect("fresh"); |
| 78 | 1× | assert!(s.open(&code, 2_000).is_none(), "spent"); |
| 79 | 1× | assert_eq!(s.get(&id, 3_000).unwrap().key, [1; 32]); |
| 80 | 1× | assert!(s.get(&id, 1_000 + SESSION_MS + 1).is_none(), "expired"); |
| 81 | 1× | let stale = s.link([1; 32], vec![9], 1_000); |
| 82 | 1× | assert!(s.open(&stale, 1_000 + LINK_MS + 1).is_none()); |
| 83 | 1× | s.close(&id); |
| 84 | 1× | assert!(s.get(&id, 3_000).is_none()); |
| 85 | 1× | } |
| 86 | | } |