tiny_proxy/auth/
headers.rs1use hyper::Request;
7
8pub fn process_header_substitution<B>(value: &str, req: &Request<B>) -> anyhow::Result<String> {
39 let mut result = value.to_string();
40
41 while let Some(start) = result.find("{header.") {
43 let end = result[start..]
44 .find('}')
45 .ok_or_else(|| anyhow::anyhow!("Unclosed header substitution at position {}", start))?
46 + start;
47
48 let header_name = &result[start + 8..end];
49
50 if let Some(header_value) = req.headers().get(header_name).and_then(|h| h.to_str().ok()) {
51 result.replace_range(start..=end, header_value);
52 } else {
53 result.replace_range(start..=end, "");
55 }
56 }
57
58 while let Some(start) = result.find("{env.") {
60 let end = result[start..].find('}').ok_or_else(|| {
61 anyhow::anyhow!(
62 "Unclosed environment variable substitution at position {}",
63 start
64 )
65 })? + start;
66
67 let var_name = &result[start + 5..end];
68
69 if let Ok(env_value) = std::env::var(var_name) {
70 result.replace_range(start..=end, &env_value);
71 } else {
72 result.replace_range(start..=end, "");
74 }
75 }
76
77 result = result.replace("{uuid}", &uuid::Uuid::new_v4().to_string());
79
80 Ok(result)
81}
82
83pub fn process_upstream_substitution<B>(
96 value: &str,
97 req: &Request<B>,
98 upstream_host: &str,
99 request_uri: &str,
100 remote_ip: &str,
101) -> anyhow::Result<String> {
102 let mut result = process_header_substitution(value, req)?;
104
105 result = result.replace("{upstream_host}", upstream_host);
106 result = result.replace("{request.uri}", request_uri);
107 result = result.replace("{remote_ip}", remote_ip);
108
109 Ok(result)
110}
111
112pub fn extract_remote_ip<B>(req: &Request<B>) -> Option<String> {
125 if let Some(xff) = req.headers().get("X-Forwarded-For") {
127 if let Ok(xff_str) = xff.to_str() {
128 let first_ip = xff_str.split(',').next()?.trim();
130 return Some(first_ip.to_string());
131 }
132 }
133
134 if let Some(xri) = req.headers().get("X-Real-IP") {
136 if let Ok(xri_str) = xri.to_str() {
137 return Some(xri_str.to_string());
138 }
139 }
140
141 None
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use bytes::Bytes;
148 use http_body_util::Empty;
149 use hyper::Request;
150
151 fn make_request() -> Request<Empty<Bytes>> {
152 Request::builder().body(Empty::new()).unwrap()
153 }
154
155 fn make_request_with_header(name: &str, value: &str) -> Request<Empty<Bytes>> {
156 Request::builder()
157 .header(name, value)
158 .body(Empty::new())
159 .unwrap()
160 }
161
162 #[test]
163 fn test_process_header_substitution_header() {
164 let req = make_request_with_header("X-User-ID", "12345");
165
166 let result = process_header_substitution("User: {header.X-User-ID}", &req).unwrap();
167 assert_eq!(result, "User: 12345");
168 }
169
170 #[test]
171 fn test_process_header_substitution_env() {
172 std::env::set_var("TEST_VAR", "test-value");
173 let req = make_request();
174
175 let result = process_header_substitution("Value: {env.TEST_VAR}", &req).unwrap();
176 assert_eq!(result, "Value: test-value");
177 std::env::remove_var("TEST_VAR");
178 }
179
180 #[test]
181 fn test_process_header_substitution_uuid() {
182 let req = make_request();
183
184 let result = process_header_substitution("ID: {uuid}", &req).unwrap();
185 assert!(result.starts_with("ID: "));
186 assert!(result.len() > 5); }
188
189 #[test]
190 fn test_process_header_substitution_missing_header() {
191 let req = make_request();
192
193 let result = process_header_substitution("Value: {header.Missing}", &req).unwrap();
194 assert_eq!(result, "Value: ");
195 }
196
197 #[test]
198 fn test_extract_remote_ip_xff() {
199 let req = make_request_with_header("X-Forwarded-For", "192.168.1.1, 10.0.0.1");
200
201 let ip = extract_remote_ip(&req);
202 assert_eq!(ip, Some("192.168.1.1".to_string()));
203 }
204
205 #[test]
206 fn test_extract_remote_ip_xri() {
207 let req = make_request_with_header("X-Real-IP", "192.168.1.2");
208
209 let ip = extract_remote_ip(&req);
210 assert_eq!(ip, Some("192.168.1.2".to_string()));
211 }
212
213 #[test]
214 fn test_extract_remote_ip_none() {
215 let req = make_request();
216
217 let ip = extract_remote_ip(&req);
218 assert!(ip.is_none());
219 }
220
221 #[test]
222 fn test_process_upstream_substitution() {
223 let req = make_request_with_header("X-Trace", "abc");
224 let result = process_upstream_substitution(
225 "host={upstream_host} uri={request.uri} ip={remote_ip} trace={header.X-Trace}",
226 &req,
227 "api.example.com:443",
228 "/v1/items?limit=10",
229 "203.0.113.7",
230 )
231 .unwrap();
232 assert_eq!(
233 result,
234 "host=api.example.com:443 uri=/v1/items?limit=10 ip=203.0.113.7 trace=abc"
235 );
236 }
237}