rwf/controller/middleware/
request_tracker.rs1use 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
72pub struct RequestTracker {}
74
75impl RequestTracker {
76 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}