Skip to main content

rws_proxy/traffic/
config.rs

1//! `[traffic]` settings (`RWS_CONFIG_TRAFFIC_*`).
2
3use std::path::PathBuf;
4
5pub const RWS_CONFIG_TRAFFIC_ENABLED: &str = "RWS_CONFIG_TRAFFIC_ENABLED";
6pub const RWS_CONFIG_TRAFFIC_RETENTION_DAYS: &str = "RWS_CONFIG_TRAFFIC_RETENTION_DAYS";
7pub const RWS_CONFIG_TRAFFIC_PERSIST: &str = "RWS_CONFIG_TRAFFIC_PERSIST";
8pub const RWS_CONFIG_TRAFFIC_EXCLUDE: &str = "RWS_CONFIG_TRAFFIC_EXCLUDE";
9pub const RWS_CONFIG_TRAFFIC_GEO_DB: &str = "RWS_CONFIG_TRAFFIC_GEO_DB";
10pub const RWS_CONFIG_TRAFFIC_SCRIPT: &str = "RWS_CONFIG_TRAFFIC_SCRIPT";
11pub const RWS_CONFIG_TRAFFIC_GOALS: &str = "RWS_CONFIG_TRAFFIC_GOALS";
12pub const RWS_CONFIG_TRAFFIC_DATABASE_URL_ENV: &str = "RWS_CONFIG_TRAFFIC_DATABASE_URL_ENV";
13pub const RWS_CONFIG_TRAFFIC_HOSTING_NETWORKS: &str = "RWS_CONFIG_TRAFFIC_HOSTING_NETWORKS";
14pub const RWS_CONFIG_TRAFFIC_APPS: &str = "RWS_CONFIG_TRAFFIC_APPS";
15pub const RWS_CONFIG_TRAFFIC_OWNER_NETWORKS: &str = "RWS_CONFIG_TRAFFIC_OWNER_NETWORKS";
16pub const RWS_CONFIG_TRAFFIC_PROBE_ADDRESSES: &str = "RWS_CONFIG_TRAFFIC_PROBE_ADDRESSES";
17
18fn read(key: &str) -> String {
19    std::env::var(key).unwrap_or_default().trim().to_string()
20}
21
22/// `enabled`: `Some(false)` turns counting off even with the admin API on;
23/// `None` when unset.
24pub fn enabled_setting() -> Option<bool> {
25    match read(RWS_CONFIG_TRAFFIC_ENABLED).to_ascii_lowercase().as_str() {
26        "" => None,
27        "false" | "0" | "no" | "off" => Some(false),
28        _ => Some(true),
29    }
30}
31
32/// `retention_days`: 1–90, default 30.
33pub fn retention_days() -> u64 {
34    read(RWS_CONFIG_TRAFFIC_RETENTION_DAYS).parse::<u64>().ok().filter(|d| *d >= 1).map_or(30, |d| d.min(90))
35}
36
37/// `exclude`: exact paths not counted (comma-separated); default the
38/// health, readiness and metrics endpoints.
39pub fn exclude() -> Vec<String> {
40    let value = std::env::var(RWS_CONFIG_TRAFFIC_EXCLUDE);
41    match value {
42        Ok(v) => v.split(',').map(str::trim).filter(|p| !p.is_empty()).map(str::to_string).collect(),
43        Err(_) => ["/healthz", "/readyz", "/metrics"].iter().map(|p| p.to_string()).collect(),
44    }
45}
46
47/// `persist`: the file the counts are saved to. Unset: `traffic.json` next
48/// to the certificate (the ACME certificate's directory, else the TLS
49/// certificate's) — the state directory; without either, nothing is
50/// saved. `"off"`: not saved.
51pub fn persist_path() -> Option<PathBuf> {
52    let value = read(RWS_CONFIG_TRAFFIC_PERSIST);
53    if value.eq_ignore_ascii_case("off") {
54        return None;
55    }
56    if !value.is_empty() {
57        return Some(PathBuf::from(value));
58    }
59    [crate::entry_point::Config::RWS_CONFIG_ACME_CERT_PATH, crate::entry_point::Config::RWS_CONFIG_TLS_CERT_FILE]
60        .iter()
61        .map(|key| read(key))
62        .find(|path| !path.is_empty())
63        .and_then(|cert| PathBuf::from(cert).parent().map(|dir| dir.join("traffic.json")))
64}
65
66/// `geo_db`: a country table (`ci/gen_geo.py`'s format) to use instead of
67/// the built-in one; unset: the built-in one.
68pub fn geo_db() -> Option<PathBuf> {
69    let value = read(RWS_CONFIG_TRAFFIC_GEO_DB);
70    (!value.is_empty()).then(|| PathBuf::from(value))
71}
72
73/// `script`: serve the first-party script at `/_rws/t.js` and take its
74/// reports at `/_rws/t`. Default off.
75pub fn script() -> bool {
76    matches!(read(RWS_CONFIG_TRAFFIC_SCRIPT).to_ascii_lowercase().as_str(), "true" | "1" | "yes" | "on")
77}
78
79/// `hosting_networks`: count browsers from cloud and server hosting
80/// networks as bots (`traffic::hosting`). Default on; `false` counts them
81/// as people when they load assets, as before 18.35.
82pub fn hosting_networks() -> bool {
83    !matches!(read(RWS_CONFIG_TRAFFIC_HOSTING_NETWORKS).to_ascii_lowercase().as_str(), "false" | "0" | "no" | "off")
84}
85
86/// The most goals counted.
87pub const MAX_GOALS: usize = 32;
88
89/// `goals`: what counts as a conversion — a page path (`/thanks/`, or a
90/// prefix ending in `*`: `/docs/*`) or a script event (`event:signup`),
91/// comma-separated; at most [`MAX_GOALS`].
92pub fn goals() -> Vec<String> {
93    read(RWS_CONFIG_TRAFFIC_GOALS).split(',').map(str::trim).filter(|g| !g.is_empty()).take(MAX_GOALS).map(str::to_string).collect()
94}
95
96/// `database_url_env`: the environment variable holding a `postgres://` URL
97/// to write page views and script reports to (the URL itself stays out of
98/// the config file and the admin API). `None` when unset or the variable is
99/// empty.
100pub fn database_url() -> Option<String> {
101    let name = read(RWS_CONFIG_TRAFFIC_DATABASE_URL_ENV);
102    if name.is_empty() {
103        return None;
104    }
105    std::env::var(&name).ok().map(|v| v.trim().to_string()).filter(|v| !v.is_empty())
106}
107
108fn list(key: &str) -> Vec<String> {
109    read(key).split(',').map(str::trim).filter(|v| !v.is_empty()).map(str::to_string).collect()
110}
111
112/// `apps`: the site's native apps and browser extensions, as
113/// `name=pattern` (`traffic::apps`): a `User-Agent` token (`HITOP/`) or an
114/// extension origin (`chrome-extension://<id>`).
115pub fn apps() -> Vec<String> {
116    list(RWS_CONFIG_TRAFFIC_APPS)
117}
118
119/// `owner_networks`: addresses or CIDR ranges whose requests are the
120/// owner's (`traffic::owner`), counted apart from everyone else's.
121pub fn owner_networks() -> Vec<String> {
122    list(RWS_CONFIG_TRAFFIC_OWNER_NETWORKS)
123}
124
125/// `probe_addresses`: keep the addresses of scanners (requests for
126/// `/.env`, `/.git/config`…) in the probes list, to block them. Default
127/// off: no address is kept, only country and network.
128pub fn probe_addresses() -> bool {
129    matches!(read(RWS_CONFIG_TRAFFIC_PROBE_ADDRESSES).to_ascii_lowercase().as_str(), "true" | "1" | "yes" | "on")
130}