1use std::collections::HashMap;
2
3use ferron_common::config::ServerConfigurationValue;
4use ferron_common::modules::SocketData;
5use serde_json::{Map, Number, Value};
6
7const DEFAULT_ACCESS_LOG_FORMAT: &str =
8 "{client_ip} - {auth_user} [{timestamp}] \"{method} {path_and_query} {version}\" \
9 {status_code} {content_length} \"{header:Referer}\" \"{header:User-Agent}\"";
10
11fn http_version_to_str(version: hyper::Version) -> &'static str {
12 match version {
13 hyper::Version::HTTP_09 => "HTTP/0.9",
14 hyper::Version::HTTP_10 => "HTTP/1.0",
15 hyper::Version::HTTP_11 => "HTTP/1.1",
16 hyper::Version::HTTP_2 => "HTTP/2.0",
17 hyper::Version::HTTP_3 => "HTTP/3.0",
18 _ => "HTTP/Unknown",
19 }
20}
21
22fn resolve_log_placeholder(
23 placeholder: &str,
24 request_parts: &hyper::http::request::Parts,
25 socket_data: &SocketData,
26 auth_user: Option<&str>,
27 timestamp_str: &str,
28 status_code: u16,
29 content_length: Option<u64>,
30) -> Option<String> {
31 Some(match placeholder {
32 "path" => request_parts.uri.path().to_string(),
33 "path_and_query" => request_parts
34 .uri
35 .path_and_query()
36 .map_or_else(|| request_parts.uri.path().to_string(), |p| p.as_str().to_string()),
37 "method" => request_parts.method.as_str().to_string(),
38 "version" => http_version_to_str(request_parts.version).to_string(),
39 "scheme" => {
40 if socket_data.encrypted {
41 "https".to_string()
42 } else {
43 "http".to_string()
44 }
45 }
46 "client_ip" => socket_data.remote_addr.ip().to_string(),
47 "client_port" => socket_data.remote_addr.port().to_string(),
48 "client_ip_canonical" => socket_data.remote_addr.ip().to_canonical().to_string(),
49 "server_ip" => socket_data.local_addr.ip().to_string(),
50 "server_port" => socket_data.local_addr.port().to_string(),
51 "server_ip_canonical" => socket_data.local_addr.ip().to_canonical().to_string(),
52 "auth_user" => auth_user.unwrap_or("-").to_string(),
53 "timestamp" => timestamp_str.to_string(),
54 "status_code" => status_code.to_string(),
55 "content_length" => content_length.map_or_else(|| "-".to_string(), |len| len.to_string()),
56 _ => {
57 if let Some(header_name) = placeholder.strip_prefix("header:") {
58 if let Some(header_value) = request_parts.headers.get(header_name) {
59 header_value.to_str().unwrap_or("").to_string()
60 } else {
61 "-".to_string()
62 }
63 } else {
64 return None;
65 }
66 }
67 })
68}
69
70pub fn replace_log_placeholders(
71 input: &str,
72 request_parts: &hyper::http::request::Parts,
73 socket_data: &SocketData,
74 auth_user: Option<&str>,
75 timestamp_str: &str,
76 status_code: u16,
77 content_length: Option<u64>,
78) -> String {
79 let mut output = String::new();
80 let mut index_rb_saved = 0;
81 loop {
82 let index_lb = input[index_rb_saved..].find("{");
83 if let Some(index_lb) = index_lb {
84 let index_rb_afterlb = input[index_rb_saved + index_lb + 1..].find("}");
85 if let Some(index_rb_afterlb) = index_rb_afterlb {
86 let index_rb = index_rb_afterlb + index_lb + 1;
87 let placeholder_value = &input[index_rb_saved + index_lb + 1..index_rb_saved + index_rb];
88 output.push_str(&input[index_rb_saved..index_rb_saved + index_lb]);
89 if let Some(value) = resolve_log_placeholder(
90 placeholder_value,
91 request_parts,
92 socket_data,
93 auth_user,
94 timestamp_str,
95 status_code,
96 content_length,
97 ) {
98 output.push_str(&value);
99 } else {
100 output.push('{');
102 output.push_str(placeholder_value);
103 output.push('}');
104 }
105 if index_rb < input.len() - 1 {
106 index_rb_saved += index_rb + 1;
107 } else {
108 break;
109 }
110 } else {
111 output.push_str(&input[index_rb_saved..]);
112 }
113 } else {
114 output.push_str(&input[index_rb_saved..]);
115 break;
116 }
117 }
118 output
119}
120
121#[allow(clippy::too_many_arguments)]
122pub fn generate_access_log_message(
123 request_parts: &hyper::http::request::Parts,
124 socket_data: &SocketData,
125 auth_user: Option<&str>,
126 timestamp_str: &str,
127 status_code: u16,
128 content_length: Option<u64>,
129 log_format: Option<&str>,
130 log_json_props: Option<&HashMap<String, ServerConfigurationValue>>,
131) -> String {
132 if let Some(log_json_props) = log_json_props {
133 let mut log_entry = Map::new();
134 log_entry.insert(
135 "auth_user".to_string(),
136 auth_user.map_or(Value::Null, |user| Value::String(user.to_string())),
137 );
138 log_entry.insert(
139 "client_ip".to_string(),
140 Value::String(socket_data.remote_addr.ip().to_string()),
141 );
142 log_entry.insert(
143 "content_length".to_string(),
144 content_length.map_or(Value::Null, |len| Value::Number(Number::from(len))),
145 );
146 log_entry.insert(
147 "method".to_string(),
148 Value::String(request_parts.method.as_str().to_string()),
149 );
150 log_entry.insert(
151 "path_and_query".to_string(),
152 Value::String(
153 request_parts
154 .uri
155 .path_and_query()
156 .map_or_else(|| request_parts.uri.path().to_string(), |p| p.as_str().to_string()),
157 ),
158 );
159 log_entry.insert("status_code".to_string(), Value::Number(Number::from(status_code)));
160 log_entry.insert("timestamp".to_string(), Value::String(timestamp_str.to_string()));
161 log_entry.insert(
162 "version".to_string(),
163 Value::String(http_version_to_str(request_parts.version).to_string()),
164 );
165 log_entry.insert(
166 "referer".to_string(),
167 request_parts
168 .headers
169 .get("Referer")
170 .and_then(|value| value.to_str().ok())
171 .map_or(Value::Null, |value| Value::String(value.to_string())),
172 );
173 log_entry.insert(
174 "user_agent".to_string(),
175 request_parts
176 .headers
177 .get("User-Agent")
178 .and_then(|value| value.to_str().ok())
179 .map_or(Value::Null, |value| Value::String(value.to_string())),
180 );
181
182 for (property_name, property_value) in log_json_props {
183 if let Some(property_template) = property_value.as_str() {
184 log_entry.insert(
185 property_name.clone(),
186 Value::String(replace_log_placeholders(
187 property_template,
188 request_parts,
189 socket_data,
190 auth_user,
191 timestamp_str,
192 status_code,
193 content_length,
194 )),
195 );
196 }
197 }
198
199 Value::Object(log_entry).to_string()
200 } else {
201 replace_log_placeholders(
202 log_format.unwrap_or(DEFAULT_ACCESS_LOG_FORMAT),
203 request_parts,
204 socket_data,
205 auth_user,
206 timestamp_str,
207 status_code,
208 content_length,
209 )
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use ferron_common::config::ServerConfigurationValue;
217 use hyper::header::HeaderName;
218 use hyper::http::{request::Parts, Method, Version};
219 use hyper::Request;
220 use serde_json::json;
221
222 fn make_parts(uri_str: &str, method: Method, version: Version, headers: Option<Vec<(&str, &str)>>) -> Parts {
223 let mut parts = Request::builder()
224 .uri(uri_str)
225 .method(method)
226 .version(version)
227 .body(())
228 .unwrap()
229 .into_parts()
230 .0;
231
232 if let Some(hdrs) = headers {
233 for (k, v) in hdrs {
234 parts
235 .headers
236 .insert(k.parse::<HeaderName>().unwrap(), v.parse().unwrap());
237 }
238 }
239 parts
240 }
241
242 #[test]
243 fn test_basic_placeholders() {
244 let parts = make_parts("/some/path", Method::GET, Version::HTTP_11, None);
245 let input = "Path: {path}, Method: {method}, Version: {version}";
246 let expected = "Path: /some/path, Method: GET, Version: HTTP/1.1";
247 let output = replace_log_placeholders(
248 input,
249 &parts,
250 &SocketData {
251 remote_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 40000)),
252 local_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 80)),
253 encrypted: false,
254 },
255 None,
256 "06/Oct/2025:15:12:51 +0200",
257 200,
258 None,
259 );
260 assert_eq!(output, expected);
261 }
262
263 #[test]
264 fn test_header_placeholder() {
265 let parts = make_parts(
266 "/test",
267 Method::POST,
268 Version::HTTP_2,
269 Some(vec![("User-Agent", "MyApp/1.0")]),
270 );
271 let input = "Header: {header:User-Agent}";
272 let expected = "Header: MyApp/1.0";
273 let output = replace_log_placeholders(
274 input,
275 &parts,
276 &SocketData {
277 remote_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 40000)),
278 local_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 80)),
279 encrypted: false,
280 },
281 None,
282 "06/Oct/2025:15:12:51 +0200",
283 200,
284 None,
285 );
286 assert_eq!(output, expected);
287 }
288
289 #[test]
290 fn test_missing_header() {
291 let parts = make_parts("/", Method::GET, Version::HTTP_11, None);
292 let input = "Header: {header:Missing}";
293 let expected = "Header: -";
294 let output = replace_log_placeholders(
295 input,
296 &parts,
297 &SocketData {
298 remote_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 40000)),
299 local_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 80)),
300 encrypted: false,
301 },
302 None,
303 "06/Oct/2025:15:12:51 +0200",
304 200,
305 None,
306 );
307 assert_eq!(output, expected);
308 }
309
310 #[test]
311 fn test_unknown_placeholder() {
312 let parts = make_parts("/", Method::GET, Version::HTTP_11, None);
313 let input = "Unknown: {foo}";
314 let expected = "Unknown: {foo}";
315 let output = replace_log_placeholders(
316 input,
317 &parts,
318 &SocketData {
319 remote_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 40000)),
320 local_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 80)),
321 encrypted: false,
322 },
323 None,
324 "06/Oct/2025:15:12:51 +0200",
325 200,
326 None,
327 );
328 assert_eq!(output, expected);
329 }
330
331 #[test]
332 fn test_no_placeholders() {
333 let parts = make_parts("/", Method::GET, Version::HTTP_11, None);
334 let input = "Static string with no placeholders.";
335 let output = replace_log_placeholders(
336 input,
337 &parts,
338 &SocketData {
339 remote_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 40000)),
340 local_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 80)),
341 encrypted: false,
342 },
343 None,
344 "06/Oct/2025:15:12:51 +0200",
345 200,
346 None,
347 );
348 assert_eq!(output, input);
349 }
350
351 #[test]
352 fn test_multiple_placeholders() {
353 let parts = make_parts(
354 "/data",
355 Method::PUT,
356 Version::HTTP_2,
357 Some(vec![("Content-Type", "application/json"), ("Host", "api.example.com")]),
358 );
359 let input = "{method} {path} {version} Host: {header:Host} Content-Type: {header:Content-Type}";
360 let expected = "PUT /data HTTP/2.0 Host: api.example.com Content-Type: application/json";
361 let output = replace_log_placeholders(
362 input,
363 &parts,
364 &SocketData {
365 remote_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 40000)),
366 local_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 80)),
367 encrypted: false,
368 },
369 None,
370 "06/Oct/2025:15:12:51 +0200",
371 200,
372 None,
373 );
374 assert_eq!(output, expected);
375 }
376
377 #[test]
378 fn test_log_placeholders() {
379 let parts = make_parts(
380 "/data",
381 Method::PUT,
382 Version::HTTP_2,
383 Some(vec![("Content-Type", "application/json"), ("Host", "api.example.com")]),
384 );
385 let input = "[{timestamp}] {auth_user} {status_code} {content_length}";
386 let expected = "[06/Oct/2025:15:12:51 +0200] - 200 -";
387 let output = replace_log_placeholders(
388 input,
389 &parts,
390 &SocketData {
391 remote_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 40000)),
392 local_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 80)),
393 encrypted: false,
394 },
395 None,
396 "06/Oct/2025:15:12:51 +0200",
397 200,
398 None,
399 );
400 assert_eq!(output, expected);
401 }
402
403 #[test]
404 fn test_generate_access_log_message_plain_text() {
405 let parts = make_parts("/test?hello=world", Method::GET, Version::HTTP_11, None);
406 let output = generate_access_log_message(
407 &parts,
408 &SocketData {
409 remote_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 40000)),
410 local_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 80)),
411 encrypted: false,
412 },
413 None,
414 "06/Oct/2025:15:12:51 +0200",
415 200,
416 Some(123),
417 None,
418 None,
419 );
420 assert_eq!(
421 output,
422 "127.0.0.1 - - [06/Oct/2025:15:12:51 +0200] \"GET /test?hello=world HTTP/1.1\" 200 123 \"-\" \"-\""
423 );
424 }
425
426 #[test]
427 fn test_generate_access_log_message_json_with_extra_props() {
428 let parts = make_parts(
429 "/api/items?id=1",
430 Method::POST,
431 Version::HTTP_2,
432 Some(vec![
433 ("Referer", "https://example.com/app"),
434 ("User-Agent", "FerronTest/1.0"),
435 ("X-Request-Id", "req-123"),
436 ]),
437 );
438 let mut extra_props = HashMap::new();
439 extra_props.insert(
440 "request_id".to_string(),
441 ServerConfigurationValue::String("{header:X-Request-Id}".to_string()),
442 );
443 extra_props.insert(
444 "request_target".to_string(),
445 ServerConfigurationValue::String("{method} {path_and_query}".to_string()),
446 );
447
448 let output = generate_access_log_message(
449 &parts,
450 &SocketData {
451 remote_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 40000)),
452 local_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 443)),
453 encrypted: true,
454 },
455 Some("alice"),
456 "06/Oct/2025:15:12:51 +0200",
457 201,
458 Some(456),
459 Some("{method} {path_and_query}"),
460 Some(&extra_props),
461 );
462
463 let output: Value = serde_json::from_str(&output).unwrap();
464 assert_eq!(
465 output,
466 json!({
467 "auth_user": "alice",
468 "client_ip": "127.0.0.1",
469 "content_length": 456,
470 "method": "POST",
471 "path_and_query": "/api/items?id=1",
472 "referer": "https://example.com/app",
473 "request_id": "req-123",
474 "request_target": "POST /api/items?id=1",
475 "status_code": 201,
476 "timestamp": "06/Oct/2025:15:12:51 +0200",
477 "user_agent": "FerronTest/1.0",
478 "version": "HTTP/2.0"
479 })
480 );
481 }
482
483 #[test]
484 fn test_generate_access_log_message_json_can_override_default_fields() {
485 let parts = make_parts("/health", Method::GET, Version::HTTP_11, None);
486 let mut extra_props = HashMap::new();
487 extra_props.insert(
488 "status_code".to_string(),
489 ServerConfigurationValue::String("ok".to_string()),
490 );
491
492 let output = generate_access_log_message(
493 &parts,
494 &SocketData {
495 remote_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 40000)),
496 local_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 80)),
497 encrypted: false,
498 },
499 None,
500 "06/Oct/2025:15:12:51 +0200",
501 200,
502 None,
503 None,
504 Some(&extra_props),
505 );
506
507 let output: Value = serde_json::from_str(&output).unwrap();
508 assert_eq!(output["status_code"], "ok");
509 assert_eq!(output["content_length"], Value::Null);
510 assert_eq!(output["auth_user"], Value::Null);
511 }
512}