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