From 7aafbba4d9a5af4305479fb63c5c8a5c822935e0 Mon Sep 17 00:00:00 2001 From: _ <> Date: Wed, 25 Nov 2020 02:17:08 +0000 Subject: [PATCH] :heavy_plus_sign: Add "last seen" to server list --- Cargo.toml | 1 + handlebars/relay/relay_server_list.html | 48 +++++-- handlebars/server/file_server_dir.html | 14 +- src/relay/mod.rs | 178 ++++++++++++++++++++---- src/server/file_server.rs | 9 +- todo.md | 1 - 6 files changed, 196 insertions(+), 55 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2ced302..bfb8f78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ license = "AGPL-3.0" aho-corasick = "0.7.14" base64 = "0.12.3" blake3 = "0.3.7" +chrono = "0.4.19" ctrlc = { version = "3.1.7", features = [ "termination" ] } dashmap = "3.11.10" futures = "0.3.7" diff --git a/handlebars/relay/relay_server_list.html b/handlebars/relay/relay_server_list.html index 2c9b6a9..08d536f 100644 --- a/handlebars/relay/relay_server_list.html +++ b/handlebars/relay/relay_server_list.html @@ -6,12 +6,25 @@ body { font-family: sans-serif; } - .entry { - display: inline-block; - padding: 20px; - min-width: 50%; + td { + padding: 0px; } - .entry_list div:nth-child(odd) { + td > * { + padding: 20px; + display: block; + } + .entry { + + } + .grey { + color: #888; + text-align: right; + } + .entry_list { + width: 100%; + border-collapse: collapse; + } + tbody tr:nth-child(odd) { background-color: #ddd; } @@ -21,19 +34,28 @@

Server list

-
- {{#if servers}} + + + + + + + + + {{#each servers}} -
- {{this.name}} -
+ + + + {{/each}} + + +
NameLast seen
{{this.name}}{{this.last_seen}}
{{else}} - (No servers are running) + (No servers have reported since this relay started) {{/if}} -
- diff --git a/handlebars/server/file_server_dir.html b/handlebars/server/file_server_dir.html index 4d4cdc0..b7b5666 100644 --- a/handlebars/server/file_server_dir.html +++ b/handlebars/server/file_server_dir.html @@ -6,19 +6,25 @@ body { font-family: sans-serif; } - .entry { - display: inline-block; + td { + padding: 0; + } + td > * { padding: 10px; - width: 100%; + display: block; + } + .entry { text-decoration: none; } .grey { color: #888; + text-align: right; } .entry_list { width: 100%; + border-collapse: collapse; } - .entry_list tr:nth-child(even) { + tbody tr:nth-child(odd) { background-color: #ddd; } diff --git a/src/relay/mod.rs b/src/relay/mod.rs index 0b44369..90a90c3 100644 --- a/src/relay/mod.rs +++ b/src/relay/mod.rs @@ -127,12 +127,32 @@ impl From <&ConfigFile> for Config { } } +use chrono::{ + DateTime, + SecondsFormat, + Utc +}; + +#[derive (Clone)] +pub struct ServerStatus { + last_seen: DateTime , +} + +impl Default for ServerStatus { + fn default () -> Self { + Self { + last_seen: Utc::now (), + } + } +} + pub struct RelayState { config: Config, handlebars: Arc >, // Key: Server ID request_rendezvous: Mutex >, + server_status: Mutex >, // Key: Request ID response_rendezvous: RwLock >, @@ -149,6 +169,7 @@ impl From <&ConfigFile> for RelayState { config: Config::from (config_file), handlebars: Arc::new (load_templates (&PathBuf::new ()).unwrap ()), request_rendezvous: Default::default (), + server_status: Default::default (), response_rendezvous: Default::default (), shutdown_watch_tx, shutdown_watch_rx, @@ -205,6 +226,16 @@ async fn handle_http_listen ( return trip_error; } + // End of early returns + + { + let mut server_status = state.server_status.lock ().await; + + let mut status = server_status.entry (watcher_code.clone ()).or_insert_with (Default::default); + + status.last_seen = Utc::now (); + } + use RequestRendezvous::*; let (tx, rx) = oneshot::channel (); @@ -454,6 +485,99 @@ async fn handle_http_request ( } } +#[derive (Debug, PartialEq)] +enum LastSeen { + Negative, + Connected, + Description (String), +} + +// Mnemonic is "now - last_seen" + +fn pretty_print_last_seen ( + now: DateTime , + last_seen: DateTime +) -> LastSeen +{ + use LastSeen::*; + + let dur = now.signed_duration_since (last_seen); + + if dur < chrono::Duration::zero () { + return Negative; + } + + if dur.num_minutes () < 1 { + return Connected; + } + + if dur.num_hours () < 1 { + return Description (format! ("{} m ago", dur.num_minutes ())); + } + + if dur.num_days () < 1 { + return Description (format! ("{} h ago", dur.num_hours ())); + } + + Description (last_seen.to_rfc3339_opts (SecondsFormat::Secs, true)) +} + + +async fn handle_server_list ( + state: Arc +) -> Response +{ + use std::borrow::Cow; + + #[derive (Serialize)] + struct ServerEntry <'a> { + path: String, + name: String, + last_seen: Cow <'a, str>, + } + + #[derive (Serialize)] + struct ServerListPage <'a> { + servers: Vec >, + } + + let servers = { + let guard = state.server_status.lock ().await; + (*guard).clone () + }; + + let now = Utc::now (); + + let mut servers: Vec <_> = servers.into_iter () + .map (|(name, server)| { + let display_name = percent_encoding::percent_decode_str (&name).decode_utf8 ().unwrap_or_else (|_| "Server name isn't UTF-8".into ()).to_string (); + + use LastSeen::*; + + let last_seen = match pretty_print_last_seen (now, server.last_seen) { + Negative => "Error (negative time)".into (), + Connected => "Connected".into (), + Description (s) => s.into (), + }; + + ServerEntry { + name: display_name, + path: name, + last_seen: last_seen, + } + }) + .collect (); + + servers.sort_by (|a, b| a.name.cmp (&b.name)); + + let page = ServerListPage { + servers, + }; + + let s = state.handlebars.render ("relay_server_list", &page).unwrap (); + ok_reply (s) +} + #[instrument (level = "trace", skip (req, state))] async fn handle_all (req: Request , state: Arc ) -> Result , Infallible> @@ -487,35 +611,7 @@ async fn handle_all (req: Request , state: Arc ) } else if let Some (rest) = prefix_match ("/frontend/servers/", path) { if rest == "" { - use std::borrow::Cow; - - #[derive (Serialize)] - struct ServerEntry <'a> { - path: &'a str, - name: Cow <'a, str>, - } - - #[derive (Serialize)] - struct ServerListPage <'a> { - servers: Vec >, - } - - let mut names = state.list_servers ().await; - names.sort (); - - //println! ("Found {} servers", names.len ()); - - let page = ServerListPage { - servers: names.iter () - .map (|name| ServerEntry { - name: percent_encoding::percent_decode_str (name).decode_utf8 ().unwrap_or_else (|_| "Server name isn't UTF-8".into ()), - path: &name, - }) - .collect (), - }; - - let s = state.handlebars.render ("relay_server_list", &page).unwrap (); - ok_reply (s) + handle_server_list (state).await } else if let Some (idx) = rest.find ('/') { let listen_code = String::from (&rest [0..idx]); @@ -635,5 +731,29 @@ pub async fn run_relay ( #[cfg (test)] mod tests { + use super::*; + #[test] + fn test_pretty_print_last_seen () { + use LastSeen::*; + + let last_seen = DateTime::parse_from_rfc3339 ("2019-05-29T00:00:00+00:00").unwrap ().with_timezone (&Utc); + + for (input, expected) in vec! [ + ("2019-05-28T23:59:59+00:00", Negative), + ("2019-05-29T00:00:00+00:00", Connected), + ("2019-05-29T00:00:59+00:00", Connected), + ("2019-05-29T00:01:30+00:00", Description ("1 m ago".into ())), + ("2019-05-29T00:59:30+00:00", Description ("59 m ago".into ())), + ("2019-05-29T01:00:30+00:00", Description ("1 h ago".into ())), + ("2019-05-29T10:00:00+00:00", Description ("10 h ago".into ())), + ("2019-05-30T00:00:00+00:00", Description ("2019-05-29T00:00:00Z".into ())), + ("2019-05-30T10:00:00+00:00", Description ("2019-05-29T00:00:00Z".into ())), + ("2019-05-31T00:00:00+00:00", Description ("2019-05-29T00:00:00Z".into ())), + ].into_iter () { + let now = DateTime::parse_from_rfc3339 (input).unwrap ().with_timezone (&Utc); + let actual = pretty_print_last_seen (now, last_seen); + assert_eq! (actual, expected); + } + } } diff --git a/src/server/file_server.rs b/src/server/file_server.rs index c5f6e9a..1e05bbc 100644 --- a/src/server/file_server.rs +++ b/src/server/file_server.rs @@ -685,20 +685,13 @@ mod tests { ffi::OsStr, path::{ Component, - Path, - PathBuf + Path, }, }; use maplit::*; use tokio::runtime::Runtime; - use always_equal::test::AlwaysEqual; - - use crate::http_serde::{ - StatusCode, - }; - #[test] fn icons () { let video = "🎞️"; diff --git a/todo.md b/todo.md index 1327803..eb775f8 100644 --- a/todo.md +++ b/todo.md @@ -10,7 +10,6 @@ - ETag cache based on mtime - Server-side hash? - Log / audit log? -- Add "Last check-in time" to server list - Prevent directory traversal attacks in file_server.rs - Error handling