http_types/proxies/
forwarded.rs

1use crate::{
2    headers::{HeaderName, HeaderValue, Headers, ToHeaderValues, FORWARDED},
3    parse_utils::{parse_quoted_string, parse_token},
4};
5use std::{borrow::Cow, convert::TryFrom, fmt::Write, net::IpAddr};
6
7// these constants are private because they are nonstandard
8const X_FORWARDED_FOR: HeaderName = HeaderName::from_lowercase_str("x-forwarded-for");
9const X_FORWARDED_PROTO: HeaderName = HeaderName::from_lowercase_str("x-forwarded-proto");
10const X_FORWARDED_BY: HeaderName = HeaderName::from_lowercase_str("x-forwarded-by");
11
12/// A rust representation of the [forwarded
13/// header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded).
14#[derive(Debug, Clone, Default, PartialEq, Eq)]
15pub struct Forwarded<'a> {
16    by: Option<Cow<'a, str>>,
17    forwarded_for: Vec<Cow<'a, str>>,
18    host: Option<Cow<'a, str>>,
19    proto: Option<Cow<'a, str>>,
20}
21
22impl<'a> Forwarded<'a> {
23    /// Attempts to parse a Forwarded from headers (or a request or
24    /// response). Builds a borrowed Forwarded by default. To build an
25    /// owned Forwarded, use
26    /// `Forwarded::from_headers(...).into_owned()`
27    ///
28    /// # X-Forwarded-For, -By, and -Proto compatability
29    ///
30    /// This implementation includes fall-back support for the
31    /// historical unstandardized headers x-forwarded-for,
32    /// x-forwarded-by, and x-forwarded-proto. If you do not wish to
33    /// support these headers, use
34    /// [`Forwarded::from_forwarded_header`]. To _only_ support these
35    /// historical headers and _not_ the standardized Forwarded
36    /// header, use [`Forwarded::from_x_headers`].
37    ///
38    /// Please note that either way, this implementation will
39    /// normalize to the standardized Forwarded header, as recommended
40    /// in
41    /// [rfc7239§7.4](https://tools.ietf.org/html/rfc7239#section-7.4)
42    ///
43    /// # Examples
44    /// ```rust
45    /// # use http_types::{proxies::Forwarded, Method::Get, Request, Url, Result};
46    /// # fn main() -> Result<()> {
47    /// let mut request = Request::new(Get, Url::parse("http://_/")?);
48    /// request.insert_header(
49    ///     "Forwarded",
50    ///     r#"for=192.0.2.43, for="[2001:db8:cafe::17]", for=unknown;proto=https"#
51    /// );
52    /// let forwarded = Forwarded::from_headers(&request)?.unwrap();
53    /// assert_eq!(forwarded.proto(), Some("https"));
54    /// assert_eq!(forwarded.forwarded_for(), vec!["192.0.2.43", "[2001:db8:cafe::17]", "unknown"]);
55    /// # Ok(()) }
56    /// ```
57    ///
58    /// ```rust
59    /// # use http_types::{proxies::Forwarded, Method::Get, Request, Url, Result};
60    /// # fn main() -> Result<()> {
61    /// let mut request = Request::new(Get, Url::parse("http://_/")?);
62    /// request.insert_header("X-Forwarded-For", "192.0.2.43, 2001:db8:cafe::17, unknown");
63    /// request.insert_header("X-Forwarded-Proto", "https");
64    /// let forwarded = Forwarded::from_headers(&request)?.unwrap();
65    /// assert_eq!(forwarded.forwarded_for(), vec!["192.0.2.43", "[2001:db8:cafe::17]", "unknown"]);
66    /// assert_eq!(forwarded.proto(), Some("https"));
67    /// assert_eq!(
68    ///     forwarded.value()?,
69    ///     r#"for=192.0.2.43, for="[2001:db8:cafe::17]", for=unknown;proto=https"#
70    /// );
71    /// # Ok(()) }
72    /// ```
73
74    pub fn from_headers(headers: &'a impl AsRef<Headers>) -> Result<Option<Self>, ParseError> {
75        if let Some(forwarded) = Self::from_forwarded_header(headers)? {
76            Ok(Some(forwarded))
77        } else {
78            Self::from_x_headers(headers)
79        }
80    }
81
82    /// Parse a borrowed Forwarded from the Forwarded header, without x-forwarded-{for,by,proto} fallback
83    ///
84    /// # Examples
85    /// ```rust
86    /// # use http_types::{proxies::Forwarded, Method::Get, Request, Url, Result};
87    /// # fn main() -> Result<()> {
88    /// let mut request = Request::new(Get, Url::parse("http://_/")?);
89    /// request.insert_header(
90    ///     "Forwarded",
91    ///     r#"for=192.0.2.43, for="[2001:db8:cafe::17]", for=unknown;proto=https"#
92    /// );
93    /// let forwarded = Forwarded::from_forwarded_header(&request)?.unwrap();
94    /// assert_eq!(forwarded.proto(), Some("https"));
95    /// assert_eq!(forwarded.forwarded_for(), vec!["192.0.2.43", "[2001:db8:cafe::17]", "unknown"]);
96    /// # Ok(()) }
97    /// ```
98    /// ```rust
99    /// # use http_types::{proxies::Forwarded, Method::Get, Request, Url, Result};
100    /// # fn main() -> Result<()> {
101    /// let mut request = Request::new(Get, Url::parse("http://_/")?);
102    /// request.insert_header("X-Forwarded-For", "192.0.2.43, 2001:db8:cafe::17");
103    /// assert!(Forwarded::from_forwarded_header(&request)?.is_none());
104    /// # Ok(()) }
105    /// ```
106    pub fn from_forwarded_header(
107        headers: &'a impl AsRef<Headers>,
108    ) -> Result<Option<Self>, ParseError> {
109        if let Some(headers) = headers.as_ref().get(FORWARDED) {
110            Ok(Some(Self::parse(headers.as_ref().as_str())?))
111        } else {
112            Ok(None)
113        }
114    }
115
116    /// Parse a borrowed Forwarded from the historical
117    /// non-standardized x-forwarded-{for,by,proto} headers, without
118    /// support for the Forwarded header.
119    ///
120    /// # Examples
121    /// ```rust
122    /// # use http_types::{proxies::Forwarded, Method::Get, Request, Url, Result};
123    /// # fn main() -> Result<()> {
124    /// let mut request = Request::new(Get, Url::parse("http://_/")?);
125    /// request.insert_header("X-Forwarded-For", "192.0.2.43, 2001:db8:cafe::17");
126    /// let forwarded = Forwarded::from_headers(&request)?.unwrap();
127    /// assert_eq!(forwarded.forwarded_for(), vec!["192.0.2.43", "[2001:db8:cafe::17]"]);
128    /// # Ok(()) }
129    /// ```
130    /// ```rust
131    /// # use http_types::{proxies::Forwarded, Method::Get, Request, Url, Result};
132    /// # fn main() -> Result<()> {
133    /// let mut request = Request::new(Get, Url::parse("http://_/")?);
134    /// request.insert_header(
135    ///     "Forwarded",
136    ///     r#"for=192.0.2.43, for="[2001:db8:cafe::17]", for=unknown;proto=https"#
137    /// );
138    /// assert!(Forwarded::from_x_headers(&request)?.is_none());
139    /// # Ok(()) }
140    /// ```
141    pub fn from_x_headers(headers: &'a impl AsRef<Headers>) -> Result<Option<Self>, ParseError> {
142        let headers = headers.as_ref();
143
144        let forwarded_for: Vec<Cow<'a, str>> = headers
145            .get(X_FORWARDED_FOR)
146            .map(|hv| {
147                hv.as_str()
148                    .split(',')
149                    .map(|v| {
150                        let v = v.trim();
151                        match v.parse::<IpAddr>().ok() {
152                            Some(IpAddr::V6(v6)) => Cow::Owned(format!(r#"[{}]"#, v6)),
153                            _ => Cow::Borrowed(v),
154                        }
155                    })
156                    .collect()
157            })
158            .unwrap_or_default();
159
160        let by = headers
161            .get(X_FORWARDED_BY)
162            .map(|hv| Cow::Borrowed(hv.as_str()));
163
164        let proto = headers
165            .get(X_FORWARDED_PROTO)
166            .map(|p| Cow::Borrowed(p.as_str()));
167
168        if !forwarded_for.is_empty() || by.is_some() || proto.is_some() {
169            Ok(Some(Self {
170                forwarded_for,
171                by,
172                proto,
173                host: None,
174            }))
175        } else {
176            Ok(None)
177        }
178    }
179
180    /// parse a &str into a borrowed Forwarded
181    ///
182    /// # Examples
183    /// ```rust
184    /// # use http_types::{proxies::Forwarded, Method::Get, Request, Url, Result};
185    /// # fn main() -> Result<()> {
186    /// let forwarded = Forwarded::parse(
187    ///     r#"for=192.0.2.43,         for="[2001:db8:cafe::17]", FOR=unknown;proto=https"#
188    /// )?;
189    /// assert_eq!(forwarded.forwarded_for(), vec!["192.0.2.43", "[2001:db8:cafe::17]", "unknown"]);
190    /// assert_eq!(
191    ///     forwarded.value()?,
192    ///     r#"for=192.0.2.43, for="[2001:db8:cafe::17]", for=unknown;proto=https"#
193    /// );
194    /// # Ok(()) }
195    /// ```
196    pub fn parse(input: &'a str) -> Result<Self, ParseError> {
197        let mut input = input;
198        let mut forwarded = Forwarded::new();
199
200        while !input.is_empty() {
201            input = if starts_with_ignore_case("for=", input) {
202                forwarded.parse_for(input)?
203            } else {
204                forwarded.parse_forwarded_pair(input)?
205            }
206        }
207
208        Ok(forwarded)
209    }
210
211    fn parse_forwarded_pair(&mut self, input: &'a str) -> Result<&'a str, ParseError> {
212        let (key, value, rest) = match parse_token(input) {
213            (Some(key), rest) if rest.starts_with('=') => match parse_value(&rest[1..]) {
214                (Some(value), rest) => Some((key, value, rest)),
215                (None, _) => None,
216            },
217            _ => None,
218        }
219        .ok_or_else(|| ParseError::new("parse error in forwarded-pair"))?;
220
221        match key {
222            "by" => {
223                if self.by.is_some() {
224                    return Err(ParseError::new("parse error, duplicate `by` key"));
225                }
226                self.by = Some(value);
227            }
228
229            "host" => {
230                if self.host.is_some() {
231                    return Err(ParseError::new("parse error, duplicate `host` key"));
232                }
233                self.host = Some(value);
234            }
235
236            "proto" => {
237                if self.proto.is_some() {
238                    return Err(ParseError::new("parse error, duplicate `proto` key"));
239                }
240                self.proto = Some(value);
241            }
242
243            _ => { /* extensions are allowed in the spec */ }
244        }
245
246        match rest.strip_prefix(';') {
247            Some(rest) => Ok(rest),
248            None => Ok(rest),
249        }
250    }
251
252    fn parse_for(&mut self, input: &'a str) -> Result<&'a str, ParseError> {
253        let mut rest = input;
254
255        loop {
256            rest = match match_ignore_case("for=", rest) {
257                (true, rest) => rest,
258                (false, _) => return Err(ParseError::new("http list must start with for=")),
259            };
260
261            let (value, rest_) = parse_value(rest);
262            rest = rest_;
263
264            if let Some(value) = value {
265                // add a successful for= value
266                self.forwarded_for.push(value);
267            } else {
268                return Err(ParseError::new("for= without valid value"));
269            }
270
271            match rest.chars().next() {
272                // we have another for=
273                Some(',') => {
274                    rest = rest[1..].trim_start();
275                }
276
277                // we have reached the end of the for= section
278                Some(';') => return Ok(&rest[1..]),
279
280                // reached the end of the input
281                None => return Ok(rest),
282
283                // bail
284                _ => return Err(ParseError::new("unexpected character after for= section")),
285            }
286        }
287    }
288
289    /// Transform a borrowed Forwarded into an owned
290    /// Forwarded. This is a noop if the Forwarded is already owned.
291    pub fn into_owned(self) -> Forwarded<'static> {
292        Forwarded {
293            by: self.by.map(|by| Cow::Owned(by.into_owned())),
294            forwarded_for: self
295                .forwarded_for
296                .into_iter()
297                .map(|ff| Cow::Owned(ff.into_owned()))
298                .collect(),
299            host: self.host.map(|h| Cow::Owned(h.into_owned())),
300            proto: self.proto.map(|p| Cow::Owned(p.into_owned())),
301        }
302    }
303
304    /// Insert a header that represents this Forwarded.
305    ///
306    /// # Example
307    ///
308    /// ```rust
309    /// let mut response = http_types::Response::new(200);
310    /// let mut forwarded = http_types::proxies::Forwarded::new();
311    /// forwarded.add_for("192.0.2.43");
312    /// forwarded.add_for("[2001:db8:cafe::17]");
313    /// forwarded.set_proto("https");
314    /// forwarded.apply(&mut response);
315    /// assert_eq!(response["Forwarded"], r#"for=192.0.2.43, for="[2001:db8:cafe::17]";proto=https"#);
316    /// ```
317    pub fn apply(&self, mut headers: impl AsMut<Headers>) {
318        headers.as_mut().insert(FORWARDED, self);
319    }
320
321    /// Builds a Forwarded header as a String.
322    ///
323    /// # Example
324    ///
325    /// ```rust
326    /// # fn main() -> http_types::Result<()> {
327    /// let mut forwarded = http_types::proxies::Forwarded::new();
328    /// forwarded.add_for("_haproxy");
329    /// forwarded.add_for("[2001:db8:cafe::17]");
330    /// forwarded.set_proto("https");
331    /// assert_eq!(forwarded.value()?, r#"for=_haproxy, for="[2001:db8:cafe::17]";proto=https"#);
332    /// # Ok(()) }
333    /// ```
334    pub fn value(&self) -> Result<String, std::fmt::Error> {
335        let mut buf = String::new();
336        if let Some(by) = self.by() {
337            write!(&mut buf, "by={};", by)?;
338        }
339
340        buf.push_str(
341            &self
342                .forwarded_for
343                .iter()
344                .map(|f| format!("for={}", format_value(f)))
345                .collect::<Vec<_>>()
346                .join(", "),
347        );
348
349        buf.push(';');
350
351        if let Some(host) = self.host() {
352            write!(&mut buf, "host={};", host)?;
353        }
354
355        if let Some(proto) = self.proto() {
356            write!(&mut buf, "proto={};", proto)?;
357        }
358
359        // remove a trailing semicolon
360        buf.pop();
361
362        Ok(buf)
363    }
364
365    /// Builds a new empty Forwarded
366    pub fn new() -> Self {
367        Self::default()
368    }
369
370    /// Adds a `for` section to this header
371    pub fn add_for(&mut self, forwarded_for: impl Into<Cow<'a, str>>) {
372        self.forwarded_for.push(forwarded_for.into());
373    }
374
375    /// Returns the `for` field of this header
376    pub fn forwarded_for(&self) -> Vec<&str> {
377        self.forwarded_for.iter().map(|x| x.as_ref()).collect()
378    }
379
380    /// Sets the `host` field of this header
381    pub fn set_host(&mut self, host: impl Into<Cow<'a, str>>) {
382        self.host = Some(host.into());
383    }
384
385    /// Returns the `host` field of this header
386    pub fn host(&self) -> Option<&str> {
387        self.host.as_deref()
388    }
389
390    /// Sets the `proto` field of this header
391    pub fn set_proto(&mut self, proto: impl Into<Cow<'a, str>>) {
392        self.proto = Some(proto.into())
393    }
394
395    /// Returns the `proto` field of this header
396    pub fn proto(&self) -> Option<&str> {
397        self.proto.as_deref()
398    }
399
400    /// Sets the `by` field of this header
401    pub fn set_by(&mut self, by: impl Into<Cow<'a, str>>) {
402        self.by = Some(by.into());
403    }
404
405    /// Returns the `by` field of this header
406    pub fn by(&self) -> Option<&str> {
407        self.by.as_deref()
408    }
409}
410
411fn parse_value(input: &str) -> (Option<Cow<'_, str>>, &str) {
412    match parse_token(input) {
413        (Some(token), rest) => (Some(Cow::Borrowed(token)), rest),
414        (None, rest) => parse_quoted_string(rest),
415    }
416}
417
418fn format_value(input: &str) -> Cow<'_, str> {
419    match parse_token(input) {
420        (_, "") => input.into(),
421        _ => {
422            let mut string = String::from("\"");
423            for ch in input.chars() {
424                if let '\\' | '"' = ch {
425                    string.push('\\');
426                }
427                string.push(ch);
428            }
429            string.push('"');
430            string.into()
431        }
432    }
433}
434
435fn match_ignore_case<'a>(start: &'static str, input: &'a str) -> (bool, &'a str) {
436    let len = start.len();
437    if input[..len].eq_ignore_ascii_case(start) {
438        (true, &input[len..])
439    } else {
440        (false, input)
441    }
442}
443
444fn starts_with_ignore_case(start: &'static str, input: &str) -> bool {
445    if start.len() <= input.len() {
446        let len = start.len();
447        input[..len].eq_ignore_ascii_case(start)
448    } else {
449        false
450    }
451}
452
453impl std::fmt::Display for Forwarded<'_> {
454    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
455        f.write_str(&self.value()?)
456    }
457}
458
459impl ToHeaderValues for Forwarded<'_> {
460    type Iter = std::option::IntoIter<HeaderValue>;
461    fn to_header_values(&self) -> crate::Result<Self::Iter> {
462        self.value()?.to_header_values()
463    }
464}
465
466impl ToHeaderValues for &Forwarded<'_> {
467    type Iter = std::option::IntoIter<HeaderValue>;
468    fn to_header_values(&self) -> crate::Result<Self::Iter> {
469        self.value()?.to_header_values()
470    }
471}
472
473#[derive(Debug, Clone)]
474pub struct ParseError(&'static str);
475impl ParseError {
476    pub fn new(msg: &'static str) -> Self {
477        Self(msg)
478    }
479}
480
481impl std::error::Error for ParseError {}
482impl std::fmt::Display for ParseError {
483    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
484        write!(f, "unable to parse forwarded header: {}", self.0)
485    }
486}
487
488impl<'a> TryFrom<&'a str> for Forwarded<'a> {
489    type Error = ParseError;
490    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
491        Self::parse(value)
492    }
493}
494
495#[cfg(test)]
496mod tests {
497    use super::*;
498    use crate::{Method::Get, Request, Response, Result};
499    use url::Url;
500
501    #[test]
502    fn starts_with_ignore_case_can_handle_short_inputs() {
503        assert!(!starts_with_ignore_case("helloooooo", "h"));
504    }
505
506    #[test]
507    fn parsing_for() -> Result<()> {
508        assert_eq!(
509            Forwarded::parse(r#"for="_gazonk""#)?.forwarded_for(),
510            vec!["_gazonk"]
511        );
512        assert_eq!(
513            Forwarded::parse(r#"For="[2001:db8:cafe::17]:4711""#)?.forwarded_for(),
514            vec!["[2001:db8:cafe::17]:4711"]
515        );
516
517        assert_eq!(
518            Forwarded::parse("for=192.0.2.60;proto=http;by=203.0.113.43")?.forwarded_for(),
519            vec!["192.0.2.60"]
520        );
521
522        assert_eq!(
523            Forwarded::parse("for=192.0.2.43,   for=198.51.100.17")?.forwarded_for(),
524            vec!["192.0.2.43", "198.51.100.17"]
525        );
526
527        assert_eq!(
528            Forwarded::parse(r#"for=192.0.2.43,for="[2001:db8:cafe::17]",for=unknown"#)?
529                .forwarded_for(),
530            Forwarded::parse(r#"for=192.0.2.43, for="[2001:db8:cafe::17]", for=unknown"#)?
531                .forwarded_for()
532        );
533
534        assert_eq!(
535            Forwarded::parse(
536                r#"for=192.0.2.43,for="this is a valid quoted-string, \" \\",for=unknown"#
537            )?
538            .forwarded_for(),
539            vec![
540                "192.0.2.43",
541                r#"this is a valid quoted-string, " \"#,
542                "unknown"
543            ]
544        );
545
546        Ok(())
547    }
548
549    #[test]
550    fn basic_parse() -> Result<()> {
551        let forwarded = Forwarded::parse("for=client.com;by=proxy.com;host=host.com;proto=https")?;
552
553        assert_eq!(forwarded.by(), Some("proxy.com"));
554        assert_eq!(forwarded.forwarded_for(), vec!["client.com"]);
555        assert_eq!(forwarded.host(), Some("host.com"));
556        assert_eq!(forwarded.proto(), Some("https"));
557        assert!(matches!(forwarded, Forwarded { .. }));
558        Ok(())
559    }
560
561    #[test]
562    fn bad_parse() {
563        let err = Forwarded::parse("by=proxy.com;for=client;host=example.com;host").unwrap_err();
564        assert_eq!(
565            err.to_string(),
566            "unable to parse forwarded header: parse error in forwarded-pair"
567        );
568
569        let err = Forwarded::parse("by;for;host;proto").unwrap_err();
570        assert_eq!(
571            err.to_string(),
572            "unable to parse forwarded header: parse error in forwarded-pair"
573        );
574
575        let err = Forwarded::parse("for=for, key=value").unwrap_err();
576        assert_eq!(
577            err.to_string(),
578            "unable to parse forwarded header: http list must start with for="
579        );
580
581        let err = Forwarded::parse(r#"for="unterminated string"#).unwrap_err();
582        assert_eq!(
583            err.to_string(),
584            "unable to parse forwarded header: for= without valid value"
585        );
586
587        let err = Forwarded::parse(r#"for=, for=;"#).unwrap_err();
588        assert_eq!(
589            err.to_string(),
590            "unable to parse forwarded header: for= without valid value"
591        );
592    }
593
594    #[test]
595    fn bad_parse_from_headers() -> Result<()> {
596        let mut response = Response::new(200);
597        response.append_header("forwarded", "uh oh");
598        assert_eq!(
599            Forwarded::from_headers(&response).unwrap_err().to_string(),
600            "unable to parse forwarded header: parse error in forwarded-pair"
601        );
602
603        let response = Response::new(200);
604        assert!(Forwarded::from_headers(&response)?.is_none());
605        Ok(())
606    }
607
608    #[test]
609    fn from_x_headers() -> Result<()> {
610        let mut request = Request::new(Get, Url::parse("http://_/")?);
611        request.append_header(X_FORWARDED_FOR, "192.0.2.43, 2001:db8:cafe::17");
612        request.append_header(X_FORWARDED_PROTO, "gopher");
613        let forwarded = Forwarded::from_headers(&request)?.unwrap();
614        assert_eq!(
615            forwarded.to_string(),
616            r#"for=192.0.2.43, for="[2001:db8:cafe::17]";proto=gopher"#
617        );
618        Ok(())
619    }
620
621    #[test]
622    fn formatting_edge_cases() {
623        let mut forwarded = Forwarded::new();
624        forwarded.add_for(r#"quote: " backslash: \"#);
625        forwarded.add_for(";proto=https");
626        assert_eq!(
627            forwarded.to_string(),
628            r#"for="quote: \" backslash: \\", for=";proto=https""#
629        );
630    }
631
632    #[test]
633    fn parse_edge_cases() -> Result<()> {
634        let forwarded =
635            Forwarded::parse(r#"for=";", for=",", for="\"", for=unquoted;by=";proto=https""#)?;
636        assert_eq!(forwarded.forwarded_for(), vec![";", ",", "\"", "unquoted"]);
637        assert_eq!(forwarded.by(), Some(";proto=https"));
638        assert!(forwarded.proto().is_none());
639
640        let forwarded = Forwarded::parse("proto=https")?;
641        assert_eq!(forwarded.proto(), Some("https"));
642        Ok(())
643    }
644
645    #[test]
646    fn owned_parse() -> Result<()> {
647        let forwarded =
648            Forwarded::parse("for=client;by=proxy.com;host=example.com;proto=https")?.into_owned();
649
650        assert_eq!(forwarded.by(), Some("proxy.com"));
651        assert_eq!(forwarded.forwarded_for(), vec!["client"]);
652        assert_eq!(forwarded.host(), Some("example.com"));
653        assert_eq!(forwarded.proto(), Some("https"));
654        assert!(matches!(forwarded, Forwarded { .. }));
655        Ok(())
656    }
657
658    #[test]
659    fn from_request() -> Result<()> {
660        let mut request = Request::new(Get, Url::parse("http://_/")?);
661        request.append_header("Forwarded", "for=for");
662
663        let forwarded = Forwarded::from_headers(&request)?.unwrap();
664        assert_eq!(forwarded.forwarded_for(), vec!["for"]);
665
666        Ok(())
667    }
668
669    #[test]
670    fn owned_can_outlive_request() -> Result<()> {
671        let forwarded = {
672            let mut request = Request::new(Get, Url::parse("http://_/")?);
673            request.append_header("Forwarded", "for=for;by=by;host=host;proto=proto");
674            Forwarded::from_headers(&request)?.unwrap().into_owned()
675        };
676        assert_eq!(forwarded.by(), Some("by"));
677        Ok(())
678    }
679
680    #[test]
681    fn round_trip() -> Result<()> {
682        let inputs = [
683            "for=client,for=b,for=c;by=proxy.com;host=example.com;proto=https",
684            "by=proxy.com;proto=https;host=example.com;for=a,for=b",
685        ];
686        for input in inputs {
687            let forwarded = Forwarded::parse(input).map_err(|_| crate::Error::new_adhoc(input))?;
688            let header = forwarded.to_header_values()?.next().unwrap();
689            let parsed = Forwarded::parse(header.as_str())?;
690            assert_eq!(forwarded, parsed);
691        }
692        Ok(())
693    }
694}