diff options
author | metamuffin <metamuffin@disroot.org> | 2023-08-05 22:25:25 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2023-08-05 22:45:30 +0200 |
commit | 7b0cdc8edec53f2b084ef28a8d6a537f1ebdd9ed (patch) | |
tree | 7dd547ece6eaa2d5e4c6f117b20db3ad5730b594 /server/src | |
parent | 246fcc704621d7c9626c990ded29b82abab47c8b (diff) | |
download | jellything-7b0cdc8edec53f2b084ef28a8d6a537f1ebdd9ed.tar jellything-7b0cdc8edec53f2b084ef28a8d6a537f1ebdd9ed.tar.bz2 jellything-7b0cdc8edec53f2b084ef28a8d6a537f1ebdd9ed.tar.zst |
in-browser server log
Diffstat (limited to 'server/src')
-rw-r--r-- | server/src/main.rs | 7 | ||||
-rw-r--r-- | server/src/routes/mod.rs | 3 | ||||
-rw-r--r-- | server/src/routes/ui/account/session/guard.rs | 56 | ||||
-rw-r--r-- | server/src/routes/ui/account/session/mod.rs | 2 | ||||
-rw-r--r-- | server/src/routes/ui/admin/log.rs | 216 | ||||
-rw-r--r-- | server/src/routes/ui/admin/mod.rs | 38 | ||||
-rw-r--r-- | server/src/routes/ui/style/layout.css | 11 |
7 files changed, 281 insertions, 52 deletions
diff --git a/server/src/main.rs b/server/src/main.rs index 60b5135..3da0e43 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -5,6 +5,7 @@ */ #![feature(lazy_cell)] +use crate::routes::ui::admin::log::enable_logging; use database::Database; use federation::Federation; use jellycommon::config::GlobalConfig; @@ -22,11 +23,7 @@ pub static CONF: Lazy<GlobalConfig> = Lazy::new(|| serde_json::from_reader(File::open("data/config.json").unwrap()).unwrap()); fn main() { - env_logger::builder() - .filter_level(log::LevelFilter::Info) - .parse_env("LOG") - .init(); - + enable_logging(); #[cfg(feature = "bypass-auth")] log::warn!("authentification bypass enabled"); diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index 1490290..38d0390 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -22,7 +22,7 @@ use ui::{ settings::{r_account_settings, r_account_settings_post}, }, admin::{ - r_admin_dashboard, r_admin_import, r_admin_invite, r_admin_remove_invite, + log::r_admin_log, r_admin_dashboard, r_admin_import, r_admin_invite, r_admin_remove_invite, r_admin_remove_user, }, assets::r_item_assets, @@ -104,6 +104,7 @@ pub fn build_rocket( r_admin_remove_user, r_admin_remove_invite, r_admin_import, + r_admin_log, r_account_settings, r_account_settings_post, r_api_version, diff --git a/server/src/routes/ui/account/session/guard.rs b/server/src/routes/ui/account/session/guard.rs index e2bc093..19d68ad 100644 --- a/server/src/routes/ui/account/session/guard.rs +++ b/server/src/routes/ui/account/session/guard.rs @@ -3,11 +3,13 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2023 metamuffin <metamuffin.org> */ -use super::Session; +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, @@ -40,31 +42,43 @@ impl Session { } } +#[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(()) + } + } + } +} - fn from_request<'life0, 'async_trait>( +#[async_trait] +impl<'r> FromRequest<'r> for AdminSession { + type Error = MyError; + async fn from_request<'life0>( request: &'r Request<'life0>, - ) -> core::pin::Pin< - Box< - dyn core::future::Future<Output = request::Outcome<Self, Self::Error>> - + core::marker::Send - + 'async_trait, - >, - > - where - 'r: 'async_trait, - 'life0: 'async_trait, - Self: 'async_trait, - { - Box::pin(async move { - match Self::from_request_ut(request).await { - Ok(x) => Outcome::Success(x), - Err(e) => { - warn!("authentificated route rejected: {e:?}"); - Outcome::Forward(()) + ) -> request::Outcome<Self, Self::Error> { + match Session::from_request_ut(request).await { + Ok(x) => { + if x.user.admin { + Outcome::Success(AdminSession(x)) + } else { + Outcome::Failure(( + Status::Unauthorized, + MyError(anyhow!("you are not an admin")), + )) } } - }) + Err(e) => { + warn!("authentificated route rejected: {e:?}"); + Outcome::Forward(()) + } + } } } diff --git a/server/src/routes/ui/account/session/mod.rs b/server/src/routes/ui/account/session/mod.rs index 2a7908f..89592c3 100644 --- a/server/src/routes/ui/account/session/mod.rs +++ b/server/src/routes/ui/account/session/mod.rs @@ -15,6 +15,8 @@ pub struct Session { pub user: User, } +pub struct AdminSession(pub Session); + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SessionData { username: String, diff --git a/server/src/routes/ui/admin/log.rs b/server/src/routes/ui/admin/log.rs new file mode 100644 index 0000000..e264e8f --- /dev/null +++ b/server/src/routes/ui/admin/log.rs @@ -0,0 +1,216 @@ +use crate::{ + routes::ui::{ + account::session::AdminSession, + error::MyResult, + layout::{DynLayoutPage, LayoutPage}, + }, + uri, +}; +use log::Level; +use rocket::get; +use std::{ + collections::VecDeque, + fmt::Write, + sync::{LazyLock, RwLock}, +}; + +const MAX_LOG_LEN: usize = 4000; + +static LOGGER: LazyLock<Log> = LazyLock::new(Log::new); + +pub fn enable_logging() { + log::set_logger(&*LOGGER).unwrap(); + log::set_max_level(log::LevelFilter::Debug); +} + +pub struct Log { + inner: env_logger::Logger, + log: RwLock<(VecDeque<LogLine>, VecDeque<LogLine>)>, +} + +pub struct LogLine { + module: Option<&'static str>, + level: Level, + message: String, +} + +#[get("/admin/log?<warnonly>")] +pub fn r_admin_log<'a>(_session: AdminSession, warnonly: bool) -> MyResult<DynLayoutPage<'a>> { + Ok(LayoutPage { + title: "Log".into(), + content: markup::new! { + h1 { "Server Log" } + a[href=uri!(r_admin_log(!warnonly))] { @if warnonly { "Show everything" } else { "Show only warnings" }} + code.log { + @let g = LOGGER.log.read().unwrap(); + table { @for e in if warnonly { g.1.iter() } else { g.0.iter() } { + tr[class=format!("level-{:?}", e.level).to_ascii_lowercase()] { + td { @format_level(e.level) } + td { @e.module } + td { @markup::raw(vt100_to_html(&e.message)) } + } + }} + } + }, + ..Default::default() + }) +} + +impl Log { + pub fn new() -> Self { + Self { + inner: env_logger::builder() + .filter_level(log::LevelFilter::Warn) + .parse_env("LOG") + .build(), + log: Default::default(), + } + } + fn should_log(&self, metadata: &log::Metadata) -> bool { + let level = metadata.level(); + level + <= match metadata.target() { + x if x.starts_with("jellything::") => Level::Debug, + x if x.starts_with("rocket::") => Level::Info, + _ => Level::Warn, + } + } + fn do_log(&self, record: &log::Record) { + let mut w = self.log.write().unwrap(); + w.0.push_back(LogLine { + module: record.module_path_static(), + level: record.level(), + message: record.args().to_string(), + }); + while w.0.len() > MAX_LOG_LEN { + w.0.pop_front(); + } + if record.level() <= Level::Warn { + w.1.push_back(LogLine { + module: record.module_path_static(), + level: record.level(), + message: record.args().to_string(), + }); + while w.1.len() > MAX_LOG_LEN { + w.1.pop_front(); + } + } + } +} + +impl log::Log for Log { + fn enabled(&self, metadata: &log::Metadata) -> bool { + self.inner.enabled(metadata) || self.should_log(metadata) + } + fn log(&self, record: &log::Record) { + match (record.module_path_static(), record.line()) { + // TODO is there a better way to ignore those? + (Some("rocket::rocket"), Some(670)) => return, + (Some("rocket::server"), Some(401)) => return, + _ => {} + } + if self.inner.enabled(record.metadata()) { + self.inner.log(record); + } + if self.should_log(record.metadata()) { + self.do_log(record) + } + } + fn flush(&self) { + self.inner.flush(); + } +} + +fn vt100_to_html(s: &str) -> String { + let mut out = HtmlOut::default(); + let mut st = vte::Parser::new(); + for b in s.bytes() { + st.advance(&mut out, b); + } + out.s +} + +fn format_level(level: Level) -> impl markup::Render { + let (s, c) = match level { + Level::Debug => ("DEBUG", "blue"), + Level::Error => ("ERROR", "red"), + Level::Warn => ("WARN", "yellow"), + Level::Info => ("INFO", "green"), + Level::Trace => ("TRACE", "lightblue"), + }; + markup::new! { span[style=format!("color:{c}")] {@s} } +} + +pub struct HtmlOut { + s: String, + color: bool, +} +impl Default for HtmlOut { + fn default() -> Self { + Self { + color: false, + s: String::new(), + } + } +} +impl HtmlOut { + pub fn set_color(&mut self, [r, g, b]: [u8; 3]) { + self.reset_color(); + self.color = true; + write!(self.s, "<span style=color:#{:02x}{:02x}{:02x}>", r, g, b).unwrap() + } + pub fn reset_color(&mut self) { + if self.color { + write!(self.s, "</span>").unwrap(); + self.color = false; + } + } +} +impl vte::Perform for HtmlOut { + fn print(&mut self, c: char) { + match c { + 'a'..='z' | 'A'..='Z' | '0'..='9' | ' ' => self.s.push(c), + x => write!(self.s, "&#{};", x as u32).unwrap(), + } + } + + fn execute(&mut self, _byte: u8) {} + fn hook(&mut self, _params: &vte::Params, _i: &[u8], _ignore: bool, _a: char) {} + fn put(&mut self, _byte: u8) {} + fn unhook(&mut self) {} + fn osc_dispatch(&mut self, _params: &[&[u8]], _bell_terminated: bool) {} + fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, _byte: u8) { + // self.s += &format!(" {:?} ", (_intermediates, _byte)) + } + + fn csi_dispatch( + &mut self, + params: &vte::Params, + _intermediates: &[u8], + _ignore: bool, + action: char, + ) { + let mut k = params.iter(); + // self.s += &format!(" {:?} ", (params, action)); + match action { + 'm' => match k.next().unwrap_or(&[0]).get(0).unwrap_or(&0) { + c @ (30..=37 | 40..=47) => { + let c = if *c >= 40 { *c - 10 } else { *c }; + self.set_color(match c { + 30 => [0, 0, 0], + 31 => [255, 0, 0], + 32 => [0, 255, 0], + 33 => [255, 255, 0], + 34 => [0, 0, 255], + 35 => [255, 0, 255], + 36 => [0, 255, 255], + 37 => [255, 255, 255], + _ => unreachable!(), + }); + } + _ => (), + }, + _ => (), + } + } +} diff --git a/server/src/routes/ui/admin/mod.rs b/server/src/routes/ui/admin/mod.rs index 0775423..f779700 100644 --- a/server/src/routes/ui/admin/mod.rs +++ b/server/src/routes/ui/admin/mod.rs @@ -3,12 +3,15 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2023 metamuffin <metamuffin.org> */ +pub mod log; + +use super::account::session::AdminSession; use crate::{ database::Database, federation::Federation, import::import, routes::ui::{ - account::session::Session, + admin::log::rocket_uri_macro_r_admin_log, error::MyResult, layout::{DynLayoutPage, FlashDisplay, LayoutPage}, }, @@ -21,12 +24,9 @@ use std::time::Instant; #[get("/admin/dashboard")] pub fn r_admin_dashboard( - session: Session, + _session: AdminSession, database: &State<Database>, ) -> MyResult<DynLayoutPage<'static>> { - if !session.user.admin { - Err(anyhow!("you not admin"))? - } admin_dashboard(database, None) } @@ -44,6 +44,10 @@ pub fn admin_dashboard<'a>( content: markup::new! { h1 { "Admin Panel" } @FlashDisplay { flash: flash.clone() } + ul { + li{a[href=uri!(r_admin_log(true))] { "Server Log (Warnings only)" }} + li{a[href=uri!(r_admin_log(false))] { "Server Log (Full) " }} + } h2 { "Library" } form[method="POST", action=uri!(r_admin_import())] { input[type="submit", value="(Re-)Import Library"]; @@ -76,13 +80,9 @@ pub fn admin_dashboard<'a>( #[post("/admin/generate_invite")] pub fn r_admin_invite( - session: Session, + _session: AdminSession, database: &State<Database>, ) -> MyResult<DynLayoutPage<'static>> { - if !session.user.admin { - Err(anyhow!("you not admin"))? - } - let i = format!("{}", rand::thread_rng().gen::<u128>()); database.invite.insert(&i, &())?; @@ -96,13 +96,11 @@ pub struct DeleteUser { #[post("/admin/remove_user", data = "<form>")] pub fn r_admin_remove_user( - session: Session, + session: AdminSession, database: &State<Database>, form: Form<DeleteUser>, ) -> MyResult<DynLayoutPage<'static>> { - if !session.user.admin { - Err(anyhow!("you not admin"))? - } + drop(session); database .user .remove(&form.name)? @@ -118,13 +116,11 @@ pub struct DeleteInvite { #[post("/admin/remove_invite", data = "<form>")] pub fn r_admin_remove_invite( - session: Session, + session: AdminSession, database: &State<Database>, form: Form<DeleteInvite>, ) -> MyResult<DynLayoutPage<'static>> { - if !session.user.admin { - Err(anyhow!("you not admin"))? - } + drop(session); database .invite .remove(&form.invite)? @@ -135,13 +131,11 @@ pub fn r_admin_remove_invite( #[post("/admin/import")] pub async fn r_admin_import( - session: Session, + session: AdminSession, database: &State<Database>, federation: &State<Federation>, ) -> MyResult<DynLayoutPage<'static>> { - if !session.user.admin { - Err(anyhow!("you not admin"))? - } + drop(session); let t = Instant::now(); let r = import(&database, &federation).await; admin_dashboard( diff --git a/server/src/routes/ui/style/layout.css b/server/src/routes/ui/style/layout.css index 09da718..e62e59a 100644 --- a/server/src/routes/ui/style/layout.css +++ b/server/src/routes/ui/style/layout.css @@ -33,12 +33,13 @@ * { color: var(--font); - font-family: "Cantarell", sans-serif; - font-weight: 500; - scrollbar-width: thin; scrollbar-color: var(--background-light) #0000; } +:root { + font-family: "Cantarell", sans-serif; + font-weight: 500; +} body { background-color: var(--background-dark); @@ -68,6 +69,10 @@ nav { align-items: center; } +code { + font-family: monospace !important; +} + nav a { border: 0px solid transparent; border-radius: 5px; |