1use crate::config::SessionLifecycle;
2use crate::{
3 config::{self, Configuration, CookieConfiguration, SessionMiddlewareBuilder},
4 storage::{LoadError, SessionKey, SessionStore},
5 Session, SessionStatus,
6};
7use actix_utils::future::{ready, Ready};
8use actix_web::{
9 body::MessageBody,
10 cookie::{Cookie, CookieJar, Key},
11 dev::{forward_ready, ResponseHead, Service, ServiceRequest, ServiceResponse, Transform},
12 http::header::{HeaderValue, SET_COOKIE},
13 HttpResponse,
14};
15use anyhow::Context;
16use serde_json::{Map, Value};
17use std::{convert::TryInto, fmt, future::Future, pin::Pin, rc::Rc};
18
19#[derive(Clone)]
99pub struct SessionMiddleware<Store: SessionStore> {
100 storage_backend: Rc<Store>,
101 configuration: Rc<Configuration>,
102}
103
104impl<Store: SessionStore> SessionMiddleware<Store> {
105 pub fn new(store: Store, key: Key) -> Self {
113 Self::builder(store, key).build()
114 }
115
116 pub fn builder(store: Store, key: Key) -> SessionMiddlewareBuilder<Store> {
123 SessionMiddlewareBuilder::new(store, config::default_configuration(key))
124 }
125
126 pub(crate) fn from_parts(store: Store, configuration: Configuration) -> Self {
127 Self {
128 storage_backend: Rc::new(store),
129 configuration: Rc::new(configuration),
130 }
131 }
132}
133
134impl<S, B, Store> Transform<S, ServiceRequest> for SessionMiddleware<Store>
135where
136 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
137 S::Future: 'static,
138 B: MessageBody + 'static,
139 Store: SessionStore + 'static,
140{
141 type Response = ServiceResponse<B>;
142 type Error = actix_web::Error;
143 type Transform = InnerSessionMiddleware<S, Store>;
144 type InitError = ();
145 type Future = Ready<Result<Self::Transform, Self::InitError>>;
146
147 fn new_transform(&self, service: S) -> Self::Future {
148 ready(Ok(InnerSessionMiddleware {
149 service: Rc::new(service),
150 configuration: Rc::clone(&self.configuration),
151 storage_backend: Rc::clone(&self.storage_backend),
152 }))
153 }
154}
155
156fn e500<E: fmt::Debug + fmt::Display + 'static>(err: E) -> actix_web::Error {
159 actix_web::error::InternalError::from_response(
166 err,
167 HttpResponse::InternalServerError().finish(),
168 )
169 .into()
170}
171
172static LIFECYCLE_KEY: &str = "lifecycle";
173
174#[doc(hidden)]
175#[non_exhaustive]
176pub struct InnerSessionMiddleware<S, Store: SessionStore + 'static> {
177 service: Rc<S>,
178 configuration: Rc<Configuration>,
179 storage_backend: Rc<Store>,
180}
181
182impl<S, B, Store> Service<ServiceRequest> for InnerSessionMiddleware<S, Store>
183where
184 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
185 S::Future: 'static,
186 Store: SessionStore + 'static,
187{
188 type Response = ServiceResponse<B>;
189 type Error = actix_web::Error;
190 #[allow(clippy::type_complexity)]
191 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
192
193 forward_ready!(service);
194
195 fn call(&self, mut req: ServiceRequest) -> Self::Future {
196 let service = Rc::clone(&self.service);
197 let storage_backend = Rc::clone(&self.storage_backend);
198 let configuration = Rc::clone(&self.configuration);
199
200 Box::pin(async move {
201 let session_key = extract_session_key(&req, &configuration.cookie);
202 let (session_key, session_state) =
203 load_session_state(session_key, storage_backend.as_ref()).await?;
204 let mut session_lifecycle = SessionLifecycle::PersistentSession;
205
206 if let Some(lifecycle) = session_state.get(LIFECYCLE_KEY) {
207 let lifecycle = lifecycle
208 .as_i64()
209 .unwrap_or(SessionLifecycle::PersistentSession as i64);
210
211 session_lifecycle = SessionLifecycle::from_i32(lifecycle as i32);
212 }
213
214 Session::set_session(&mut req, session_state, session_lifecycle);
215
216 let mut res = service.call(req).await?;
217 let (lifecycle, status, mut session_state) = Session::get_changes(&mut res);
218
219 if session_key.is_some() || !session_state.is_empty() {
221 session_state.insert(
222 LIFECYCLE_KEY.to_string(),
223 Value::from(lifecycle.clone() as i32),
224 );
225 }
226
227 match session_key {
228 None => {
229 if !session_state.is_empty() {
232 let session_key = storage_backend
233 .save(session_state, &configuration.session.state_ttl)
234 .await
235 .map_err(e500)?;
236
237 set_session_cookie(
238 res.response_mut().head_mut(),
239 session_key,
240 &configuration.cookie,
241 lifecycle,
242 )
243 .map_err(e500)?;
244 }
245 }
246
247 Some(session_key) => {
248 match status {
249 SessionStatus::Changed => {
250 let session_key = storage_backend
251 .update(
252 session_key,
253 session_state,
254 &configuration.session.state_ttl,
255 )
256 .await
257 .map_err(e500)?;
258
259 set_session_cookie(
260 res.response_mut().head_mut(),
261 session_key,
262 &configuration.cookie,
263 lifecycle,
264 )
265 .map_err(e500)?;
266 }
267
268 SessionStatus::Purged => {
269 storage_backend.delete(&session_key).await.map_err(e500)?;
270
271 delete_session_cookie(
272 res.response_mut().head_mut(),
273 &configuration.cookie,
274 )
275 .map_err(e500)?;
276 }
277
278 SessionStatus::Renewed => {
279 storage_backend.delete(&session_key).await.map_err(e500)?;
280
281 let session_key = storage_backend
282 .save(session_state, &configuration.session.state_ttl)
283 .await
284 .map_err(e500)?;
285
286 set_session_cookie(
287 res.response_mut().head_mut(),
288 session_key,
289 &configuration.cookie,
290 lifecycle,
291 )
292 .map_err(e500)?;
293 }
294
295 SessionStatus::Unchanged => {}
296 };
297 }
298 }
299
300 Ok(res)
301 })
302 }
303}
304
305fn extract_session_key(req: &ServiceRequest, config: &CookieConfiguration) -> Option<SessionKey> {
311 let cookies = req.cookies().ok()?;
312 let session_cookie = cookies
313 .iter()
314 .find(|&cookie| cookie.name() == config.name)?;
315
316 let mut jar = CookieJar::new();
317 jar.add_original(session_cookie.clone());
318
319 let verification_result = jar.signed(&config.key).get(&config.name);
320
321 if verification_result.is_none() {
322 tracing::warn!(
323 "The session cookie attached to the incoming request failed to pass cryptographic \
324 checks (signature verification/decryption)."
325 );
326 }
327
328 match verification_result?.value().to_owned().try_into() {
329 Ok(session_key) => Some(session_key),
330 Err(err) => {
331 tracing::warn!(
332 error.message = %err,
333 error.cause_chain = ?err,
334 "Invalid session key, ignoring."
335 );
336
337 None
338 }
339 }
340}
341
342async fn load_session_state<Store: SessionStore>(
343 session_key: Option<SessionKey>,
344 storage_backend: &Store,
345) -> Result<(Option<SessionKey>, Map<String, Value>), actix_web::Error> {
346 if let Some(session_key) = session_key {
347 match storage_backend.load(&session_key).await {
348 Ok(state) => {
349 if let Some(state) = state {
350 Ok((Some(session_key), state))
351 } else {
352 tracing::info!(
359 "No session state has been found for a valid session key, creating a new \
360 empty session."
361 );
362
363 Ok((None, Map::new()))
364 }
365 }
366
367 Err(err) => match err {
368 LoadError::Deserialization(err) => {
369 tracing::warn!(
370 error.message = %err,
371 error.cause_chain = ?err,
372 "Invalid session state, creating a new empty session."
373 );
374
375 Ok((Some(session_key), Map::new()))
376 }
377
378 LoadError::Other(err) => Err(e500(err)),
379 },
380 }
381 } else {
382 Ok((None, Map::new()))
383 }
384}
385
386fn set_session_cookie(
387 response: &mut ResponseHead,
388 session_key: SessionKey,
389 config: &CookieConfiguration,
390 session_lifecycle: SessionLifecycle,
391) -> Result<(), anyhow::Error> {
392 let value: String = session_key.into();
393 let mut cookie = Cookie::new(config.name.clone(), value);
394
395 cookie.set_secure(config.secure);
396 cookie.set_http_only(config.http_only);
397 cookie.set_same_site(config.same_site);
398 cookie.set_path(config.path.clone());
399
400 if session_lifecycle == SessionLifecycle::PersistentSession {
402 if let Some(max_age) = config.max_age {
403 cookie.set_max_age(max_age);
404 }
405 }
406
407 if let Some(ref domain) = config.domain {
408 cookie.set_domain(domain.clone());
409 }
410
411 let mut jar = CookieJar::new();
412 jar.signed_mut(&config.key).add(cookie);
413
414 let cookie = jar.delta().next().unwrap();
416 let val = HeaderValue::from_str(&cookie.encoded().to_string())
417 .context("Failed to attach a session cookie to the outgoing response")?;
418
419 response.headers_mut().append(SET_COOKIE, val);
420
421 Ok(())
422}
423
424fn delete_session_cookie(
425 response: &mut ResponseHead,
426 config: &CookieConfiguration,
427) -> Result<(), anyhow::Error> {
428 let removal_cookie = Cookie::build(config.name.clone(), "")
429 .path(config.path.clone())
430 .secure(config.secure)
431 .http_only(config.http_only)
432 .same_site(config.same_site);
433
434 let mut removal_cookie = if let Some(ref domain) = config.domain {
435 removal_cookie.domain(domain)
436 } else {
437 removal_cookie
438 }
439 .finish();
440
441 removal_cookie.make_removal();
442
443 let val = HeaderValue::from_str(&removal_cookie.to_string())
444 .context("Failed to attach a session removal cookie to the outgoing response")?;
445 response.headers_mut().append(SET_COOKIE, val);
446
447 Ok(())
448}