1use 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> {
83 let ip = parts.extensions.get::<ClientIp>().map(|c| c.0.to_string());
84
85 let user_agent = parts
86 .headers
87 .get(http::header::USER_AGENT)
88 .and_then(|v| v.to_str().ok())
89 .map(|s| s.to_string());
90
91 let fingerprint = parts
92 .headers
93 .get("x-fingerprint")
94 .and_then(|v| v.to_str().ok())
95 .map(|s| s.to_string());
96
97 Ok(Self {
98 ip,
99 user_agent,
100 fingerprint,
101 })
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn default_has_all_none() {
111 let info = ClientInfo::new();
112 assert!(info.ip_value().is_none());
113 assert!(info.user_agent_value().is_none());
114 assert!(info.fingerprint_value().is_none());
115 }
116
117 #[test]
118 fn builder_sets_fields() {
119 let info = ClientInfo::new()
120 .ip("1.2.3.4")
121 .user_agent("Mozilla/5.0")
122 .fingerprint("abc123");
123 assert_eq!(info.ip_value(), Some("1.2.3.4"));
124 assert_eq!(info.user_agent_value(), Some("Mozilla/5.0"));
125 assert_eq!(info.fingerprint_value(), Some("abc123"));
126 }
127
128 #[tokio::test]
129 async fn extracts_from_request_parts() {
130 use crate::ip::ClientIp;
131 use std::net::IpAddr;
132
133 let mut req = http::Request::builder()
134 .header("user-agent", "TestAgent/1.0")
135 .header("x-fingerprint", "fp_abc")
136 .body(())
137 .unwrap();
138 let ip: IpAddr = "10.0.0.1".parse().unwrap();
139 req.extensions_mut().insert(ClientIp(ip));
140
141 let (mut parts, _) = req.into_parts();
142 let info = ClientInfo::from_request_parts(&mut parts, &())
143 .await
144 .unwrap();
145
146 assert_eq!(info.ip_value(), Some("10.0.0.1"));
147 assert_eq!(info.user_agent_value(), Some("TestAgent/1.0"));
148 assert_eq!(info.fingerprint_value(), Some("fp_abc"));
149 }
150
151 #[tokio::test]
152 async fn extracts_with_missing_fields() {
153 let req = http::Request::builder().body(()).unwrap();
154 let (mut parts, _) = req.into_parts();
155 let info = ClientInfo::from_request_parts(&mut parts, &())
156 .await
157 .unwrap();
158
159 assert!(info.ip_value().is_none());
160 assert!(info.user_agent_value().is_none());
161 assert!(info.fingerprint_value().is_none());
162 }
163}