Skip to main content

http_nu/
request.rs

1use nu_protocol::{Record, Span, Value};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::net::IpAddr;
5
6/// Resolve client IP from X-Forwarded-For header using trusted proxy list.
7/// Parses right-to-left, stopping at first untrusted IP.
8/// Falls back to remote_ip if no valid header or all IPs are trusted proxies.
9pub fn resolve_trusted_ip(
10    headers: &http::header::HeaderMap,
11    remote_ip: Option<IpAddr>,
12    trusted_proxies: &[ipnet::IpNet],
13) -> Option<IpAddr> {
14    // If no trusted proxies configured, just use remote_ip
15    if trusted_proxies.is_empty() {
16        return remote_ip;
17    }
18
19    // Check if remote_ip itself is trusted
20    // None (Unix socket) is implicitly trusted when --trust-proxy is configured
21    let remote_is_trusted = remote_ip
22        .map(|ip| trusted_proxies.iter().any(|net| net.contains(&ip)))
23        .unwrap_or(true);
24
25    if !remote_is_trusted {
26        return remote_ip;
27    }
28
29    // Get X-Forwarded-For header
30    let xff = match headers.get("x-forwarded-for") {
31        Some(v) => v.to_str().ok()?,
32        None => return remote_ip,
33    };
34
35    // Parse IPs from right to left
36    let ips: Vec<&str> = xff.split(',').map(|s| s.trim()).collect();
37
38    let mut leftmost_ip = None;
39    for ip_str in ips.into_iter().rev() {
40        if let Ok(ip) = ip_str.parse::<IpAddr>() {
41            leftmost_ip = Some(ip);
42            // If this IP is not in trusted proxies, it's the client
43            if !trusted_proxies.iter().any(|net| net.contains(&ip)) {
44                return Some(ip);
45            }
46        }
47    }
48
49    // All IPs were trusted proxies - use leftmost IP from XFF, or fall back to remote_ip
50    leftmost_ip.or(remote_ip)
51}
52
53#[derive(Clone, Debug, Serialize, Deserialize)]
54pub struct Request {
55    pub proto: String,
56    #[serde(with = "http_serde::method")]
57    pub method: http::method::Method,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub authority: Option<String>,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub remote_ip: Option<std::net::IpAddr>,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub remote_port: Option<u16>,
64    /// Client IP resolved from X-Forwarded-For using trusted proxy list, or remote_ip as fallback
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub trusted_ip: Option<std::net::IpAddr>,
67    #[serde(with = "http_serde::header_map")]
68    pub headers: http::header::HeaderMap,
69    #[serde(with = "http_serde::uri")]
70    pub uri: http::Uri,
71    pub path: String,
72    pub query: HashMap<String, String>,
73}
74
75pub fn request_to_value(request: &Request, span: Span) -> Value {
76    let mut record = Record::new();
77
78    record.push("proto", Value::string(request.proto.clone(), span));
79    record.push("method", Value::string(request.method.to_string(), span));
80    record.push("uri", Value::string(request.uri.to_string(), span));
81    record.push("path", Value::string(request.path.clone(), span));
82
83    if let Some(authority) = &request.authority {
84        record.push("authority", Value::string(authority.clone(), span));
85    }
86
87    if let Some(remote_ip) = &request.remote_ip {
88        record.push("remote_ip", Value::string(remote_ip.to_string(), span));
89    }
90
91    if let Some(remote_port) = &request.remote_port {
92        record.push("remote_port", Value::int(*remote_port as i64, span));
93    }
94
95    if let Some(trusted_ip) = &request.trusted_ip {
96        record.push("trusted_ip", Value::string(trusted_ip.to_string(), span));
97    }
98
99    // Convert headers to a record
100    let mut headers_record = Record::new();
101    for (key, value) in request.headers.iter() {
102        headers_record.push(
103            key.to_string(),
104            Value::string(value.to_str().unwrap_or_default().to_string(), span),
105        );
106    }
107    record.push("headers", Value::record(headers_record, span));
108
109    // Convert query parameters to a record
110    let mut query_record = Record::new();
111    for (key, value) in &request.query {
112        query_record.push(key.clone(), Value::string(value.clone(), span));
113    }
114    record.push("query", Value::record(query_record, span));
115
116    Value::record(record, span)
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    fn headers_with_xff(xff: &str) -> http::header::HeaderMap {
124        let mut headers = http::header::HeaderMap::new();
125        headers.insert("x-forwarded-for", xff.parse().unwrap());
126        headers
127    }
128
129    fn parse_cidr(s: &str) -> ipnet::IpNet {
130        s.parse().unwrap()
131    }
132
133    #[test]
134    fn test_no_trusted_proxies_returns_remote_ip() {
135        let headers = http::header::HeaderMap::new();
136        let remote: IpAddr = "1.2.3.4".parse().unwrap();
137        let result = resolve_trusted_ip(&headers, Some(remote), &[]);
138        assert_eq!(result, Some(remote));
139    }
140
141    #[test]
142    fn test_remote_not_trusted_returns_remote_ip() {
143        let headers = headers_with_xff("5.6.7.8");
144        let remote: IpAddr = "1.2.3.4".parse().unwrap();
145        let trusted = vec![parse_cidr("10.0.0.0/8")];
146        let result = resolve_trusted_ip(&headers, Some(remote), &trusted);
147        assert_eq!(result, Some(remote));
148    }
149
150    #[test]
151    fn test_xff_extracts_client_ip() {
152        // Client -> Proxy1 (10.0.0.1) -> Proxy2 (10.0.0.2) -> Server
153        // XFF: "5.6.7.8, 10.0.0.1"
154        let headers = headers_with_xff("5.6.7.8, 10.0.0.1");
155        let remote: IpAddr = "10.0.0.2".parse().unwrap();
156        let trusted = vec![parse_cidr("10.0.0.0/8")];
157        let result = resolve_trusted_ip(&headers, Some(remote), &trusted);
158        assert_eq!(result, Some("5.6.7.8".parse().unwrap()));
159    }
160
161    #[test]
162    fn test_xff_stops_at_first_untrusted() {
163        // Attacker spoofs XFF with fake IP, but we stop at first untrusted
164        // XFF: "fake.ip, 5.6.7.8, 10.0.0.1"
165        let headers = headers_with_xff("1.1.1.1, 5.6.7.8, 10.0.0.1");
166        let remote: IpAddr = "10.0.0.2".parse().unwrap();
167        let trusted = vec![parse_cidr("10.0.0.0/8")];
168        let result = resolve_trusted_ip(&headers, Some(remote), &trusted);
169        // Should return 5.6.7.8, not 1.1.1.1 (which could be spoofed)
170        assert_eq!(result, Some("5.6.7.8".parse().unwrap()));
171    }
172
173    #[test]
174    fn test_all_xff_trusted_returns_leftmost() {
175        // When all XFF IPs are trusted, return the leftmost (original client)
176        let headers = headers_with_xff("10.0.0.5, 10.0.0.1");
177        let remote: IpAddr = "10.0.0.2".parse().unwrap();
178        let trusted = vec![parse_cidr("10.0.0.0/8")];
179        let result = resolve_trusted_ip(&headers, Some(remote), &trusted);
180        assert_eq!(result, Some("10.0.0.5".parse().unwrap()));
181    }
182
183    #[test]
184    fn test_no_xff_header_returns_remote() {
185        let headers = http::header::HeaderMap::new();
186        let remote: IpAddr = "10.0.0.2".parse().unwrap();
187        let trusted = vec![parse_cidr("10.0.0.0/8")];
188        let result = resolve_trusted_ip(&headers, Some(remote), &trusted);
189        assert_eq!(result, Some(remote));
190    }
191
192    #[test]
193    fn test_multiple_trusted_cidrs() {
194        let headers = headers_with_xff("5.6.7.8, 192.168.1.1");
195        let remote: IpAddr = "10.0.0.2".parse().unwrap();
196        let trusted = vec![parse_cidr("10.0.0.0/8"), parse_cidr("192.168.0.0/16")];
197        let result = resolve_trusted_ip(&headers, Some(remote), &trusted);
198        assert_eq!(result, Some("5.6.7.8".parse().unwrap()));
199    }
200
201    #[test]
202    fn test_ipv6_support() {
203        let headers = headers_with_xff("2001:db8::1, ::ffff:10.0.0.1");
204        let remote: IpAddr = "::ffff:10.0.0.2".parse().unwrap();
205        let trusted = vec![parse_cidr("::ffff:10.0.0.0/104")];
206        let result = resolve_trusted_ip(&headers, Some(remote), &trusted);
207        assert_eq!(result, Some("2001:db8::1".parse().unwrap()));
208    }
209
210    #[test]
211    fn test_unix_socket_with_xff() {
212        // Unix socket: remote_ip is None, but trust-proxy is configured
213        let headers = headers_with_xff("5.6.7.8, 10.0.0.1");
214        let trusted = vec![parse_cidr("10.0.0.0/8")];
215        let result = resolve_trusted_ip(&headers, None, &trusted);
216        assert_eq!(result, Some("5.6.7.8".parse().unwrap()));
217    }
218
219    #[test]
220    fn test_unix_socket_no_xff() {
221        // Unix socket with no XFF header → None
222        let headers = http::header::HeaderMap::new();
223        let trusted = vec![parse_cidr("10.0.0.0/8")];
224        let result = resolve_trusted_ip(&headers, None, &trusted);
225        assert_eq!(result, None);
226    }
227
228    #[test]
229    fn test_trust_all_uses_leftmost_xff() {
230        // When trusting 0.0.0.0/0, all IPs are "trusted" but we should still
231        // return the leftmost (client) IP from the XFF chain
232        let headers = headers_with_xff("38.147.250.103");
233        let trusted = vec![parse_cidr("0.0.0.0/0")];
234        let result = resolve_trusted_ip(&headers, None, &trusted);
235        assert_eq!(result, Some("38.147.250.103".parse().unwrap()));
236    }
237}