From 28ce6a32cd03d9c4961033da6bc2c78988294be2 Mon Sep 17 00:00:00 2001 From: _ <> Date: Thu, 26 Nov 2020 23:30:33 +0000 Subject: [PATCH] :lipstick: Show servers in the server list even if they aren't connected yet --- Cargo.toml | 3 +- handlebars/relay/relay_server_list.html | 2 +- src/bin/ptth_relay.rs | 9 +- src/lib.rs | 12 ++- src/relay/config.rs | 108 ++++++++++++++++++++ src/relay/mod.rs | 125 +++++++++--------------- todo.md | 2 - 7 files changed, 171 insertions(+), 90 deletions(-) create mode 100644 src/relay/config.rs diff --git a/Cargo.toml b/Cargo.toml index bfb8f78..e85a51d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ futures = "0.3.7" handlebars = "3.5.1" http = "0.2.1" hyper = "0.13.8" +itertools = "0.9.0" lazy_static = "1.4.0" maplit = "1.0.2" percent-encoding = "2.1.0" @@ -30,7 +31,7 @@ reqwest = { version = "0.10.8", features = ["stream"] } rmp-serde = "0.14.4" serde = {version = "1.0.117", features = ["derive"]} structopt = "0.3.20" -# thiserror = "1.0.22" +thiserror = "1.0.22" tokio = { version = "0.2.22", features = ["full"] } tracing = "0.1.21" tracing-futures = "0.2.4" diff --git a/handlebars/relay/relay_server_list.html b/handlebars/relay/relay_server_list.html index 08d536f..3fd8a8a 100644 --- a/handlebars/relay/relay_server_list.html +++ b/handlebars/relay/relay_server_list.html @@ -46,7 +46,7 @@ {{#each servers}} - {{this.name}} + {{this.display_name}} {{this.last_seen}} {{/each}} diff --git a/src/bin/ptth_relay.rs b/src/bin/ptth_relay.rs index cd78d21..ecf8867 100644 --- a/src/bin/ptth_relay.rs +++ b/src/bin/ptth_relay.rs @@ -12,7 +12,10 @@ use tracing_subscriber::{ }; use ptth::relay; -use ptth::relay::RelayState; +use ptth::relay::{ + Config, + RelayState, +}; #[tokio::main] async fn main () -> Result <(), Box > { @@ -23,7 +26,7 @@ async fn main () -> Result <(), Box > { ; let config_path = PathBuf::from ("config/ptth_relay.toml"); - let config_file = ptth::load_toml::load_public (&config_path); + let config = Config::from_file (&config_path).await?; info! ("ptth_relay Git version: {:?}", ptth::git_version::GIT_VERSION); @@ -31,7 +34,7 @@ async fn main () -> Result <(), Box > { forced_shutdown.wrap_server ( relay::run_relay ( - Arc::new (RelayState::from (&config_file)), + Arc::new (RelayState::from (config)), shutdown_rx, Some (config_path) ) diff --git a/src/lib.rs b/src/lib.rs index bc89e25..9877e04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,6 +43,7 @@ pub fn password_is_bad (mut password: String) -> bool { #[cfg (test)] mod tests { use std::{ + convert::TryFrom, sync::{ Arc, }, @@ -104,14 +105,17 @@ mod tests { let api_key = "AnacondaHardcoverGrannyUnlatchLankinessMutate"; let tripcode = base64::encode (blake3::hash (api_key.as_bytes ()).as_bytes ()); debug! ("Relay is expecting tripcode {}", tripcode); - let config_file = relay::ConfigFile { + let config_file = relay::config::file::Config { port: None, - server_tripcodes: hashmap! { - server_name.into () => tripcode, + servers: hashmap! { + server_name.into () => relay::config::file::Server { + tripcode, + display_name: None, + }, }, }; - let relay_state = Arc::new (relay::RelayState::from (&config_file)); + let relay_state = Arc::new (relay::RelayState::from (relay::config::Config::try_from (config_file).unwrap ())); let relay_state_2 = relay_state.clone (); let (stop_relay_tx, stop_relay_rx) = oneshot::channel (); diff --git a/src/relay/config.rs b/src/relay/config.rs new file mode 100644 index 0000000..d48b2df --- /dev/null +++ b/src/relay/config.rs @@ -0,0 +1,108 @@ +use std::{ + collections::*, + convert::{TryFrom, TryInto}, + iter::FromIterator, + path::Path, +}; + +use serde::Deserialize; +use thiserror::Error; + +#[derive (Error, Debug)] +pub enum ConfigError { + #[error ("I/O error")] + Io (#[from] std::io::Error), + + #[error ("UTF-8 decoding failed")] + Utf8 (#[from] std::string::FromUtf8Error), + + #[error ("TOML parsing failed")] + Toml (#[from] toml::de::Error), + + #[error ("base64 decoding failed")] + Base64Decode (#[from] base64::DecodeError), + + #[error ("tripcode not 32 bytes after decoding")] + TripcodeBadLength, + + #[error ("unknown config error")] + Unknown, +} + +// Stuff we need to load from the config file and use to +// set up the HTTP server + +pub mod file { + use super::*; + + #[derive (Deserialize)] + pub struct Server { + pub tripcode: String, + pub display_name: Option , + } + + #[derive (Deserialize)] + pub struct Config { + pub port: Option , + pub servers: HashMap , + } +} + +// Stuff we actually need at runtime + +pub struct Server { + pub tripcode: blake3::Hash, + pub display_name: Option , +} + +pub struct Config { + pub servers: HashMap , +} + +impl TryFrom for Server { + type Error = ConfigError; + + fn try_from (f: file::Server) -> Result { + let bytes: Vec = base64::decode (f.tripcode)?; + let bytes: [u8; 32] = (&bytes [..]).try_into ().map_err (|_| ConfigError::TripcodeBadLength)?; + + let tripcode = blake3::Hash::from (bytes); + + Ok (Self { + tripcode, + display_name: f.display_name, + }) + } +} + +impl TryFrom for Config { + type Error = ConfigError; + + fn try_from (f: file::Config) -> Result { + let servers = f.servers.into_iter () + .map (|(k, v)| Ok::<_, ConfigError> ((k, v.try_into ()?))); + + let servers = itertools::process_results (servers, |i| HashMap::from_iter (i))?; + + Ok (Self { + servers, + }) + } +} + +impl Config { + pub async fn from_file (path: &Path) -> Result { + use tokio::prelude::*; + + let mut f = tokio::fs::File::open (path).await?; + + let mut buffer = vec! [0u8; 4096]; + let bytes_read = f.read (&mut buffer).await?; + buffer.truncate (bytes_read); + + let config_s = String::from_utf8 (buffer)?; + let new_config: file::Config = toml::from_str (&config_s)?; + + Self::try_from (new_config) + } +} diff --git a/src/relay/mod.rs b/src/relay/mod.rs index 9b9171e..11cfdd5 100644 --- a/src/relay/mod.rs +++ b/src/relay/mod.rs @@ -27,7 +27,6 @@ use hyper::{ }; use hyper::service::{make_service_fn, service_fn}; use serde::{ - Deserialize, Serialize, }; use tokio::{ @@ -51,6 +50,10 @@ use crate::{ prefix_match, }; +pub mod config; + +pub use config::{Config, ConfigError}; + /* Here's what we need to handle: @@ -92,48 +95,6 @@ enum RequestRendezvous { type ResponseRendezvous = oneshot::Sender >; -// Stuff we need to load from the config file and use to -// set up the HTTP server - -#[derive (Default, Deserialize)] -pub struct ConfigServers { - pub tripcodes: HashMap , - pub display_names: HashMap , -} - -#[derive (Default, Deserialize)] -pub struct ConfigFile { - pub port: Option , - pub servers: ConfigServers, -} - -// Stuff we actually need at runtime - -struct Config { - server_tripcodes: HashMap , -} - -impl From <&ConfigFile> for Config { - fn from (f: &ConfigFile) -> Self { - let server_tripcodes = HashMap::from_iter (f.servers.tripcodes.iter () - .map (|(k, v)| { - use std::convert::TryInto; - let bytes: Vec = base64::decode (v).unwrap (); - let bytes: [u8; 32] = (&bytes [..]).try_into ().unwrap (); - - let v = blake3::Hash::from (bytes); - - debug! ("Tripcode {} => {}", k, v.to_hex ()); - - (k.clone (), v) - })); - - Self { - server_tripcodes, - } - } -} - use chrono::{ DateTime, SecondsFormat, @@ -168,12 +129,12 @@ pub struct RelayState { shutdown_watch_rx: watch::Receiver , } -impl From <&ConfigFile> for RelayState { - fn from (config_file: &ConfigFile) -> Self { +impl From for RelayState { + fn from (config: Config) -> Self { let (shutdown_watch_tx, shutdown_watch_rx) = watch::channel (false); Self { - config: Config::from (config_file).into (), + config: config.into (), handlebars: Arc::new (load_templates (&PathBuf::new ()).unwrap ()), request_rendezvous: Default::default (), server_status: Default::default (), @@ -222,12 +183,12 @@ async fn handle_http_listen ( let expected_tripcode = { let config = state.config.read ().await; - match config.server_tripcodes.get (&watcher_code) { + match config.servers.get (&watcher_code) { None => { error! ("Denied http_listen for non-existent server name {}", watcher_code); return trip_error; }, - Some (x) => (*x).clone (), + Some (x) => (*x).tripcode.clone (), } }; let actual_tripcode = blake3::hash (api_key); @@ -395,7 +356,7 @@ async fn handle_http_request ( { { let config = state.config.read ().await; - if ! config.server_tripcodes.contains_key (&watcher_code) { + if ! config.servers.contains_key (&watcher_code) { return error_reply (StatusCode::NOT_FOUND, "Unknown server"); } } @@ -538,8 +499,8 @@ fn pretty_print_last_seen ( #[derive (Serialize)] struct ServerEntry <'a> { - path: String, - name: String, + id: String, + display_name: String, last_seen: Cow <'a, str>, } @@ -551,39 +512,56 @@ struct ServerListPage <'a> { async fn handle_server_list_internal (state: &Arc ) -> ServerListPage <'static> { - let all_servers: Vec <_> = { + let display_names: HashMap = { let guard = state.config.read ().await; - (*guard).server_tripcodes.keys ().cloned ().collect () + + let servers = (*guard).servers.iter () + .map (|(k, v)| { + let display_name = v.display_name + .clone () + .unwrap_or_else (|| k.clone ()); + + (k.clone (), display_name) + }); + + HashMap::from_iter (servers) }; - let server_status = { + let server_statuses = { let guard = state.server_status.lock ().await; (*guard).clone () }; let now = Utc::now (); - let mut servers: Vec <_> = server_status.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 (); - + let mut servers: Vec <_> = display_names.into_iter () + .map (|(id, display_name)| { use LastSeen::*; - let last_seen = match pretty_print_last_seen (now, server.last_seen) { + let status = match server_statuses.get (&id) { + None => return ServerEntry { + display_name, + id, + last_seen: "Never".into (), + }, + Some (x) => x, + }; + + let last_seen = match pretty_print_last_seen (now, status.last_seen) { Negative => "Error (negative time)".into (), Connected => "Connected".into (), Description (s) => s.into (), }; ServerEntry { - name: display_name, - path: name, - last_seen: last_seen, + display_name, + id, + last_seen, } }) .collect (); - servers.sort_by (|a, b| a.name.cmp (&b.name)); + servers.sort_by (|a, b| a.display_name.cmp (&b.display_name)); ServerListPage { servers, @@ -681,26 +659,15 @@ pub fn load_templates (asset_root: &Path) async fn reload_config ( state: &Arc , config_reload_path: &Path -) -> Option <()> { - use tokio::prelude::*; - - let mut f = tokio::fs::File::open (config_reload_path).await.ok ()?; - - let mut buffer = vec! [0u8; 4096]; - let bytes_read = f.read (&mut buffer).await.ok ()?; - buffer.truncate (bytes_read); - - let config_s = String::from_utf8 (buffer).ok ()?; - let new_config: ConfigFile = toml::from_str (&config_s).ok ()?; - - let new_config = Config::from (&new_config); +) -> Result <(), ConfigError> { + let new_config = Config::from_file (config_reload_path).await?; let mut config = state.config.write ().await; (*config) = new_config; - debug! ("Loaded {} server tripcodes", config.server_tripcodes.len ()); + debug! ("Loaded {} server configs", config.servers.len ()); - Some (()) + Ok (()) } pub async fn run_relay ( @@ -722,7 +689,7 @@ pub async fn run_relay ( loop { reload_interval.tick ().await; - reload_config (&state_2, &config_reload_path).await; + reload_config (&state_2, &config_reload_path).await.ok (); } }); } diff --git a/todo.md b/todo.md index d2f7013..02488b8 100644 --- a/todo.md +++ b/todo.md @@ -1,5 +1,3 @@ -- Server list should include offline or never-launched servers -- Allow relay to rename servers - Estimate bandwidth per server? - "Preview as" feature for Markdown (It's not threaded through the relay yet) - Remote `tail -f` (_Complicated_) (Maybe use chunked encoding or something?)