use actix_web::body::MessageBody; use actix_web::cookie::time::Duration; use actix_web::cookie::{Cookie, SameSite as CookieSameSite}; use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform, forward_ready}; use actix_web::http::header::{HeaderName, HeaderValue, USER_AGENT}; use actix_web::{HttpMessage, HttpResponse}; use config::session::{SameSite, SessionConfig}; use std::fmt; use std::future::{Ready, ready}; use std::pin::Pin; use std::rc::Rc; use crate::error::SessionError; use crate::extract::{REQUEST_ID_HEADER, RequestId}; use crate::key::SessionKey; use crate::state::{ SESSION_CLIENT_ID, SESSION_CLIENT_IP, SESSION_USER_AGENT, SESSION_USER_ID, Session, SessionStatus, }; use crate::store::{SessionState, SessionStore}; const CLIENT_IP_MAX_LEN: usize = 64; const USER_AGENT_MAX_LEN: usize = 512; pub struct SessionMiddleware where Store: SessionStore, { config: Rc, store: Rc, } impl SessionMiddleware where Store: SessionStore, { pub fn new(config: SessionConfig, store: Store) -> Self { Self { config: Rc::new(config), store: Rc::new(store), } } } impl Transform for SessionMiddleware where S: Service, Error = actix_web::Error> + 'static, S::Future: 'static, B: MessageBody + 'static, Store: SessionStore + 'static, { type Response = ServiceResponse; type Error = actix_web::Error; type Transform = InnerSessionMiddleware; type InitError = (); type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { ready(Ok(InnerSessionMiddleware { service: Rc::new(service), config: Rc::clone(&self.config), storage_backend: Rc::clone(&self.store), })) } } fn e500(err: E) -> actix_web::Error { actix_web::error::InternalError::from_response( err, HttpResponse::InternalServerError().finish(), ) .into() } pub struct InnerSessionMiddleware { service: Rc, config: Rc, storage_backend: Rc, } impl Service for InnerSessionMiddleware where S: Service, Error = actix_web::Error> + 'static, S::Future: 'static, Store: SessionStore + 'static, { type Response = ServiceResponse; type Error = actix_web::Error; #[allow(clippy::type_complexity)] type Future = Pin>>>; forward_ready!(service); fn call(&self, req: ServiceRequest) -> Self::Future { let service = Rc::clone(&self.service); let storage_backend = Rc::clone(&self.storage_backend); let configuration = Rc::clone(&self.config); Box::pin(async move { let provided_key = req .cookie(&configuration.cookie_name) .and_then(|cookie| SessionKey::try_from(cookie.value().to_owned()).ok()); let (session_key, loaded) = load_session_state(provided_key.clone(), storage_backend.as_ref()).await?; let remaining_ttl = loaded.as_ref().map(|(_, ttl)| *ttl); let session_state = loaded.map(|(state, _)| state).unwrap_or_default(); let session = Session::from_state(session_state); let previous_user_id = session.uid(); req.extensions_mut().insert(session.clone()); { let session = &session; let req = req.request(); let trust_proxy = configuration.trust_proxy; let connection = req.connection_info(); let ip_addr = if trust_proxy { connection.realip_remote_addr() } else { connection.peer_addr() }; if let Some(ip_addr) = ip_addr { let _ = session.set_client_ip(truncate(ip_addr, CLIENT_IP_MAX_LEN)); } let user_agent = req .headers() .get(USER_AGENT) .and_then(|user_agent| user_agent.to_str().ok()); if let Some(user_agent) = user_agent { let _ = session.set_user_agent(truncate(user_agent, USER_AGENT_MAX_LEN)); } } let request_id = req .extensions() .get::() .cloned() .unwrap_or_else(|| { RequestId::from_header( req.headers() .get(REQUEST_ID_HEADER) .and_then(|value| value.to_str().ok()), ) }); req.extensions_mut().insert(request_id.clone()); let mut res = service.call(req).await?; if let Ok(value) = HeaderValue::from_str(request_id.as_str()) { res.headers_mut() .insert(HeaderName::from_static(REQUEST_ID_HEADER), value); } let session = res .request() .extensions() .get::() .cloned() .unwrap_or(session); let (status, state) = session.snapshot(); let user_id = session.uid(); let ttl = Duration::seconds('inline_session_ttl: { let configuration = &configuration; let session = &session; let max_age = configuration.max_age.max(1); if matches!(session.get::(SESSION_USER_ID), Ok(Some(_))) { break 'inline_session_ttl max_age; } configuration.anonymous_max_age.clamp(1, max_age) }); match session_key { None => { let __inl_cond = { let state = &state; state.keys().any(|key| { !matches!( key.as_str(), SESSION_CLIENT_ID | SESSION_CLIENT_IP | SESSION_USER_AGENT ) }) }; if __inl_cond { let session_key = storage_backend.save(state, &ttl).await.map_err(e500)?; set_session_cookie(res.response_mut(), &configuration, session_key, ttl)?; } else if provided_key.is_some() { clear_session_cookie(res.response_mut(), &configuration, ttl)?; } } Some(session_key) => match status { SessionStatus::Changed => { match storage_backend .update(session_key, session.diff(), &ttl, previous_user_id, user_id) .await { Ok(session_key) => { set_session_cookie( res.response_mut(), &configuration, session_key, ttl, )?; } Err(SessionError::Purged) => {} Err(error) => return Err(e500(error)), } } SessionStatus::Renewed => { match storage_backend .rotate(session_key, session.diff(), &ttl, previous_user_id, user_id) .await { Ok(Some(new_session_key)) => { set_session_cookie( res.response_mut(), &configuration, new_session_key, ttl, )?; } Ok(None) => { clear_session_cookie(res.response_mut(), &configuration, ttl)?; } Err(SessionError::Purged) => {} Err(error) => return Err(e500(error)), } } SessionStatus::Purged => { storage_backend .delete(&session_key, previous_user_id) .await .map_err(e500)?; clear_session_cookie(res.response_mut(), &configuration, ttl)?; } SessionStatus::UnChanged => { let remaining = remaining_ttl.map(|ttl| ttl.whole_seconds()).unwrap_or(0); if remaining < ttl.whole_seconds() / 2 { match storage_backend .update_ttl(&session_key, &ttl, user_id) .await { Ok(()) => { set_session_cookie( res.response_mut(), &configuration, session_key, ttl, )?; } Err(SessionError::Purged) => { clear_session_cookie(res.response_mut(), &configuration, ttl)?; } Err(error) => return Err(e500(error)), } } } }, } Ok(res) }) } } fn truncate(value: &str, max_len: usize) -> String { match value.char_indices().find(|(index, _)| *index >= max_len) { Some((index, _)) => value[..index].to_owned(), None => value.to_owned(), } } async fn load_session_state( session_key: Option, storage_backend: &Store, ) -> Result<(Option, Option<(SessionState, Duration)>), actix_web::Error> { match session_key { Some(session_key) => match storage_backend.load(&session_key).await.map_err(e500)? { Some(loaded) => Ok((Some(session_key), Some(loaded))), None => Ok((None, None)), }, None => Ok((None, None)), } } fn set_session_cookie( response: &mut HttpResponse, configuration: &SessionConfig, session_key: SessionKey, max_age: Duration, ) -> Result<(), actix_web::Error> { response .add_cookie(&session_cookie(configuration, session_key.into(), max_age)) .map_err(e500) } fn clear_session_cookie( response: &mut HttpResponse, configuration: &SessionConfig, max_age: Duration, ) -> Result<(), actix_web::Error> { response .add_removal_cookie(&session_cookie(configuration, String::new(), max_age)) .map_err(e500) } fn session_cookie( configuration: &SessionConfig, value: String, max_age: Duration, ) -> Cookie<'static> { let mut cookie = Cookie::new(configuration.cookie_name.clone(), value); cookie.set_secure(configuration.secure || matches!(configuration.same_site, SameSite::None)); // 会话 cookie 恒为 HttpOnly:不给脚本读,XSS 拿不到会话 id。 cookie.set_http_only(true); cookie.set_same_site(match configuration.same_site { SameSite::Strict => CookieSameSite::Strict, SameSite::Lax => CookieSameSite::Lax, SameSite::None => CookieSameSite::None, }); cookie.set_max_age(max_age); if let Some(path) = &configuration.path { cookie.set_path(path.clone()); } if let Some(domain) = &configuration.domain { cookie.set_domain(domain.clone()); } cookie }