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 output.push('-');
55 }
56 } else {
57 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}