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/routes | |
| parent | 246fcc704621d7c9626c990ded29b82abab47c8b (diff) | |
| download | jellything-7b0cdc8edec53f2b084ef28a8d6a537f1ebdd9ed.tar jellything-7b0cdc8edec53f2b084ef28a8d6a537f1ebdd9ed.tar.bz2 jellything-7b0cdc8edec53f2b084ef28a8d6a537f1ebdd9ed.tar.zst | |
in-browser server log
Diffstat (limited to 'server/src/routes')
| -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 | 
6 files changed, 279 insertions, 47 deletions
| 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; | 
