b6f7f73
ZhenYi
refactor: reorganize configuration files and update module paths for improved structure
b6f7f73
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, HttpRequest, 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<Store>
where
Store: SessionStore,
{
config: Rc<SessionConfig>,
store: Rc<Store>,
}
impl<Store> SessionMiddleware<Store>
where
Store: SessionStore,
{
pub fn new(config: SessionConfig, store: Store) -> Self {
Self {
config: Rc::new(config),
store: Rc::new(store),
}
}
}
impl<S, B, Store> Transform<S, ServiceRequest> for SessionMiddleware<Store>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
S::Future: 'static,
B: MessageBody + 'static,
Store: SessionStore + 'static,
{
type Response = ServiceResponse<B>;
type Error = actix_web::Error;
type Transform = InnerSessionMiddleware<S, Store>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
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<E: fmt::Debug + fmt::Display + 'static>(err: E) -> actix_web::Error {
actix_web::error::InternalError::from_response(
err,
HttpResponse::InternalServerError().finish(),
)
.into()
}
pub struct InnerSessionMiddleware<S, Store: SessionStore + 'static> {
service: Rc<S>,
config: Rc<SessionConfig>,
storage_backend: Rc<Store>,
}
impl<S, B, Store> Service<ServiceRequest> for InnerSessionMiddleware<S, Store>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
S::Future: 'static,
Store: SessionStore + 'static,
{
type Response = ServiceResponse<B>;
type Error = actix_web::Error;
#[allow(clippy::type_complexity)]
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
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 session_key = req
.cookie(&configuration.cookie_name)
.and_then(|cookie| SessionKey::try_from(cookie.value().to_owned()).ok());
let (session_key, session_state) =
load_session_state(session_key, storage_backend.as_ref()).await?;
let session = Session::from_state(session_state);
req.extensions_mut().insert(session.clone());
record_client(&session, req.request(), configuration.trust_proxy);
let request_id = req
.extensions()
.get::<RequestId>()
.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::<Session>()
.cloned()
.unwrap_or(session);
let (status, state) = session.snapshot();
let ttl = Duration::seconds(session_ttl(&configuration, &session));
match session_key {
None => {
if has_identity(&state) {
let session_key = storage_backend.save(state, &ttl).await.map_err(e500)?;
set_session_cookie(res.response_mut(), &configuration, session_key, ttl)?;
}
}
Some(session_key) => match status {
SessionStatus::Changed => {
match storage_backend
.update(session_key, session.diff(), &ttl)
.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)
.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).await.map_err(e500)?;
clear_session_cookie(res.response_mut(), &configuration, ttl)?;
}
SessionStatus::UnChanged => {}
},
}
Ok(res)
})
}
}
fn session_ttl(configuration: &SessionConfig, session: &Session) -> i64 {
let max_age = configuration.max_age.max(1);
if matches!(session.get::<i64>(SESSION_USER_ID), Ok(Some(_))) {
return max_age;
}
configuration.anonymous_max_age.clamp(1, max_age)
}
fn record_client(session: &Session, req: &HttpRequest, trust_proxy: bool) {
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));
}
}
fn has_identity(state: &SessionState) -> bool {
state.keys().any(|key| {
!matches!(
key.as_str(),
SESSION_CLIENT_ID | SESSION_CLIENT_IP | SESSION_USER_AGENT
)
})
}
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<Store: SessionStore>(
session_key: Option<SessionKey>,
storage_backend: &Store,
) -> Result<(Option<SessionKey>, SessionState), actix_web::Error> {
match session_key {
Some(session_key) => match storage_backend.load(&session_key).await.map_err(e500)? {
Some(session_state) => Ok((Some(session_key), session_state)),
None => Ok((None, SessionState::new())),
},
None => Ok((None, SessionState::new())),
}
}
fn set_session_cookie<B>(
response: &mut HttpResponse<B>,
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<B>(
response: &mut HttpResponse<B>,
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
}
#[cfg(test)]
mod tests {
use super::*;
fn config(secure: bool, same_site: SameSite) -> SessionConfig {
SessionConfig {
cookie_name: "session".into(),
max_age: 3600,
anonymous_max_age: 300,
path: Some("/".into()),
domain: None,
secure,
same_site,
trust_proxy: false,
}
}
#[test]
fn session_cookie_is_always_httponly() {
let cookie = session_cookie(
&config(true, SameSite::Lax),
"key".into(),
Duration::days(1),
);
assert_eq!(cookie.http_only(), Some(true));
}
#[test]
fn same_site_none_forces_secure() {
let cookie = session_cookie(
&config(false, SameSite::None),
"key".into(),
Duration::days(1),
);
assert_eq!(cookie.secure(), Some(true));
// Lax 下 secure 跟随配置(这里为 false),强制只发生在 SameSite=None。
let cookie = session_cookie(
&config(false, SameSite::Lax),
"key".into(),
Duration::days(1),
);
assert_eq!(cookie.secure(), Some(false));
let cookie = session_cookie(
&config(true, SameSite::Lax),
"key".into(),
Duration::days(1),
);
assert_eq!(cookie.secure(), Some(true));
}
}