Skip to main content

tiny_proxy/auth/
headers.rs

1//! Header manipulation utilities for authentication
2//!
3//! This module provides utilities for processing header value substitutions
4//! including request headers, UUIDs, and environment variables.
5
6use hyper::Request;
7
8/// Process header value substitutions
9///
10/// Replaces placeholders in the header value with actual values:
11/// - `{header.Name}` - value of request header with that name
12/// - `{uuid}` - generates a random UUID
13/// - `{env.VAR}` - value of environment variable VAR
14///
15/// # Arguments
16///
17/// * `value` - The header value template with placeholders
18/// * `req` - The HTTP request to extract headers from
19///
20/// # Returns
21///
22/// The processed header value with all placeholders replaced
23///
24/// # Example
25///
26/// ```no_run
27/// # use hyper::Request;
28/// # use bytes::Bytes;
29/// # use http_body_util::Empty;
30/// # use tiny_proxy::auth::headers::process_header_substitution;
31/// # fn main() -> anyhow::Result<()> {
32/// # let req = Request::builder().body(Empty::<Bytes>::new()).unwrap();
33/// let result = process_header_substitution("X-Request-ID: {uuid}", &req)?;
34/// assert!(result.contains("X-Request-ID:"));
35/// # Ok(())
36/// # }
37/// ```
38pub fn process_header_substitution<B>(value: &str, req: &Request<B>) -> anyhow::Result<String> {
39    let mut result = value.to_string();
40
41    // Process {header.Name} substitutions
42    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            // If header doesn't exist, remove the placeholder
54            result.replace_range(start..=end, "");
55        }
56    }
57
58    // Process {env.VAR} substitutions
59    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            // If env var doesn't exist, remove the placeholder
73            result.replace_range(start..=end, "");
74        }
75    }
76
77    // Process {uuid} substitutions
78    result = result.replace("{uuid}", &uuid::Uuid::new_v4().to_string());
79
80    Ok(result)
81}
82
83/// Process header value substitutions for upstream (`header_up`) operations.
84///
85/// Supports all the placeholders of [`process_header_substitution`] plus three
86/// extra placeholders that only make sense for outbound (upstream) headers:
87///
88/// - `{upstream_host}` — hostname:port of the `reverse_proxy` backend
89/// - `{request.uri}` — the full path + query of the incoming request
90/// - `{remote_ip}` — client IP from `remote_addr` (or `X-Forwarded-For` / `X-Real-IP`)
91///
92/// The order of substitution is: base placeholders first (`{header.*}`, `{env.*}`,
93/// `{uuid}`), then the upstream-specific ones. This lets you write e.g.
94/// `header_up Host {upstream_host}` while still being able to use `{header.X-Foo}`.
95pub 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    // Base substitutions ({header.*}, {env.*}, {uuid}).
103    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
112/// Extract remote IP address from request headers
113///
114/// Looks for the X-Forwarded-For or X-Real-IP headers to determine the
115/// original client IP address.
116///
117/// # Arguments
118///
119/// * `req` - The HTTP request
120///
121/// # Returns
122///
123/// The remote IP address if found, None otherwise
124pub fn extract_remote_ip<B>(req: &Request<B>) -> Option<String> {
125    // Check X-Forwarded-For header (set by proxies)
126    if let Some(xff) = req.headers().get("X-Forwarded-For") {
127        if let Ok(xff_str) = xff.to_str() {
128            // X-Forwarded-For can contain multiple IPs, take the first one
129            let first_ip = xff_str.split(',').next()?.trim();
130            return Some(first_ip.to_string());
131        }
132    }
133
134    // Check X-Real-IP header
135    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); // UUID should be present
187    }
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}