rwf/controller/middleware/
request_tracker.rs

1//! Record HTTP requests served by the application.
2//!
3//! Requests record metadata like client IP, request duration, path, query, and HTTP method.
4//! Each client is given a cookie which uniquely identifies that browser. This allows to record unique sessions.
5//!
6//! You can view requests in real time in the [admin panel](https://github.com/levkk/rwf/tree/main/rwf-admin), or by querying the `rwf_requests` table, e.g.:
7//!
8//! ```sql
9//! SELECT * FROM rwf_requests
10//! WHERE created_at > NOW() - INTERVAL '5 minutes';
11//! ```
12use base64::{engine::general_purpose, Engine as _};
13use serde::{Deserialize, Serialize};
14use time::{Duration, OffsetDateTime};
15use uuid::Uuid;
16
17use crate::analytics::Request as AnalyticsRequest;
18use crate::controller::middleware::prelude::*;
19use crate::http::CookieBuilder;
20use crate::model::{Model, Pool, ToValue};
21
22static COOKIE_NAME: &str = "rwf_aid";
23static COOKIE_DURATION: Duration = Duration::days(399);
24
25#[derive(Serialize, Deserialize)]
26struct AnalyticsCookie {
27    #[serde(rename = "u")]
28    uuid: String,
29    #[serde(rename = "e")]
30    expires: i64,
31}
32
33impl AnalyticsCookie {
34    fn uuid(&self) -> Option<Uuid> {
35        match Uuid::parse_str(&self.uuid) {
36            Ok(uuid) => Some(uuid),
37            Err(_) => None,
38        }
39    }
40
41    pub fn new() -> Self {
42        Self {
43            uuid: Uuid::new_v4().to_string(),
44            expires: (OffsetDateTime::now_utc() + COOKIE_DURATION).unix_timestamp(),
45        }
46    }
47
48    fn should_renew(&self) -> bool {
49        match OffsetDateTime::from_unix_timestamp(self.expires) {
50            Ok(timestamp) => timestamp - OffsetDateTime::now_utc() < Duration::days(7),
51            Err(_) => true,
52        }
53    }
54
55    fn to_network(&self) -> String {
56        let json = serde_json::to_string(self).unwrap();
57        general_purpose::STANDARD_NO_PAD.encode(&json)
58    }
59
60    fn from_network(s: &str) -> Option<Self> {
61        match general_purpose::STANDARD_NO_PAD.decode(s) {
62            Ok(v) => match serde_json::from_slice::<Self>(&v) {
63                Ok(cookie) => Some(cookie),
64                Err(_) => None,
65            },
66
67            Err(_) => None,
68        }
69    }
70}
71
72/// HTTP request tracker.
73pub struct RequestTracker {}
74
75impl RequestTracker {
76    /// Creates new HTTP request tracker.
77    pub fn new() -> Self {
78        Self {}
79    }
80}
81
82#[crate::async_trait]
83impl Middleware for RequestTracker {
84    async fn handle_request(&self, request: Request) -> Result<Outcome, Error> {
85        Ok(Outcome::Forward(request))
86    }
87
88    async fn handle_response(
89        &self,
90        request: &Request,
91        mut response: Response,
92    ) -> Result<Response, Error> {
93        let method = request.method().to_string();
94        let path = request.path().path().to_string();
95        let query = request.path().query().to_json();
96        let code = response.status().code() as i32;
97        let duration =
98            ((OffsetDateTime::now_utc() - request.received_at()).as_seconds_f64() * 1000.0) as f32;
99        let client = request.peer().ip();
100
101        let (create, cookie) = match request
102            .cookies()
103            .get(COOKIE_NAME)
104            .map(|cookie| AnalyticsCookie::from_network(&cookie.value()))
105        {
106            Some(Some(cookie)) => (cookie.should_renew(), cookie),
107            _ => (true, AnalyticsCookie::new()),
108        };
109
110        if create {
111            let cookie = CookieBuilder::new()
112                .name(COOKIE_NAME)
113                .value(cookie.to_network())
114                .max_age(Duration::days(399))
115                .build();
116
117            response = response.cookie(cookie);
118        }
119
120        if let Ok(mut conn) = Pool::connection().await {
121            if let Some(client_id) = cookie.uuid() {
122                let _ = AnalyticsRequest::create(&[
123                    ("method", method.to_value()),
124                    ("path", path.to_value()),
125                    ("query", query.to_value()),
126                    ("client_ip", client.to_value()),
127                    ("client_id", client_id.to_value()),
128                    ("code", code.to_value()),
129                    ("duration", duration.to_value()),
130                ])
131                .execute(&mut conn)
132                .await;
133            }
134        }
135
136        Ok(response)
137    }
138}
139
140#[cfg(test)]
141mod test {
142    use super::*;
143
144    #[tokio::test]
145    async fn test_request_tracker() {
146        let request = Request::default();
147        let response = Response::default();
148
149        let mut response = RequestTracker::new()
150            .handle_response(&request, response)
151            .await
152            .unwrap();
153        assert!(response.cookies().get(COOKIE_NAME).is_some());
154    }
155}