session handling
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::extract::FromRequestParts;
|
||||
use axum::http::request::Parts;
|
||||
use axum::http::header;
|
||||
use rand::Rng;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
pub struct Session {
|
||||
pub token: String,
|
||||
pub display_name: Option<String>,
|
||||
pub is_new: bool,
|
||||
pub cookie_max_age: u64,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn cookie_header(&self) -> String {
|
||||
let secure = std::env::var("SECURE_COOKIES").as_deref() == Ok("true");
|
||||
let same_site = if secure { "Strict" } else { "Lax" };
|
||||
let secure_flag = if secure { "; Secure" } else { "" };
|
||||
|
||||
if self.cookie_max_age == 0 {
|
||||
format!("cruciverb_session={}; Path=/; HttpOnly; SameSite={}{}", self.token, same_site, secure_flag)
|
||||
} else {
|
||||
format!(
|
||||
"cruciverb_session={}; Path=/; HttpOnly; SameSite={}; Max-Age={}{}",
|
||||
self.token, same_site, self.cookie_max_age, secure_flag
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequestParts<Arc<AppState>> for Session {
|
||||
type Rejection = std::convert::Infallible;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut Parts,
|
||||
state: &Arc<AppState>,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let token = parts
|
||||
.headers
|
||||
.get(header::COOKIE)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|cookies| {
|
||||
cookies.split(';')
|
||||
.map(|c| c.trim())
|
||||
.find(|c| c.starts_with("cruciverb_session="))
|
||||
.map(|c| c["cruciverb_session=".len()..].to_string())
|
||||
});
|
||||
|
||||
let user_agent_hash = parts
|
||||
.headers
|
||||
.get(header::USER_AGENT)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(ua_hash)
|
||||
.unwrap_or_default();
|
||||
|
||||
let dbi = state.db.lock().await;
|
||||
let (mut token, mut name, mut is_new) = match token {
|
||||
Some(t) if !t.is_empty() => {
|
||||
let (token, name) = crate::db::get_or_create_session(&dbi.db, &t);
|
||||
(token, name, false)
|
||||
}
|
||||
_ => {
|
||||
let new_token = generate_token();
|
||||
let (token, name) = crate::db::get_or_create_session(&dbi.db, &new_token);
|
||||
(token, name, true)
|
||||
}
|
||||
};
|
||||
|
||||
// session binding: when enabled (and a passkey exists), the cookie only
|
||||
// works from the user agent it was bound to; a mismatch rotates to a
|
||||
// fresh session instead of letting a stolen cookie ride along
|
||||
let mut settings_json = crate::db::get_settings(&dbi.db, &token);
|
||||
{
|
||||
let settings_value: serde_json::Value =
|
||||
serde_json::from_str(&settings_json).unwrap_or_default();
|
||||
let binding_enabled = settings_value
|
||||
.get("sessionBinding")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false)
|
||||
&& crate::db::has_passkey(&dbi.db, &token);
|
||||
if binding_enabled && !user_agent_hash.is_empty() {
|
||||
let stored = crate::db::get_user_agent_hash(&dbi.db, &token);
|
||||
match stored {
|
||||
None => crate::db::set_user_agent_hash(&dbi.db, &token, &user_agent_hash),
|
||||
Some(s) if s != user_agent_hash => {
|
||||
let new_token = generate_token();
|
||||
let (t2, n2) = crate::db::get_or_create_session(&dbi.db, &new_token);
|
||||
crate::db::set_user_agent_hash(&dbi.db, &t2, &user_agent_hash);
|
||||
token = t2;
|
||||
name = n2;
|
||||
is_new = true;
|
||||
settings_json = crate::db::get_settings(&dbi.db, &token);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// read cookie lifetime from user settings (default 30 days)
|
||||
let cookie_max_age = serde_json::from_str::<serde_json::Value>(&settings_json)
|
||||
.ok()
|
||||
.and_then(|v| v.get("cookieLifetime")?.as_u64())
|
||||
.unwrap_or(2_592_000);
|
||||
|
||||
Ok(Session { token, display_name: name, is_new, cookie_max_age })
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_token() -> String {
|
||||
let mut rng = rand::rng();
|
||||
let bytes: Vec<u8> = (0..32).map(|_| rng.random::<u8>()).collect();
|
||||
bytes.iter().map(|b| format!("{:02x}", b)).collect()
|
||||
}
|
||||
|
||||
fn ua_hash(ua: &str) -> String {
|
||||
use sha2::{Digest, Sha256};
|
||||
hex::encode(Sha256::digest(ua.as_bytes()))
|
||||
}
|
||||
Reference in New Issue
Block a user