1use axum::{
4 extract::{ConnectInfo, MatchedPath, Request},
5 http::HeaderMap,
6 middleware::Next,
7 response::Response,
8};
9use mockforge_core::{create_http_log_entry, log_request_global};
10use std::collections::HashMap;
11use std::net::SocketAddr;
12use std::time::Instant;
13use tracing::info;
14
15pub async fn log_http_requests(
17 ConnectInfo(addr): ConnectInfo<SocketAddr>,
18 matched_path: Option<MatchedPath>,
19 req: Request,
20 next: Next,
21) -> Response {
22 let start_time = Instant::now();
23 let method = req.method().to_string();
24 let uri = req.uri().to_string();
25 let path = matched_path
26 .map(|mp| mp.as_str().to_string())
27 .unwrap_or_else(|| uri.split('?').next().unwrap_or(&uri).to_string());
28
29 let headers = extract_safe_headers(req.headers());
31
32 let user_agent = req
34 .headers()
35 .get("user-agent")
36 .and_then(|h| h.to_str().ok())
37 .map(|s| s.to_string());
38
39 let response = next.run(req).await;
41
42 let response_time_ms = start_time.elapsed().as_millis() as u64;
44 let status_code = response.status().as_u16();
45
46 let response_size_bytes = response
48 .headers()
49 .get("content-length")
50 .and_then(|h| h.to_str().ok())
51 .and_then(|s| s.parse::<u64>().ok())
52 .unwrap_or(0);
53
54 let error_message = if status_code >= 400 {
56 Some(format!(
57 "HTTP {} {}",
58 status_code,
59 response.status().canonical_reason().unwrap_or("Unknown")
60 ))
61 } else {
62 None
63 };
64
65 let log_entry = create_http_log_entry(
67 &method,
68 &path,
69 status_code,
70 response_time_ms,
71 Some(addr.ip().to_string()),
72 user_agent,
73 headers,
74 response_size_bytes,
75 error_message,
76 );
77
78 log_request_global(log_entry).await;
80
81 info!(
83 method = %method,
84 path = %path,
85 status = status_code,
86 duration_ms = response_time_ms,
87 client_ip = %addr.ip(),
88 "HTTP request processed"
89 );
90
91 response
92}
93
94fn extract_safe_headers(headers: &HeaderMap) -> HashMap<String, String> {
96 let mut safe_headers = HashMap::new();
97
98 let safe_header_names = [
100 "accept",
101 "accept-encoding",
102 "accept-language",
103 "cache-control",
104 "content-type",
105 "content-length",
106 "user-agent",
107 "referer",
108 "host",
109 "x-forwarded-for",
110 "x-real-ip",
111 ];
112
113 for name in safe_header_names {
114 if let Some(value) = headers.get(name) {
115 if let Ok(value_str) = value.to_str() {
116 safe_headers.insert(name.to_string(), value_str.to_string());
117 }
118 }
119 }
120
121 safe_headers
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use axum::http::HeaderValue;
128
129 #[test]
130 fn test_extract_safe_headers_empty() {
131 let headers = HeaderMap::new();
132 let safe_headers = extract_safe_headers(&headers);
133 assert_eq!(safe_headers.len(), 0);
134 }
135
136 #[test]
137 fn test_extract_safe_headers_with_safe_headers() {
138 let mut headers = HeaderMap::new();
139 headers.insert("content-type", HeaderValue::from_static("application/json"));
140 headers.insert("user-agent", HeaderValue::from_static("test-agent"));
141 headers.insert("accept", HeaderValue::from_static("application/json"));
142
143 let safe_headers = extract_safe_headers(&headers);
144
145 assert_eq!(safe_headers.len(), 3);
146 assert_eq!(safe_headers.get("content-type"), Some(&"application/json".to_string()));
147 assert_eq!(safe_headers.get("user-agent"), Some(&"test-agent".to_string()));
148 assert_eq!(safe_headers.get("accept"), Some(&"application/json".to_string()));
149 }
150
151 #[test]
152 fn test_extract_safe_headers_excludes_sensitive_headers() {
153 let mut headers = HeaderMap::new();
154 headers.insert("content-type", HeaderValue::from_static("application/json"));
155 headers.insert("authorization", HeaderValue::from_static("Bearer token123"));
156 headers.insert("cookie", HeaderValue::from_static("session=abc123"));
157 headers.insert("x-api-key", HeaderValue::from_static("secret-key"));
158
159 let safe_headers = extract_safe_headers(&headers);
160
161 assert_eq!(safe_headers.len(), 1);
163 assert_eq!(safe_headers.get("content-type"), Some(&"application/json".to_string()));
164
165 assert!(!safe_headers.contains_key("authorization"));
167 assert!(!safe_headers.contains_key("cookie"));
168 assert!(!safe_headers.contains_key("x-api-key"));
169 }
170
171 #[test]
172 fn test_extract_safe_headers_all_safe_header_types() {
173 let mut headers = HeaderMap::new();
174
175 headers.insert("accept", HeaderValue::from_static("application/json"));
177 headers.insert("accept-encoding", HeaderValue::from_static("gzip, deflate"));
178 headers.insert("accept-language", HeaderValue::from_static("en-US"));
179 headers.insert("cache-control", HeaderValue::from_static("no-cache"));
180 headers.insert("content-type", HeaderValue::from_static("application/json"));
181 headers.insert("content-length", HeaderValue::from_static("123"));
182 headers.insert("user-agent", HeaderValue::from_static("Mozilla/5.0"));
183 headers.insert("referer", HeaderValue::from_static("https://example.com"));
184 headers.insert("host", HeaderValue::from_static("api.example.com"));
185 headers.insert("x-forwarded-for", HeaderValue::from_static("192.168.1.1"));
186 headers.insert("x-real-ip", HeaderValue::from_static("192.168.1.2"));
187
188 let safe_headers = extract_safe_headers(&headers);
189
190 assert_eq!(safe_headers.len(), 11);
191 assert_eq!(safe_headers.get("accept"), Some(&"application/json".to_string()));
192 assert_eq!(safe_headers.get("accept-encoding"), Some(&"gzip, deflate".to_string()));
193 assert_eq!(safe_headers.get("accept-language"), Some(&"en-US".to_string()));
194 assert_eq!(safe_headers.get("cache-control"), Some(&"no-cache".to_string()));
195 assert_eq!(safe_headers.get("content-type"), Some(&"application/json".to_string()));
196 assert_eq!(safe_headers.get("content-length"), Some(&"123".to_string()));
197 assert_eq!(safe_headers.get("user-agent"), Some(&"Mozilla/5.0".to_string()));
198 assert_eq!(safe_headers.get("referer"), Some(&"https://example.com".to_string()));
199 assert_eq!(safe_headers.get("host"), Some(&"api.example.com".to_string()));
200 assert_eq!(safe_headers.get("x-forwarded-for"), Some(&"192.168.1.1".to_string()));
201 assert_eq!(safe_headers.get("x-real-ip"), Some(&"192.168.1.2".to_string()));
202 }
203
204 #[test]
205 fn test_extract_safe_headers_handles_invalid_utf8() {
206 let mut headers = HeaderMap::new();
207 headers.insert("content-type", HeaderValue::from_static("application/json"));
208 let safe_headers = extract_safe_headers(&headers);
212 assert!(safe_headers.contains_key("content-type"));
213 }
214
215 #[test]
216 fn test_extract_safe_headers_case_insensitive() {
217 let mut headers = HeaderMap::new();
218 headers.insert("Content-Type", HeaderValue::from_static("application/json"));
220 headers.insert("User-Agent", HeaderValue::from_static("test"));
221
222 let safe_headers = extract_safe_headers(&headers);
223
224 assert_eq!(safe_headers.len(), 2);
226 assert!(safe_headers.contains_key("content-type"));
227 assert!(safe_headers.contains_key("user-agent"));
228 }
229
230 #[test]
231 fn test_extract_safe_headers_mixed_safe_and_unsafe() {
232 let mut headers = HeaderMap::new();
233 headers.insert("content-type", HeaderValue::from_static("application/json"));
234 headers.insert("authorization", HeaderValue::from_static("Bearer token"));
235 headers.insert("user-agent", HeaderValue::from_static("Mozilla/5.0"));
236 headers.insert("x-api-key", HeaderValue::from_static("secret"));
237 headers.insert("accept", HeaderValue::from_static("*/*"));
238
239 let safe_headers = extract_safe_headers(&headers);
240
241 assert_eq!(safe_headers.len(), 3);
243 assert!(safe_headers.contains_key("content-type"));
244 assert!(safe_headers.contains_key("user-agent"));
245 assert!(safe_headers.contains_key("accept"));
246
247 assert!(!safe_headers.contains_key("authorization"));
249 assert!(!safe_headers.contains_key("x-api-key"));
250 }
251}