1mod internals;
29mod misc;
30
31use std::num::NonZeroU64;
32
33use hyper::{http::HeaderValue, Request, StatusCode};
34use serde::{Deserialize, Serialize};
35
36pub use self::{internals::*, misc::*};
37
38#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
45pub struct Statistic {
46 pub link: IdOrVanity,
48 #[serde(rename = "type")]
50 pub stat_type: StatisticType,
51 pub data: StatisticData,
53 pub time: StatisticTime,
55}
56
57impl Statistic {
58 pub fn new(
61 link: impl Into<IdOrVanity>,
62 stat_type: StatisticType,
63 data: impl Into<StatisticData>,
64 ) -> Self {
65 Self {
66 link: link.into(),
67 stat_type,
68 data: data.into(),
69 time: StatisticTime::now(),
70 }
71 }
72
73 pub fn get_misc(
85 link: Option<&IdOrVanity>,
86 stat_info: ExtraStatisticInfo,
87 status_code: StatusCode,
88 categories: StatisticCategories,
89 ) -> impl Iterator<Item = Statistic> {
90 link.map_or_else(
91 || Vec::new().into_iter(),
92 |link| {
93 let mut stats = Vec::with_capacity(5);
94
95 if categories.specifies(StatisticType::Request) {
96 stats.push(Self::new(
97 link,
98 StatisticType::Request,
99 StatisticData::default(),
100 ));
101 }
102
103 if categories.specifies(StatisticType::StatusCode) {
104 stats.push(Self::new(
105 link,
106 StatisticType::StatusCode,
107 status_code.as_str(),
108 ));
109 }
110
111 if categories.specifies(StatisticType::SniRequest) {
112 if let Some(sni) = stat_info.tls_sni {
113 stats.push(Self::new(link, StatisticType::SniRequest, sni.to_string()));
114 }
115 }
116
117 if categories.specifies(StatisticType::TlsVersion) {
118 if let Some(Some(version)) = stat_info.tls_version.map(|v| v.as_str()) {
119 stats.push(Self::new(link, StatisticType::TlsVersion, version));
120 }
121 }
122
123 if categories.specifies(StatisticType::TlsCipherSuite) {
124 if let Some(Some(suite)) =
125 stat_info.tls_cipher_suite.map(|s| s.suite().as_str())
126 {
127 stats.push(Self::new(link, StatisticType::TlsCipherSuite, suite));
128 }
129 }
130
131 stats.into_iter()
132 },
133 )
134 }
135
136 pub fn from_req<T>(
147 link: Option<&IdOrVanity>,
148 req: &Request<T>,
149 categories: StatisticCategories,
150 ) -> impl Iterator<Item = Statistic> {
151 link.map_or_else(
152 || Vec::new().into_iter(),
153 |link| {
154 let mut stats = Vec::with_capacity(5);
155
156 if categories.specifies(StatisticType::HttpVersion) {
157 stats.push(Self::new(
158 link,
159 StatisticType::HttpVersion,
160 HttpVersion::from(req.version()).as_str(),
161 ));
162 }
163
164 let headers = req.headers();
165
166 if categories.specifies(StatisticType::HostRequest) {
167 if let Some(Ok(host)) = req
168 .uri()
169 .host()
170 .map(Ok)
171 .or_else(|| headers.get("host").map(HeaderValue::to_str))
172 {
173 stats.push(Self::new(link, StatisticType::HostRequest, host));
174 }
175 }
176
177 if categories.user_agent {
178 if let Some(Ok(val)) = headers.get("sec-ch-ua").map(HeaderValue::to_str) {
179 stats.push(Self::new(link, StatisticType::UserAgent, val));
180 } else if let Some(Ok(val)) = headers.get("user-agent").map(HeaderValue::to_str)
181 {
182 stats.push(Self::new(link, StatisticType::UserAgent, val));
183 }
184 }
185
186 if categories.specifies(StatisticType::UserAgentMobile) {
187 if let Some(Ok(val)) = headers.get("sec-ch-ua-mobile").map(HeaderValue::to_str)
188 {
189 stats.push(Self::new(link, StatisticType::UserAgentMobile, val));
190 }
191 }
192
193 if categories.specifies(StatisticType::UserAgentPlatform) {
194 if let Some(Ok(val)) =
195 headers.get("sec-ch-ua-platform").map(HeaderValue::to_str)
196 {
197 stats.push(Self::new(link, StatisticType::UserAgentPlatform, val));
198 }
199 }
200
201 stats.into_iter()
202 },
203 )
204 }
205}
206
207#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
218pub struct StatisticDescription {
219 pub link: Option<IdOrVanity>,
221 #[serde(rename = "type")]
223 pub stat_type: Option<StatisticType>,
224 pub data: Option<StatisticData>,
226 pub time: Option<StatisticTime>,
228}
229
230impl StatisticDescription {
231 #[must_use]
233 pub fn matches(&self, stat: &Statistic) -> bool {
234 (self.link.is_none() || self.link.as_ref() == Some(&stat.link))
235 && (self.stat_type.is_none() || self.stat_type.as_ref() == Some(&stat.stat_type))
236 && (self.data.is_none() || self.data.as_ref() == Some(&stat.data))
237 && (self.time.is_none() || self.time.as_ref() == Some(&stat.time))
238 }
239}
240
241#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
247pub struct StatisticValue {
248 count: NonZeroU64,
249}
250
251impl StatisticValue {
252 #[must_use]
256 pub fn new(count: u64) -> Option<Self> {
257 Some(Self {
258 count: NonZeroU64::new(count)?,
259 })
260 }
261
262 #[must_use]
268 pub const fn get(self) -> u64 {
269 self.count.get()
270 }
271
272 #[must_use]
274 pub const fn get_nonzero(self) -> NonZeroU64 {
275 self.count
276 }
277
278 #[must_use]
280 pub const fn increment(self) -> Self {
281 Self {
282 count: self.count.saturating_add(1),
283 }
284 }
285}
286
287impl Default for StatisticValue {
288 fn default() -> Self {
289 Self::new(1).expect("1 is not 0")
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use links_id::Id;
296 use links_normalized::Normalized;
297 use tokio_rustls::rustls::{crypto::ring::ALL_CIPHER_SUITES, ProtocolVersion};
298
299 use super::*;
300
301 #[test]
302 fn statistic() {
303 assert_eq!(
304 Statistic::new(Id::new(), StatisticType::Request, "").time,
305 StatisticTime::now()
306 );
307
308 let id = Id::new();
309 assert_eq!(
310 Statistic::new(id, StatisticType::Request, "").link,
311 id.into()
312 );
313
314 assert_eq!(
315 Statistic::new(Id::new(), StatisticType::Request, "").stat_type,
316 StatisticType::Request
317 );
318
319 assert_eq!(
320 Statistic::new(Id::new(), StatisticType::Request, "this is a test").data,
321 "this is a test".into()
322 );
323
324 assert_eq!(
325 Statistic::new(Normalized::new("a link"), StatisticType::StatusCode, "501"),
326 Statistic {
327 link: IdOrVanity::Vanity(Normalized::new("a link")),
328 stat_type: StatisticType::StatusCode,
329 time: StatisticTime::now(),
330 data: StatisticData::from("501")
331 }
332 );
333 }
334
335 #[test]
336 fn statistic_collection() {
337 let stats = Statistic::get_misc(
338 Some(&Normalized::new("test").into()),
339 ExtraStatisticInfo {
340 tls_sni: Some("example.com".into()),
341 tls_version: Some(ProtocolVersion::TLSv1_3),
342 tls_cipher_suite: Some(ALL_CIPHER_SUITES[0]),
343 },
344 StatusCode::TEMPORARY_REDIRECT,
345 StatisticCategories::ALL,
346 )
347 .map(|s| s.stat_type)
348 .collect::<Vec<_>>();
349
350 assert!(stats.contains(&StatisticType::Request));
351 assert!(stats.contains(&StatisticType::StatusCode));
352 assert!(stats.contains(&StatisticType::SniRequest));
353 assert!(stats.contains(&StatisticType::TlsVersion));
354 assert!(stats.contains(&StatisticType::TlsCipherSuite));
355
356 let stats = Statistic::from_req(
357 Some(&Normalized::new("test").into()),
358 &Request::builder()
359 .header("Host", "example.com")
360 .header(
361 "Sec-CH-UA",
362 r#"" Not A;Brand";v="99", "Chromium";v="96", "Google Chrome";v="96""#,
363 )
364 .header("Sec-CH-UA-Mobile", "?0")
365 .header("Sec-CH-UA-Platform", "Windows")
366 .body(Vec::<u8>::new())
367 .unwrap(),
368 StatisticCategories::ALL,
369 )
370 .map(|s| s.stat_type)
371 .collect::<Vec<_>>();
372
373 assert!(stats.contains(&StatisticType::HostRequest));
374 assert!(stats.contains(&StatisticType::HttpVersion));
375 assert!(stats.contains(&StatisticType::UserAgent));
376 assert!(stats.contains(&StatisticType::UserAgentMobile));
377 assert!(stats.contains(&StatisticType::UserAgentPlatform));
378
379 let mut stats = Statistic::from_req(
380 Some(&Normalized::new("test").into()),
381 &Request::builder()
382 .header("Host", "example.com")
383 .header(
384 "Sec-CH-UA",
385 r#"" Not A;Brand";v="99", "Chromium";v="96", "Google Chrome";v="96""#,
386 )
387 .header("Sec-CH-UA-Mobile", "?0")
388 .header("Sec-CH-UA-Platform", "Windows")
389 .body(Vec::<u8>::new())
390 .unwrap(),
391 StatisticCategories::NONE,
392 );
393
394 assert!(stats.next().is_none());
396
397 let stats = Statistic::from_req(
398 Some(&Normalized::new("test").into()),
399 &Request::builder()
400 .header("Host", "example.com")
401 .header(
402 "Sec-CH-UA",
403 r#"" Not A;Brand";v="99", "Chromium";v="96", "Google Chrome";v="96""#,
404 )
405 .header("Sec-CH-UA-Mobile", "?0")
406 .header("Sec-CH-UA-Platform", "Windows")
407 .body(Vec::<u8>::new())
408 .unwrap(),
409 StatisticCategories::default(),
410 )
411 .map(|s| s.stat_type)
412 .collect::<Vec<_>>();
413
414 assert!(stats.contains(&StatisticType::HostRequest));
415 assert!(stats.contains(&StatisticType::HttpVersion));
416 assert!(!stats.contains(&StatisticType::UserAgent));
417 assert!(!stats.contains(&StatisticType::UserAgentMobile));
418 assert!(!stats.contains(&StatisticType::UserAgentPlatform));
419 }
420
421 #[test]
422 fn statistic_serde() {
423 let vanity = Normalized::new("test-vanity");
424
425 let stats = vec![
426 Statistic::new(vanity.clone(), StatisticType::Request, ""),
427 Statistic::new(
428 vanity.clone(),
429 StatisticType::UserAgent,
430 r#"" Not A;Brand";v="99", "Chromium";v="96", "Google Chrome";v="96""#,
431 ),
432 Statistic::new(vanity, StatisticType::HttpVersion, HttpVersion::V2.as_str()),
433 ];
434
435 let json = serde_json::to_string(&stats).unwrap();
436
437 let parsed_stats = serde_json::from_str::<Vec<Statistic>>(&json).unwrap();
438
439 assert_eq!(stats, parsed_stats);
440
441 let id = Id::from([1, 2, 3, 4, 5]);
442
443 let stats = vec![
444 Statistic::new(id, StatisticType::Request, ""),
445 Statistic::new(
446 id,
447 StatisticType::UserAgent,
448 r#"" Not A;Brand";v="99", "Chromium";v="96", "Google Chrome";v="96""#,
449 ),
450 Statistic::new(id, StatisticType::HttpVersion, HttpVersion::V2.as_str()),
451 ];
452
453 let json = serde_json::to_string_pretty(&stats).unwrap();
454
455 let parsed_stats = serde_json::from_str::<Vec<Statistic>>(&json).unwrap();
456
457 assert_eq!(stats, parsed_stats);
458 }
459
460 #[test]
461 fn statistic_description() {
462 let desc = StatisticDescription {
463 link: None,
464 stat_type: None,
465 data: None,
466 time: None,
467 };
468
469 assert!(desc.matches(&Statistic::new(Id::new(), StatisticType::Request, "")));
470 assert!(desc.matches(&Statistic::new(
471 Normalized::new("a test"),
472 StatisticType::StatusCode,
473 "400"
474 )));
475
476 let desc = StatisticDescription {
477 link: Some(Normalized::new("a test").into()),
478 stat_type: None,
479 data: None,
480 time: None,
481 };
482
483 assert!(!desc.matches(&Statistic::new(Id::new(), StatisticType::Request, "")));
484 assert!(desc.matches(&Statistic::new(
485 Normalized::new("a test"),
486 StatisticType::StatusCode,
487 "400"
488 )));
489
490 let desc = StatisticDescription {
491 link: Some(Normalized::new("a test").into()),
492 stat_type: Some(StatisticType::Request),
493 data: None,
494 time: None,
495 };
496
497 assert!(!desc.matches(&Statistic::new(Id::new(), StatisticType::Request, "")));
498 assert!(!desc.matches(&Statistic::new(
499 Normalized::new("a test"),
500 StatisticType::StatusCode,
501 "400"
502 )));
503
504 let desc = StatisticDescription {
505 link: None,
506 stat_type: Some(StatisticType::Request),
507 data: None,
508 time: None,
509 };
510
511 assert!(desc.matches(&Statistic::new(Id::new(), StatisticType::Request, "")));
512 assert!(!desc.matches(&Statistic::new(
513 Normalized::new("a test"),
514 StatisticType::StatusCode,
515 "400"
516 )));
517
518 let desc = StatisticDescription {
519 link: None,
520 stat_type: None,
521 data: None,
522 time: Some(StatisticTime::try_from("2020-01-01T12:34:56.789Z").unwrap()),
523 };
524
525 assert!(!desc.matches(&Statistic::new(Id::new(), StatisticType::Request, "")));
526 assert!(!desc.matches(&Statistic::new(
527 Normalized::new("a test"),
528 StatisticType::StatusCode,
529 "400"
530 )));
531 }
532
533 #[test]
534 fn statistic_value() {
535 assert_eq!(StatisticValue::new(1), Some(StatisticValue::default()));
536 assert!(StatisticValue::new(0).is_none());
537 assert!(StatisticValue::new(1).is_some());
538 assert!(StatisticValue::new(1_000_000).is_some());
539
540 let stat_val = StatisticValue::default();
541 assert_eq!(stat_val.get(), 1);
542 let stat_val = stat_val.increment();
543 assert_eq!(stat_val.get(), 2);
544
545 let stat_val = StatisticValue::default();
546 assert_eq!(stat_val.get_nonzero(), NonZeroU64::new(1).unwrap());
547 let stat_val = stat_val.increment();
548 assert_eq!(stat_val.get_nonzero(), NonZeroU64::new(2).unwrap());
549
550 let stat_val = StatisticValue::new(u64::MAX - 1).unwrap();
551 assert_eq!(stat_val.get(), u64::MAX - 1);
552 let stat_val = stat_val.increment();
553 assert_eq!(stat_val.get(), u64::MAX);
554 let stat_val = stat_val.increment();
555 assert_eq!(stat_val.get(), u64::MAX);
556 }
557}