diff options
Diffstat (limited to 'server/src/routes/ui/account/session')
| -rw-r--r-- | server/src/routes/ui/account/session/guard.rs | 106 | ||||
| -rw-r--r-- | server/src/routes/ui/account/session/mod.rs | 24 | ||||
| -rw-r--r-- | server/src/routes/ui/account/session/token.rs | 97 | 
3 files changed, 0 insertions, 227 deletions
| diff --git a/server/src/routes/ui/account/session/guard.rs b/server/src/routes/ui/account/session/guard.rs deleted file mode 100644 index 295c2d4..0000000 --- a/server/src/routes/ui/account/session/guard.rs +++ /dev/null @@ -1,106 +0,0 @@ -/* -    This file is part of jellything (https://codeberg.org/metamuffin/jellything) -    which is licensed under the GNU Affero General Public License (version 3); see /COPYING. -    Copyright (C) 2025 metamuffin <metamuffin.org> -*/ -use super::{AdminSession, Session}; -use crate::{database::Database, routes::ui::error::MyError}; -use anyhow::anyhow; -use log::warn; -use rocket::{ -    async_trait, -    http::Status, -    outcome::Outcome, -    request::{self, FromRequest}, -    Request, State, -}; - -impl Session { -    pub async fn from_request_ut(req: &Request<'_>) -> Result<Self, MyError> { -        let username; - -        #[cfg(not(feature = "bypass-auth"))] -        { -            let token = req -                .query_value("session") -                .map(|e| e.unwrap()) -                .or_else(|| req.query_value("api_key").map(|e| e.unwrap())) -                .or_else(|| req.headers().get_one("X-MediaBrowser-Token")) -                .or_else(|| { -                    req.headers() -                        .get_one("Authorization") -                        .and_then(parse_jellyfin_auth) -                }) // for jellyfin compat -                .or(req.cookies().get("session").map(|cookie| cookie.value())) -                .ok_or(anyhow!("not logged in"))?; - -            // jellyfin urlescapes the token for *some* requests -            let token = token.replace("%3D", "="); -            username = super::token::validate(&token)?; -        }; - -        #[cfg(feature = "bypass-auth")] -        { -            parse_jellyfin_auth("a"); // unused warning is annoying -            username = "admin".to_string(); -        } - -        let db = req.guard::<&State<Database>>().await.unwrap(); - -        let user = db.get_user(&username)?.ok_or(anyhow!("user not found"))?; - -        Ok(Session { user }) -    } -} - -fn parse_jellyfin_auth(h: &str) -> Option<&str> { -    for tok in h.split(" ") { -        if let Some(tok) = tok.strip_prefix("Token=\"") { -            if let Some(tok) = tok.strip_suffix("\"") { -                return Some(tok); -            } -        } -    } -    None -} - -#[async_trait] -impl<'r> FromRequest<'r> for Session { -    type Error = MyError; -    async fn from_request<'life0>( -        request: &'r Request<'life0>, -    ) -> request::Outcome<Self, Self::Error> { -        match Session::from_request_ut(request).await { -            Ok(x) => Outcome::Success(x), -            Err(e) => { -                warn!("authentificated route rejected: {e:?}"); -                Outcome::Forward(Status::Unauthorized) -            } -        } -    } -} - -#[async_trait] -impl<'r> FromRequest<'r> for AdminSession { -    type Error = MyError; -    async fn from_request<'life0>( -        request: &'r Request<'life0>, -    ) -> request::Outcome<Self, Self::Error> { -        match Session::from_request_ut(request).await { -            Ok(x) => { -                if x.user.admin { -                    Outcome::Success(AdminSession(x)) -                } else { -                    Outcome::Error(( -                        Status::Unauthorized, -                        MyError(anyhow!("you are not an admin")), -                    )) -                } -            } -            Err(e) => { -                warn!("authentificated route rejected: {e:?}"); -                Outcome::Forward(Status::Unauthorized) -            } -        } -    } -} diff --git a/server/src/routes/ui/account/session/mod.rs b/server/src/routes/ui/account/session/mod.rs deleted file mode 100644 index cb06255..0000000 --- a/server/src/routes/ui/account/session/mod.rs +++ /dev/null @@ -1,24 +0,0 @@ -/* -    This file is part of jellything (https://codeberg.org/metamuffin/jellything) -    which is licensed under the GNU Affero General Public License (version 3); see /COPYING. -    Copyright (C) 2025 metamuffin <metamuffin.org> -*/ -use chrono::{DateTime, Utc}; -use jellycommon::user::{PermissionSet, User}; -use serde::{Deserialize, Serialize}; - -pub mod guard; -pub mod token; - -pub struct Session { -    pub user: User, -} - -pub struct AdminSession(pub Session); - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SessionData { -    username: String, -    expire: DateTime<Utc>, -    permissions: PermissionSet, -} diff --git a/server/src/routes/ui/account/session/token.rs b/server/src/routes/ui/account/session/token.rs deleted file mode 100644 index 3ada0ec..0000000 --- a/server/src/routes/ui/account/session/token.rs +++ /dev/null @@ -1,97 +0,0 @@ -/* -    This file is part of jellything (https://codeberg.org/metamuffin/jellything) -    which is licensed under the GNU Affero General Public License (version 3); see /COPYING. -    Copyright (C) 2025 metamuffin <metamuffin.org> -*/ -use super::SessionData; -use aes_gcm_siv::{ -    aead::{generic_array::GenericArray, Aead}, -    KeyInit, -}; -use anyhow::anyhow; -use base64::Engine; -use chrono::{Duration, Utc}; -use jellybase::SECRETS; -use jellycommon::user::PermissionSet; -use log::warn; -use std::sync::LazyLock; - -static SESSION_KEY: LazyLock<[u8; 32]> = LazyLock::new(|| { -    if let Some(sk) = &SECRETS.session_key { -        let r = base64::engine::general_purpose::STANDARD -            .decode(sk) -            .expect("key invalid; should be valid base64"); -        r.try_into() -            .expect("key has the wrong length; should be 32 bytes") -    } else { -        warn!("session_key not configured; generating a random one."); -        [(); 32].map(|_| rand::random()) -    } -}); - -pub fn create(username: String, permissions: PermissionSet, expire: Duration) -> String { -    let session_data = SessionData { -        expire: Utc::now() + expire, -        username: username.to_owned(), -        permissions, -    }; -    let mut plaintext = -        bincode::serde::encode_to_vec(&session_data, bincode::config::standard()).unwrap(); - -    while plaintext.len() % 16 == 0 { -        plaintext.push(0); -    } - -    let cipher = aes_gcm_siv::Aes256GcmSiv::new_from_slice(&*SESSION_KEY).unwrap(); -    let nonce = [(); 12].map(|_| rand::random()); -    let mut ciphertext = cipher -        .encrypt(&GenericArray::from(nonce), plaintext.as_slice()) -        .unwrap(); -    ciphertext.extend(nonce); - -    base64::engine::general_purpose::URL_SAFE.encode(&ciphertext) -} - -pub fn validate(token: &str) -> anyhow::Result<String> { -    let ciphertext = base64::engine::general_purpose::URL_SAFE.decode(token)?; -    let cipher = aes_gcm_siv::Aes256GcmSiv::new_from_slice(&*SESSION_KEY).unwrap(); -    let (ciphertext, nonce) = ciphertext.split_at(ciphertext.len() - 12); -    let plaintext = cipher -        .decrypt(nonce.into(), ciphertext) -        .map_err(|e| anyhow!("decryption failed: {e:?}"))?; - -    let (session_data, _): (SessionData, _) = -        bincode::serde::decode_from_slice(&plaintext, bincode::config::standard())?; - -    if session_data.expire < Utc::now() { -        Err(anyhow!("session expired"))? -    } - -    Ok(session_data.username) -} - -#[test] -fn test() { -    jellybase::use_test_config(); -    let tok = create( -        "blub".to_string(), -        jellycommon::user::PermissionSet::default(), -        Duration::days(1), -    ); -    validate(&tok).unwrap(); -} - -#[test] -fn test_crypto() { -    jellybase::use_test_config(); -    let nonce = [(); 12].map(|_| rand::random()); -    let cipher = aes_gcm_siv::Aes256GcmSiv::new_from_slice(&*SESSION_KEY).unwrap(); -    let plaintext = b"testing stuff---"; -    let ciphertext = cipher -        .encrypt(&GenericArray::from(nonce), plaintext.as_slice()) -        .unwrap(); -    let plaintext2 = cipher -        .decrypt((&nonce).into(), ciphertext.as_slice()) -        .unwrap(); -    assert_eq!(plaintext, plaintext2.as_slice()); -} | 
