rust-web-server 18.16.0

Single-binary web server and reverse proxy: automatic HTTPS (Let's Encrypt), HTTP/1.1, HTTP/2 and HTTP/3, load balancing, auth and rate limiting, configured in one TOML file. Also a Rust library: routing, middleware, MCP server, ORM, background jobs. No hyper, no Tower; HTTP/2 and HTTP/3 framing via h2 and quinn.
Documentation
//! Traffic insights (18.15.0): who visits — people, AI agents, bots —
//! worked out by rws from each request, for the admin UI's Traffic page.
//!
//! This first part is the classifier, [`classify`]: from the client's
//! address, User-Agent and path it names what kind of client this is, using
//! - the IP ranges Google, Bing, Apple, DuckDuckGo, OpenAI, Perplexity and
//!   Common Crawl publish for their crawlers (a match is *verified*; a
//!   User-Agent that claims one of them from elsewhere is *spoofed*);
//! - [ai.robots.txt](https://github.com/ai-robots-txt/ai.robots.txt)'s AI
//!   agents and [crawler-user-agents](https://github.com/monperrus/crawler-user-agents)'
//!   bot patterns (both MIT, bundled, regenerated by `ci/gen_bot_lists.py`);
//! - requests only scanners make (`/.env`, `/.git/config`, `/wp-login.php`…).
//!
//! A browser User-Agent that matches none of them is a [`Kind::Browser`]:
//! whether it's a person is decided later, by whether that client loads the
//! page's assets as a browser does.

mod classify;
pub mod config;
mod data;
pub(crate) mod persist;
pub mod record;
pub mod summary;

pub use classify::{classify, Class, Group, Kind};
pub use record::{enable, enabled};

/// Count one finished request (called with the access log line, whatever
/// the log format).
pub(crate) fn observe(request: &crate::request::Request, response: &crate::response::Response, ip: std::net::IpAddr) {
    if !enabled() {
        return;
    }
    let header = |name: &str| request.header(name).map(|h| h.value.as_ref()).filter(|v: &&str| !v.is_empty());
    let content_type = response
        .content_range_list
        .first()
        .map(|c| c.content_type.as_str())
        .or_else(|| response.headers.iter().find(|h| h.name.eq_ignore_ascii_case("content-type")).map(|h| h.value.as_ref()));
    record::record(&record::Hit {
        ip,
        host: header("host").unwrap_or(""),
        user_agent: header("user-agent"),
        referer: header("referer"),
        method: &request.method,
        path: &request.request_uri,
        status: response.status_code,
        content_type,
        now: record::now(),
    });
}

/// Start counting, with the saved counts loaded and saved again every five
/// minutes (when `persist` gives a file). The `rws` binary calls this when
/// the admin API is on and `[traffic] enabled` isn't `false`.
pub fn start() {
    if enabled() {
        return;
    }
    enable();
    let Some(path) = config::persist_path() else { return };
    persist::load(&path);
    let _ = std::thread::Builder::new().name("rws-traffic".to_string()).spawn(move || loop {
        std::thread::sleep(std::time::Duration::from_secs(300));
        if let Err(e) = persist::save(&path) {
            eprintln!("traffic: can't save {}: {e}", path.display());
        }
    });
}

/// Save the counts now (at shutdown).
pub fn save() {
    if let (true, Some(path)) = (enabled(), config::persist_path()) {
        if let Err(e) = persist::save(&path) {
            eprintln!("traffic: can't save {}: {e}", path.display());
        }
    }
}

#[cfg(test)]
mod tests;