Skip to main content

rama_net/forwarded/element/
mod.rs

1use core::fmt;
2use core::net::{IpAddr, Ipv4Addr};
3use core::net::{Ipv6Addr, SocketAddr};
4
5use crate::std::collections::BTreeMap;
6use crate::std::string::String;
7use crate::std::vec::Vec;
8
9use super::{ForwardedProtocol, ForwardedVersion, NodeId};
10use crate::address::{Domain, HostWithOptPort};
11use crate::address::{Host, HostWithPort, SocketAddress};
12
13use rama_core::error::BoxError;
14
15mod parser;
16#[doc(inline)]
17pub(crate) use parser::{parse_one_plus_forwarded_elements, parse_single_forwarded_element};
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20/// A single entry in the [`Forwarded`] chain.
21///
22/// [`Forwarded`]: crate::forwarded::Forwarded
23pub struct ForwardedElement {
24    by_node: Option<NodeId>,
25    for_node: Option<NodeId>,
26    authority: Option<ForwardedAuthority>,
27    proto: Option<ForwardedProtocol>,
28    proto_version: Option<ForwardedVersion>,
29
30    // not expected, but if used these parameters (keys)
31    // should be registered ideally also in
32    // <https://www.iana.org/assignments/http-parameters/http-parameters.xhtml#forwarded>
33    // BTreeMap (not a hash map): tiny-or-absent in practice, and it keeps
34    // this type free of hasher deps in no_std builds.
35    extensions: Option<BTreeMap<String, ExtensionValue>>,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39struct ExtensionValue {
40    value: String,
41    quoted: bool,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Hash)]
45/// Wrapper of a value [`HostWithOptPort`] to provide some forward-specific utilities.
46pub struct ForwardedAuthority(pub HostWithOptPort);
47
48impl ForwardedAuthority {
49    /// Create a new [`ForwardedAuthority`]
50    #[must_use]
51    #[inline(always)]
52    pub const fn new(host: Host) -> Self {
53        Self(HostWithOptPort::new(host))
54    }
55
56    /// Create a new [`ForwardedAuthority`] with port
57    #[must_use]
58    #[inline(always)]
59    pub const fn new_with_port(host: Host, port: u16) -> Self {
60        Self(HostWithOptPort::new_with_port(host, port))
61    }
62}
63
64impl From<Host> for ForwardedAuthority {
65    #[inline(always)]
66    fn from(value: Host) -> Self {
67        Self::new(value)
68    }
69}
70
71impl From<Domain> for ForwardedAuthority {
72    #[inline(always)]
73    fn from(value: Domain) -> Self {
74        Self::new(value.into())
75    }
76}
77
78impl From<IpAddr> for ForwardedAuthority {
79    #[inline(always)]
80    fn from(value: IpAddr) -> Self {
81        Self::new(value.into())
82    }
83}
84
85impl From<Ipv4Addr> for ForwardedAuthority {
86    #[inline(always)]
87    fn from(value: Ipv4Addr) -> Self {
88        Self::new(value.into())
89    }
90}
91
92impl From<[u8; 4]> for ForwardedAuthority {
93    #[inline(always)]
94    fn from(value: [u8; 4]) -> Self {
95        Self::new(Host::Address(value.into()))
96    }
97}
98
99impl From<[u8; 16]> for ForwardedAuthority {
100    #[inline(always)]
101    fn from(value: [u8; 16]) -> Self {
102        Self::new(Host::Address(value.into()))
103    }
104}
105
106impl From<Ipv6Addr> for ForwardedAuthority {
107    #[inline(always)]
108    fn from(value: Ipv6Addr) -> Self {
109        Self::new(value.into())
110    }
111}
112
113impl From<SocketAddr> for ForwardedAuthority {
114    #[inline(always)]
115    fn from(value: SocketAddr) -> Self {
116        Self(HostWithOptPort {
117            host: Host::Address(value.ip()),
118            port: crate::address::OptPort::Set(value.port()),
119        })
120    }
121}
122
123impl From<SocketAddress> for ForwardedAuthority {
124    #[inline(always)]
125    fn from(value: SocketAddress) -> Self {
126        Self(HostWithOptPort {
127            host: Host::Address(value.ip_addr),
128            port: crate::address::OptPort::Set(value.port),
129        })
130    }
131}
132
133impl From<HostWithOptPort> for ForwardedAuthority {
134    #[inline(always)]
135    fn from(value: HostWithOptPort) -> Self {
136        Self(value)
137    }
138}
139
140impl From<HostWithPort> for ForwardedAuthority {
141    #[inline(always)]
142    fn from(value: HostWithPort) -> Self {
143        Self::new(value.into())
144    }
145}
146
147impl ForwardedElement {
148    /// Merge the properties of another [`ForwardedElement`] into this one.
149    pub fn merge(&mut self, other: Self) -> &mut Self {
150        if let Some(by_node) = other.by_node {
151            self.by_node = Some(by_node);
152        }
153        if let Some(for_node) = other.for_node {
154            self.for_node = Some(for_node);
155        }
156        if let Some(authority) = other.authority {
157            self.authority = Some(authority);
158        }
159        if let Some(proto) = other.proto {
160            self.proto = Some(proto);
161        }
162        if let Some(extensions) = other.extensions {
163            match &mut self.extensions {
164                Some(map) => {
165                    map.extend(extensions);
166                }
167                None => {
168                    self.extensions = Some(extensions);
169                }
170            }
171        }
172        self
173    }
174
175    /// Return the host if one is defined.
176    #[must_use]
177    pub fn authority(&self) -> Option<HostWithOptPort> {
178        self.authority.as_ref().map(|authority| authority.0.clone())
179    }
180
181    /// Create a new [`ForwardedElement`] with the "host" parameter set
182    /// using the given [`Host`], [`Domain`], [`HostWithPort`], [`IpAddr`], [`SocketAddress`] and more.
183    pub fn new_forwarded_host(authority: impl Into<ForwardedAuthority>) -> Self {
184        Self {
185            by_node: None,
186            for_node: None,
187            authority: Some(authority.into()),
188            proto: None,
189            proto_version: None,
190            extensions: None,
191        }
192    }
193
194    rama_utils::macros::generate_set_and_with! {
195        /// Sets the "host" parameter in this [`ForwardedElement`] using
196        /// the given authority value.
197        pub fn forwarded_host(mut self, authority: impl Into<ForwardedAuthority>) -> Self {
198            self.authority = Some(authority.into());
199            self
200        }
201    }
202
203    /// Get a reference to the "host" parameter if it is set.
204    #[must_use]
205    pub fn forwarded_host(&self) -> Option<&ForwardedAuthority> {
206        self.authority.as_ref()
207    }
208
209    /// Create a new [`ForwardedElement`] with the "for" parameter
210    /// set to the given valid node identifier. Examples are
211    /// an Ip Address or Domain, with or without a port.
212    pub fn new_forwarded_for(node_id: impl Into<NodeId>) -> Self {
213        Self {
214            by_node: None,
215            for_node: Some(node_id.into()),
216            authority: None,
217            proto: None,
218            proto_version: None,
219            extensions: None,
220        }
221    }
222
223    rama_utils::macros::generate_set_and_with! {
224        /// Sets the "for" parameter for this [`ForwardedElement`] using the given valid node identifier.
225        /// Examples are an Ip Address or Domain, with or without a port.
226        pub fn forwarded_for(mut self, node_id: impl Into<NodeId>) -> Self {
227            self.for_node = Some(node_id.into());
228            self
229        }
230    }
231
232    /// Get a reference to the "for" parameter if it is set.
233    #[must_use]
234    pub fn forwarded_for(&self) -> Option<&NodeId> {
235        self.for_node.as_ref()
236    }
237
238    /// Create a new [`ForwardedElement`] with the "by" parameter
239    /// set to the given valid node identifier. Examples are
240    /// an Ip Address or Domain, with or without a port.
241    pub fn new_forwarded_by(node_id: impl Into<NodeId>) -> Self {
242        Self {
243            by_node: Some(node_id.into()),
244            for_node: None,
245            authority: None,
246            proto: None,
247            proto_version: None,
248            extensions: None,
249        }
250    }
251
252    rama_utils::macros::generate_set_and_with! {
253        /// Sets the "by" parameter for this [`ForwardedElement`] using the given valid node identifier.
254        /// Examples are an Ip Address or Domain, with or without a port.
255        pub fn forwarded_by(mut self, node_id: impl Into<NodeId>) -> Self {
256            self.by_node = Some(node_id.into());
257            self
258        }
259    }
260
261    /// Get a reference to the "by" parameter if it is set.
262    #[must_use]
263    pub fn forwarded_by(&self) -> Option<&NodeId> {
264        self.by_node.as_ref()
265    }
266
267    /// Create a new [`ForwardedElement`] with the "proto" parameter
268    /// set to the given valid/recognised [`ForwardedProtocol`]
269    #[must_use]
270    pub fn new_forwarded_proto(protocol: ForwardedProtocol) -> Self {
271        Self {
272            by_node: None,
273            for_node: None,
274            authority: None,
275            proto: Some(protocol),
276            proto_version: None,
277            extensions: None,
278        }
279    }
280
281    rama_utils::macros::generate_set_and_with! {
282        /// Set the "proto" parameter to the given valid/recognised [`ForwardedProtocol`].
283        pub fn forwarded_proto(mut self, protocol: ForwardedProtocol) -> Self {
284            self.proto = Some(protocol);
285            self
286        }
287    }
288
289    /// Get a reference to the "proto" parameter if it is set.
290    #[must_use]
291    pub fn forwarded_proto(&self) -> Option<ForwardedProtocol> {
292        self.proto.clone()
293    }
294
295    /// Create a new [`ForwardedElement`] with the "version" parameter
296    /// set to the given valid/recognised [`ForwardedVersion`].
297    #[must_use]
298    pub fn new_forwarded_version(version: ForwardedVersion) -> Self {
299        Self {
300            by_node: None,
301            for_node: None,
302            authority: None,
303            proto: None,
304            proto_version: Some(version),
305            extensions: None,
306        }
307    }
308
309    rama_utils::macros::generate_set_and_with! {
310        /// Set the "version" parameter to the given valid/recognised [`ForwardedVersion`].
311        pub fn forwarded_version(mut self, version: ForwardedVersion) -> Self {
312            self.proto_version = Some(version);
313            self
314        }
315    }
316
317    /// Get a copy of the "version" parameter, if it is set.
318    #[must_use]
319    pub fn forwarded_version(&self) -> Option<ForwardedVersion> {
320        self.proto_version
321    }
322}
323
324impl fmt::Display for ForwardedAuthority {
325    #[inline(always)]
326    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327        self.0.fmt(f)
328    }
329}
330
331impl fmt::Display for ForwardedElement {
332    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333        let mut separator = "";
334
335        if let Some(ref by_node) = self.by_node {
336            write!(f, "by=")?;
337            let quoted =
338                by_node.has_any_port() || by_node.ip().map(|ip| ip.is_ipv6()).unwrap_or_default();
339            if quoted {
340                write!(f, r##""{by_node}""##)?;
341            } else {
342                by_node.fmt(f)?;
343            }
344            separator = ";";
345        }
346
347        if let Some(ref for_node) = self.for_node {
348            write!(f, "{separator}for=")?;
349            let quoted =
350                for_node.has_any_port() || for_node.ip().map(|ip| ip.is_ipv6()).unwrap_or_default();
351            if quoted {
352                write!(f, r##""{for_node}""##)?;
353            } else {
354                for_node.fmt(f)?;
355            }
356            separator = ";";
357        }
358
359        if let Some(ref authority) = self.authority {
360            write!(f, "{separator}host=")?;
361            // `host=` syntax requires quoting when there's any colon
362            // (port present in any form, OR an IPv6 address) — see
363            // RFC 7239 §4.
364            let quoted = authority.0.port.is_explicit()
365                || matches!(authority.0.host, Host::Address(IpAddr::V6(_)));
366            if quoted {
367                write!(f, r##""{authority}""##)?;
368            } else {
369                authority.fmt(f)?;
370            }
371            separator = ";";
372        }
373
374        if let Some(ref proto) = self.proto {
375            write!(f, "{separator}proto=")?;
376            proto.fmt(f)?;
377        }
378
379        Ok(())
380    }
381}
382
383impl core::str::FromStr for ForwardedElement {
384    type Err = BoxError;
385
386    fn from_str(s: &str) -> Result<Self, Self::Err> {
387        parse_single_forwarded_element(s.as_bytes())
388    }
389}
390
391impl TryFrom<String> for ForwardedElement {
392    type Error = BoxError;
393
394    fn try_from(s: String) -> Result<Self, Self::Error> {
395        parse_single_forwarded_element(s.as_bytes())
396    }
397}
398
399impl TryFrom<&str> for ForwardedElement {
400    type Error = BoxError;
401
402    fn try_from(s: &str) -> Result<Self, Self::Error> {
403        parse_single_forwarded_element(s.as_bytes())
404    }
405}
406
407impl TryFrom<Vec<u8>> for ForwardedElement {
408    type Error = BoxError;
409
410    fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
411        parse_single_forwarded_element(bytes.as_ref())
412    }
413}
414
415impl TryFrom<&[u8]> for ForwardedElement {
416    type Error = BoxError;
417
418    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
419        parse_single_forwarded_element(bytes)
420    }
421}
422
423impl core::str::FromStr for ForwardedAuthority {
424    type Err = BoxError;
425
426    fn from_str(s: &str) -> Result<Self, Self::Err> {
427        let address = HostWithOptPort::from_str(s)?;
428        Ok(Self(address))
429    }
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435
436    #[test]
437    fn test_forwarded_element_parse_invalid() {
438        for s in [
439            "",
440            "foobar",
441            "127.0.0.1",
442            "⌨️",
443            "for=_foo;for=_bar",
444            "for=foo,proto=http",
445        ] {
446            if let Ok(el) = ForwardedElement::try_from(s) {
447                panic!("unexpected parse success: input {s}: {el:?}");
448            }
449        }
450    }
451
452    #[test]
453    fn test_forwarded_element_parse_happy_spec() {
454        for (s, expected) in [
455            (
456                r##"for="_gazonk""##,
457                ForwardedElement::new_forwarded_for(NodeId::try_from("_gazonk").unwrap()),
458            ),
459            (
460                r##"For="[2001:db8:cafe::17]:4711""##,
461                ForwardedElement::new_forwarded_for(
462                    NodeId::try_from("[2001:db8:cafe::17]:4711").unwrap(),
463                ),
464            ),
465            (
466                r##"For="[2001:db8:cafe::17]:4711";proto=http"##,
467                ForwardedElement {
468                    by_node: None,
469                    for_node: Some(NodeId::try_from("[2001:db8:cafe::17]:4711").unwrap()),
470                    authority: None,
471                    proto: Some(ForwardedProtocol::HTTP),
472                    proto_version: None,
473                    extensions: None,
474                },
475            ),
476            (
477                r##"For="[2001:db8:cafe::17]:4711";proto=http;foo=bar"##,
478                ForwardedElement {
479                    by_node: None,
480                    for_node: Some(NodeId::try_from("[2001:db8:cafe::17]:4711").unwrap()),
481                    authority: None,
482                    proto: Some(ForwardedProtocol::HTTP),
483                    proto_version: None,
484                    extensions: Some(
485                        [(
486                            "foo".to_owned(),
487                            ExtensionValue {
488                                value: "bar".to_owned(),
489                                quoted: false,
490                            },
491                        )]
492                        .into_iter()
493                        .collect(),
494                    ),
495                },
496            ),
497            (
498                r##"for=192.0.2.60;proto=http;by=203.0.113.43"##,
499                ForwardedElement {
500                    by_node: Some(NodeId::try_from("203.0.113.43").unwrap()),
501                    for_node: Some(NodeId::try_from("192.0.2.60").unwrap()),
502                    authority: None,
503                    proto: Some(ForwardedProtocol::HTTP),
504                    proto_version: None,
505                    extensions: None,
506                },
507            ),
508        ] {
509            let element = match ForwardedElement::try_from(s) {
510                Ok(el) => el,
511                Err(err) => panic!("failed to parse happy spec el '{s}': {err}"),
512            };
513            assert_eq!(element, expected, "input: {s}");
514        }
515    }
516
517    /// Regression: RFC 7230 §3.2.6 `quoted-string` allows `quoted-pair`
518    /// (`\` followed by one byte). The first parser version found the
519    /// closing `"` with a naive `position` scan, mis-parsing legal
520    /// values like `host="a\"b"` (split mid-value at the escaped quote).
521    #[test]
522    fn regression_forwarded_quoted_pair_rfc7230() {
523        // Escaped `"` inside an extension value must round-trip into the
524        // decoded value.
525        let el = ForwardedElement::try_from(r#"for=_a;ext="x\"y""#).unwrap();
526        let ext = el
527            .extensions
528            .as_ref()
529            .expect("extensions parsed")
530            .get("ext")
531            .expect("ext present");
532        assert_eq!(ext.value, r#"x"y"#);
533        assert!(ext.quoted);
534
535        // Escaped backslash.
536        let el = ForwardedElement::try_from(r#"for=_a;ext="x\\y""#).unwrap();
537        let ext = el.extensions.unwrap().remove("ext").unwrap();
538        assert_eq!(ext.value, r#"x\y"#);
539
540        // A trailing `\` with no escapable byte must error, not silently
541        // succeed (would otherwise be a `quoted-string missing trailer`).
542        ForwardedElement::try_from(r#"for=_a;ext="abc\"#).unwrap_err();
543    }
544
545    /// Regression: RFC 7230 §3.2.6 `qdtext` permits `obs-text` (0x80–0xFF).
546    /// The first parser version rejected the entire 0x80–0xFF range inside
547    /// quoted values via a `(32..127)` charset check, which made any
548    /// UTF-8 high-byte fail.
549    #[test]
550    fn regression_forwarded_obs_text_in_qdtext() {
551        // UTF-8 encoding of `é` (0xC3 0xA9) inside a quoted ext value.
552        let el = ForwardedElement::try_from("for=_a;ext=\"café\"").unwrap();
553        let ext = el.extensions.unwrap().remove("ext").unwrap();
554        assert_eq!(ext.value, "café");
555        // Token-form values stay strict (no obs-text outside quotes).
556        ForwardedElement::try_from("for=_a;ext=café").unwrap_err();
557    }
558
559    /// Regression: RFC 7230 OWS = `*( SP / HTAB )`. The first parser version
560    /// only trimmed SP, so any `\t` around `;`/`=`/list-separator caused a
561    /// parse error on otherwise legal Forwarded headers.
562    #[test]
563    fn regression_forwarded_ows_handles_htab() {
564        // HTAB after the `;` separator.
565        let el = ForwardedElement::try_from("for=_a;\tproto=http").unwrap();
566        assert_eq!(el.forwarded_proto(), Some(ForwardedProtocol::HTTP));
567        // HTAB padding around `=` and inside the list comma between elements.
568        let s = "for=_a;\tproto=http,\tfor=_b";
569        let (first, others) = parse_one_plus_forwarded_elements(s.as_bytes()).unwrap();
570        assert_eq!(first.forwarded_for().unwrap().to_string(), "_a");
571        assert_eq!(others.len(), 1);
572        assert_eq!(others[0].forwarded_for().unwrap().to_string(), "_b");
573    }
574
575    /// Regression: a zone-id (RFC 6874) inside a Forwarded `for=` value
576    /// must never be accepted. The lower-level `Ipv6Addr` parser already
577    /// rejects `%`; this pins the rejection at the Forwarded entry point
578    /// so future changes can't silently re-allow it.
579    #[test]
580    fn regression_forwarded_rejects_ipv6_zone_id() {
581        for s in [r#"for="[fe80::1%eth0]""#, r#"for="[fe80::1%25eth0]:80""#] {
582            assert!(
583                ForwardedElement::try_from(s).is_err(),
584                "forwarded element should reject zone-id input {s:?}",
585            );
586        }
587    }
588}