diff options
| -rw-r--r-- | logic/src/session.rs | 134 | ||||
| -rw-r--r-- | server/src/auth.rs | 134 | ||||
| -rw-r--r-- | server/src/main.rs | 1 | ||||
| -rw-r--r-- | server/src/request_info.rs | 4 | ||||
| -rw-r--r-- | stream/src/hls.rs | 5 | ||||
| -rw-r--r-- | stream/src/lib.rs | 6 | ||||
| -rw-r--r-- | stream/src/stream_info.rs | 2 |
7 files changed, 143 insertions, 143 deletions
diff --git a/logic/src/session.rs b/logic/src/session.rs index 03ff3a3..e69de29 100644 --- a/logic/src/session.rs +++ b/logic/src/session.rs @@ -1,134 +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) 2026 metamuffin <metamuffin.org> -*/ -use crate::{CONF, DATABASE}; -use aes_gcm_siv::{ - KeyInit, - aead::{Aead, generic_array::GenericArray}, -}; -use anyhow::anyhow; -use base64::Engine; -use log::warn; -use serde::{Deserialize, Serialize}; -use std::{sync::LazyLock, time::Duration}; - -pub struct Session { - pub user: User, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SessionData { - username: String, - expire: DateTime<Utc>, - permissions: PermissionSet, -} - -static SESSION_KEY: LazyLock<[u8; 32]> = LazyLock::new(|| { - if let Some(sk) = &CONF.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().is_multiple_of(16) { - 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) -} - -pub fn token_to_session(token: &str) -> anyhow::Result<Session> { - let username = validate(token)?; - let user = DATABASE - .get_user(&username)? - .ok_or(anyhow!("user does not exist"))?; - Ok(Session { user }) -} -pub fn bypass_auth_session() -> anyhow::Result<Session> { - let user = DATABASE - .get_user(CONF.admin_username.as_ref().unwrap())? - .ok_or(anyhow!("user does not exist"))?; - Ok(Session { user }) -} - -#[cfg(test)] -fn load_test_config() { - use std::path::PathBuf; - - use crate::{CONF_PRELOAD, Config}; - *CONF_PRELOAD.lock().unwrap() = Some(Config { - database_path: PathBuf::default(), - login_expire: 10, - session_key: None, - admin_password: None, - admin_username: None, - }); -} - -#[test] -fn test() { - load_test_config(); - let tok = create( - "blub".to_string(), - jellycommon::user::PermissionSet::default(), - Duration::from_days(1), - ); - validate(&tok).unwrap(); -} - -#[test] -fn test_crypto() { - load_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()); -} diff --git a/server/src/auth.rs b/server/src/auth.rs new file mode 100644 index 0000000..03ff3a3 --- /dev/null +++ b/server/src/auth.rs @@ -0,0 +1,134 @@ +/* + 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) 2026 metamuffin <metamuffin.org> +*/ +use crate::{CONF, DATABASE}; +use aes_gcm_siv::{ + KeyInit, + aead::{Aead, generic_array::GenericArray}, +}; +use anyhow::anyhow; +use base64::Engine; +use log::warn; +use serde::{Deserialize, Serialize}; +use std::{sync::LazyLock, time::Duration}; + +pub struct Session { + pub user: User, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SessionData { + username: String, + expire: DateTime<Utc>, + permissions: PermissionSet, +} + +static SESSION_KEY: LazyLock<[u8; 32]> = LazyLock::new(|| { + if let Some(sk) = &CONF.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().is_multiple_of(16) { + 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) +} + +pub fn token_to_session(token: &str) -> anyhow::Result<Session> { + let username = validate(token)?; + let user = DATABASE + .get_user(&username)? + .ok_or(anyhow!("user does not exist"))?; + Ok(Session { user }) +} +pub fn bypass_auth_session() -> anyhow::Result<Session> { + let user = DATABASE + .get_user(CONF.admin_username.as_ref().unwrap())? + .ok_or(anyhow!("user does not exist"))?; + Ok(Session { user }) +} + +#[cfg(test)] +fn load_test_config() { + use std::path::PathBuf; + + use crate::{CONF_PRELOAD, Config}; + *CONF_PRELOAD.lock().unwrap() = Some(Config { + database_path: PathBuf::default(), + login_expire: 10, + session_key: None, + admin_password: None, + admin_username: None, + }); +} + +#[test] +fn test() { + load_test_config(); + let tok = create( + "blub".to_string(), + jellycommon::user::PermissionSet::default(), + Duration::from_days(1), + ); + validate(&tok).unwrap(); +} + +#[test] +fn test_crypto() { + load_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()); +} diff --git a/server/src/main.rs b/server/src/main.rs index be1aba4..9b6463f 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -22,6 +22,7 @@ pub mod request_info; pub mod responders; pub mod routes; pub mod ui; +pub mod auth; #[rocket::main] async fn main() { diff --git a/server/src/request_info.rs b/server/src/request_info.rs index 779b4e1..3a1023f 100644 --- a/server/src/request_info.rs +++ b/server/src/request_info.rs @@ -93,7 +93,7 @@ pub(super) fn accept_language<'a>(request: &'a Request<'_>) -> &'a str { .unwrap_or("en") } -pub(super) async fn session_from_request(req: &Request<'_>) -> Result<Session, MyError> { +pub(super) async fn user_from_request(req: &Request<'_>) -> Result<Session, MyError> { if cfg!(feature = "bypass-auth") { Ok(bypass_auth_session()?) } else { @@ -112,7 +112,7 @@ pub(super) async fn session_from_request(req: &Request<'_>) -> Result<Session, M // jellyfin urlescapes the token for *some* requests let token = token.replace("%3D", "="); - Ok(token_to_session(&token)?) + Ok(token_to_user(&token)?) } } diff --git a/stream/src/hls.rs b/stream/src/hls.rs index 5557e48..b8d04ef 100644 --- a/stream/src/hls.rs +++ b/stream/src/hls.rs @@ -11,11 +11,10 @@ use std::{ fmt::Write, io::{Cursor, Read}, ops::Range, - sync::Arc, }; pub fn hls_multivariant_stream( - info: Arc<SMediaInfo>, + info: &SMediaInfo, container: StreamContainer, ) -> Result<Box<dyn Read + Send + Sync>> { let (_iinfo, info) = stream_info(&info)?; @@ -48,7 +47,7 @@ pub fn hls_multivariant_stream( } pub fn hls_variant_stream( - info: Arc<SMediaInfo>, + info: &SMediaInfo, track: TrackNum, format: FormatNum, container: StreamContainer, diff --git a/stream/src/lib.rs b/stream/src/lib.rs index 20a5e1c..424e44b 100644 --- a/stream/src/lib.rs +++ b/stream/src/lib.rs @@ -92,13 +92,13 @@ pub fn stream( ) -> Result<Box<dyn Read + Send + Sync>> { match spec { StreamSpec::Original { track } => original_stream(info, track, range), - StreamSpec::HlsMultiVariant { container } => hls_multivariant_stream(info, container), + StreamSpec::HlsMultiVariant { container } => hls_multivariant_stream(&info, container), StreamSpec::HlsVariant { track, container, format, - } => hls_variant_stream(info, track, format, container), - StreamSpec::Info => write_stream_info(info), + } => hls_variant_stream(&info, track, format, container), + StreamSpec::Info => write_stream_info(&info), StreamSpec::FragmentIndex { track } => fragment_index_stream(info, track), StreamSpec::Fragment { track, diff --git a/stream/src/stream_info.rs b/stream/src/stream_info.rs index 4a2605e..33dd288 100644 --- a/stream/src/stream_info.rs +++ b/stream/src/stream_info.rs @@ -157,7 +157,7 @@ fn containers_by_codec(codec: &str) -> Vec<StreamContainer> { } } -pub(crate) fn write_stream_info(info: Arc<SMediaInfo>) -> Result<Box<dyn Read + Send + Sync>> { +pub(crate) fn write_stream_info(info: &SMediaInfo) -> Result<Box<dyn Read + Send + Sync>> { let (_, info) = stream_info(&info)?; Ok(Box::new(Cursor::new(serde_json::to_vec(&info)?))) } |