modo/extractor/
client_info.rs1use axum::extract::FromRequestParts;
2use http::request::Parts;
3
4use crate::error::Error;
5use crate::ip::ClientIp;
6
7#[derive(Debug, Clone, Default)]
23pub struct ClientInfo {
24 ip: Option<String>,
25 user_agent: Option<String>,
26 fingerprint: Option<String>,
27}
28
29impl ClientInfo {
30 pub fn new() -> Self {
32 Self::default()
33 }
34
35 pub fn ip(mut self, ip: impl Into<String>) -> Self {
37 self.ip = Some(ip.into());
38 self
39 }
40
41 pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
43 self.user_agent = Some(ua.into());
44 self
45 }
46
47 pub fn fingerprint(mut self, fp: impl Into<String>) -> Self {
49 self.fingerprint = Some(fp.into());
50 self
51 }
52
53 pub fn ip_value(&self) -> Option<&str> {
55 self.ip.as_deref()
56 }
57
58 pub fn user_agent_value(&self) -> Option<&str> {
60 self.user_agent.as_deref()
61 }
62
63 pub fn fingerprint_value(&self) -> Option<&str> {
65 self.fingerprint.as_deref()
66 }
67}
68
69impl<S: Send + Sync> FromRequestParts<S> for ClientInfo {
70 type Rejection = Error;
71
72 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
73 let ip = parts.extensions.get::<ClientIp>().map(|c| c.0.to_string());
74
75 let user_agent = parts
76 .headers
77 .get(http::header::USER_AGENT)
78 .and_then(|v| v.to_str().ok())
79 .map(|s| s.to_string());
80
81 let fingerprint = parts
82 .headers
83 .get("x-fingerprint")
84 .and_then(|v| v.to_str().ok())
85 .map(|s| s.to_string());
86
87 Ok(Self {
88 ip,
89 user_agent,
90 fingerprint,
91 })
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn default_has_all_none() {
101 let info = ClientInfo::new();
102 assert!(info.ip_value().is_none());
103 assert!(info.user_agent_value().is_none());
104 assert!(info.fingerprint_value().is_none());
105 }
106
107 #[test]
108 fn builder_sets_fields() {
109 let info = ClientInfo::new()
110 .ip("1.2.3.4")
111 .user_agent("Mozilla/5.0")
112 .fingerprint("abc123");
113 assert_eq!(info.ip_value(), Some("1.2.3.4"));
114 assert_eq!(info.user_agent_value(), Some("Mozilla/5.0"));
115 assert_eq!(info.fingerprint_value(), Some("abc123"));
116 }
117
118 #[tokio::test]
119 async fn extracts_from_request_parts() {
120 use crate::ip::ClientIp;
121 use std::net::IpAddr;
122
123 let mut req = http::Request::builder()
124 .header("user-agent", "TestAgent/1.0")
125 .header("x-fingerprint", "fp_abc")
126 .body(())
127 .unwrap();
128 let ip: IpAddr = "10.0.0.1".parse().unwrap();
129 req.extensions_mut().insert(ClientIp(ip));
130
131 let (mut parts, _) = req.into_parts();
132 let info = ClientInfo::from_request_parts(&mut parts, &())
133 .await
134 .unwrap();
135
136 assert_eq!(info.ip_value(), Some("10.0.0.1"));
137 assert_eq!(info.user_agent_value(), Some("TestAgent/1.0"));
138 assert_eq!(info.fingerprint_value(), Some("fp_abc"));
139 }
140
141 #[tokio::test]
142 async fn extracts_with_missing_fields() {
143 let req = http::Request::builder().body(()).unwrap();
144 let (mut parts, _) = req.into_parts();
145 let info = ClientInfo::from_request_parts(&mut parts, &())
146 .await
147 .unwrap();
148
149 assert!(info.ip_value().is_none());
150 assert!(info.user_agent_value().is_none());
151 assert!(info.fingerprint_value().is_none());
152 }
153}