implement CLF option

This commit is contained in:
yggverse 2025-06-25 06:34:27 +03:00
parent 50a0a567a8
commit ad9bb1ae49
7 changed files with 72 additions and 16 deletions

View file

@ -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"

View file

@ -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 <ACCESS_LOG>
Absolute path to the access log file
-b, --bind <BIND>
Bind server(s) `host:port` to listen incoming connections

View file

@ -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<String>,
/// Bind server(s) `host:port` to listen incoming connections
///
/// * use `[host]:port` notation for IPv6

View file

@ -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) => self.response(Response::InternalServerError(format!(
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);
}
}
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) {

View file

@ -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<Self> {
Ok(Self {
debug: Debug::init(config)?,
log: Log::init(config)?,
storage: Storage::init(config)?,
template: Template::init(config)?,
})

View file

@ -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()
}

37
src/session/log.rs Normal file
View file

@ -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<RwLock<File>>);
impl Log {
pub fn init(config: &crate::config::Config) -> anyhow::Result<Self> {
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()
}
}
}