1use std::{borrow::Cow, collections::HashMap, fmt, future::Future, pin::Pin, rc::Rc, sync::Arc};
2
3use actix_utils::future::{ready, Ready};
4use actix_web::{
5 body::MessageBody,
6 cookie::{time::Duration, Cookie, CookieJar, Key},
7 dev::{forward_ready, ResponseHead, Service, ServiceRequest, ServiceResponse, Transform},
8 http::header::{HeaderValue, SET_COOKIE},
9 HttpResponse,
10};
11
12use super::{
13 config::{
14 self, Configuration, CookieConfiguration, CookieContentSecurity, SessionMiddlewareBuilder,
15 TtlExtensionPolicy,
16 },
17 storage::{SessionKey, SessionStore},
18 Session, SessionStatus,
19};
20use crate::{memorydb::MemoryDB, Result};
21
22#[derive(Clone)]
46pub struct SessionMiddleware {
47 storage_backend: Rc<SessionStore>,
48 configuration: Rc<Configuration>,
49}
50
51impl SessionMiddleware {
52 pub fn new(client: Arc<dyn MemoryDB>, key: Key) -> Self {
59 Self::builder(client, key).build()
60 }
61
62 pub fn builder(client: Arc<dyn MemoryDB>, key: Key) -> SessionMiddlewareBuilder {
68 SessionMiddlewareBuilder::new(client, config::default_configuration(key))
69 }
70
71 pub(crate) fn from_parts(store: SessionStore, configuration: Configuration) -> Self {
72 Self {
73 storage_backend: Rc::new(store),
74 configuration: Rc::new(configuration),
75 }
76 }
77}
78
79impl<S, B> Transform<S, ServiceRequest> for SessionMiddleware
80where
81 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
82 S::Future: 'static,
83 B: MessageBody + 'static,
84{
85 type Response = ServiceResponse<B>;
86 type Error = actix_web::Error;
87 type Transform = InnerSessionMiddleware<S>;
88 type InitError = ();
89 type Future = Ready<Result<Self::Transform, Self::InitError>>;
90
91 fn new_transform(&self, service: S) -> Self::Future {
92 ready(Ok(InnerSessionMiddleware {
93 service: Rc::new(service),
94 configuration: Rc::clone(&self.configuration),
95 storage_backend: Rc::clone(&self.storage_backend),
96 }))
97 }
98}
99
100fn e500<E: fmt::Debug + fmt::Display + 'static>(err: E) -> actix_web::Error {
103 actix_web::error::InternalError::from_response(
110 err,
111 HttpResponse::InternalServerError().finish(),
112 )
113 .into()
114}
115
116#[doc(hidden)]
117#[non_exhaustive]
118pub struct InnerSessionMiddleware<S> {
119 service: Rc<S>,
120 configuration: Rc<Configuration>,
121 storage_backend: Rc<SessionStore>,
122}
123
124impl<S, B> Service<ServiceRequest> for InnerSessionMiddleware<S>
125where
126 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
127 S::Future: 'static,
128{
129 type Response = ServiceResponse<B>;
130 type Error = actix_web::Error;
131 #[allow(clippy::type_complexity)]
132 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
133
134 forward_ready!(service);
135
136 fn call(&self, mut req: ServiceRequest) -> Self::Future {
137 let service = Rc::clone(&self.service);
138 let storage_backend = Rc::clone(&self.storage_backend);
139 let configuration = Rc::clone(&self.configuration);
140
141 Box::pin(async move {
142 let session_key = extract_session_key(&req, &configuration.cookie);
143 let (session_key, session_state) =
144 load_session_state(session_key, storage_backend.as_ref()).await?;
145
146 Session::set_session(&mut req, session_state);
147
148 let mut res = service.call(req).await?;
149 let (status, session_state) = Session::get_changes(&mut res);
150
151 let mut ttl = configuration.session.state_ttl;
152 let mut cookie = Cow::Borrowed(&configuration.cookie);
153 if let Some(x) = session_state.get("_ttl") {
154 if let Ok(x) = x.parse() {
155 ttl = Duration::seconds(x);
156 let mut tmp = cookie.into_owned();
157 tmp.max_age = Some(ttl);
158 cookie = Cow::Owned(tmp);
159 }
160 }
161 let id = session_state
162 .get("_id")
163 .map(|x| x.trim_matches('"').to_owned());
164
165 match session_key {
166 None => {
167 if !session_state.is_empty() {
170 let session_key = storage_backend
171 .save(session_state, &id, &ttl)
172 .await
173 .map_err(e500)?;
174
175 set_session_cookie(res.response_mut().head_mut(), session_key, &cookie)
176 .map_err(e500)?;
177 }
178 }
179
180 Some(session_key) => {
181 match status {
182 SessionStatus::Changed => {
183 let session_key = storage_backend
184 .update(session_key, session_state, &id, &ttl)
185 .await
186 .map_err(e500)?;
187
188 set_session_cookie(res.response_mut().head_mut(), session_key, &cookie)
189 .map_err(e500)?;
190 }
191
192 SessionStatus::Purged => {
193 storage_backend
194 .delete(&session_key, &id)
195 .await
196 .map_err(e500)?;
197
198 delete_session_cookie(res.response_mut().head_mut(), &cookie)
199 .map_err(e500)?;
200 }
201
202 SessionStatus::Renewed => {
203 storage_backend
204 .delete(&session_key, &id)
205 .await
206 .map_err(e500)?;
207
208 let session_key = storage_backend
209 .save(session_state, &id, &ttl)
210 .await
211 .map_err(e500)?;
212
213 set_session_cookie(res.response_mut().head_mut(), session_key, &cookie)
214 .map_err(e500)?;
215 }
216
217 SessionStatus::Unchanged => {
218 if matches!(
219 configuration.ttl_extension_policy,
220 TtlExtensionPolicy::OnEveryRequest
221 ) {
222 storage_backend
223 .update_ttl(&session_key, &id, &ttl)
224 .await
225 .map_err(e500)?;
226
227 if configuration.cookie.max_age.is_some() {
228 set_session_cookie(
229 res.response_mut().head_mut(),
230 session_key,
231 &cookie,
232 )
233 .map_err(e500)?;
234 }
235 }
236 }
237 };
238 }
239 }
240
241 Ok(res)
242 })
243 }
244}
245
246fn extract_session_key(req: &ServiceRequest, config: &CookieConfiguration) -> Option<SessionKey> {
252 let cookies = req.cookies().ok()?;
253 let session_cookie = cookies
254 .iter()
255 .find(|&cookie| cookie.name() == config.name)?;
256
257 let mut jar = CookieJar::new();
258 jar.add_original(session_cookie.clone());
259
260 let verification_result = match config.content_security {
261 CookieContentSecurity::Signed => jar.signed(&config.key).get(&config.name),
262 CookieContentSecurity::Private => jar.private(&config.key).get(&config.name),
263 };
264
265 verification_result?.value().to_owned().try_into().ok()
266}
267
268async fn load_session_state(
269 session_key: Option<SessionKey>,
270 storage_backend: &SessionStore,
271) -> Result<(Option<SessionKey>, HashMap<String, String>), actix_web::Error> {
272 if let Some(session_key) = session_key {
273 match storage_backend.load(&session_key).await {
274 Ok(state) => {
275 if let Some(state) = state {
276 Ok((Some(session_key), state))
277 } else {
278 Ok((None, HashMap::new()))
285 }
286 }
287
288 Err(err) => Err(e500(err)),
289 }
290 } else {
291 Ok((None, HashMap::new()))
292 }
293}
294
295fn set_session_cookie(
296 response: &mut ResponseHead,
297 session_key: SessionKey,
298 config: &CookieConfiguration,
299) -> Result<()> {
300 let value: String = session_key.into();
301 let mut cookie = Cookie::new(config.name.clone(), value);
302
303 cookie.set_secure(config.secure);
304 cookie.set_http_only(config.http_only);
305 cookie.set_same_site(config.same_site);
306 cookie.set_path(config.path.clone());
307
308 if let Some(max_age) = config.max_age {
309 cookie.set_max_age(max_age);
310 }
311
312 if let Some(ref domain) = config.domain {
313 cookie.set_domain(domain.clone());
314 }
315
316 let mut jar = CookieJar::new();
317 match config.content_security {
318 CookieContentSecurity::Signed => jar.signed_mut(&config.key).add(cookie),
319 CookieContentSecurity::Private => jar.private_mut(&config.key).add(cookie),
320 }
321
322 let cookie = jar.delta().next().unwrap();
324 let val = HeaderValue::from_str(&cookie.encoded().to_string())?;
325
326 response.headers_mut().append(SET_COOKIE, val);
327
328 Ok(())
329}
330
331fn delete_session_cookie(response: &mut ResponseHead, config: &CookieConfiguration) -> Result<()> {
332 let removal_cookie = Cookie::build(config.name.clone(), "")
333 .path(config.path.clone())
334 .secure(config.secure)
335 .http_only(config.http_only)
336 .same_site(config.same_site);
337
338 let mut removal_cookie = if let Some(ref domain) = config.domain {
339 removal_cookie.domain(domain)
340 } else {
341 removal_cookie
342 }
343 .finish();
344
345 removal_cookie.make_removal();
346
347 let val = HeaderValue::from_str(&removal_cookie.to_string())?;
348 response.headers_mut().append(SET_COOKIE, val);
349
350 Ok(())
351}