1use crate::config::{SessionConfig, SessionConfigError};
4use crate::id::{id_hash, well_formed};
5use crate::session::{CookieAction, Loaded, Plan, Save, Session};
6use crate::store::{SessionRecord, SessionStore, StoreError};
7use http::header::COOKIE;
8use http::{HeaderMap, Method, StatusCode};
9use siderite_core::middleware::{BoxService, Next, from_fn};
10use siderite_core::{
11 ApiError, IntoResponse, Request, Response, SameSite, SetCookie,
12 WithCookies,
13};
14use std::future::Future;
15use std::sync::Arc;
16use std::time::{Duration, SystemTime, UNIX_EPOCH};
17use tower::Layer;
18
19#[derive(Clone)]
44pub struct SessionLayer {
45 inner: Arc<Inner>,
46}
47
48struct Inner {
49 store: Box<dyn SessionStore>,
50 config: Arc<SessionConfig>,
51}
52
53impl SessionLayer {
54 pub fn new(
59 store: impl SessionStore,
60 config: SessionConfig,
61 ) -> Result<Self, SessionConfigError> {
62 config.validate()?;
63 Ok(Self {
64 inner: Arc::new(Inner {
65 store: Box::new(store),
66 config: Arc::new(config),
67 }),
68 })
69 }
70
71 fn wrap(&self, inner: BoxService) -> BoxService {
72 let this = Arc::clone(&self.inner);
73 from_fn(move |req: Request, next: Next| {
74 let this = Arc::clone(&this);
75 async move { this.handle(req, next).await }
76 })
77 .layer(inner)
78 }
79}
80
81impl Layer<BoxService> for SessionLayer {
82 type Service = BoxService;
83
84 fn layer(&self, inner: BoxService) -> BoxService {
85 self.wrap(inner)
86 }
87}
88
89fn unavailable() -> Response {
90 ApiError::new(StatusCode::SERVICE_UNAVAILABLE, "Sessions unavailable.")
91 .into_response()
92}
93
94fn forbidden() -> Response {
95 ApiError::new(StatusCode::FORBIDDEN, "Request forbidden.").into_response()
96}
97
98fn unix_now() -> u64 {
99 SystemTime::now()
101 .duration_since(UNIX_EPOCH)
102 .map_or(u64::MAX, |elapsed| elapsed.as_secs())
103}
104
105fn is_safe(method: &Method) -> bool {
106 matches!(
107 *method,
108 Method::GET | Method::HEAD | Method::OPTIONS | Method::TRACE
109 )
110}
111
112fn cookie_value(headers: &HeaderMap, name: &str) -> Option<String> {
114 headers
115 .get_all(COOKIE)
116 .iter()
117 .filter_map(|value| value.to_str().ok())
118 .flat_map(|line| line.split(';'))
119 .filter_map(|pair| pair.trim().split_once('='))
120 .find(|(key, _)| *key == name)
121 .map(|(_, value)| value.to_owned())
122}
123
124impl Inner {
125 async fn timed<T>(
127 &self,
128 call: impl Future<Output = Result<T, StoreError>>,
129 ) -> Result<T, StoreError> {
130 match tokio::time::timeout(self.config.store_timeout, call).await {
131 Ok(result) => result,
132 Err(_) => Err(StoreError::new("operation timed out")),
133 }
134 }
135
136 fn alive(&self, record: &SessionRecord, now: u64) -> Option<bool> {
138 let age = now.saturating_sub(record.created_at);
139 let idle = now.saturating_sub(record.last_seen);
140 let config = &self.config;
141 if age >= config.absolute.as_secs() || idle >= config.idle.as_secs() {
142 return None;
143 }
144 Some(idle >= config.effective_write_interval().as_secs())
145 }
146
147 async fn load(
149 &self,
150 cookie: Option<String>,
151 now: u64,
152 ) -> Result<(Option<Loaded>, bool), Box<Response>> {
153 let Some(raw) = cookie else {
154 return Ok((None, false));
155 };
156 if !well_formed(&raw) {
157 return Ok((None, true));
158 }
159 let hash = id_hash(&raw);
160 let record = match self.timed(self.store.load(&hash)).await {
161 Ok(Some(record)) => record,
162 Ok(None) => return Ok((None, true)),
163 Err(error) => {
164 tracing::warn!(%error, "session store read failed");
165 return Err(Box::new(unavailable()));
166 }
167 };
168 let Some(touch) = self.alive(&record, now) else {
169 let _ = self.timed(self.store.delete(&hash)).await;
171 return Ok((None, true));
172 };
173 Ok((
174 Some(Loaded {
175 hash,
176 record,
177 touch,
178 }),
179 false,
180 ))
181 }
182
183 fn token_ok(&self, req: &Request, session: &Session) -> bool {
185 let mut values =
186 req.headers().get_all(&self.config.csrf_header).iter();
187 match (values.next(), values.next()) {
188 (Some(value), None) => {
189 value.to_str().is_ok_and(|token| session.verify_csrf(token))
190 }
191 _ => false,
192 }
193 }
194
195 async fn handle(&self, mut req: Request, next: Next) -> Response {
196 let now = unix_now();
197 let cookie = cookie_value(req.headers(), &self.config.name);
198 let (loaded, expire) = match self.load(cookie, now).await {
199 Ok(found) => found,
200 Err(response) => return *response,
201 };
202 let session = Session::new(Arc::clone(&self.config), loaded, expire);
203 let enforce = self.config.same_site == SameSite::None
204 && !is_safe(req.method())
205 && session.is_loaded();
206 if enforce && !self.token_ok(&req, &session) {
207 return forbidden();
208 }
209 req.extensions_mut().insert(session.clone());
210 let response = next.run(req).await;
211 self.commit(session.plan(now), response).await
212 }
213
214 async fn commit(&self, mut plan: Plan, response: Response) -> Response {
216 if let Err(error) = self.apply(&mut plan).await {
217 tracing::warn!(%error, "session store write failed");
218 return unavailable();
219 }
220 let cookie = match plan.cookie {
221 CookieAction::Keep => return response,
222 CookieAction::Set { value, max_age } => {
223 self.cookie(&value, max_age)
224 }
225 CookieAction::Expire => self.cookie("", Duration::ZERO),
226 };
227 WithCookies::new(response).cookie(cookie).into_response()
228 }
229
230 async fn apply(&self, plan: &mut Plan) -> Result<(), StoreError> {
231 if plan.failed {
232 return Err(StoreError::new("session id generation failed"));
233 }
234 if let Some(save) = plan.save.as_mut() {
235 if let Some(user) = &plan.login {
236 let epoch = self.timed(self.store.auth_epoch(user)).await?;
237 save.record.auth_epoch = epoch;
238 }
239 match self.timed(self.write(save)).await {
240 Ok(true) => {}
241 Ok(false) => plan.cookie = CookieAction::Expire,
244 Err(error) if save.required => return Err(error),
245 Err(error) => {
246 tracing::warn!(%error, "session touch write failed");
247 }
248 }
249 }
250 for hash in &plan.delete {
251 if let Err(error) = self.timed(self.store.delete(hash)).await {
252 if let Some(fresh) = &plan.fresh {
254 let _ = self.timed(self.store.delete(fresh)).await;
255 }
256 return Err(error);
257 }
258 }
259 Ok(())
260 }
261
262 async fn write(&self, save: &Save) -> Result<bool, StoreError> {
265 let (hash, record, ttl) = (&save.hash, &save.record, save.ttl);
266 if save.existing {
267 self.store.update(hash, record, ttl).await
268 } else {
269 self.store.save(hash, record, ttl).await.map(|()| true)
270 }
271 }
272
273 fn cookie(&self, value: &str, max_age: Duration) -> SetCookie {
274 SetCookie::new(self.config.name.clone(), value)
275 .http_only(true)
276 .secure(self.config.secure)
277 .same_site(self.config.same_site)
278 .path("/")
279 .max_age(max_age)
280 }
281}