1use actix_web::{
2 rt::spawn,
3 dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
4 http::header::{HeaderValue, HOST, USER_AGENT},
5 Error,
6};
7use chrono::Utc;
8use futures::future::LocalBoxFuture;
9use reqwest::Client;
10use serde::Serialize;
11use std::sync::{Arc, Mutex};
12use std::{
13 future::{ready, Ready},
14 time::{Duration, Instant},
15};
16
17#[derive(Debug, Clone, Serialize)]
18struct RequestData {
19 hostname: String,
20 ip_address: Option<String>,
21 path: String,
22 user_agent: String,
23 method: String,
24 response_time: u32,
25 status: u16,
26 user_id: Option<String>,
27 created_at: String,
28}
29
30type StringMapper = dyn for<'a> Fn(&ServiceRequest) -> String + Send + Sync;
31
32#[derive(Clone)]
33struct Config {
34 privacy_level: i32,
35 server_url: String,
36 get_hostname: Arc<StringMapper>,
37 get_ip_address: Arc<StringMapper>,
38 get_path: Arc<StringMapper>,
39 get_user_agent: Arc<StringMapper>,
40 get_user_id: Arc<StringMapper>,
41}
42
43impl Default for Config {
44 fn default() -> Self {
45 Self {
46 privacy_level: 0,
47 server_url: String::from("https://www.apianalytics-server.com/"),
48 get_hostname: Arc::new(get_hostname),
49 get_ip_address: Arc::new(get_ip_address),
50 get_path: Arc::new(get_path),
51 get_user_agent: Arc::new(get_user_agent),
52 get_user_id: Arc::new(get_user_id),
53 }
54 }
55}
56
57pub trait HeaderValueExt {
58 fn to_string(&self) -> String;
59}
60
61impl HeaderValueExt for HeaderValue {
62 fn to_string(&self) -> String {
63 self.to_str().unwrap_or_default().to_string()
64 }
65}
66
67fn get_hostname(req: &ServiceRequest) -> String {
68 req.headers()
69 .get(HOST)
70 .map(|x| x.to_string())
71 .unwrap_or_default()
72}
73
74fn get_ip_address(req: &ServiceRequest) -> String {
75 let headers = req.headers();
76
77 if let Some(val) = headers
78 .get("cf-connecting-ip")
79 .and_then(|v| v.to_str().ok())
80 .map(|s| s.trim().to_owned())
81 .filter(|s| !s.is_empty())
82 {
83 return val;
84 }
85
86 if let Some(val) = headers
87 .get("x-forwarded-for")
88 .and_then(|v| v.to_str().ok())
89 .and_then(|s| s.split(',').next())
90 .map(|s| s.trim().to_owned())
91 .filter(|s| !s.is_empty())
92 {
93 return val;
94 }
95
96 if let Some(val) = headers
97 .get("x-real-ip")
98 .and_then(|v| v.to_str().ok())
99 .map(|s| s.trim().to_owned())
100 .filter(|s| !s.is_empty())
101 {
102 return val;
103 }
104
105 req.peer_addr()
106 .map(|addr| addr.ip().to_string())
107 .unwrap_or_default()
108}
109
110fn get_path(req: &ServiceRequest) -> String {
111 req.path().to_string()
112}
113
114fn get_user_agent(req: &ServiceRequest) -> String {
115 req.headers()
116 .get(USER_AGENT)
117 .map(|x| x.to_string())
118 .unwrap_or_default()
119}
120
121fn get_user_id(_req: &ServiceRequest) -> String {
122 String::new()
123}
124
125struct RequestBuffer {
126 requests: Vec<RequestData>,
127 last_posted: Instant,
128}
129
130impl RequestBuffer {
131 fn new() -> Self {
132 Self {
133 requests: Vec::new(),
134 last_posted: Instant::now(),
135 }
136 }
137}
138
139pub struct Analytics {
140 api_key: String,
141 config: Config,
142}
143
144impl Analytics {
145 pub fn new(api_key: String) -> Self {
146 Self {
147 api_key,
148 config: Config::default(),
149 }
150 }
151
152 pub fn with_privacy_level(mut self, privacy_level: i32) -> Self {
153 self.config.privacy_level = privacy_level;
154 self
155 }
156
157 pub fn with_server_url(mut self, server_url: String) -> Self {
158 self.config.server_url = if server_url.ends_with('/') {
159 server_url
160 } else {
161 server_url + "/"
162 };
163 self
164 }
165
166 pub fn with_hostname_mapper<F>(mut self, mapper: F) -> Self
167 where
168 F: Fn(&ServiceRequest) -> String + Send + Sync + 'static,
169 {
170 self.config.get_hostname = Arc::new(mapper);
171 self
172 }
173
174 pub fn with_ip_address_mapper<F>(mut self, mapper: F) -> Self
175 where
176 F: Fn(&ServiceRequest) -> String + Send + Sync + 'static,
177 {
178 self.config.get_ip_address = Arc::new(mapper);
179 self
180 }
181
182 pub fn with_path_mapper<F>(mut self, mapper: F) -> Self
183 where
184 F: Fn(&ServiceRequest) -> String + Send + Sync + 'static,
185 {
186 self.config.get_path = Arc::new(mapper);
187 self
188 }
189
190 pub fn with_user_agent_mapper<F>(mut self, mapper: F) -> Self
191 where
192 F: Fn(&ServiceRequest) -> String + Send + Sync + 'static,
193 {
194 self.config.get_user_agent = Arc::new(mapper);
195 self
196 }
197
198 pub fn with_user_id_mapper<F>(mut self, mapper: F) -> Self
199 where
200 F: Fn(&ServiceRequest) -> String + Send + Sync + 'static,
201 {
202 self.config.get_user_id = Arc::new(mapper);
203 self
204 }
205}
206
207impl<S, B> Transform<S, ServiceRequest> for Analytics
208where
209 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
210 S::Future: 'static,
211 B: 'static,
212{
213 type Response = ServiceResponse<B>;
214 type Error = Error;
215 type InitError = ();
216 type Transform = AnalyticsMiddleware<S>;
217 type Future = Ready<Result<Self::Transform, Self::InitError>>;
218
219 fn new_transform(&self, service: S) -> Self::Future {
220 let client = Client::builder()
221 .timeout(Duration::from_secs(10))
222 .build()
223 .unwrap_or_else(|_| Client::new());
224
225 ready(Ok(AnalyticsMiddleware {
226 api_key: Arc::new(self.api_key.clone()),
227 config: Arc::new(self.config.clone()),
228 buffer: Arc::new(Mutex::new(RequestBuffer::new())),
229 client: Arc::new(client),
230 service,
231 }))
232 }
233}
234
235pub struct AnalyticsMiddleware<S> {
236 api_key: Arc<String>,
237 config: Arc<Config>,
238 buffer: Arc<Mutex<RequestBuffer>>,
239 client: Arc<Client>,
240 service: S,
241}
242
243#[derive(Debug, Clone, Serialize)]
244struct Payload {
245 api_key: String,
246 requests: Vec<RequestData>,
247 framework: String,
248 privacy_level: i32,
249}
250
251impl Payload {
252 pub fn new(api_key: String, requests: Vec<RequestData>, privacy_level: i32) -> Self {
253 Self {
254 api_key,
255 requests,
256 framework: String::from("Actix"),
257 privacy_level,
258 }
259 }
260}
261
262async fn post_requests(client: &Client, data: Payload, server_url: &str) {
263 let url = format!("{}api/log-request", server_url);
264 let _ = client.post(url).json(&data).send().await;
265}
266
267fn log_request(
268 buffer: &Arc<Mutex<RequestBuffer>>,
269 client: &Arc<Client>,
270 api_key: &str,
271 request_data: RequestData,
272 config: &Config,
273) {
274 let batch = {
275 let mut buf = buffer.lock().unwrap();
276 buf.requests.push(request_data);
277 if buf.last_posted.elapsed().as_secs_f64() > 60.0 {
278 buf.last_posted = Instant::now();
279 std::mem::take(&mut buf.requests)
280 } else {
281 vec![]
282 }
283 };
284
285 if !batch.is_empty() {
286 let payload = Payload::new(api_key.to_owned(), batch, config.privacy_level);
287 let server_url = config.server_url.clone();
288 let client = Arc::clone(client);
289 spawn(async move {
290 post_requests(&client, payload, &server_url).await;
291 });
292 }
293}
294
295impl<S, B> Service<ServiceRequest> for AnalyticsMiddleware<S>
296where
297 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
298 S::Future: 'static,
299 B: 'static,
300{
301 type Response = ServiceResponse<B>;
302 type Error = Error;
303 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
304
305 forward_ready!(service);
306
307 fn call(&self, req: ServiceRequest) -> Self::Future {
308 let start = Instant::now();
309
310 let api_key = Arc::clone(&self.api_key);
311 let config = Arc::clone(&self.config);
312 let buffer = Arc::clone(&self.buffer);
313 let client = Arc::clone(&self.client);
314 let hostname = (self.config.get_hostname)(&req);
315 let ip_address = if self.config.privacy_level >= 2 {
316 None
317 } else {
318 let val = (self.config.get_ip_address)(&req);
319 if val.is_empty() { None } else { Some(val) }
320 };
321 let path = (self.config.get_path)(&req);
322 let method = req.method().to_string();
323 let user_agent = (self.config.get_user_agent)(&req);
324 let user_id = {
325 let val = (self.config.get_user_id)(&req);
326 if val.is_empty() { None } else { Some(val) }
327 };
328
329 let future = self.service.call(req);
330
331 Box::pin(async move {
332 let res = future.await?;
333 let response_time = start.elapsed().as_millis().min(u32::MAX as u128) as u32;
334
335 let request_data = RequestData {
336 hostname,
337 ip_address,
338 path,
339 user_agent,
340 method,
341 response_time,
342 status: res.status().as_u16(),
343 user_id,
344 created_at: Utc::now().to_rfc3339(),
345 };
346
347 log_request(&buffer, &client, &api_key, request_data, &config);
348
349 Ok(res)
350 })
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357 use actix_web::test::TestRequest;
358
359 #[test]
360 fn test_get_ip_cf_connecting_ip() {
361 let req = TestRequest::get()
362 .insert_header(("cf-connecting-ip", "203.0.113.1"))
363 .insert_header(("x-forwarded-for", "10.0.0.1"))
364 .to_srv_request();
365 assert_eq!(get_ip_address(&req), "203.0.113.1");
366 }
367
368 #[test]
369 fn test_get_ip_x_forwarded_for_first() {
370 let req = TestRequest::get()
371 .insert_header(("x-forwarded-for", "203.0.113.1, 10.0.0.1"))
372 .to_srv_request();
373 assert_eq!(get_ip_address(&req), "203.0.113.1");
374 }
375
376 #[test]
377 fn test_get_ip_x_real_ip() {
378 let req = TestRequest::get()
379 .insert_header(("x-real-ip", "203.0.113.1"))
380 .to_srv_request();
381 assert_eq!(get_ip_address(&req), "203.0.113.1");
382 }
383
384 #[test]
385 fn test_get_ip_empty_when_no_headers() {
386 let req = TestRequest::get().to_srv_request();
387 let ip = get_ip_address(&req);
389 let _ = ip;
391 }
392
393 #[test]
394 fn test_get_hostname() {
395 let req = TestRequest::get()
396 .insert_header(("host", "example.com"))
397 .to_srv_request();
398 assert_eq!(get_hostname(&req), "example.com");
399 }
400
401 #[test]
402 fn test_get_path() {
403 let req = TestRequest::get().uri("/api/users").to_srv_request();
404 assert_eq!(get_path(&req), "/api/users");
405 }
406
407 #[test]
408 fn test_get_user_agent() {
409 let req = TestRequest::get()
410 .insert_header(("user-agent", "TestAgent/1.0"))
411 .to_srv_request();
412 assert_eq!(get_user_agent(&req), "TestAgent/1.0");
413 }
414
415 #[test]
416 fn test_get_user_id_default_empty() {
417 let req = TestRequest::get().to_srv_request();
418 assert_eq!(get_user_id(&req), "");
419 }
420}