diff --git a/Cargo.lock b/Cargo.lock index c6fc347..3bb3d07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1218,6 +1218,7 @@ dependencies = [ "serde", "structopt", "tokio", + "tracing", "tracing-subscriber", ] @@ -1257,6 +1258,7 @@ dependencies = [ "anyhow", "base64 0.12.3", "blake3", + "chrono", "futures", "handlebars", "http", @@ -1278,6 +1280,7 @@ dependencies = [ "tracing", "tracing-futures", "tracing-subscriber", + "ulid", ] [[package]] diff --git a/crates/ptth_file_server_bin/Cargo.toml b/crates/ptth_file_server_bin/Cargo.toml index bf2fc2d..04b67b4 100644 --- a/crates/ptth_file_server_bin/Cargo.toml +++ b/crates/ptth_file_server_bin/Cargo.toml @@ -15,6 +15,7 @@ hyper = "0.13.8" serde = {version = "1.0.117", features = ["derive"]} structopt = "0.3.20" tokio = { version = "0.2.22", features = ["full"] } +tracing = "0.1.21" tracing-subscriber = "0.2.15" ptth_core = { path = "../ptth_core" } diff --git a/crates/ptth_file_server_bin/src/main.rs b/crates/ptth_file_server_bin/src/main.rs index 33acbd7..affc7c1 100644 --- a/crates/ptth_file_server_bin/src/main.rs +++ b/crates/ptth_file_server_bin/src/main.rs @@ -18,13 +18,17 @@ use hyper::{ StatusCode, }; use serde::Deserialize; +use tracing::debug; use ptth_core::{ http_serde::RequestParts, prelude::*, }; use ptth_server::{ - file_server, + file_server::{ + self, + metrics::InstanceMetrics, + }, load_toml, }; @@ -36,7 +40,7 @@ pub struct Config { struct ServerState <'a> { config: Config, handlebars: handlebars::Handlebars <'a>, - server_info: file_server::ServerInfo, + instance_metrics: InstanceMetrics, hidden_path: Option , } @@ -63,7 +67,7 @@ async fn handle_all (req: Request , state: Arc >) let ptth_resp = file_server::serve_all ( &state.handlebars, - &state.server_info, + &state.instance_metrics, file_server_root, ptth_req.method, &ptth_req.uri, @@ -101,14 +105,18 @@ async fn main () -> Result <(), anyhow::Error> { let handlebars = file_server::load_templates (&PathBuf::new ())?; + let instance_metrics = InstanceMetrics::new ( + config_file.name.unwrap_or_else (|| "PTTH File Server".to_string ()) + ); + + debug! ("{:?}", instance_metrics); + let state = Arc::new (ServerState { config: Config { file_server_root: config_file.file_server_root, }, handlebars, - server_info: crate::file_server::ServerInfo { - server_name: config_file.name.unwrap_or_else (|| "PTTH File Server".to_string ()), - }, + instance_metrics, hidden_path: Some (path), }); diff --git a/crates/ptth_server/Cargo.toml b/crates/ptth_server/Cargo.toml index 81345a8..4f5b160 100644 --- a/crates/ptth_server/Cargo.toml +++ b/crates/ptth_server/Cargo.toml @@ -12,6 +12,7 @@ aho-corasick = "0.7.14" anyhow = "1.0.34" base64 = "0.12.3" blake3 = "0.3.7" +chrono = {version = "0.4.19", features = ["serde"]} futures = "0.3.7" handlebars = "3.5.1" http = "0.2.1" @@ -30,6 +31,7 @@ tracing = "0.1.21" tracing-futures = "0.2.4" tracing-subscriber = "0.2.15" toml = "0.5.7" +ulid = "0.4.1" always_equal = { path = "../always_equal" } ptth_core = { path = "../ptth_core" } diff --git a/crates/ptth_server/src/file_server/internal.rs b/crates/ptth_server/src/file_server/internal.rs index 9b5080a..de6335a 100644 --- a/crates/ptth_server/src/file_server/internal.rs +++ b/crates/ptth_server/src/file_server/internal.rs @@ -218,6 +218,9 @@ async fn serve_api ( Ok (NotFound) } +// Handle the requests internally without knowing anything about PTTH or +// HTML / handlebars + pub async fn serve_all ( root: &Path, method: Method, diff --git a/crates/ptth_server/src/file_server/metrics.rs b/crates/ptth_server/src/file_server/metrics.rs new file mode 100644 index 0000000..19f697c --- /dev/null +++ b/crates/ptth_server/src/file_server/metrics.rs @@ -0,0 +1,76 @@ +use chrono::{DateTime, Utc}; +use ulid::Ulid; + +fn serialize_ulid (t: &Ulid, s: S) +-> Result +{ + let t = t.to_string (); + s.serialize_str (&t) +} + +// Instance metrics are captured when the ptth_server process starts. +// They don't change after that. + +#[derive (Debug, serde::Serialize)] +pub struct InstanceMetrics { + // D-Bus machine ID, if we're on Linux + pub machine_id: Option , + + // Git version that ptth_server was built from (unimplemented) + pub _git_version: Option , + + // User-assigned and human-readable name for this server. + // Must be unique within a relay. + pub server_name: String, + + // Random base64 instance ID. ptth_server generates this at process start. + // It's a fallback for detecting outages without relying on any clocks. + #[serde (serialize_with = "serialize_ulid")] + pub instance_id: Ulid, + + pub startup_utc: DateTime , +} + +fn get_machine_id () -> Option { + use std::{ + fs::File, + io::Read, + }; + + let mut buf = vec! [0u8; 1024]; + let mut f = File::open ("/etc/machine-id").ok ()?; + let bytes_read = f.read (&mut buf).ok ()?; + + buf.truncate (bytes_read); + + let s = std::str::from_utf8 (&buf).ok ()?; + let s = s.trim_end ().to_string (); + + Some (s) +} + +impl InstanceMetrics { + pub fn new (server_name: String) -> Self + { + Self { + machine_id: get_machine_id (), + _git_version: None, + server_name, + instance_id: ulid::Ulid::new (), + startup_utc: Utc::now (), + } + } +} + +#[cfg (test)] +mod tests { + use super::*; + + #[test] + fn ulid_null () { + let a = InstanceMetrics::new ("bogus".to_string ()); + let b = InstanceMetrics::new ("bogus".to_string ()); + + assert_ne! (a.instance_id, b.instance_id); + } +} diff --git a/crates/ptth_server/src/file_server/mod.rs b/crates/ptth_server/src/file_server/mod.rs index 40d9d84..019fc98 100644 --- a/crates/ptth_server/src/file_server/mod.rs +++ b/crates/ptth_server/src/file_server/mod.rs @@ -8,7 +8,6 @@ use std::{ cmp::min, collections::HashMap, convert::{Infallible, TryFrom}, - fmt::Debug, io::SeekFrom, path::Path, }; @@ -38,11 +37,14 @@ use ptth_core::{ }; pub mod errors; +pub mod metrics; + mod internal; mod markdown; mod range; use errors::FileServerError; +use metrics::InstanceMetrics; mod emoji { pub const VIDEO: &str = "\u{1f39e}\u{fe0f}"; @@ -52,11 +54,6 @@ mod emoji { pub const ERROR: &str = "\u{26a0}\u{fe0f}"; } -#[derive (Debug, Serialize)] -pub struct ServerInfo { - pub server_name: String, -} - #[derive (Serialize)] struct DirEntryJson { name: String, @@ -93,7 +90,7 @@ struct DirEntryHtml { #[derive (Serialize)] struct DirHtml <'a> { #[serde (flatten)] - server_info: &'a ServerInfo, + instance_metrics: &'a InstanceMetrics, path: Cow <'a, str>, entries: Vec , @@ -191,10 +188,10 @@ async fn read_dir_entry_json (entry: DirEntry) -> Option async fn serve_root ( handlebars: &Handlebars <'static>, - server_info: &ServerInfo + instance_metrics: &InstanceMetrics ) -> Result { - let s = handlebars.render ("file_server_root", &server_info)?; + let s = handlebars.render ("file_server_root", &instance_metrics)?; Ok (serve_html (s)) } @@ -233,10 +230,10 @@ async fn serve_dir_json ( Ok (response) } -#[instrument (level = "debug", skip (handlebars, dir))] +#[instrument (level = "debug", skip (handlebars, instance_metrics, dir))] async fn serve_dir_html ( handlebars: &Handlebars <'static>, - server_info: &ServerInfo, + instance_metrics: &InstanceMetrics, path: Cow <'_, str>, mut dir: ReadDir ) -> Result @@ -252,7 +249,7 @@ async fn serve_dir_html ( let s = handlebars.render ("file_server_dir", &DirHtml { path, entries, - server_info, + instance_metrics, })?; Ok (serve_html (s)) @@ -355,10 +352,14 @@ async fn serve_file ( Ok (response) } -#[instrument (level = "debug", skip (handlebars, headers))] +// Pass a request to the internal decision-making logic. +// When it returns, prettify it as HTML or JSON based on what the client +// asked for. + +#[instrument (level = "debug", skip (handlebars, headers, instance_metrics))] pub async fn serve_all ( handlebars: &Handlebars <'static>, - server_info: &ServerInfo, + instance_metrics: &InstanceMetrics, root: &Path, method: Method, uri: &str, @@ -404,14 +405,14 @@ pub async fn serve_all ( }, InvalidQuery => serve_error (StatusCode::BadRequest, "Query is invalid for this object\n"), - Root => serve_root (handlebars, server_info).await?, + Root => serve_root (handlebars, instance_metrics).await?, ServeDir (internal::ServeDirParams { path, dir, format }) => match format { OutputFormat::Json => serve_dir_json (dir.into_inner ()).await?, - OutputFormat::Html => serve_dir_html (handlebars, server_info, path.to_string_lossy (), dir.into_inner ()).await?, + OutputFormat::Html => serve_dir_html (handlebars, instance_metrics, path.to_string_lossy (), dir.into_inner ()).await?, }, ServeFile (internal::ServeFileParams { file, diff --git a/crates/ptth_server/src/lib.rs b/crates/ptth_server/src/lib.rs index bccc824..3fed368 100644 --- a/crates/ptth_server/src/lib.rs +++ b/crates/ptth_server/src/lib.rs @@ -51,11 +51,14 @@ pub fn password_is_bad (mut password: String) -> bool { struct ServerState { config: Config, handlebars: Handlebars <'static>, - server_info: file_server::ServerInfo, + instance_metrics: file_server::metrics::InstanceMetrics, client: Client, hidden_path: Option , } +// Unwrap a request from PTTH format and pass it into file_server. +// When file_server responds, wrap it back up and stream it to the relay. + async fn handle_one_req ( state: &Arc , wrapped_req: http_serde::WrappedRequest @@ -72,7 +75,7 @@ async fn handle_one_req ( let response = file_server::serve_all ( &state.handlebars, - &state.server_info, + &state.instance_metrics, file_server_root, parts.method, &parts.uri, @@ -186,10 +189,6 @@ pub async fn run_server ( return Err (ServerError::WeakApiKey); } - let server_info = file_server::ServerInfo { - server_name: config_file.name.clone (), - }; - info! ("Server name is {}", config_file.name); info! ("Tripcode is {}", config_file.tripcode ()); @@ -202,13 +201,15 @@ pub async fn run_server ( .build ().map_err (ServerError::CantBuildHttpClient)?; let handlebars = file_server::load_templates (&asset_root)?; + let instance_metrics = file_server::metrics::InstanceMetrics::new (config_file.name); + let state = Arc::new (ServerState { config: Config { relay_url: config_file.relay_url, file_server_root: config_file.file_server_root, }, handlebars, - server_info, + instance_metrics, client, hidden_path, }); @@ -233,7 +234,7 @@ pub async fn run_server ( debug! ("http_listen"); - let req_req = state.client.get (&format! ("{}/http_listen/{}", state.config.relay_url, config_file.name)).send (); + let req_req = state.client.get (&format! ("{}/http_listen/{}", state.config.relay_url, state.instance_metrics.server_name)).send (); let err_backoff_delay = std::cmp::min (30_000, backoff_delay * 2 + 500);