use std::borrow::Borrow; use std::collections::BTreeSet; use std::ops::Deref; use std::sync::Arc; use std::time::{Duration, Instant}; use axum::extract::Request; use axum::response::Response; use basic_cookies::Cookie; use chrono::Utc; use http::HeaderValue; use http::header::{COOKIE, USER_AGENT}; use rpc_toolkit::yajrc::INTERNAL_ERROR; use rpc_toolkit::{Middleware, RpcRequest, RpcResponse}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use crate::auth::{Sessions, check_password, write_shadow}; use crate::context::RpcContext; use crate::middleware::auth::DbContext; use crate::prelude::*; use crate::rpc_continuations::OpenAuthedContinuations; use crate::util::Invoke; use crate::util::io::{create_file_mod, read_file_to_string}; use crate::util::serde::{BASE64, const_true}; use crate::util::sync::SyncMutex; pub trait SessionAuthContext: DbContext { fn ephemeral_sessions(&self) -> &SyncMutex; fn open_authed_continuations(&self) -> &OpenAuthedContinuations>; fn access_sessions(db: &mut Model) -> &mut Model; fn check_password(db: &Model, password: &str) -> Result<(), Error>; #[allow(unused_variables)] fn post_login_hook(&self, password: &str) -> impl Future> + Send { async { Ok(()) } } } impl SessionAuthContext for RpcContext { fn ephemeral_sessions(&self) -> &SyncMutex { &self.ephemeral_sessions } fn open_authed_continuations(&self) -> &OpenAuthedContinuations> { &self.open_authed_continuations } fn access_sessions(db: &mut Model) -> &mut Model { db.as_private_mut().as_sessions_mut() } fn check_password(db: &Model, password: &str) -> Result<(), Error> { check_password(&db.as_private().as_password().de()?, password) } async fn post_login_hook(&self, password: &str) -> Result<(), Error> { if tokio::fs::metadata("/media/startos/config/overlay/etc/shadow") .await .is_err() { write_shadow(&password).await?; } Ok(()) } } #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct LoginRes { pub session: InternedString, } pub trait AsLogoutSessionId { fn as_logout_session_id(self) -> InternedString; } /// Will need to know when we have logged out from a route #[derive(Serialize, Deserialize)] pub struct HasLoggedOutSessions(()); impl HasLoggedOutSessions { pub async fn new( sessions: impl IntoIterator, ctx: &C, ) -> Result { let to_log_out: BTreeSet<_> = sessions .into_iter() .map(|s| s.as_logout_session_id()) .collect(); for sid in &to_log_out { ctx.open_authed_continuations().kill(&Some(sid.clone())) } ctx.ephemeral_sessions().mutate(|s| { for sid in &to_log_out { s.0.remove(sid); } }); ctx.db() .mutate(|db| { let sessions = C::access_sessions(db); for sid in &to_log_out { sessions.remove(sid)?; } Ok(()) }) .await .result?; Ok(HasLoggedOutSessions(())) } } /// When we have a need to create a new session, /// Or when we are using internal valid authenticated service. #[derive(Debug, Clone)] pub struct HashSessionToken { hashed: InternedString, token: InternedString, } impl HashSessionToken { pub fn new() -> Self { Self::from_token(InternedString::intern( base32::encode( base32::Alphabet::Rfc4648 { padding: false }, &rand::random::<[u8; 16]>(), ) .to_lowercase(), )) } pub fn from_token(token: InternedString) -> Self { let hashed = Self::hash(&*token); Self { hashed, token } } pub fn from_cookie(cookie: &Cookie) -> Self { Self::from_token(InternedString::intern(cookie.get_value())) } pub fn from_header(header: Option<&HeaderValue>) -> Result { if let Some(cookie_header) = header { let cookies = Cookie::parse( cookie_header .to_str() .with_kind(crate::ErrorKind::Authorization)?, ) .with_kind(crate::ErrorKind::Authorization)?; if let Some(session) = cookies.iter().find(|c| c.get_name() == "session") { return Ok(Self::from_cookie(session)); } } Err(Error::new( eyre!("{}", t!("middleware.auth.unauthorized")), crate::ErrorKind::Authorization, )) } pub fn to_login_res(&self) -> LoginRes { LoginRes { session: self.token.clone(), } } pub fn hashed(&self) -> &InternedString { &self.hashed } fn hash(token: &str) -> InternedString { let mut hasher = Sha256::new(); hasher.update(token.as_bytes()); InternedString::intern( base32::encode( base32::Alphabet::Rfc4648 { padding: false }, hasher.finalize().as_slice(), ) .to_lowercase(), ) } } impl AsLogoutSessionId for HashSessionToken { fn as_logout_session_id(self) -> InternedString { self.hashed } } impl PartialEq for HashSessionToken { fn eq(&self, other: &Self) -> bool { self.hashed == other.hashed } } impl Eq for HashSessionToken {} impl PartialOrd for HashSessionToken { fn partial_cmp(&self, other: &Self) -> Option { self.hashed.partial_cmp(&other.hashed) } } impl Ord for HashSessionToken { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.hashed.cmp(&other.hashed) } } impl Borrow for HashSessionToken { fn borrow(&self) -> &str { &*self.hashed } } pub struct ValidSessionToken(pub HashSessionToken); impl ValidSessionToken { pub async fn from_header( header: Option<&HeaderValue>, ctx: &C, ) -> Result { if let Some(cookie_header) = header { let cookies = Cookie::parse( cookie_header .to_str() .with_kind(crate::ErrorKind::Authorization)?, ) .with_kind(crate::ErrorKind::Authorization)?; if let Some(cookie) = cookies.iter().find(|c| c.get_name() == "session") { if let Ok(s) = Self::from_session(HashSessionToken::from_cookie(cookie), ctx).await { return Ok(s); } } } Err(Error::new( eyre!("{}", t!("middleware.auth.unauthorized")), crate::ErrorKind::Authorization, )) } pub async fn from_session( session_token: HashSessionToken, ctx: &C, ) -> Result { let session_hash = session_token.hashed(); if !ctx.ephemeral_sessions().mutate(|s| { if let Some(session) = s.0.get_mut(session_hash) { session.last_active = Utc::now(); true } else { false } }) { ctx.db() .mutate(|db| { C::access_sessions(db) .as_idx_mut(session_hash) .ok_or_else(|| { Error::new( eyre!("{}", t!("middleware.auth.unauthorized")), crate::ErrorKind::Authorization, ) })? .mutate(|s| { s.last_active = Utc::now(); Ok(()) }) }) .await .result?; } Ok(Self(session_token)) } } #[derive(Deserialize)] pub struct Metadata { #[serde(default)] login: bool, #[serde(default)] get_session: bool, } #[derive(Clone)] pub struct SessionAuth { rate_limiter: Arc>, is_login: bool, cookie: Option, set_cookie: Option, user_agent: Option, } impl SessionAuth { pub fn new() -> Self { Self { rate_limiter: Arc::new(SyncMutex::new((0, Instant::now()))), is_login: false, cookie: None, set_cookie: None, user_agent: None, } } } impl Middleware for SessionAuth { type Metadata = Metadata; async fn process_http_request(&mut self, _: &C, request: &mut Request) -> Result<(), Response> { self.cookie = request.headers().get(COOKIE).cloned(); self.user_agent = request.headers().get(USER_AGENT).cloned(); Ok(()) } async fn process_rpc_request( &mut self, context: &C, metadata: Self::Metadata, request: &mut RpcRequest, ) -> Result<(), RpcResponse> { async { if metadata.login { self.is_login = true; self.rate_limiter.mutate(|(count, time)| { if time.elapsed() < Duration::from_secs(20) && *count >= 3 { Err(Error::new( eyre!("{}", t!("middleware.auth.rate-limited-login")), crate::ErrorKind::RateLimited, )) } else { *count += 1; *time = Instant::now(); Ok(()) } })?; if let Some(user_agent) = self.user_agent.as_ref().and_then(|h| h.to_str().ok()) { request.params["__Auth_userAgent"] = Value::String(Arc::new(user_agent.to_owned())) } } else { let ValidSessionToken(s) = ValidSessionToken::from_header(self.cookie.as_ref(), context).await?; if metadata.get_session { request.params["__Auth_session"] = Value::String(Arc::new(s.hashed().deref().to_owned())); } } Ok::<_, Error>(()) } .await .map_err(|e| RpcResponse::from_result(Err(e))) } async fn process_rpc_response(&mut self, _: &C, response: &mut RpcResponse) { if self.is_login { if response.result.is_ok() { let res = std::mem::replace(&mut response.result, Err(INTERNAL_ERROR)); response.result = async { let res = res?; let login_res = from_value::(res.clone())?; self.set_cookie = Some( HeaderValue::from_str(&format!( "session={}; Path=/; SameSite=Strict; Expires=Fri, 31 Dec 9999 23:59:59 GMT;", login_res.session )) .with_kind(crate::ErrorKind::Network)?, ); Ok(res) } .await; } } } async fn process_http_response(&mut self, _: &C, response: &mut Response) { if let Some(set_cookie) = self.set_cookie.take() { response.headers_mut().insert("set-cookie", set_cookie); } } }