Skip to main content

rws_proxy/traffic/
mod.rs

1//! Traffic insights (18.15.0): who visits — people, AI agents, bots —
2//! worked out by rws from each request, for the admin UI's Traffic page.
3//!
4//! This first part is the classifier, [`classify`]: from the client's
5//! address, User-Agent and path it names what kind of client this is, using
6//! - the IP ranges Google, Bing, Apple, DuckDuckGo, OpenAI, Perplexity and
7//!   Common Crawl publish for their crawlers (a match is *verified*; a
8//!   User-Agent that claims one of them from elsewhere is *spoofed*);
9//! - [ai.robots.txt](https://github.com/ai-robots-txt/ai.robots.txt)'s AI
10//!   agents and [crawler-user-agents](https://github.com/monperrus/crawler-user-agents)'
11//!   bot patterns (both MIT, bundled, regenerated by `ci/gen_bot_lists.py`);
12//! - requests only scanners make (`/.env`, `/.git/config`, `/wp-login.php`…).
13//!
14//! A browser User-Agent that matches none of them is a [`Kind::Browser`]:
15//! whether it's a person is decided later, by whether that client loads the
16//! page's assets as a browser does.
17
18mod classify;
19pub mod config;
20mod data;
21pub mod device;
22pub mod geo;
23#[doc(hidden)]
24pub mod persist;
25pub mod record;
26pub mod script;
27#[cfg(feature = "model-postgres")]
28pub mod store;
29pub mod summary;
30
31pub use classify::{classify, Class, Group, Kind};
32pub use record::{enable, enabled};
33pub use script::TrafficScriptLayer;
34
35/// Count one finished request (called with the access log line, whatever
36/// the log format).
37#[doc(hidden)]
38pub fn observe(request: &crate::request::Request, response: &crate::response::Response, ip: std::net::IpAddr) {
39    if !enabled() {
40        return;
41    }
42    let header = |name: &str| request.header(name).map(|h| h.value.as_ref()).filter(|v: &&str| !v.is_empty());
43    let content_type = response
44        .content_range_list
45        .first()
46        .map(|c| c.content_type.as_str())
47        .or_else(|| response.headers.iter().find(|h| h.name.eq_ignore_ascii_case("content-type")).map(|h| h.value.as_ref()));
48    record::record(&record::Hit {
49        ip,
50        host: header("host").unwrap_or(""),
51        user_agent: header("user-agent"),
52        referer: header("referer"),
53        method: &request.method,
54        path: &request.request_uri,
55        status: response.status_code,
56        content_type,
57        headers: &request.headers,
58        now: record::now(),
59    });
60}
61
62/// Start counting, with the saved counts loaded and saved again every five
63/// minutes (when `persist` gives a file). The `rws` binary calls this when
64/// the admin API is on and `[traffic] enabled` isn't `false`.
65pub fn start() {
66    if enabled() {
67        return;
68    }
69    enable();
70    // Decode the country table now rather than on the first visitor's request.
71    let _ = std::thread::Builder::new().name("rws-traffic-geo".to_string()).spawn(|| {
72        geo::table();
73    });
74    start_store();
75    let Some(path) = config::persist_path() else { return };
76    persist::load(&path);
77    let _ = std::thread::Builder::new().name("rws-traffic".to_string()).spawn(move || loop {
78        std::thread::sleep(std::time::Duration::from_secs(300));
79        if let Err(e) = persist::save(&path) {
80            eprintln!("traffic: can't save {}: {e}", path.display());
81        }
82    });
83}
84
85/// Connect to `[traffic] database_url_env`'s PostgreSQL, when set.
86fn start_store() {
87    let Some(url) = config::database_url() else { return };
88    #[cfg(feature = "model-postgres")]
89    match store::start(&url) {
90        Ok(()) => println!("traffic: writing page views to PostgreSQL."),
91        Err(e) => eprintln!("traffic: can't use the traffic database: {e}; counting without it"),
92    }
93    #[cfg(not(feature = "model-postgres"))]
94    {
95        let _ = url;
96        eprintln!("traffic: [traffic] database_url_env is set, but this rws is built without the model-postgres feature; counting without it");
97    }
98}
99
100/// Save the counts now (at shutdown).
101pub fn save() {
102    #[cfg(feature = "model-postgres")]
103    store::stop();
104    if let (true, Some(path)) = (enabled(), config::persist_path()) {
105        if let Err(e) = persist::save(&path) {
106            eprintln!("traffic: can't save {}: {e}", path.display());
107        }
108    }
109}
110
111#[cfg(test)]
112mod tests;