rfc7239/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2
3//! Parser for [rfc7239] formatted `Forwarded` headers.
4//!
5//! ## Usage
6//!
7//! ```
8//! use rfc7239::parse;
9//! # use core::error::Error;
10//!
11//! # fn main() -> Result<(), Box<dyn Error>> {
12//! // get the header value from your favorite http server library
13//! let header_value = "for=192.0.2.60;proto=http;by=203.0.113.43,for=192.168.10.10";
14//!
15//! for node_result in parse(header_value) {
16//!     let node = node_result?;
17//!     if let Some(forwarded_for) = node.forwarded_for {
18//!         println!("Forwarded by {}", forwarded_for)
19//!     }
20//! }
21//! # Ok(())
22//! # }
23//! ```
24//!
25//! [rfc7239]: https://tools.ietf.org/html/rfc7239
26
27#[cfg(all(test, not(feature = "std")))]
28extern crate std;
29#[cfg(all(test, not(feature = "std")))]
30use std::{format, vec, vec::Vec};
31
32#[cfg(not(feature = "std"))]
33use core::{
34    error::Error,
35    fmt::{Debug, Display, Formatter},
36    net::IpAddr,
37    str::FromStr,
38};
39#[cfg(feature = "std")]
40use std::{
41    error::Error,
42    fmt::{Debug, Display, Formatter},
43    net::IpAddr,
44    str::FromStr,
45};
46use uncased::UncasedStr;
47
48#[derive(Debug)]
49pub enum RfcError {
50    InvalidIdentifier,
51    InvalidPort,
52    UnknownParameter,
53    MalformedParameter,
54}
55
56impl Display for RfcError {
57    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
58        let str = match self {
59            RfcError::InvalidIdentifier => "Invalid node identifier",
60            RfcError::InvalidPort => "Invalid node port",
61            RfcError::UnknownParameter => "Unknown parameter name",
62            RfcError::MalformedParameter => "Parameter doesn't consist of key value pair",
63        };
64        write!(f, "{}", str)
65    }
66}
67
68impl Error for RfcError {}
69
70/// Parse an rfc7239 header value into a list of forwarded nodes
71pub fn parse(header_value: &str) -> impl DoubleEndedIterator<Item = Result<Forwarded, RfcError>> {
72    header_value.split(',').map(str::trim).map(Forwarded::parse)
73}
74
75#[test]
76fn test_parse() {
77    assert_eq!(
78        parse("for=192.0.2.60;proto=http;by=203.0.113.43,for=192.168.10.10")
79            .collect::<Result<Vec<_>, _>>()
80            .unwrap(),
81        vec![
82            Forwarded {
83                forwarded_for: Some(NodeIdentifier::parse("192.0.2.60").unwrap()),
84                forwarded_by: Some(NodeIdentifier::parse("203.0.113.43").unwrap()),
85                protocol: Some("http"),
86                ..Default::default()
87            },
88            Forwarded {
89                forwarded_for: Some(NodeIdentifier::parse("192.168.10.10").unwrap()),
90                ..Default::default()
91            },
92        ]
93    )
94}
95
96#[derive(Debug, Default, PartialEq)]
97pub struct Forwarded<'a> {
98    pub forwarded_for: Option<NodeIdentifier<'a>>,
99    pub forwarded_by: Option<NodeIdentifier<'a>>,
100    pub host: Option<&'a str>,
101    pub protocol: Option<&'a str>,
102}
103
104impl<'a> Forwarded<'a> {
105    fn parse(forward: &'a str) -> Result<Self, RfcError> {
106        let mut result = Forwarded::default();
107
108        let parts = forward.split(';');
109
110        for part in parts {
111            if let Some(i) = part.find('=') {
112                let param = UncasedStr::new(&part[..i]);
113                let value = &part[i + 1..];
114                if param == "by" {
115                    result.forwarded_by = Some(NodeIdentifier::parse(value.trim_matches('"'))?);
116                }
117                if param == "for" {
118                    result.forwarded_for = Some(NodeIdentifier::parse(value.trim_matches('"'))?);
119                }
120                if param == "host" {
121                    result.host = Some(value);
122                }
123                if param == "proto" {
124                    result.protocol = Some(value);
125                }
126            } else {
127                return Err(RfcError::MalformedParameter);
128            }
129        }
130
131        Ok(result)
132    }
133}
134
135impl Display for Forwarded<'_> {
136    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
137        let mut needs_delim = false;
138        if let Some(ident) = &self.forwarded_for {
139            if ident.display_needs_quote() {
140                write!(f, "for=\"{}\"", ident)?;
141            } else {
142                write!(f, "for={}", ident)?;
143            }
144            needs_delim = true;
145        }
146        if let Some(ident) = &self.forwarded_by {
147            if needs_delim {
148                write!(f, ";")?;
149            }
150
151            if ident.display_needs_quote() {
152                write!(f, "by=\"{}\"", ident)?;
153            } else {
154                write!(f, "by={}", ident)?;
155            }
156            needs_delim = true;
157        }
158        if let Some(ident) = &self.host {
159            if needs_delim {
160                write!(f, ";")?;
161            }
162
163            write!(f, "host={}", ident)?;
164            needs_delim = true;
165        }
166        if let Some(ident) = &self.protocol {
167            if needs_delim {
168                write!(f, ";")?;
169            }
170
171            write!(f, "proto={}", ident)?;
172        }
173
174        Ok(())
175    }
176}
177
178#[test]
179fn test_parse_forwarded() {
180    assert_eq!(
181        Forwarded {
182            forwarded_for: Some(NodeIdentifier::parse("1.2.3.4").unwrap()),
183            ..Default::default()
184        },
185        Forwarded::parse("for=1.2.3.4").unwrap()
186    );
187    assert_eq!(
188        Forwarded {
189            forwarded_for: Some(NodeIdentifier::parse("1.2.3.4").unwrap()),
190            ..Default::default()
191        },
192        Forwarded::parse("For=1.2.3.4").unwrap()
193    );
194    assert_eq!(
195        Forwarded {
196            forwarded_for: Some(NodeIdentifier::parse("1.2.3.4").unwrap()),
197            forwarded_by: Some(NodeIdentifier::parse("[1::1]:80").unwrap()),
198            host: Some("foo"),
199            protocol: Some("https")
200        },
201        Forwarded::parse("for=1.2.3.4;by=\"[1::1]:80\";host=foo;proto=https").unwrap()
202    );
203}
204
205#[test]
206fn test_display_forwarded() {
207    assert_eq!(
208        format!(
209            "{}",
210            Forwarded {
211                forwarded_for: Some(NodeIdentifier::parse("1.2.3.4").unwrap()),
212                ..Default::default()
213            }
214        ),
215        "for=1.2.3.4"
216    );
217    assert_eq!(
218        format!(
219            "{}",
220            Forwarded {
221                forwarded_for: Some(NodeIdentifier::parse("1.2.3.4").unwrap()),
222                forwarded_by: Some(NodeIdentifier::parse("[1::1]:80").unwrap()),
223                host: Some("foo"),
224                protocol: Some("https")
225            }
226        ),
227        "for=1.2.3.4;by=\"[1::1]:80\";host=foo;proto=https"
228    );
229}
230
231#[derive(Debug, Eq, PartialEq)]
232pub struct NodeIdentifier<'a> {
233    pub name: NodeName<'a>,
234    pub port: Option<u16>,
235}
236
237impl<'a> NodeIdentifier<'a> {
238    fn parse(name: &'a str) -> Result<Self, RfcError> {
239        match (name.rfind(':'), name.rfind(']')) {
240            (Some(delim), Some(ip6_end)) if delim > ip6_end => {
241                Self::parse_name_port(&name[0..delim], Some(&name[delim + 1..]))
242            }
243            (Some(delim), None) => Self::parse_name_port(&name[..delim], Some(&name[delim + 1..])),
244            _ => Self::parse_name_port(name, None),
245        }
246    }
247
248    fn parse_name_port(name: &'a str, port: Option<&str>) -> Result<Self, RfcError> {
249        Ok(NodeIdentifier {
250            name: NodeName::parse(name)?,
251            port: port
252                .map(u16::from_str)
253                .transpose()
254                .map_err(|_| RfcError::InvalidPort)?,
255        })
256    }
257
258    pub fn ip(&self) -> Option<&IpAddr> {
259        self.name.ip()
260    }
261
262    /// values containing `:` or `[]` characters need to be quoted
263    fn display_needs_quote(&self) -> bool {
264        self.port.is_some() || matches!(self.name, NodeName::Ip(IpAddr::V6(_)))
265    }
266}
267
268impl Display for NodeIdentifier<'_> {
269    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
270        match self.port {
271            Some(port) => write!(f, "{}:{}", self.name, port),
272            None => write!(f, "{}", self.name),
273        }
274    }
275}
276
277#[test]
278fn test_parse_node_identifier() {
279    assert_eq!(
280        NodeIdentifier {
281            name: NodeName::Ip("1.2.3.4".parse().unwrap()),
282            port: None
283        },
284        NodeIdentifier::parse("1.2.3.4").unwrap()
285    );
286    assert_eq!(
287        NodeIdentifier {
288            name: NodeName::Ip("1.2.3.4".parse().unwrap()),
289            port: Some(8080)
290        },
291        NodeIdentifier::parse("1.2.3.4:8080").unwrap()
292    );
293    assert_eq!(
294        NodeIdentifier {
295            name: NodeName::Ip("2001:db8:cafe::17".parse().unwrap()),
296            port: Some(8080)
297        },
298        NodeIdentifier::parse("[2001:db8:cafe::17]:8080").unwrap()
299    );
300
301    assert!(matches!(
302        NodeIdentifier::parse("unknown:99999").unwrap_err(),
303        RfcError::InvalidPort
304    ));
305}
306
307#[test]
308fn test_node_identifier_ip() {
309    assert_eq!(
310        Some(&IpAddr::from([192, 0, 2, 42])),
311        NodeIdentifier::parse("192.0.2.42:31337").unwrap().ip()
312    );
313    assert_eq!(
314        Some(&IpAddr::from([0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x45])),
315        NodeIdentifier::parse("[2001:db8::45]:31337").unwrap().ip()
316    );
317}
318
319#[test]
320fn test_display_node_identifier() {
321    assert_eq!(
322        format!(
323            "{}",
324            NodeIdentifier {
325                name: NodeName::Ip("1.2.3.4".parse().unwrap()),
326                port: None
327            }
328        ),
329        "1.2.3.4"
330    );
331    assert_eq!(
332        format!(
333            "{}",
334            NodeIdentifier {
335                name: NodeName::Ip("1.2.3.4".parse().unwrap()),
336                port: Some(8080)
337            }
338        ),
339        "1.2.3.4:8080"
340    );
341    assert_eq!(
342        format!(
343            "{}",
344            NodeIdentifier {
345                name: NodeName::Ip("2001:db8:cafe::17".parse().unwrap()),
346                port: Some(8080)
347            }
348        ),
349        "[2001:db8:cafe::17]:8080"
350    );
351}
352
353#[derive(Debug, Eq, PartialEq)]
354pub enum NodeName<'a> {
355    Ip(IpAddr),
356    Unknown,
357    Obfuscated(&'a str),
358}
359
360impl<'a> NodeName<'a> {
361    fn parse(name: &'a str) -> Result<Self, RfcError> {
362        match name {
363            "unknown" => Ok(NodeName::Unknown),
364            obfuscated if obfuscated.starts_with("_") => {
365                if obfuscated
366                    .chars()
367                    .all(|c| c.is_alphanumeric() || c == '.' || c == '_')
368                {
369                    Ok(NodeName::Obfuscated(obfuscated))
370                } else {
371                    Err(RfcError::InvalidIdentifier)
372                }
373            }
374            ip6 if ip6.starts_with('[') && ip6.ends_with(']') => ip6[1..ip6.len() - 1]
375                .parse()
376                .map(IpAddr::V6)
377                .map(NodeName::Ip)
378                .map_err(|_| RfcError::InvalidIdentifier),
379            ip4 => ip4
380                .parse()
381                .map(IpAddr::V4)
382                .map(NodeName::Ip)
383                .map_err(|_| RfcError::InvalidIdentifier),
384        }
385    }
386
387    pub fn ip(&self) -> Option<&IpAddr> {
388        if let NodeName::Ip(ip) = self {
389            Some(ip)
390        } else {
391            None
392        }
393    }
394}
395
396impl Display for NodeName<'_> {
397    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
398        match self {
399            NodeName::Ip(IpAddr::V4(ip)) => write!(f, "{}", ip),
400            NodeName::Ip(IpAddr::V6(ip)) => write!(f, "[{}]", ip),
401            NodeName::Unknown => {
402                write!(f, "unknown")
403            }
404            NodeName::Obfuscated(name) => write!(f, "{}", name),
405        }
406    }
407}
408
409#[test]
410fn test_parse_node_name() {
411    assert_eq!(
412        NodeName::Ip("1.2.3.4".parse().unwrap()),
413        NodeName::parse("1.2.3.4").unwrap()
414    );
415    assert_eq!(
416        NodeName::Ip("2001:db8:cafe::17".parse().unwrap()),
417        NodeName::parse("[2001:db8:cafe::17]").unwrap()
418    );
419    assert_eq!(NodeName::Unknown, NodeName::parse("unknown").unwrap());
420    assert_eq!(
421        NodeName::Obfuscated("_FOO"),
422        NodeName::parse("_FOO").unwrap()
423    );
424
425    // obfuscated identifiers must be _[a-zA-Z0-9_.]*
426    assert!(matches!(
427        NodeName::parse("_FOO-INVALID").unwrap_err(),
428        RfcError::InvalidIdentifier
429    ));
430    assert!(matches!(
431        NodeName::parse("FOO").unwrap_err(),
432        RfcError::InvalidIdentifier
433    ));
434
435    // ip6 identifiers must be surrounded with []
436    assert!(matches!(
437        NodeName::parse("2001:db8:cafe::17").unwrap_err(),
438        RfcError::InvalidIdentifier
439    ));
440}
441
442#[test]
443fn test_display_node_name() {
444    assert_eq!(
445        format!("{}", NodeName::Ip("1.2.3.4".parse().unwrap())),
446        "1.2.3.4"
447    );
448    assert_eq!(
449        format!("{}", NodeName::Ip("2001:db8:cafe::17".parse().unwrap())),
450        "[2001:db8:cafe::17]"
451    );
452    assert_eq!(format!("{}", NodeName::Unknown), "unknown");
453    assert_eq!(format!("{}", NodeName::Obfuscated("_FOO")), "_FOO");
454}