1#[derive(Debug, Clone, PartialEq)]
7#[doc(hidden)]
8pub struct IpRange {
9 pub network: u128,
10 pub mask: u128,
11}
12
13#[derive(Debug, Clone, PartialEq)]
18pub enum TrustedProxies {
19 All,
20 Ranges(Vec<IpRangeEntry>),
21}
22
23#[derive(Debug, Clone, PartialEq)]
25pub struct IpRangeEntry(IpRange);
26
27pub const DEFAULT_TRUSTED_PROXIES: &str = "127.0.0.0/8, ::1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10, fc00::/7";
29
30impl TrustedProxies {
31 pub fn parse(list: &str) -> Result<TrustedProxies, String> {
34 let list = if list.trim().is_empty() { DEFAULT_TRUSTED_PROXIES } else { list.trim() };
35 if list == "*" {
36 return Ok(TrustedProxies::All);
37 }
38 if list.eq_ignore_ascii_case("none") {
39 return Ok(TrustedProxies::Ranges(Vec::new()));
40 }
41 list.split(',')
42 .map(str::trim)
43 .filter(|e| !e.is_empty())
44 .map(|e| IpRange::parse(e).map(IpRangeEntry).ok_or_else(|| format!("trusted_proxies: {e:?} is not an address or CIDR range")))
45 .collect::<Result<Vec<_>, _>>()
46 .map(TrustedProxies::Ranges)
47 }
48
49 pub fn trusts(&self, ip: &str) -> bool {
51 match self {
52 TrustedProxies::All => true,
53 TrustedProxies::Ranges(ranges) => parse_ip(ip).is_some_and(|a| ranges.iter().any(|r| (a & r.0.mask) == r.0.network)),
54 }
55 }
56}
57
58pub fn is_valid_entry(entry: &str) -> bool {
60 IpRange::parse(entry).is_some()
61}
62
63impl IpRange {
64 #[doc(hidden)]
65 pub fn parse(entry: &str) -> Option<Self> {
66 let entry = entry.trim();
67 let (addr, prefix_len) = match entry.split_once('/') {
68 Some((addr, len)) => {
69 let len: u32 = len.parse().ok()?;
70 let is_v4 = addr.parse::<std::net::Ipv4Addr>().is_ok();
71 let max = if is_v4 { 32 } else { 128 };
72 if len > max {
73 return None;
74 }
75 (addr, if is_v4 { len + 96 } else { len })
76 }
77 None => (entry, 128),
78 };
79 let network = parse_ip(addr)?;
80 let mask = if prefix_len == 0 { 0 } else { !0u128 << (128 - prefix_len) };
81 Some(IpRange { network: network & mask, mask })
82 }
83}
84
85#[doc(hidden)]
88pub fn parse_ip(s: &str) -> Option<u128> {
89 let s = s.trim().trim_start_matches('[').trim_end_matches(']');
90 let s = s.split('%').next()?;
91 match s.parse::<std::net::IpAddr>().ok()? {
92 std::net::IpAddr::V4(v4) => Some(u128::from(v4.to_ipv6_mapped())),
93 std::net::IpAddr::V6(v6) => Some(u128::from(v6)),
94 }
95}
96
97pub fn client_ip<'a>(request: &crate::request::Request, peer: &'a str) -> std::borrow::Cow<'a, str> {
112 let config = crate::server_config::ServerConfig::current();
113 client_ip_with(request, peer, &config.trusted_proxies, config.client_ip_forwarded)
114}
115
116pub fn request_scheme(request: &crate::request::Request, peer: &str, tls: bool) -> &'static str {
125 let config = crate::server_config::ServerConfig::current();
126 request_scheme_with(request, peer, tls, &config.trusted_proxies, config.client_ip_forwarded)
127}
128
129#[doc(hidden)]
131pub fn request_scheme_with(request: &crate::request::Request, peer: &str, tls: bool, trusted: &TrustedProxies, forwarded: bool) -> &'static str {
132 if tls {
133 return "https";
134 }
135 if !forwarded || !trusted.trusts(peer) {
136 return "http";
137 }
138 let first = |name: &str| request.headers.iter().find(|h| h.name.eq_ignore_ascii_case(name)).map(|h| h.value.split(',').next().unwrap_or("").trim().to_string());
139 let proto = first("x-forwarded-proto").or_else(|| {
140 let element = first("forwarded")?;
141 element.split(';').find_map(|pair| {
142 let (k, v) = pair.split_once('=')?;
143 k.trim().eq_ignore_ascii_case("proto").then(|| v.trim().trim_matches('"').to_string())
144 })
145 });
146 match proto {
147 Some(p) if p.eq_ignore_ascii_case("https") => "https",
148 _ => "http",
149 }
150}
151
152#[doc(hidden)]
153pub fn client_ip_with<'a>(request: &crate::request::Request, peer: &'a str, trusted: &TrustedProxies, forwarded: bool) -> std::borrow::Cow<'a, str> {
154 use std::borrow::Cow;
155 if !forwarded || !trusted.trusts(peer) {
156 return Cow::Borrowed(peer);
157 }
158 let xff: Vec<&str> = request
160 .headers
161 .iter()
162 .filter(|h| h.name.eq_ignore_ascii_case("x-forwarded-for"))
163 .flat_map(|h| h.value.split(','))
164 .collect();
165 let hops: Vec<&str> = if xff.is_empty() {
166 request
167 .headers
168 .iter()
169 .filter(|h| h.name.eq_ignore_ascii_case("forwarded"))
170 .flat_map(|h| h.value.split(','))
171 .filter_map(|element| {
172 element.split(';').find_map(|pair| {
173 let (k, v) = pair.split_once('=')?;
174 k.trim().eq_ignore_ascii_case("for").then_some(v)
175 })
176 })
177 .collect()
178 } else {
179 xff
180 };
181 let mut chosen: Option<String> = None;
182 for hop in hops.iter().rev() {
183 let Some(ip) = hop_address(hop) else { break };
184 let trusted_hop = trusted.trusts(&ip);
185 chosen = Some(ip);
186 if !trusted_hop {
187 break;
188 }
189 }
190 chosen.map_or(Cow::Borrowed(peer), Cow::Owned)
191}
192
193fn hop_address(hop: &str) -> Option<String> {
197 let hop = hop.trim().trim_matches('"');
198 let bare = if let Some(rest) = hop.strip_prefix('[') {
199 rest.split(']').next()?
200 } else if hop.matches(':').count() == 1 {
201 hop.split(':').next()?
202 } else {
203 hop
204 };
205 bare.parse::<std::net::IpAddr>().ok().map(|ip| ip.to_string())
206}
207
208#[cfg(test)]
209mod client_ip_tests {
210 use super::*;
211 use crate::header::Header;
212 use crate::request::Request;
213
214 fn with(headers: &[(&str, &str)]) -> Request {
215 Request {
216 method: "GET".into(),
217 request_uri: "/".into(),
218 http_version: "HTTP/1.1".into(),
219 headers: headers.iter().map(|(n, v)| Header::new(n.to_string(), v.to_string())).collect(),
220 body: Default::default(),
221 }
222 }
223 fn private() -> TrustedProxies {
224 TrustedProxies::parse("").unwrap()
225 }
226
227 #[test]
228 fn a_trusted_peer_gives_the_client_in_x_forwarded_for() {
229 let r = with(&[("X-Forwarded-For", "88.99.218.47, 172.30.0.3")]);
230 assert_eq!(client_ip_with(&r, "172.30.0.3", &private(), true), "88.99.218.47");
231 }
232
233 #[test]
234 fn a_client_cant_choose_its_address() {
235 let r = with(&[("X-Forwarded-For", "6.6.6.6,88.99.218.47, 172.30.0.3")]);
237 assert_eq!(client_ip_with(&r, "172.30.0.3", &private(), true), "88.99.218.47");
238 let r = with(&[("X-Forwarded-For", "10.9.9.9, 88.99.218.47")]);
241 assert_eq!(client_ip_with(&r, "10.0.0.1", &private(), true), "88.99.218.47");
242 }
243
244 #[test]
245 fn an_untrusted_peer_or_peer_mode_keeps_the_peer() {
246 let r = with(&[("X-Forwarded-For", "1.2.3.4")]);
247 assert_eq!(client_ip_with(&r, "88.99.218.47", &private(), true), "88.99.218.47");
248 assert_eq!(client_ip_with(&r, "10.0.0.1", &private(), false), "10.0.0.1");
249 assert_eq!(client_ip_with(&r, "10.0.0.1", &TrustedProxies::parse("none").unwrap(), true), "10.0.0.1");
250 }
251
252 #[test]
253 fn several_lines_ports_and_ipv6() {
254 let r = with(&[("X-Forwarded-For", "203.0.113.7:4711"), ("X-Forwarded-For", "10.0.0.2")]);
255 assert_eq!(client_ip_with(&r, "10.0.0.1", &private(), true), "203.0.113.7");
256 let r = with(&[("X-Forwarded-For", "[2001:db8::1]:443, fd00::2")]);
257 assert_eq!(client_ip_with(&r, "::1", &private(), true), "2001:db8::1");
258 }
259
260 #[test]
261 fn forwarded_when_there_is_no_x_forwarded_for() {
262 let r = with(&[("Forwarded", "for=\"[2001:db8::7]\";proto=https, for=10.0.0.9")]);
263 assert_eq!(client_ip_with(&r, "10.0.0.1", &private(), true), "2001:db8::7");
264 }
265
266 #[test]
267 fn garbage_or_only_trusted_hops() {
268 let r = with(&[("X-Forwarded-For", "203.0.113.7, unknown, 10.0.0.5")]);
270 assert_eq!(client_ip_with(&r, "10.0.0.1", &private(), true), "10.0.0.5");
271 let r = with(&[("X-Forwarded-For", "nonsense")]);
273 assert_eq!(client_ip_with(&r, "10.0.0.1", &private(), true), "10.0.0.1");
274 let r = with(&[]);
275 assert_eq!(client_ip_with(&r, "10.0.0.1", &private(), true), "10.0.0.1");
276 let r = with(&[("X-Forwarded-For", "10.1.1.1, 10.2.2.2")]);
278 assert_eq!(client_ip_with(&r, "10.0.0.1", &private(), true), "10.1.1.1");
279 }
280}