tako_rs_plugins/middleware/session/
layer.rs1use std::future::Future;
4use std::pin::Pin;
5use std::sync::Arc;
6use std::time::Duration;
7use std::time::Instant;
8
9use http::HeaderValue;
10use tako_rs_core::middleware::IntoMiddleware;
11use tako_rs_core::middleware::Next;
12use tako_rs_core::types::Request;
13use tako_rs_core::types::Response;
14
15use super::cookie::SameSite;
16use super::cookie::build_cookie;
17use super::cookie::build_expired_cookie;
18use super::cookie::extract_cookie_value;
19use super::cookie::generate_session_id;
20use super::data::Session;
21use super::store::SessionEntry;
22use super::store::SessionStoreHandle;
23use super::store::SessionTtl;
24use super::store::Store;
25
26pub struct SessionMiddleware {
28 cookie_name: String,
29 ttl: SessionTtl,
30 path: String,
31 domain: Option<String>,
32 secure: bool,
33 http_only: bool,
34 same_site: SameSite,
35 store: Store,
36}
37
38impl Default for SessionMiddleware {
39 fn default() -> Self {
40 Self::new()
41 }
42}
43
44impl SessionMiddleware {
45 pub fn new() -> Self {
47 Self {
48 cookie_name: "tako_session".to_string(),
49 ttl: SessionTtl::default(),
50 path: "/".to_string(),
51 domain: None,
52 secure: false,
53 http_only: true,
54 same_site: SameSite::Lax,
55 store: Store::new(),
56 }
57 }
58
59 pub fn cookie_name(mut self, name: &str) -> Self {
61 self.cookie_name = name.to_string();
62 self
63 }
64
65 pub fn ttl_secs(mut self, secs: u64) -> Self {
68 self.ttl.idle_secs = secs;
69 self
70 }
71
72 pub fn ttl(mut self, ttl: SessionTtl) -> Self {
74 self.ttl = ttl;
75 self
76 }
77
78 pub fn path(mut self, path: &str) -> Self {
80 self.path = path.to_string();
81 self
82 }
83
84 pub fn domain(mut self, domain: &str) -> Self {
86 self.domain = Some(domain.to_string());
87 self
88 }
89
90 pub fn secure(mut self, secure: bool) -> Self {
92 self.secure = secure;
93 self
94 }
95
96 pub fn http_only(mut self, on: bool) -> Self {
98 self.http_only = on;
99 self
100 }
101
102 pub fn same_site(mut self, ss: SameSite) -> Self {
105 self.same_site = ss;
106 self
107 }
108
109 pub fn handle(&self) -> SessionStoreHandle {
111 SessionStoreHandle {
112 store: self.store.clone(),
113 }
114 }
115}
116
117impl IntoMiddleware for SessionMiddleware {
118 fn into_middleware(
119 self,
120 ) -> impl Fn(Request, Next) -> Pin<Box<dyn Future<Output = Response> + Send + 'static>>
121 + Clone
122 + Send
123 + Sync
124 + 'static {
125 let store = self.store.clone();
126 let cookie_name = Arc::new(self.cookie_name);
127 let ttl = self.ttl;
128 let path = Arc::new(self.path);
129 let domain = self.domain.map(Arc::new);
130 let secure = self.secure;
131 let http_only = self.http_only;
132 let same_site = self.same_site;
133
134 {
137 let store = store.clone();
138 let interval = Duration::from_secs(ttl.idle_secs.clamp(60, 3_600));
139 #[cfg(not(feature = "compio"))]
140 tokio::spawn(async move {
141 let mut tick = tokio::time::interval(interval);
142 loop {
143 tick.tick().await;
144 store.retain_expired(ttl);
145 }
146 });
147 #[cfg(feature = "compio")]
148 compio::runtime::spawn(async move {
149 loop {
150 compio::time::sleep(interval).await;
151 store.retain_expired(ttl);
152 }
153 })
154 .detach();
155 }
156
157 move |mut req: Request, next: Next| {
158 let store = store.clone();
159 let cookie_name = cookie_name.clone();
160 let path = path.clone();
161 let domain = domain.clone();
162
163 Box::pin(async move {
164 let now = Instant::now();
165 let idle = Duration::from_secs(ttl.idle_secs);
166 let absolute = ttl.absolute_secs.map(Duration::from_secs);
167
168 let inbound_id = extract_cookie_value(&req, &cookie_name).map(str::to_string);
169 let (sid, data, created_at, was_existing) = match inbound_id {
170 Some(ref id) => match store.get(id) {
171 Some(entry)
172 if now.duration_since(entry.last_seen_at) <= idle
173 && absolute.is_none_or(|abs| now.duration_since(entry.created_at) <= abs) =>
174 {
175 (id.clone(), entry.data, entry.created_at, true)
176 }
177 _ => {
178 if let Some(id) = inbound_id.as_ref() {
179 store.remove(id);
180 }
181 (generate_session_id(), serde_json::Map::new(), now, false)
182 }
183 },
184 None => (generate_session_id(), serde_json::Map::new(), now, false),
185 };
186
187 let session = Session::new(data);
188 req.extensions_mut().insert(session.clone());
189
190 let resp_outcome = next.run(req).await;
191 let mut resp = resp_outcome;
192
193 let dirty = session.is_dirty();
194 let rotated = session.rotation_requested();
195 let destroyed = session.is_destroyed();
196
197 if destroyed {
201 if was_existing {
202 store.remove(&sid);
203 }
204 let expired = build_expired_cookie(
205 &cookie_name,
206 &path,
207 domain.as_deref().map(String::as_str),
208 secure,
209 http_only,
210 same_site,
211 );
212 if let Ok(v) = HeaderValue::from_str(&expired) {
213 resp.headers_mut().append(http::header::SET_COOKIE, v);
214 }
215 let _ = dirty;
216 return resp;
217 }
218
219 let effective_sid = if rotated {
221 if was_existing {
222 store.remove(&sid);
223 }
224 generate_session_id()
225 } else {
226 sid
227 };
228
229 let updated_entry = SessionEntry {
233 data: session.snapshot(),
234 created_at,
235 last_seen_at: now,
236 };
237 store.upsert(effective_sid.clone(), updated_entry);
238
239 let max_age = match absolute {
243 Some(abs) => {
244 let elapsed = now.duration_since(created_at);
245 let absolute_remaining = abs.saturating_sub(elapsed);
246 absolute_remaining.as_secs().min(idle.as_secs())
247 }
248 None => idle.as_secs(),
249 };
250
251 let cookie_value = build_cookie(
252 &cookie_name,
253 &effective_sid,
254 &path,
255 domain.as_deref().map(String::as_str),
256 max_age,
257 secure,
258 http_only,
259 same_site,
260 );
261 if let Ok(v) = HeaderValue::from_str(&cookie_value) {
262 resp.headers_mut().append(http::header::SET_COOKIE, v);
263 }
264
265 let _ = dirty;
266
267 resp
268 })
269 }
270 }
271}