aboutsummaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2026-01-25 03:31:59 +0100
committermetamuffin <metamuffin@disroot.org>2026-01-25 03:31:59 +0100
commit53361f4c6027d1569a707ce58889bc2c2ea3749c (patch)
tree65ccafebd8df99019b039cc0d94c61291b1fd437 /server
parente3708145efd0bbed5d10320ee0a0deff0f38479e (diff)
downloadjellything-53361f4c6027d1569a707ce58889bc2c2ea3749c.tar
jellything-53361f4c6027d1569a707ce58889bc2c2ea3749c.tar.bz2
jellything-53361f4c6027d1569a707ce58889bc2c2ea3749c.tar.zst
finish migrating auth code; refactor config/state more
Diffstat (limited to 'server')
-rw-r--r--server/Cargo.toml8
-rw-r--r--server/src/auth.rs171
-rw-r--r--server/src/config.rs59
-rw-r--r--server/src/main.rs79
-rw-r--r--server/src/request_info.rs58
-rw-r--r--server/src/routes.rs105
6 files changed, 191 insertions, 289 deletions
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 01854b5..534dce5 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -9,10 +9,13 @@ jellystream = { path = "../stream" }
jellytranscoder = { path = "../transcoder" }
jellyimport = { path = "../import" }
jellycache = { path = "../cache" }
+jellydb = { path = "../database" }
jellyui = { path = "../ui" }
-jellykv = { path = "../kv" }
+jellykv = { path = "../kv", features = ["rocksdb"] }
+aes-gcm-siv = "0.11.1"
anyhow = { workspace = true }
+argon2 = "0.5.3"
async-recursion = "1.1.1"
base64 = "0.22.1"
bincode = { version = "2.0.1", features = ["serde", "derive"] }
@@ -29,6 +32,3 @@ serde_json = "1.0.145"
serde_yaml_ng = "0.10.0"
tokio = { workspace = true }
tokio-util = { version = "0.7.17", features = ["io", "io-util"] }
-
-[features]
-bypass-auth = []
diff --git a/server/src/auth.rs b/server/src/auth.rs
index 03ff3a3..e84c4d1 100644
--- a/server/src/auth.rs
+++ b/server/src/auth.rs
@@ -3,132 +3,73 @@
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())
- }
-});
+use crate::State;
+use anyhow::{Result, anyhow};
+use jellycommon::jellyobject::ObjectBuffer;
-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);
- }
+pub fn token_to_user(state: &State, token: &str) -> Result<ObjectBuffer> {
+ let user_row = token::validate(&state.session_key, token)?;
- 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);
+ let mut user = None;
+ state.database.read_transaction(&mut |txn| {
+ user = state.users.get(txn, user_row)?;
+ Ok(())
+ })?;
- base64::engine::general_purpose::URL_SAFE.encode(&ciphertext)
+ user.ok_or(anyhow!("user was deleted"))
}
-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:?}"))?;
+pub mod token {
+ use aes_gcm_siv::{
+ Aes256GcmSiv, KeyInit,
+ aead::{Aead, generic_array::GenericArray},
+ };
+ use anyhow::{Result, anyhow, bail};
+ use base64::{
+ Engine,
+ engine::general_purpose::{STANDARD, URL_SAFE},
+ };
+ use chrono::Utc;
+ use jellydb::table::RowNum;
+ use std::time::Duration;
- let (session_data, _): (SessionData, _) =
- bincode::serde::decode_from_slice(&plaintext, bincode::config::standard())?;
+ pub struct SessionKey(Aes256GcmSiv);
- if session_data.expire < Utc::now() {
- Err(anyhow!("session expired"))?
+ impl SessionKey {
+ pub fn parse(s: &str) -> Result<Self> {
+ let k = STANDARD.decode(s)?;
+ Ok(Self(Aes256GcmSiv::new_from_slice(&k)?))
+ }
}
- 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;
+ pub fn create(sk: &SessionKey, user: RowNum, expire: Duration) -> String {
+ let expire_ts = Utc::now().timestamp() + expire.as_secs() as i64;
+ let mut plain = Vec::new();
+ plain.extend(user.to_be_bytes());
+ plain.extend(expire_ts.to_be_bytes());
- 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,
- });
-}
+ let nonce = [(); 12].map(|_| rand::random());
+ let mut ciper =
+ sk.0.encrypt(&GenericArray::from(nonce), plain.as_slice())
+ .unwrap();
+ ciper.extend(nonce);
+ URL_SAFE.encode(&ciper)
+ }
+ pub fn validate(sk: &SessionKey, token: &str) -> Result<RowNum> {
+ let cipher = URL_SAFE.decode(token)?;
+ let (cipher, nonce) = cipher.split_at(cipher.len() - 12);
+ let plain =
+ sk.0.decrypt(nonce.into(), cipher)
+ .map_err(|_| anyhow!("invalid session"))?;
-#[test]
-fn test() {
- load_test_config();
- let tok = create(
- "blub".to_string(),
- jellycommon::user::PermissionSet::default(),
- Duration::from_days(1),
- );
- validate(&tok).unwrap();
-}
+ let user = RowNum::from_be_bytes(plain[0..8].try_into().unwrap());
+ let expire_ts = i64::from_be_bytes(plain[8..16].try_into().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());
+ if Utc::now().timestamp() > expire_ts {
+ bail!("session expired")
+ } else {
+ Ok(user)
+ }
+ }
}
diff --git a/server/src/config.rs b/server/src/config.rs
deleted file mode 100644
index 73d6b73..0000000
--- a/server/src/config.rs
+++ /dev/null
@@ -1,59 +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 anyhow::{Context, Result, anyhow};
-use jellycache::init_cache;
-use serde::Deserialize;
-use std::{
- env::{args, var},
- path::PathBuf,
- sync::{LazyLock, Mutex},
-};
-use tokio::fs::read_to_string;
-
-static CONF_PRELOAD: Mutex<Option<AllConfigs>> = Mutex::new(None);
-pub static CONF: LazyLock<AllConfigs> =
- LazyLock::new(|| CONF_PRELOAD.lock().unwrap().take().unwrap());
-
-pub struct AllConfigs {
- pub ui: jellyui::Config,
- pub transcoder: jellytranscoder::Config,
- pub stream: jellystream::Config,
- pub cache: jellycache::Config,
- pub import: jellyimport::Config,
- pub server: Config,
-}
-
-#[derive(Debug, Deserialize)]
-pub struct Config {
- pub asset_path: PathBuf,
- pub cookie_key: Option<String>,
- pub tls: bool,
- pub hostname: String,
-}
-
-pub async fn load_config() -> Result<()> {
- let path = args()
- .nth(1)
- .or_else(|| var("JELLYTHING_CONFIG").ok())
- .ok_or(anyhow!(
- "No config supplied. Use first argument or JELLYTHING_CONFIG environment variable."
- ))?;
-
- let config = read_to_string(path).await.context("reading main config")?;
- *CONF_PRELOAD.lock().unwrap() = Some(AllConfigs {
- ui: serde_yaml_ng::from_str(&config).context("ui config")?,
- transcoder: serde_yaml_ng::from_str(&config).context("transcoder config")?,
- stream: serde_yaml_ng::from_str(&config).context("stream config")?,
- cache: serde_yaml_ng::from_str(&config).context("cache config")?,
- import: serde_yaml_ng::from_str(&config).context("import config")?,
- server: serde_yaml_ng::from_str(&config).context("server config")?,
- });
-
- init_cache()?;
-
- Ok(())
-}
diff --git a/server/src/main.rs b/server/src/main.rs
index 9b6463f..bd9901a 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -7,38 +7,83 @@
#![allow(clippy::needless_borrows_for_generic_args)]
#![recursion_limit = "4096"]
-use crate::logger::setup_logger;
-use config::load_config;
-use log::{error, info, warn};
+use crate::{auth::token::SessionKey, logger::setup_logger};
+use anyhow::Result;
+use jellycache::Cache;
+use jellydb::table::Table;
+use jellykv::Database;
+use log::{error, info};
use routes::build_rocket;
-use std::process::exit;
+use serde::Deserialize;
+use std::{env::args, fs::read_to_string, path::PathBuf, process::exit, sync::Arc};
pub mod api;
+pub mod auth;
pub mod compat;
-pub mod config;
pub mod logger;
pub mod logic;
pub mod request_info;
pub mod responders;
pub mod routes;
pub mod ui;
-pub mod auth;
#[rocket::main]
async fn main() {
setup_logger();
- info!("loading config...");
- if let Err(e) = load_config().await {
- error!("error {e:?}");
- exit(1);
- }
-
- #[cfg(feature = "bypass-auth")]
- logger::warn!("authentification bypass enabled");
- let r = build_rocket().launch().await;
+ let state = match create_state() {
+ Ok(s) => s,
+ Err(e) => {
+ error!("unable to start: {e:#}");
+ exit(1);
+ }
+ };
+ let r = build_rocket(state).launch().await;
match r {
- Ok(_) => warn!("server shutdown"),
- Err(e) => error!("server exited: {e}"),
+ Ok(_) => info!("server shutdown"),
+ Err(e) => {
+ error!("server exited: {e}");
+ exit(1);
+ }
}
}
+
+pub(crate) struct State {
+ pub config: Config,
+
+ pub cache: Cache,
+ pub database: Arc<dyn Database>,
+ pub session_key: SessionKey,
+
+ pub nodes: Table,
+ pub users: Table,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Config {
+ pub ui: jellyui::Config,
+ pub session_key: String,
+ pub asset_path: PathBuf,
+ pub database_path: PathBuf,
+ pub cache_path: PathBuf,
+ pub max_memory_cache_size: usize,
+ pub tls: bool,
+ pub hostname: String,
+}
+
+pub fn create_state() -> Result<Arc<State>> {
+ let config_path = args().nth(1).unwrap();
+ let config: Config = serde_yaml_ng::from_str(&read_to_string(config_path)?)?;
+
+ let cache_storage = jellykv::rocksdb::new(&config.cache_path)?;
+ let db_storage = jellykv::rocksdb::new(&config.database_path)?;
+
+ Ok(Arc::new(State {
+ cache: Cache::new(Box::new(cache_storage), config.max_memory_cache_size),
+ database: Arc::new(db_storage),
+ session_key: SessionKey::parse(&config.session_key)?,
+ nodes: Table::new(0),
+ users: Table::new(1),
+ config,
+ }))
+}
diff --git a/server/src/request_info.rs b/server/src/request_info.rs
index 3a1023f..3468c58 100644
--- a/server/src/request_info.rs
+++ b/server/src/request_info.rs
@@ -4,8 +4,11 @@
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-use crate::ui::error::{MyError, MyResult};
-use anyhow::anyhow;
+use crate::{
+ State,
+ auth::token_to_user,
+ ui::error::{MyError, MyResult},
+};
use jellycommon::jellyobject::ObjectBuffer;
use jellyui::RenderInfo;
use rocket::{
@@ -13,11 +16,13 @@ use rocket::{
http::{MediaType, Status},
request::{FromRequest, Outcome},
};
+use std::sync::Arc;
pub struct RequestInfo<'a> {
pub lang: &'a str,
pub accept: Accept,
pub user: Option<ObjectBuffer>,
+ pub state: Arc<State>,
}
#[async_trait]
@@ -33,19 +38,20 @@ impl<'r> FromRequest<'r> for RequestInfo<'r> {
impl<'a> RequestInfo<'a> {
pub async fn from_request_ut(request: &'a Request<'_>) -> MyResult<Self> {
+ let state: &Arc<State> = request.rocket().state().unwrap();
Ok(Self {
lang: accept_language(request),
accept: Accept::from_request_ut(request),
- user: None,
- // session: session_from_request(request).await?,
+ user: user_from_request(state, request)?,
+ state: state.clone(),
})
}
- pub fn render_info(&self) -> RenderInfo<'a> {
+ pub fn render_info(&'a self) -> RenderInfo<'a> {
RenderInfo {
lang: self.lang,
status_message: None,
user: self.user.as_ref().map(|u| u.as_object()),
- config: CONF.ui,
+ config: &self.state.config.ui,
}
}
}
@@ -77,7 +83,7 @@ impl Accept {
}
}
-pub(super) fn accept_language<'a>(request: &'a Request<'_>) -> &'a str {
+fn accept_language<'a>(request: &'a Request<'_>) -> &'a str {
request
.headers()
.get_one("accept-language")
@@ -93,27 +99,25 @@ pub(super) fn accept_language<'a>(request: &'a Request<'_>) -> &'a str {
.unwrap_or("en")
}
-pub(super) async fn user_from_request(req: &Request<'_>) -> Result<Session, MyError> {
- if cfg!(feature = "bypass-auth") {
- Ok(bypass_auth_session()?)
- } else {
- 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"))?;
+fn user_from_request(state: &State, req: &Request<'_>) -> Result<Option<ObjectBuffer>, MyError> {
+ let Some(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()))
+ else {
+ return Ok(None);
+ };
- // jellyfin urlescapes the token for *some* requests
- let token = token.replace("%3D", "=");
- Ok(token_to_user(&token)?)
- }
+ // jellyfin urlescapes the token for *some* requests
+ let token = token.replace("%3D", "=");
+ Ok(Some(token_to_user(state, &token)?))
}
fn parse_jellyfin_auth(h: &str) -> Option<&str> {
diff --git a/server/src/routes.rs b/server/src/routes.rs
index 3b410f7..62a70b7 100644
--- a/server/src/routes.rs
+++ b/server/src/routes.rs
@@ -1,69 +1,49 @@
-use crate::config::CONF;
/*
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::logic::playersync::{PlayersyncChannels, r_playersync};
-use crate::ui::account::{r_account_login, r_account_logout, r_account_register};
-use crate::ui::admin::import::{r_admin_import, r_admin_import_post, r_admin_import_stream};
-use crate::ui::{
- account::{
- r_account_login_post, r_account_logout_post, r_account_register_post,
- settings::{r_account_settings, r_account_settings_post},
- },
- admin::{
- log::{r_admin_log, r_admin_log_stream},
- r_admin_dashboard, r_admin_invite, r_admin_remove_invite, r_admin_update_search,
- user::{r_admin_remove_user, r_admin_user, r_admin_user_permission, r_admin_users},
- },
- assets::{r_image, r_item_poster, r_node_thumbnail},
- error::{r_api_catch, r_catch},
- home::r_home,
- items::r_items,
- node::r_node,
- player::r_player,
- 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 crate::{
+ State,
api::{r_api_account_login, r_api_root, r_nodes_modified_since, r_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},
- },
+ compat::youtube::{r_youtube_channel, r_youtube_embed, r_youtube_watch},
logic::{
+ playersync::{PlayersyncChannels, r_playersync},
stream::r_stream,
userdata::{
r_node_userdata, r_node_userdata_progress, r_node_userdata_rating,
r_node_userdata_watched,
},
},
+ ui::{
+ account::{
+ r_account_login, r_account_login_post, r_account_logout, r_account_logout_post,
+ r_account_register, r_account_register_post,
+ settings::{r_account_settings, r_account_settings_post},
+ },
+ admin::{
+ import::{r_admin_import, r_admin_import_post, r_admin_import_stream},
+ log::{r_admin_log, r_admin_log_stream},
+ r_admin_dashboard, r_admin_invite, r_admin_remove_invite, r_admin_update_search,
+ user::{r_admin_remove_user, r_admin_user, r_admin_user_permission, r_admin_users},
+ },
+ assets::{r_image, r_item_poster, r_node_thumbnail},
+ error::{r_api_catch, r_catch},
+ home::r_home,
+ items::r_items,
+ node::r_node,
+ player::r_player,
+ 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 base64::Engine;
-use log::warn;
-use rand::random;
use rocket::{
- Build, Config, Rocket, catchers, config::SecretKey, fairing::AdHoc, fs::FileServer,
- http::Header, routes, shield::Shield,
+ Build, Config, Rocket, catchers, fairing::AdHoc, fs::FileServer, http::Header, routes,
+ shield::Shield,
};
+use std::sync::Arc;
#[macro_export]
macro_rules! uri {
@@ -72,7 +52,7 @@ macro_rules! uri {
};
}
-pub fn build_rocket() -> Rocket<Build> {
+pub fn build_rocket(state: Arc<State>) -> Rocket<Build> {
rocket::build()
.configure(Config {
address: std::env::var("BIND_ADDR")
@@ -81,34 +61,25 @@ pub fn build_rocket() -> Rocket<Build> {
port: std::env::var("PORT")
.map(|e| e.parse().unwrap())
.unwrap_or(8000),
- secret_key: SecretKey::derive_from(
- CONF.cookie_key
- .clone()
- .unwrap_or_else(|| {
- warn!("cookie_key not configured, generating a random one.");
- base64::engine::general_purpose::STANDARD.encode([(); 32].map(|_| random()))
- })
- .as_bytes(),
- ),
ip_header: Some("x-real-ip".into()),
..Default::default()
})
.manage(PlayersyncChannels::default())
+ .manage(state.clone())
.attach(AdHoc::on_response("set server header", |_req, res| {
res.set_header(Header::new("server", "jellything"));
Box::pin(async {})
}))
- // TODO this would be useful but needs to handle not only the entry-point
- // .attach(AdHoc::on_response("frame options", |req, resp| {
- // if !req.uri().path().as_str().starts_with("/embed") {
- // resp.set_raw_header("X-Frame-Options", "SAMEORIGIN");
- // }
- // Box::pin(async {})
- // }))
+ .attach(AdHoc::on_response("frame options", |req, resp| {
+ if !req.uri().path().as_str().starts_with("/embed") {
+ resp.set_raw_header("X-Frame-Options", "SAMEORIGIN");
+ }
+ Box::pin(async {})
+ }))
.attach(Shield::new())
.register("/", catchers![r_catch])
.register("/api", catchers![r_api_catch])
- .mount("/assets", FileServer::from(&CONF.asset_path))
+ .mount("/assets", FileServer::from(&state.config.asset_path))
.mount(
"/",
routes![