1use axum::{
4 extract::{ConnectInfo, MatchedPath, Request},
5 http::HeaderMap,
6 middleware::Next,
7 response::Response,
8};
9use mockforge_core::{
10 create_http_log_entry_with_query, log_request_global,
11 reality_continuum::response_trace::ResponseGenerationTrace,
12 request_logger::RealityTraceMetadata,
13};
14use std::collections::HashMap;
15use std::net::SocketAddr;
16use std::time::Instant;
17use tracing::info;
18
19pub async fn log_http_requests(
21 ConnectInfo(addr): ConnectInfo<SocketAddr>,
22 matched_path: Option<MatchedPath>,
23 req: Request,
24 next: Next,
25) -> Response {
26 let start_time = Instant::now();
27 let method = req.method().to_string();
28 let uri = req.uri().to_string();
29 let path = matched_path
30 .map(|mp| mp.as_str().to_string())
31 .unwrap_or_else(|| uri.split('?').next().unwrap_or(&uri).to_string());
32
33 let query_params: HashMap<String, String> = req
35 .uri()
36 .query()
37 .map(|q| url::form_urlencoded::parse(q.as_bytes()).into_owned().collect())
38 .unwrap_or_default();
39
40 let headers = extract_safe_headers(req.headers());
42
43 let user_agent = req
45 .headers()
46 .get("user-agent")
47 .and_then(|h| h.to_str().ok())
48 .map(|s| s.to_string());
49
50 let reality_metadata = req.extensions().get::<RealityTraceMetadata>().cloned();
53
54 let response = next.run(req).await;
56
57 let response_time_ms = start_time.elapsed().as_millis() as u64;
59 let status_code = response.status().as_u16();
60
61 let response_size_bytes = response
63 .headers()
64 .get("content-length")
65 .and_then(|h| h.to_str().ok())
66 .and_then(|s| s.parse::<u64>().ok())
67 .unwrap_or(0);
68
69 let error_message = if status_code >= 400 {
71 Some(format!(
72 "HTTP {} {}",
73 status_code,
74 response.status().canonical_reason().unwrap_or("Unknown")
75 ))
76 } else {
77 None
78 };
79
80 let mut log_entry = create_http_log_entry_with_query(
82 &method,
83 &path,
84 status_code,
85 response_time_ms,
86 Some(addr.ip().to_string()),
87 user_agent,
88 headers,
89 query_params.clone(),
90 response_size_bytes,
91 error_message,
92 );
93
94 log_entry.reality_metadata = reality_metadata;
96
97 if let Some(trace) = response.extensions().get::<ResponseGenerationTrace>() {
99 if let Ok(trace_json) = serde_json::to_string(trace) {
101 log_entry.metadata.insert("response_generation_trace".to_string(), trace_json);
102 }
103 }
104
105 log_request_global(log_entry).await;
107
108 if !query_params.is_empty() {
110 let query_params_clone = query_params.clone();
111 info!(
112 method = %method,
113 path = %path,
114 query = ?query_params_clone,
115 status = status_code,
116 duration_ms = response_time_ms,
117 client_ip = %addr.ip(),
118 "HTTP request processed"
119 );
120 } else {
121 info!(
122 method = %method,
123 path = %path,
124 status = status_code,
125 duration_ms = response_time_ms,
126 client_ip = %addr.ip(),
127 "HTTP request processed"
128 );
129 }
130
131 response
132}
133
134fn extract_safe_headers(headers: &HeaderMap) -> HashMap<String, String> {
136 let mut safe_headers = HashMap::new();
137
138 let safe_header_names = [
140 "accept",
141 "accept-encoding",
142 "accept-language",
143 "cache-control",
144 "content-type",
145 "content-length",
146 "user-agent",
147 "referer",
148 "host",
149 "x-forwarded-for",
150 "x-real-ip",
151 ];
152
153 for name in safe_header_names {
154 if let Some(value) = headers.get(name) {
155 if let Ok(value_str) = value.to_str() {
156 safe_headers.insert(name.to_string(), value_str.to_string());
157 }
158 }
159 }
160
161 safe_headers
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use axum::http::HeaderValue;
168
169 #[test]
170 fn test_extract_safe_headers_empty() {
171 let headers = HeaderMap::new();
172 let safe_headers = extract_safe_headers(&headers);
173 assert_eq!(safe_headers.len(), 0);
174 }
175
176 #[test]
177 fn test_extract_safe_headers_with_safe_headers() {
178 let mut headers = HeaderMap::new();
179 headers.insert("content-type", HeaderValue::from_static("application/json"));
180 headers.insert("user-agent", HeaderValue::from_static("test-agent"));
181 headers.insert("accept", HeaderValue::from_static("application/json"));
182
183 let safe_headers = extract_safe_headers(&headers);
184
185 assert_eq!(safe_headers.len(), 3);
186 assert_eq!(safe_headers.get("content-type"), Some(&"application/json".to_string()));
187 assert_eq!(safe_headers.get("user-agent"), Some(&"test-agent".to_string()));
188 assert_eq!(safe_headers.get("accept"), Some(&"application/json".to_string()));
189 }
190
191 #[test]
192 fn test_extract_safe_headers_excludes_sensitive_headers() {
193 let mut headers = HeaderMap::new();
194 headers.insert("content-type", HeaderValue::from_static("application/json"));
195 headers.insert("authorization", HeaderValue::from_static("Bearer token123"));
196 headers.insert("cookie", HeaderValue::from_static("session=abc123"));
197 headers.insert("x-api-key", HeaderValue::from_static("secret-key"));
198
199 let safe_headers = extract_safe_headers(&headers);
200
201 assert_eq!(safe_headers.len(), 1);
203 assert_eq!(safe_headers.get("content-type"), Some(&"application/json".to_string()));
204
205 assert!(!safe_headers.contains_key("authorization"));
207 assert!(!safe_headers.contains_key("cookie"));
208 assert!(!safe_headers.contains_key("x-api-key"));
209 }
210
211 #[test]
212 fn test_extract_safe_headers_all_safe_header_types() {
213 let mut headers = HeaderMap::new();
214
215 headers.insert("accept", HeaderValue::from_static("application/json"));
217 headers.insert("accept-encoding", HeaderValue::from_static("gzip, deflate"));
218 headers.insert("accept-language", HeaderValue::from_static("en-US"));
219 headers.insert("cache-control", HeaderValue::from_static("no-cache"));
220 headers.insert("content-type", HeaderValue::from_static("application/json"));
221 headers.insert("content-length", HeaderValue::from_static("123"));
222 headers.insert("user-agent", HeaderValue::from_static("Mozilla/5.0"));
223 headers.insert("referer", HeaderValue::from_static("https://example.com"));
224 headers.insert("host", HeaderValue::from_static("api.example.com"));
225 headers.insert("x-forwarded-for", HeaderValue::from_static("192.168.1.1"));
226 headers.insert("x-real-ip", HeaderValue::from_static("192.168.1.2"));
227
228 let safe_headers = extract_safe_headers(&headers);
229
230 assert_eq!(safe_headers.len(), 11);
231 assert_eq!(safe_headers.get("accept"), Some(&"application/json".to_string()));
232 assert_eq!(safe_headers.get("accept-encoding"), Some(&"gzip, deflate".to_string()));
233 assert_eq!(safe_headers.get("accept-language"), Some(&"en-US".to_string()));
234 assert_eq!(safe_headers.get("cache-control"), Some(&"no-cache".to_string()));
235 assert_eq!(safe_headers.get("content-type"), Some(&"application/json".to_string()));
236 assert_eq!(safe_headers.get("content-length"), Some(&"123".to_string()));
237 assert_eq!(safe_headers.get("user-agent"), Some(&"Mozilla/5.0".to_string()));
238 assert_eq!(safe_headers.get("referer"), Some(&"https://example.com".to_string()));
239 assert_eq!(safe_headers.get("host"), Some(&"api.example.com".to_string()));
240 assert_eq!(safe_headers.get("x-forwarded-for"), Some(&"192.168.1.1".to_string()));
241 assert_eq!(safe_headers.get("x-real-ip"), Some(&"192.168.1.2".to_string()));
242 }
243
244 #[test]
245 fn test_extract_safe_headers_handles_invalid_utf8() {
246 let mut headers = HeaderMap::new();
247 headers.insert("content-type", HeaderValue::from_static("application/json"));
248 let safe_headers = extract_safe_headers(&headers);
252 assert!(safe_headers.contains_key("content-type"));
253 }
254
255 #[test]
256 fn test_extract_safe_headers_case_insensitive() {
257 let mut headers = HeaderMap::new();
258 headers.insert("Content-Type", HeaderValue::from_static("application/json"));
260 headers.insert("User-Agent", HeaderValue::from_static("test"));
261
262 let safe_headers = extract_safe_headers(&headers);
263
264 assert_eq!(safe_headers.len(), 2);
266 assert!(safe_headers.contains_key("content-type"));
267 assert!(safe_headers.contains_key("user-agent"));
268 }
269
270 #[test]
271 fn test_extract_safe_headers_mixed_safe_and_unsafe() {
272 let mut headers = HeaderMap::new();
273 headers.insert("content-type", HeaderValue::from_static("application/json"));
274 headers.insert("authorization", HeaderValue::from_static("Bearer token"));
275 headers.insert("user-agent", HeaderValue::from_static("Mozilla/5.0"));
276 headers.insert("x-api-key", HeaderValue::from_static("secret"));
277 headers.insert("accept", HeaderValue::from_static("*/*"));
278
279 let safe_headers = extract_safe_headers(&headers);
280
281 assert_eq!(safe_headers.len(), 3);
283 assert!(safe_headers.contains_key("content-type"));
284 assert!(safe_headers.contains_key("user-agent"));
285 assert!(safe_headers.contains_key("accept"));
286
287 assert!(!safe_headers.contains_key("authorization"));
289 assert!(!safe_headers.contains_key("x-api-key"));
290 }
291}