ferron/util/
log_placeholders.rs

1use ferron_common::modules::SocketData;
2
3pub fn replace_log_placeholders(
4  input: &str,
5  request_parts: &hyper::http::request::Parts,
6  socket_data: &SocketData,
7  auth_user: Option<&str>,
8  timestamp_str: &str,
9  status_code: u16,
10  content_length: Option<u64>,
11) -> String {
12  let mut output = String::new();
13  let mut index_rb_saved = 0;
14  loop {
15    let index_lb = input[index_rb_saved..].find("{");
16    if let Some(index_lb) = index_lb {
17      let index_rb_afterlb = input[index_rb_saved + index_lb + 1..].find("}");
18      if let Some(index_rb_afterlb) = index_rb_afterlb {
19        let index_rb = index_rb_afterlb + index_lb + 1;
20        let placeholder_value = &input[index_rb_saved + index_lb + 1..index_rb_saved + index_rb];
21        output.push_str(&input[index_rb_saved..index_rb_saved + index_lb]);
22        match placeholder_value {
23          "path" => output.push_str(request_parts.uri.path()),
24          "path_and_query" => output.push_str(
25            request_parts
26              .uri
27              .path_and_query()
28              .map_or(request_parts.uri.path(), |p| p.as_str()),
29          ),
30          "method" => output.push_str(request_parts.method.as_str()),
31          "version" => output.push_str(match request_parts.version {
32            hyper::Version::HTTP_09 => "HTTP/0.9",
33            hyper::Version::HTTP_10 => "HTTP/1.0",
34            hyper::Version::HTTP_11 => "HTTP/1.1",
35            hyper::Version::HTTP_2 => "HTTP/2.0",
36            hyper::Version::HTTP_3 => "HTTP/3.0",
37            _ => "HTTP/Unknown",
38          }),
39          "scheme" => output.push_str(if socket_data.encrypted { "https" } else { "http" }),
40          "client_ip" => output.push_str(&socket_data.remote_addr.ip().to_string()),
41          "client_port" => output.push_str(&socket_data.remote_addr.port().to_string()),
42          "client_ip_canonical" => output.push_str(&socket_data.remote_addr.ip().to_canonical().to_string()),
43          "server_ip" => output.push_str(&socket_data.local_addr.ip().to_string()),
44          "server_port" => output.push_str(&socket_data.local_addr.port().to_string()),
45          "server_ip_canonical" => output.push_str(&socket_data.local_addr.ip().to_canonical().to_string()),
46          "auth_user" => output.push_str(auth_user.unwrap_or("-")),
47          "timestamp" => output.push_str(timestamp_str),
48          "status_code" => output.push_str(&status_code.to_string()),
49          "content_length" => output.push_str(&content_length.map_or("-".to_string(), |len| len.to_string())),
50          _ => {
51            if let Some(header_name) = placeholder_value.strip_prefix("header:") {
52              if let Some(header_value) = request_parts.headers.get(header_name) {
53                output.push_str(header_value.to_str().unwrap_or(""));
54              } else {
55                // Header not found, leave "-"
56                output.push('-');
57              }
58            } else {
59              // Unknown placeholder, leave it as is
60              output.push('{');
61              output.push_str(placeholder_value);
62              output.push('}');
63            }
64          }
65        }
66        if index_rb < input.len() - 1 {
67          index_rb_saved += index_rb + 1;
68        } else {
69          break;
70        }
71      } else {
72        output.push_str(&input[index_rb_saved..]);
73      }
74    } else {
75      output.push_str(&input[index_rb_saved..]);
76      break;
77    }
78  }
79  output
80}
81
82#[cfg(test)]
83mod tests {
84  use super::*;
85  use hyper::header::HeaderName;
86  use hyper::http::{request::Parts, Method, Version};
87  use hyper::Request;
88
89  fn make_parts(uri_str: &str, method: Method, version: Version, headers: Option<Vec<(&str, &str)>>) -> Parts {
90    let mut parts = Request::builder()
91      .uri(uri_str)
92      .method(method)
93      .version(version)
94      .body(())
95      .unwrap()
96      .into_parts()
97      .0;
98
99    if let Some(hdrs) = headers {
100      for (k, v) in hdrs {
101        parts
102          .headers
103          .insert(k.parse::<HeaderName>().unwrap(), v.parse().unwrap());
104      }
105    }
106    parts
107  }
108
109  #[test]
110  fn test_basic_placeholders() {
111    let parts = make_parts("/some/path", Method::GET, Version::HTTP_11, None);
112    let input = "Path: {path}, Method: {method}, Version: {version}";
113    let expected = "Path: /some/path, Method: GET, Version: HTTP/1.1";
114    let output = replace_log_placeholders(
115      input,
116      &parts,
117      &SocketData {
118        remote_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 40000)),
119        local_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 80)),
120        encrypted: false,
121      },
122      None,
123      "06/Oct/2025:15:12:51 +0200",
124      200,
125      None,
126    );
127    assert_eq!(output, expected);
128  }
129
130  #[test]
131  fn test_header_placeholder() {
132    let parts = make_parts(
133      "/test",
134      Method::POST,
135      Version::HTTP_2,
136      Some(vec![("User-Agent", "MyApp/1.0")]),
137    );
138    let input = "Header: {header:User-Agent}";
139    let expected = "Header: MyApp/1.0";
140    let output = replace_log_placeholders(
141      input,
142      &parts,
143      &SocketData {
144        remote_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 40000)),
145        local_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 80)),
146        encrypted: false,
147      },
148      None,
149      "06/Oct/2025:15:12:51 +0200",
150      200,
151      None,
152    );
153    assert_eq!(output, expected);
154  }
155
156  #[test]
157  fn test_missing_header() {
158    let parts = make_parts("/", Method::GET, Version::HTTP_11, None);
159    let input = "Header: {header:Missing}";
160    let expected = "Header: -";
161    let output = replace_log_placeholders(
162      input,
163      &parts,
164      &SocketData {
165        remote_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 40000)),
166        local_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 80)),
167        encrypted: false,
168      },
169      None,
170      "06/Oct/2025:15:12:51 +0200",
171      200,
172      None,
173    );
174    assert_eq!(output, expected);
175  }
176
177  #[test]
178  fn test_unknown_placeholder() {
179    let parts = make_parts("/", Method::GET, Version::HTTP_11, None);
180    let input = "Unknown: {foo}";
181    let expected = "Unknown: {foo}";
182    let output = replace_log_placeholders(
183      input,
184      &parts,
185      &SocketData {
186        remote_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 40000)),
187        local_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 80)),
188        encrypted: false,
189      },
190      None,
191      "06/Oct/2025:15:12:51 +0200",
192      200,
193      None,
194    );
195    assert_eq!(output, expected);
196  }
197
198  #[test]
199  fn test_no_placeholders() {
200    let parts = make_parts("/", Method::GET, Version::HTTP_11, None);
201    let input = "Static string with no placeholders.";
202    let output = replace_log_placeholders(
203      input,
204      &parts,
205      &SocketData {
206        remote_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 40000)),
207        local_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 80)),
208        encrypted: false,
209      },
210      None,
211      "06/Oct/2025:15:12:51 +0200",
212      200,
213      None,
214    );
215    assert_eq!(output, input);
216  }
217
218  #[test]
219  fn test_multiple_placeholders() {
220    let parts = make_parts(
221      "/data",
222      Method::PUT,
223      Version::HTTP_2,
224      Some(vec![("Content-Type", "application/json"), ("Host", "api.example.com")]),
225    );
226    let input = "{method} {path} {version} Host: {header:Host} Content-Type: {header:Content-Type}";
227    let expected = "PUT /data HTTP/2.0 Host: api.example.com Content-Type: application/json";
228    let output = replace_log_placeholders(
229      input,
230      &parts,
231      &SocketData {
232        remote_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 40000)),
233        local_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 80)),
234        encrypted: false,
235      },
236      None,
237      "06/Oct/2025:15:12:51 +0200",
238      200,
239      None,
240    );
241    assert_eq!(output, expected);
242  }
243
244  #[test]
245  fn test_log_placeholders() {
246    let parts = make_parts(
247      "/data",
248      Method::PUT,
249      Version::HTTP_2,
250      Some(vec![("Content-Type", "application/json"), ("Host", "api.example.com")]),
251    );
252    let input = "[{timestamp}] {auth_user} {status_code} {content_length}";
253    let expected = "[06/Oct/2025:15:12:51 +0200] - 200 -";
254    let output = replace_log_placeholders(
255      input,
256      &parts,
257      &SocketData {
258        remote_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 40000)),
259        local_addr: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 80)),
260        encrypted: false,
261      },
262      None,
263      "06/Oct/2025:15:12:51 +0200",
264      200,
265      None,
266    );
267    assert_eq!(output, expected);
268  }
269}