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