diff options
author | metamuffin <metamuffin@disroot.org> | 2025-04-27 19:25:11 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2025-04-27 19:25:11 +0200 |
commit | 11a585b3dbe620dcc8772e713b22f1d9ba80d598 (patch) | |
tree | 44f8d97137412aefc79a2425a489c34fa3e5f6c5 | |
parent | d871aa7c5bba49ff55170b5d2dac9cd440ae7170 (diff) | |
download | jellything-11a585b3dbe620dcc8772e713b22f1d9ba80d598.tar jellything-11a585b3dbe620dcc8772e713b22f1d9ba80d598.tar.bz2 jellything-11a585b3dbe620dcc8772e713b22f1d9ba80d598.tar.zst |
move files around
-rw-r--r-- | server/src/api.rs (renamed from server/src/routes/api.rs) | 11 | ||||
-rw-r--r-- | server/src/compat/jellyfin/mod.rs (renamed from server/src/routes/compat/jellyfin/mod.rs) | 18 | ||||
-rw-r--r-- | server/src/compat/jellyfin/models.rs (renamed from server/src/routes/compat/jellyfin/models.rs) | 1 | ||||
-rw-r--r-- | server/src/compat/mod.rs (renamed from server/src/routes/compat/mod.rs) | 0 | ||||
-rw-r--r-- | server/src/compat/youtube.rs (renamed from server/src/routes/compat/youtube.rs) | 12 | ||||
-rw-r--r-- | server/src/helper/cors.rs | 20 | ||||
-rw-r--r-- | server/src/helper/mod.rs | 6 | ||||
-rw-r--r-- | server/src/locale.rs (renamed from server/src/routes/locale.rs) | 0 | ||||
-rw-r--r-- | server/src/logic/mod.rs | 9 | ||||
-rw-r--r-- | server/src/logic/playersync.rs (renamed from server/src/routes/playersync.rs) | 3 | ||||
-rw-r--r-- | server/src/logic/session.rs | 208 | ||||
-rw-r--r-- | server/src/logic/stream.rs (renamed from server/src/routes/stream.rs) | 4 | ||||
-rw-r--r-- | server/src/logic/userdata.rs (renamed from server/src/routes/userdata.rs) | 5 | ||||
-rw-r--r-- | server/src/main.rs | 8 | ||||
-rw-r--r-- | server/src/routes.rs (renamed from server/src/routes/mod.rs) | 112 | ||||
-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 | ||||
-rw-r--r-- | server/src/ui/account/mod.rs (renamed from server/src/routes/ui/account/mod.rs) | 13 | ||||
-rw-r--r-- | server/src/ui/account/settings.rs (renamed from server/src/routes/ui/account/settings.rs) | 12 | ||||
-rw-r--r-- | server/src/ui/admin/log.rs (renamed from server/src/routes/ui/admin/log.rs) | 4 | ||||
-rw-r--r-- | server/src/ui/admin/mod.rs (renamed from server/src/routes/ui/admin/mod.rs) | 8 | ||||
-rw-r--r-- | server/src/ui/admin/user.rs (renamed from server/src/routes/ui/admin/user.rs) | 4 | ||||
-rw-r--r-- | server/src/ui/assets.rs (renamed from server/src/routes/ui/assets.rs) | 3 | ||||
-rw-r--r-- | server/src/ui/browser.rs (renamed from server/src/routes/ui/browser.rs) | 5 | ||||
-rw-r--r-- | server/src/ui/error.rs (renamed from server/src/routes/ui/error.rs) | 2 | ||||
-rw-r--r-- | server/src/ui/home.rs (renamed from server/src/routes/ui/home.rs) | 13 | ||||
-rw-r--r-- | server/src/ui/layout.rs (renamed from server/src/routes/ui/layout.rs) | 24 | ||||
-rw-r--r-- | server/src/ui/mod.rs (renamed from server/src/routes/ui/mod.rs) | 7 | ||||
-rw-r--r-- | server/src/ui/node.rs (renamed from server/src/routes/ui/node.rs) | 18 | ||||
-rw-r--r-- | server/src/ui/player.rs (renamed from server/src/routes/ui/player.rs) | 10 | ||||
-rw-r--r-- | server/src/ui/search.rs (renamed from server/src/routes/ui/search.rs) | 3 | ||||
-rw-r--r-- | server/src/ui/sort.rs (renamed from server/src/routes/ui/sort.rs) | 13 | ||||
-rw-r--r-- | server/src/ui/stats.rs (renamed from server/src/routes/ui/stats.rs) | 18 | ||||
-rw-r--r-- | server/src/ui/style.rs (renamed from server/src/routes/ui/style.rs) | 4 |
35 files changed, 396 insertions, 409 deletions
diff --git a/server/src/routes/api.rs b/server/src/api.rs index 13708ce..f246eab 100644 --- a/server/src/routes/api.rs +++ b/server/src/api.rs @@ -3,14 +3,11 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use super::ui::{ - account::{ - login_logic, - session::{AdminSession, Session}, - }, - error::MyResult, +use super::ui::{account::login_logic, error::MyResult}; +use crate::{ + database::Database, + logic::session::{AdminSession, Session}, }; -use crate::database::Database; use jellybase::assetfed::AssetInner; use jellycommon::{user::CreateSessionParams, NodeID, Visibility}; use rocket::{ diff --git a/server/src/routes/compat/jellyfin/mod.rs b/server/src/compat/jellyfin/mod.rs index e37d7d1..9d5c93e 100644 --- a/server/src/routes/compat/jellyfin/mod.rs +++ b/server/src/compat/jellyfin/mod.rs @@ -5,14 +5,18 @@ */ pub mod models; -use crate::routes::ui::{ - account::{login_logic, session::Session}, - assets::{ - rocket_uri_macro_r_asset, rocket_uri_macro_r_item_backdrop, rocket_uri_macro_r_item_poster, +use crate::{ + logic::session::Session, + ui::{ + account::login_logic, + assets::{ + rocket_uri_macro_r_asset, rocket_uri_macro_r_item_backdrop, + rocket_uri_macro_r_item_poster, + }, + error::MyResult, + node::{aspect_class, DatabaseNodeUserDataExt}, + sort::{filter_and_sort_nodes, FilterProperty, NodeFilterSort, SortOrder, SortProperty}, }, - error::MyResult, - node::{aspect_class, DatabaseNodeUserDataExt}, - sort::{filter_and_sort_nodes, FilterProperty, NodeFilterSort, SortOrder, SortProperty}, }; use anyhow::{anyhow, Context}; use jellybase::{database::Database, CONF}; diff --git a/server/src/routes/compat/jellyfin/models.rs b/server/src/compat/jellyfin/models.rs index be41835..6a68455 100644 --- a/server/src/routes/compat/jellyfin/models.rs +++ b/server/src/compat/jellyfin/models.rs @@ -3,7 +3,6 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ - use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::BTreeMap; diff --git a/server/src/routes/compat/mod.rs b/server/src/compat/mod.rs index a7b8c0d..a7b8c0d 100644 --- a/server/src/routes/compat/mod.rs +++ b/server/src/compat/mod.rs diff --git a/server/src/routes/compat/youtube.rs b/server/src/compat/youtube.rs index 78eee8a..1df2751 100644 --- a/server/src/routes/compat/youtube.rs +++ b/server/src/compat/youtube.rs @@ -3,11 +3,13 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::routes::ui::{ - account::session::Session, - error::MyResult, - node::rocket_uri_macro_r_library_node, - player::{rocket_uri_macro_r_player, PlayerConfig}, +use crate::{ + logic::session::Session, + ui::{ + error::MyResult, + node::rocket_uri_macro_r_library_node, + player::{rocket_uri_macro_r_player, PlayerConfig}, + }, }; use anyhow::anyhow; use jellybase::database::Database; diff --git a/server/src/helper/cors.rs b/server/src/helper/cors.rs new file mode 100644 index 0000000..ca513e3 --- /dev/null +++ b/server/src/helper/cors.rs @@ -0,0 +1,20 @@ +/* + 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 rocket::{ + http::Header, + response::{self, Responder}, + Request, +}; + +pub struct Cors<T>(pub T); +impl<'r, T: Responder<'r, 'static>> Responder<'r, 'static> for Cors<T> { + fn respond_to(self, request: &'r Request<'_>) -> response::Result<'static> { + let mut r = self.0.respond_to(request)?; + r.adjoin_header(Header::new("access-controll-allow-origin", "*")); + Ok(r) + } +} diff --git a/server/src/helper/mod.rs b/server/src/helper/mod.rs new file mode 100644 index 0000000..946e8fa --- /dev/null +++ b/server/src/helper/mod.rs @@ -0,0 +1,6 @@ +/* + 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> +*/ +pub mod cors; diff --git a/server/src/routes/locale.rs b/server/src/locale.rs index 6d16c17..6d16c17 100644 --- a/server/src/routes/locale.rs +++ b/server/src/locale.rs diff --git a/server/src/logic/mod.rs b/server/src/logic/mod.rs new file mode 100644 index 0000000..745d11b --- /dev/null +++ b/server/src/logic/mod.rs @@ -0,0 +1,9 @@ +/* + 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> +*/ +pub mod playersync; +pub mod session; +pub mod stream; +pub mod userdata; diff --git a/server/src/routes/playersync.rs b/server/src/logic/playersync.rs index 9eb6175..b4cc51b 100644 --- a/server/src/routes/playersync.rs +++ b/server/src/logic/playersync.rs @@ -1,4 +1,3 @@ -use super::Cors; use anyhow::bail; use chashmap::CHashMap; use futures::{SinkExt, StreamExt}; @@ -8,6 +7,8 @@ use rocket_ws::{stream::DuplexStream, Channel, Message, WebSocket}; use serde::{Deserialize, Serialize}; use tokio::sync::broadcast::{self, Sender}; +use crate::helper::cors::Cors; + #[derive(Default)] pub struct PlayersyncChannels { channels: CHashMap<String, broadcast::Sender<Message>>, diff --git a/server/src/logic/session.rs b/server/src/logic/session.rs new file mode 100644 index 0000000..790e070 --- /dev/null +++ b/server/src/logic/session.rs @@ -0,0 +1,208 @@ +/* + 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 crate::ui::error::MyError; +use aes_gcm_siv::{ + aead::{generic_array::GenericArray, Aead}, + KeyInit, +}; +use anyhow::anyhow; +use base64::Engine; +use chrono::{DateTime, Duration, Utc}; +use jellybase::{database::Database, SECRETS}; +use jellycommon::user::{PermissionSet, User}; +use log::warn; +use rocket::{ + async_trait, + http::Status, + outcome::Outcome, + request::{self, FromRequest}, + Request, State, +}; +use serde::{Deserialize, Serialize}; +use std::sync::LazyLock; + +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, +} + +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 = 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) + } + } + } +} + +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()); +} diff --git a/server/src/routes/stream.rs b/server/src/logic/stream.rs index 0fbeb3a..5bba9c2 100644 --- a/server/src/routes/stream.rs +++ b/server/src/logic/stream.rs @@ -3,8 +3,8 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use super::ui::{account::session::Session, error::MyError}; -use crate::database::Database; +use super::session::Session; +use crate::{database::Database, ui::error::MyError}; use anyhow::{anyhow, Result}; use jellybase::{assetfed::AssetInner, federation::Federation}; use jellycommon::{stream::StreamSpec, TrackSource}; diff --git a/server/src/routes/userdata.rs b/server/src/logic/userdata.rs index 01776da..64a136f 100644 --- a/server/src/routes/userdata.rs +++ b/server/src/logic/userdata.rs @@ -3,8 +3,7 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use super::ui::{account::session::Session, error::MyResult}; -use crate::routes::ui::node::rocket_uri_macro_r_library_node; +use crate::{ui::error::MyResult, ui::node::rocket_uri_macro_r_library_node}; use jellybase::database::Database; use jellycommon::{ user::{NodeUserData, WatchedState}, @@ -15,6 +14,8 @@ use rocket::{ UriDisplayQuery, }; +use super::session::Session; + #[derive(Debug, FromFormField, UriDisplayQuery)] pub enum UrlWatchedState { None, diff --git a/server/src/main.rs b/server/src/main.rs index ced2f02..b583823 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -7,16 +7,22 @@ #![allow(clippy::needless_borrows_for_generic_args)] #![recursion_limit = "4096"] -use crate::routes::ui::{account::hash_password, admin::log::enable_logging}; use anyhow::Context; use database::Database; use jellybase::{federation::Federation, CONF, SECRETS}; use log::{error, info, warn}; use routes::build_rocket; use tokio::fs::create_dir_all; +use ui::{account::hash_password, admin::log::enable_logging}; pub use jellybase::database; +pub mod api; +pub mod compat; +pub mod helper; +pub mod locale; +pub mod logic; pub mod routes; +pub mod ui; #[rocket::main] async fn main() { diff --git a/server/src/routes/mod.rs b/server/src/routes.rs index e7e0a60..4e452c3 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes.rs @@ -3,50 +3,9 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use self::playersync::{r_playersync, PlayersyncChannels}; -use crate::{database::Database, routes::ui::error::MyResult}; -use api::{ - r_api_account_login, r_api_asset_token_raw, r_api_nodes_modified_since, r_api_root, - r_api_version, -}; -use base64::Engine; -use compat::{ - jellyfin::{ - r_jellyfin_artists, r_jellyfin_branding_configuration, r_jellyfin_branding_css, - r_jellyfin_displaypreferences_usersettings, - r_jellyfin_displaypreferences_usersettings_post, r_jellyfin_items, - r_jellyfin_items_image_primary, r_jellyfin_items_images_backdrop, r_jellyfin_items_intros, - r_jellyfin_items_item, r_jellyfin_items_playbackinfo, r_jellyfin_items_similar, - r_jellyfin_livetv_programs_recommended, r_jellyfin_persons, - r_jellyfin_playback_bitratetest, r_jellyfin_quickconnect_enabled, - r_jellyfin_sessions_capabilities_full, r_jellyfin_sessions_playing, - r_jellyfin_sessions_playing_progress, r_jellyfin_shows_nextup, r_jellyfin_socket, - r_jellyfin_system_endpoint, r_jellyfin_system_info, r_jellyfin_system_info_public, - r_jellyfin_system_info_public_case, r_jellyfin_users_authenticatebyname, - r_jellyfin_users_authenticatebyname_case, r_jellyfin_users_id, r_jellyfin_users_items, - r_jellyfin_users_items_item, r_jellyfin_users_public, r_jellyfin_users_views, - r_jellyfin_video_stream, - }, - youtube::{r_youtube_channel, r_youtube_embed, r_youtube_watch}, -}; -use jellybase::{federation::Federation, CONF, SECRETS}; -use log::warn; -use rand::random; -use rocket::{ - catchers, - config::SecretKey, - fairing::AdHoc, - fs::FileServer, - get, - http::Header, - response::{self, Responder}, - routes, - shield::Shield, - Build, Config, Request, Rocket, -}; -use std::fs::File; -use stream::r_stream; -use ui::{ +use crate::database::Database; +use crate::logic::playersync::{r_playersync, PlayersyncChannels}; +use crate::ui::{ account::{ r_account_login, r_account_login_post, r_account_logout, r_account_logout_post, r_account_register, r_account_register_post, @@ -64,22 +23,51 @@ use ui::{ home::r_home, node::r_library_node_filter, player::r_player, - r_index, + r_favicon, r_index, search::r_search, stats::r_stats, style::{r_assets_font, r_assets_js, r_assets_js_map, r_assets_style}, }; -use userdata::{ - r_node_userdata, r_node_userdata_progress, r_node_userdata_rating, r_node_userdata_watched, +use crate::{ + api::{ + r_api_account_login, r_api_asset_token_raw, r_api_nodes_modified_since, r_api_root, + r_api_version, + }, + compat::{ + jellyfin::{ + r_jellyfin_artists, r_jellyfin_branding_configuration, r_jellyfin_branding_css, + r_jellyfin_displaypreferences_usersettings, + r_jellyfin_displaypreferences_usersettings_post, r_jellyfin_items, + r_jellyfin_items_image_primary, r_jellyfin_items_images_backdrop, + r_jellyfin_items_intros, r_jellyfin_items_item, r_jellyfin_items_playbackinfo, + r_jellyfin_items_similar, r_jellyfin_livetv_programs_recommended, r_jellyfin_persons, + r_jellyfin_playback_bitratetest, r_jellyfin_quickconnect_enabled, + r_jellyfin_sessions_capabilities_full, r_jellyfin_sessions_playing, + r_jellyfin_sessions_playing_progress, r_jellyfin_shows_nextup, r_jellyfin_socket, + r_jellyfin_system_endpoint, r_jellyfin_system_info, r_jellyfin_system_info_public, + r_jellyfin_system_info_public_case, r_jellyfin_users_authenticatebyname, + r_jellyfin_users_authenticatebyname_case, r_jellyfin_users_id, r_jellyfin_users_items, + r_jellyfin_users_items_item, r_jellyfin_users_public, r_jellyfin_users_views, + r_jellyfin_video_stream, + }, + youtube::{r_youtube_channel, r_youtube_embed, r_youtube_watch}, + }, + logic::{ + stream::r_stream, + userdata::{ + r_node_userdata, r_node_userdata_progress, r_node_userdata_rating, + r_node_userdata_watched, + }, + }, +}; +use base64::Engine; +use jellybase::{federation::Federation, CONF, SECRETS}; +use log::warn; +use rand::random; +use rocket::{ + catchers, config::SecretKey, fairing::AdHoc, fs::FileServer, http::Header, routes, + shield::Shield, Build, Config, Rocket, }; - -pub mod api; -pub mod compat; -pub mod locale; -pub mod playersync; -pub mod stream; -pub mod ui; -pub mod userdata; #[macro_export] macro_rules! uri { @@ -222,17 +210,3 @@ pub fn build_rocket(database: Database, federation: Federation) -> Rocket<Build> ], ) } - -#[get("/favicon.ico")] -fn r_favicon() -> MyResult<File> { - Ok(File::open(CONF.asset_path.join("favicon.ico"))?) -} - -pub struct Cors<T>(pub T); -impl<'r, T: Responder<'r, 'static>> Responder<'r, 'static> for Cors<T> { - fn respond_to(self, request: &'r Request<'_>) -> response::Result<'static> { - let mut r = self.0.respond_to(request)?; - r.adjoin_header(Header::new("access-controll-allow-origin", "*")); - Ok(r) - } -} 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()); -} diff --git a/server/src/routes/ui/account/mod.rs b/server/src/ui/account/mod.rs index 83a1447..312b40c 100644 --- a/server/src/routes/ui/account/mod.rs +++ b/server/src/ui/account/mod.rs @@ -3,7 +3,6 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -pub mod session; pub mod settings; use super::{ @@ -12,13 +11,9 @@ use super::{ }; use crate::{ database::Database, - routes::{ - locale::AcceptLanguage, - ui::{ - account::session::Session, error::MyResult, home::rocket_uri_macro_r_home, - layout::DynLayoutPage, - }, - }, + locale::AcceptLanguage, + logic::session::{self, Session}, + ui::{error::MyResult, home::rocket_uri_macro_r_home, layout::DynLayoutPage}, uri, }; use anyhow::anyhow; @@ -226,7 +221,7 @@ pub fn login_logic( .retain(|p, val| if *val { !ep.contains(p) } else { true }) } - Ok(session::token::create( + Ok(session::create( user.name, user.permissions, Duration::days(CONF.login_expire.min(expire.unwrap_or(i64::MAX))), diff --git a/server/src/routes/ui/account/settings.rs b/server/src/ui/account/settings.rs index 2e170b0..4047e4f 100644 --- a/server/src/routes/ui/account/settings.rs +++ b/server/src/ui/account/settings.rs @@ -6,13 +6,11 @@ use super::{format_form_error, hash_password}; use crate::{ database::Database, - routes::{ - locale::AcceptLanguage, - ui::{ - account::{rocket_uri_macro_r_account_login, session::Session}, - error::MyResult, - layout::{trs, DynLayoutPage, LayoutPage}, - }, + locale::AcceptLanguage, + ui::{ + account::{rocket_uri_macro_r_account_login, session::Session}, + error::MyResult, + layout::{trs, DynLayoutPage, LayoutPage}, }, uri, }; diff --git a/server/src/routes/ui/admin/log.rs b/server/src/ui/admin/log.rs index fc85b37..dff6d1b 100644 --- a/server/src/routes/ui/admin/log.rs +++ b/server/src/ui/admin/log.rs @@ -4,8 +4,8 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use crate::{ - routes::ui::{ - account::session::AdminSession, + logic::session::AdminSession, + ui::{ error::MyResult, layout::{DynLayoutPage, LayoutPage}, }, diff --git a/server/src/routes/ui/admin/mod.rs b/server/src/ui/admin/mod.rs index f44b36c..de06610 100644 --- a/server/src/routes/ui/admin/mod.rs +++ b/server/src/ui/admin/mod.rs @@ -6,13 +6,11 @@ pub mod log; pub mod user; -use super::{ - account::session::AdminSession, - assets::{resolve_asset, AVIF_QUALITY, AVIF_SPEED}, -}; +use super::assets::{resolve_asset, AVIF_QUALITY, AVIF_SPEED}; use crate::{ database::Database, - routes::ui::{ + logic::session::AdminSession, + ui::{ admin::log::rocket_uri_macro_r_admin_log, error::MyResult, layout::{DynLayoutPage, FlashDisplay, LayoutPage}, diff --git a/server/src/routes/ui/admin/user.rs b/server/src/ui/admin/user.rs index 7ba6d4e..c5239f7 100644 --- a/server/src/routes/ui/admin/user.rs +++ b/server/src/ui/admin/user.rs @@ -5,8 +5,8 @@ */ use crate::{ database::Database, - routes::ui::{ - account::session::AdminSession, + logic::session::AdminSession, + ui::{ error::MyResult, layout::{DynLayoutPage, FlashDisplay, LayoutPage}, }, diff --git a/server/src/routes/ui/assets.rs b/server/src/ui/assets.rs index c661771..ce2a8e2 100644 --- a/server/src/routes/ui/assets.rs +++ b/server/src/ui/assets.rs @@ -3,7 +3,8 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::routes::ui::{account::session::Session, error::MyResult, CacheControlFile}; +use super::{error::MyResult, CacheControlFile}; +use crate::logic::session::Session; use anyhow::{anyhow, bail, Context}; use base64::Engine; use jellybase::{ diff --git a/server/src/routes/ui/browser.rs b/server/src/ui/browser.rs index 96c005d..f7eac93 100644 --- a/server/src/routes/ui/browser.rs +++ b/server/src/ui/browser.rs @@ -4,16 +4,13 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use super::{ - account::session::Session, error::MyError, layout::{trs, DynLayoutPage, LayoutPage}, node::NodeCard, sort::{filter_and_sort_nodes, NodeFilterSort, NodeFilterSortForm, SortOrder, SortProperty}, }; use crate::{ - database::Database, - routes::{api::AcceptJson, locale::AcceptLanguage}, - uri, + api::AcceptJson, database::Database, locale::AcceptLanguage, logic::session::Session, uri, }; use jellybase::locale::tr; use jellycommon::{api::ApiItemsResponse, Visibility}; diff --git a/server/src/routes/ui/error.rs b/server/src/ui/error.rs index ee593a2..c9620bb 100644 --- a/server/src/routes/ui/error.rs +++ b/server/src/ui/error.rs @@ -4,7 +4,7 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use super::layout::{DynLayoutPage, LayoutPage}; -use crate::{routes::ui::account::rocket_uri_macro_r_account_login, uri}; +use crate::{ui::account::rocket_uri_macro_r_account_login, uri}; use jellybase::CONF; use log::info; use rocket::{ diff --git a/server/src/routes/ui/home.rs b/server/src/ui/home.rs index 8f8a876..fbce99b 100644 --- a/server/src/routes/ui/home.rs +++ b/server/src/ui/home.rs @@ -4,18 +4,11 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use super::{ - account::session::Session, - layout::{trs, LayoutPage}, + error::MyResult, + layout::{trs, DynLayoutPage, LayoutPage}, node::{DatabaseNodeUserDataExt, NodeCard}, }; -use crate::{ - database::Database, - routes::{ - api::AcceptJson, - locale::AcceptLanguage, - ui::{error::MyResult, layout::DynLayoutPage}, - }, -}; +use crate::{api::AcceptJson, database::Database, locale::AcceptLanguage, logic::session::Session}; use anyhow::Context; use chrono::{Datelike, Utc}; use jellybase::{locale::tr, CONF}; diff --git a/server/src/routes/ui/layout.rs b/server/src/ui/layout.rs index 0a0d036..0e8d7b9 100644 --- a/server/src/routes/ui/layout.rs +++ b/server/src/ui/layout.rs @@ -4,20 +4,18 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use crate::{ - routes::{ - locale::lang_from_request, - ui::{ - account::{ - rocket_uri_macro_r_account_login, rocket_uri_macro_r_account_logout, - rocket_uri_macro_r_account_register, session::Session, - settings::rocket_uri_macro_r_account_settings, - }, - admin::rocket_uri_macro_r_admin_dashboard, - browser::rocket_uri_macro_r_all_items, - node::rocket_uri_macro_r_library_node, - search::rocket_uri_macro_r_search, - stats::rocket_uri_macro_r_stats, + locale::lang_from_request, + logic::session::Session, + ui::{ + account::{ + rocket_uri_macro_r_account_login, rocket_uri_macro_r_account_logout, + rocket_uri_macro_r_account_register, settings::rocket_uri_macro_r_account_settings, }, + admin::rocket_uri_macro_r_admin_dashboard, + browser::rocket_uri_macro_r_all_items, + node::rocket_uri_macro_r_library_node, + search::rocket_uri_macro_r_search, + stats::rocket_uri_macro_r_stats, }, uri, }; diff --git a/server/src/routes/ui/mod.rs b/server/src/ui/mod.rs index d61ef9e..b98fbec 100644 --- a/server/src/routes/ui/mod.rs +++ b/server/src/ui/mod.rs @@ -3,7 +3,7 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use account::session::Session; +use crate::logic::session::Session; use error::MyResult; use home::rocket_uri_macro_r_home; use jellybase::CONF; @@ -61,6 +61,11 @@ pub async fn r_index(sess: Option<Session>) -> MyResult<Either<Redirect, DynLayo } } +#[get("/favicon.ico")] +pub async fn r_favicon() -> MyResult<File> { + Ok(File::open(CONF.asset_path.join("favicon.ico")).await?) +} + pub struct HtmlTemplate<'a>(pub markup::DynRender<'a>); impl<'r> Responder<'r, 'static> for HtmlTemplate<'_> { diff --git a/server/src/routes/ui/node.rs b/server/src/ui/node.rs index d968f0a..bf65a3e 100644 --- a/server/src/routes/ui/node.rs +++ b/server/src/ui/node.rs @@ -13,21 +13,21 @@ use super::{ sort::{filter_and_sort_nodes, NodeFilterSort, NodeFilterSortForm, SortOrder, SortProperty}, }; use crate::{ + api::AcceptJson, database::Database, - routes::{ - api::AcceptJson, - locale::AcceptLanguage, - ui::{ - account::session::Session, - assets::rocket_uri_macro_r_person_asset, - layout::{DynLayoutPage, LayoutPage}, - player::{rocket_uri_macro_r_player, PlayerConfig}, - }, + locale::AcceptLanguage, + logic::{ + session::Session, userdata::{ rocket_uri_macro_r_node_userdata_rating, rocket_uri_macro_r_node_userdata_watched, UrlWatchedState, }, }, + ui::{ + assets::rocket_uri_macro_r_person_asset, + layout::{DynLayoutPage, LayoutPage}, + player::{rocket_uri_macro_r_player, PlayerConfig}, + }, uri, }; use anyhow::{anyhow, Result}; diff --git a/server/src/routes/ui/player.rs b/server/src/ui/player.rs index 2bb439b..cd4d03c 100644 --- a/server/src/routes/ui/player.rs +++ b/server/src/ui/player.rs @@ -4,17 +4,15 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use super::{ - account::session::{token, Session}, layout::LayoutPage, node::{get_similar_media, DatabaseNodeUserDataExt, NodePage}, sort::NodeFilterSort, }; use crate::{ database::Database, - routes::{ - locale::AcceptLanguage, - ui::{error::MyResult, layout::DynLayoutPage}, - }, + locale::AcceptLanguage, + logic::session::{self, Session}, + ui::{error::MyResult, layout::DynLayoutPage}, }; use anyhow::anyhow; use jellybase::CONF; @@ -87,7 +85,7 @@ pub fn r_player( conf.t.unwrap_or(0.), &session.user.native_secret, &id.to_string(), - &token::create( + &session::create( session.user.name, PermissionSet::default(), // TODO chrono::Duration::hours(24), diff --git a/server/src/routes/ui/search.rs b/server/src/ui/search.rs index bc84e57..96be3a6 100644 --- a/server/src/routes/ui/search.rs +++ b/server/src/ui/search.rs @@ -4,12 +4,11 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use super::{ - account::session::Session, error::MyResult, layout::{trs, DynLayoutPage, LayoutPage}, node::{DatabaseNodeUserDataExt, NodeCard}, }; -use crate::routes::{api::AcceptJson, locale::AcceptLanguage}; +use crate::{api::AcceptJson, locale::AcceptLanguage, logic::session::Session}; use anyhow::anyhow; use jellybase::{database::Database, locale::tr}; use jellycommon::{api::ApiSearchResponse, Visibility}; diff --git a/server/src/routes/ui/sort.rs b/server/src/ui/sort.rs index 6d38e11..a241030 100644 --- a/server/src/routes/ui/sort.rs +++ b/server/src/ui/sort.rs @@ -1,3 +1,9 @@ +/* + 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 crate::ui::layout::trs; use jellybase::locale::Language; use jellycommon::{helpers::SortAnyway, user::NodeUserData, Node, NodeKind, Rating}; use markup::RenderAttributeValue; @@ -7,8 +13,6 @@ use rocket::{ }; use std::sync::Arc; -use crate::routes::ui::layout::trs; - #[derive(FromForm, UriDisplayQuery, Default, Clone)] pub struct NodeFilterSort { pub sort_by: Option<SortProperty>, @@ -112,7 +116,10 @@ impl FilterProperty { ), ( "filter_sort.filter.federation", - &[(FederationLocal, "federation.local"), (FederationRemote, "federation.remote")], + &[ + (FederationLocal, "federation.local"), + (FederationRemote, "federation.remote"), + ], ), ( "filter_sort.filter.watched", diff --git a/server/src/routes/ui/stats.rs b/server/src/ui/stats.rs index 4e4eef1..4c5bed8 100644 --- a/server/src/routes/ui/stats.rs +++ b/server/src/ui/stats.rs @@ -4,21 +4,19 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use super::{ - account::session::Session, error::MyError, layout::{DynLayoutPage, LayoutPage}, }; use crate::{ + api::AcceptJson, database::Database, - routes::{ - api::AcceptJson, - locale::AcceptLanguage, - ui::{ - layout::trs, - node::{ - format_duration, format_duration_long, format_kind, format_size, - rocket_uri_macro_r_library_node, - }, + locale::AcceptLanguage, + logic::session::Session, + ui::{ + layout::trs, + node::{ + format_duration, format_duration_long, format_kind, format_size, + rocket_uri_macro_r_library_node, }, }, uri, diff --git a/server/src/routes/ui/style.rs b/server/src/ui/style.rs index c935c8a..77f0fe1 100644 --- a/server/src/routes/ui/style.rs +++ b/server/src/ui/style.rs @@ -35,7 +35,7 @@ macro_rules! concat_files { fn css_bundle() -> String { concat_files!( - ["../../../../web/style"], + ["../../../web/style"], "layout.css", "player.css", "nodepage.css", @@ -76,7 +76,7 @@ pub fn r_assets_style() -> CachedAsset<(ContentType, String)> { pub fn r_assets_font() -> CachedAsset<(ContentType, &'static [u8])> { CachedAsset(( ContentType::WOFF2, - include_bytes!("../../../../web/cantarell.woff2"), + include_bytes!("../../../web/cantarell.woff2"), )) } |