diff --git a/Cargo.toml b/Cargo.toml index 3ac7316..a4f72d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nexy" -version = "0.2.1" +version = "0.3.0" edition = "2024" license = "MIT" readme = "README.md" @@ -11,5 +11,6 @@ repository = "https://github.com/YGGverse/nexy" [dependencies] anyhow = "1.0" +chrono = "^0.4.20" clap = { version = "4.5", features = ["derive"] } urlencoding = "2.1" diff --git a/README.md b/README.md index a92a030..dedc37c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,12 @@ [![Dependencies](https://deps.rs/repo/github/yggverse/nexy/status.svg)](https://deps.rs/repo/github/yggverse/nexy) [![crates.io](https://img.shields.io/crates/v/nexy)](https://crates.io/crates/nexy) -Run server accessible to Internet IPv4/IPv6, [Yggdrasil](https://yggdrasil-network.github.io/), [Mycelium](https://github.com/threefoldtech/mycelium), and other networks simultaneously, as many as desired. Optimized for streaming large files (in chunks) without memory overload on reading. +## Features + +* Run server accessible to Internet IPv4/IPv6, [Yggdrasil](https://yggdrasil-network.github.io/), [Mycelium](https://github.com/threefoldtech/mycelium), and other networks simultaneously, as many as desired +* Optimized for streaming large files (in chunks) without memory overload on buffering the data +* Supports the [CLF](https://en.wikipedia.org/wiki/Common_Log_Format) access log, which is compatible with analytics tools such as [GoAccess](https://goaccess.io/), [GoatCounter](https://www.goatcounter.com/) or just [htcount](https://github.com/yggverse/htcount) +* See the [Options](#options) section for a complete list of other features. ## Install @@ -22,6 +27,9 @@ nexy -p /path/to/public_dir ### Options ``` bash +-a, --access-log + Absolute path to the access log file + -b, --bind Bind server(s) `host:port` to listen incoming connections diff --git a/src/config.rs b/src/config.rs index f485563..ebf68c3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,6 +7,10 @@ const PORT: u16 = 1900; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] pub struct Config { + /// Absolute path to the access log file + #[arg(short, long)] + pub access_log: Option, + /// Bind server(s) `host:port` to listen incoming connections /// /// * use `[host]:port` notation for IPv6 diff --git a/src/server/connection.rs b/src/server/connection.rs index 15474ea..91f4d62 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -32,6 +32,7 @@ impl Connection { } pub fn handle(mut self) { + let mut t = 0; // total bytes match self.request() { Ok(q) => { self.session.debug.info(&format!( @@ -41,12 +42,16 @@ impl Connection { self.session .clone() .storage - .request(&q, |r| self.response(r)) + .request(&q, |r| t += self.response(r)); // chunk loop + self.session.log.clf(&self.address.client, Some(&q), 0, t); + } + Err(e) => { + t += self.response(Response::InternalServerError(format!( + "[{}] < [{}] failed to handle incoming request: `{e}`", + self.address.server, self.address.client + ))); + self.session.log.clf(&self.address.client, None, 1, t); } - Err(e) => self.response(Response::InternalServerError(format!( - "[{}] < [{}] failed to handle incoming request: `{e}`", - self.address.server, self.address.client - ))), } self.shutdown() } @@ -57,7 +62,7 @@ impl Connection { Ok(urlencoding::decode(std::str::from_utf8(&b[..n])?.trim())?.to_string()) } - fn response(&mut self, response: Response) { + fn response(&mut self, response: Response) -> usize { let bytes = match response { Response::File(b) => b, Response::Directory(ref s, is_root) => { @@ -97,7 +102,8 @@ impl Connection { "[{}] ! [{}] failed to response: `{e}`", self.address.server, self.address.client, )), - } + }; + bytes.len() } fn shutdown(self) { diff --git a/src/session.rs b/src/session.rs index 3cba573..edd0d89 100644 --- a/src/session.rs +++ b/src/session.rs @@ -1,12 +1,14 @@ mod debug; +mod log; mod storage; mod template; -use {debug::Debug, storage::Storage, template::Template}; +use {debug::Debug, log::Log, storage::Storage, template::Template}; -/// Single container for the current session +/// Shared, multi-thread features for the current server session pub struct Session { pub debug: Debug, + pub log: Log, pub storage: Storage, pub template: Template, } @@ -15,6 +17,7 @@ impl Session { pub fn init(config: &crate::config::Config) -> anyhow::Result { Ok(Self { debug: Debug::init(config)?, + log: Log::init(config)?, storage: Storage::init(config)?, template: Template::init(config)?, }) diff --git a/src/session/debug.rs b/src/session/debug.rs index 347173d..85f69e0 100644 --- a/src/session/debug.rs +++ b/src/session/debug.rs @@ -25,9 +25,6 @@ impl Debug { } } -fn now() -> u128 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_millis() +fn now() -> String { + chrono::Local::now().to_rfc3339() } diff --git a/src/session/log.rs b/src/session/log.rs new file mode 100644 index 0000000..d8162ea --- /dev/null +++ b/src/session/log.rs @@ -0,0 +1,37 @@ +//! Standard access logs feature +//! that is compatible with analytics tools such as [GoAccess](https://goaccess.io/), +//! [GoatCounter](https://www.goatcounter.com/) or [htcount](https://github.com/yggverse/htcount) + +use std::{fs::File, io::Write, net::SocketAddr, sync::RwLock}; + +/// Writes log as +pub struct Log(Option>); + +impl Log { + pub fn init(config: &crate::config::Config) -> anyhow::Result { + Ok(Self(match config.access_log { + Some(ref p) => Some(RwLock::new(File::create(p)?)), + None => None, + })) + } + /// [CLF](https://en.wikipedia.org/wiki/Common_Log_Format) + /// + /// * the code value (`u8`) is relative, use 1|0 for failure / success + pub fn clf(&self, client: &SocketAddr, query: Option<&str>, code: u8, size: usize) { + if let Some(ref f) = self.0 { + f.write() + .unwrap() + .write_all( + format!( + "{} {} - [{}] \"GET {}\" {code} {size}\n", + client.ip(), + client.port(), + chrono::Local::now().format("%d/%b/%Y:%H:%M:%S %z"), + query.unwrap_or_default(), + ) + .as_bytes(), + ) + .unwrap() + } + } +}