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