diff --git a/.gitignore b/.gitignore index ac0d3c74..0b08c532 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist/ npm-debug.log* Cargo.lock .DS_Store +.nvim/ diff --git a/src/parse_utils.rs b/src/parse_utils.rs index 99d0ad2e..56fb99dd 100644 --- a/src/parse_utils.rs +++ b/src/parse_utils.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; -/// https://tools.ietf.org/html/rfc7230#section-3.2.6 -pub(crate) fn parse_token(input: &str) -> (Option<&str>, &str) { +/// +pub(crate) fn parse_token(input: &str) -> Option<(Cow<'_, str>, &str)> { let mut end_of_token = 0; for (i, c) in input.char_indices() { if tchar(c) { @@ -12,14 +12,15 @@ pub(crate) fn parse_token(input: &str) -> (Option<&str>, &str) { } if end_of_token == 0 { - (None, input) + None } else { - (Some(&input[..end_of_token]), &input[end_of_token..]) + let (token, rest) = input.split_at(end_of_token); + Some((Cow::from(token), rest)) } } -/// https://tools.ietf.org/html/rfc7230#section-3.2.6 -fn tchar(c: char) -> bool { +/// +pub(crate) fn tchar(c: char) -> bool { matches!( c, 'a'..='z' | 'A'..='Z' @@ -42,16 +43,16 @@ fn tchar(c: char) -> bool { ) } -/// https://tools.ietf.org/html/rfc7230#section-3.2.6 +/// fn vchar(c: char) -> bool { matches!(c as u8, b'\t' | 32..=126 | 128..=255) } -/// https://tools.ietf.org/html/rfc7230#section-3.2.6 -pub(crate) fn parse_quoted_string(input: &str) -> (Option>, &str) { +/// +pub(crate) fn parse_quoted_string(input: &str) -> Option<(Cow<'_, str>, &str)> { // quoted-string must start with a DQUOTE if !input.starts_with('"') { - return (None, input); + return None; } let mut end_of_string = None; @@ -61,7 +62,7 @@ pub(crate) fn parse_quoted_string(input: &str) -> (Option>, &str) { if i > 1 && backslashes.last() == Some(&(i - 2)) { if !vchar(c) { // only VCHARs can be escaped - return (None, input); + return None; } // otherwise, we skip over this character while parsing } else { @@ -81,7 +82,7 @@ pub(crate) fn parse_quoted_string(input: &str) -> (Option>, &str) { b'\t' | b' ' | 15 | 35..=91 | 93..=126 | 128..=255 => {} // unexpected character, bail - _ => return (None, input), + _ => return None, } } } @@ -110,10 +111,10 @@ pub(crate) fn parse_quoted_string(input: &str) -> (Option>, &str) { .into() }; - (Some(value), &input[end_of_string..]) + Some((value, &input[end_of_string..])) } else { // we never reached a closing DQUOTE, so we do not have a valid quoted-string - (None, input) + None } } @@ -122,42 +123,51 @@ mod test { use super::*; #[test] fn token_successful_parses() { - assert_eq!(parse_token("key=value"), (Some("key"), "=value")); - assert_eq!(parse_token("KEY=value"), (Some("KEY"), "=value")); - assert_eq!(parse_token("0123)=value"), (Some("0123"), ")=value")); - assert_eq!(parse_token("a=b"), (Some("a"), "=b")); - assert_eq!(parse_token("!#$%&'*+-.^_`|~=value"), (Some("!#$%&'*+-.^_`|~"), "=value",)); + assert_eq!(parse_token("key=value"), Some(("key".into(), "=value"))); + assert_eq!(parse_token("KEY=value"), Some(("KEY".into(), "=value"))); + assert_eq!(parse_token("0123)=value"), Some(("0123".into(), ")=value"))); + assert_eq!(parse_token("a=b"), Some(("a".into(), "=b"))); + assert_eq!( + parse_token("!#$%&'*+-.^_`|~=value"), + Some(("!#$%&'*+-.^_`|~".into(), "=value")) + ); } #[test] fn token_unsuccessful_parses() { - assert_eq!(parse_token(""), (None, "")); - assert_eq!(parse_token("=value"), (None, "=value")); + assert_eq!(parse_token(""), None); + assert_eq!(parse_token("=value"), None); for c in r#"(),/:;<=>?@[\]{}"#.chars() { let s = c.to_string(); - assert_eq!(parse_token(&s), (None, &*s)); + assert_eq!(parse_token(&s), None); let s = format!("match{}rest", s); - assert_eq!(parse_token(&s), (Some("match"), &*format!("{}rest", c))); + assert_eq!( + parse_token(&s), + Some(("match".into(), &*format!("{}rest", c))) + ); } } #[test] fn qstring_successful_parses() { - assert_eq!(parse_quoted_string(r#""key"=value"#), (Some(Cow::Borrowed("key")), "=value")); + assert_eq!( + parse_quoted_string(r#""key"=value"#), + Some((Cow::Borrowed("key"), "=value")) + ); assert_eq!( parse_quoted_string(r#""escaped \" quote \""rest"#), - (Some(Cow::Owned(String::from(r#"escaped " quote ""#))), r#"rest"#) + Some((Cow::Owned(String::from(r#"escaped " quote ""#)), r#"rest"#)) ); } #[test] fn qstring_unsuccessful_parses() { - assert_eq!(parse_quoted_string(r#""abc"#), (None, "\"abc")); - assert_eq!(parse_quoted_string(r#"hello""#), (None, "hello\"",)); - assert_eq!(parse_quoted_string(r#"=value\"#), (None, "=value\\")); - assert_eq!(parse_quoted_string(r#"\""#), (None, r#"\""#)); - assert_eq!(parse_quoted_string(r#""\""#), (None, r#""\""#)); + assert_eq!(parse_quoted_string(r#""abc"#), None); + assert_eq!(parse_quoted_string(r#"hello""#), None); + assert_eq!(parse_quoted_string(r#"=value\"#), None); + assert_eq!(parse_quoted_string(r#"\""#), None); + assert_eq!(parse_quoted_string(r#""\""#), None); } } diff --git a/src/proxies/forwarded.rs b/src/proxies/forwarded.rs index d20b2055..f129dc84 100644 --- a/src/proxies/forwarded.rs +++ b/src/proxies/forwarded.rs @@ -1,624 +1,1015 @@ +use std::{borrow::Cow, collections::HashMap, net::IpAddr, ops::Deref}; + use crate::{ headers::{Header, HeaderName, HeaderValue, Headers, FORWARDED}, - parse_utils::{parse_quoted_string, parse_token}, + parse_utils::{parse_quoted_string, parse_token, tchar}, }; -use std::{borrow::Cow, convert::TryFrom, fmt::Write, net::IpAddr}; -// these constants are private because they are nonstandard -const X_FORWARDED_FOR: HeaderName = HeaderName::from_lowercase_str("x-forwarded-for"); -const X_FORWARDED_PROTO: HeaderName = HeaderName::from_lowercase_str("x-forwarded-proto"); +// These constants are private because they are non-standard. const X_FORWARDED_BY: HeaderName = HeaderName::from_lowercase_str("x-forwarded-by"); +const X_FORWARDED_FOR: HeaderName = HeaderName::from_lowercase_str("x-forwarded-for"); const X_FORWARDED_HOST: HeaderName = HeaderName::from_lowercase_str("x-forwarded-host"); +const X_FORWARDED_PROTO: HeaderName = HeaderName::from_lowercase_str("x-forwarded-proto"); + +/// Error type for parsing `Forwarded` headers. +#[derive(Debug, PartialEq, Eq)] +pub enum ForwardedError { + /// Returned when parsing a [`ForwardedElement`] failed. + ForwardedElementError(ForwardedElementError), + /// Returned when the input string contained trailing data. + TrailingData(String), + /// Returned when the input contained multiple `X-Forwarded-*` headers when falling back. + MultipleXForwardedHeaders(Vec), +} + +impl std::error::Error for ForwardedError {} + +impl std::fmt::Display for ForwardedError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ForwardedError::ForwardedElementError(err) => { + write!(f, "Failed to parse ForwardedElement: {err}") + } + ForwardedError::TrailingData(data) => write!(f, "Input had trailing data: {data:?}"), + ForwardedError::MultipleXForwardedHeaders(headers) => { + let headers = headers.join(", "); + write!(f, "Multiple X-Forwarded-* headers found: {}", headers) + } + } + } +} + +impl From for ForwardedError { + fn from(err: ForwardedElementError) -> Self { + ForwardedError::ForwardedElementError(err) + } +} -/// A rust representation of the [forwarded -/// header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded). +/// A Rust representation of the [RFC 7329](https://www.rfc-editor.org/rfc/rfc7239#section-4) +/// `Forwarded` production. #[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct Forwarded<'a> { - by: Option>, - forwarded_for: Vec>, - host: Option>, - proto: Option>, +pub struct Forwarded<'fe> { + /// Ordered vector of `forwarded-element`. + pub elements: Vec>, } -impl<'a> Forwarded<'a> { - /// Attempts to parse a Forwarded from headers (or a request or - /// response). Builds a borrowed Forwarded by default. To build an - /// owned Forwarded, use - /// `Forwarded::from_headers(...).into_owned()` - /// - /// # X-Forwarded-For, -By, and -Proto compatability +impl<'fe, 'input: 'fe> Forwarded<'fe> { + /// Builds a new `Forwarded`. + pub fn new() -> Self { + Forwarded::default() + } + + /// Parse a list of `Forwarded` HTTP headers into a borrowed `Forwarded` instance, with + /// `X-Forwarded-*` fallback. /// - /// This implementation includes fall-back support for the - /// historical unstandardized headers x-forwarded-for, - /// x-forwarded-by, and x-forwarded-proto. If you do not wish to - /// support these headers, use - /// [`Forwarded::from_forwarded_header`]. To _only_ support these - /// historical headers and _not_ the standardized Forwarded - /// header, use [`Forwarded::from_x_headers`]. + /// # Fallback behaviour /// - /// Please note that either way, this implementation will - /// normalize to the standardized Forwarded header, as recommended - /// in - /// [rfc7239§7.4](https://tools.ietf.org/html/rfc7239#section-7.4) + /// If no `Forwarded` HTTP header was set it falls back to trying to parse one of the supported + /// kinds of `X-Forwarded-*` headers. See + /// [`from_x_forwarded_headers`](Self::from_x_forwarded_headers) for more information. /// /// # Examples /// ```rust - /// # fn main() -> http_types_rs::Result<()> { - /// use http_types_rs::{Request}; - /// use http_types_rs::proxies::Forwarded; - /// - /// let mut request = Request::get("http://_/"); + /// # use http_types_rs::{proxies::Forwarded, Method::Get, Request, Url, Result}; + /// # fn main() -> Result<()> { + /// let mut request = Request::new(Get, Url::parse("http://_/")?); + /// request.insert_header("X-Forwarded-For", "198.51.100.46"); /// request.insert_header( /// "Forwarded", /// r#"for=192.0.2.43, for="[2001:db8:cafe::17]", for=unknown;proto=https"# /// ); /// let forwarded = Forwarded::from_headers(&request)?.unwrap(); - /// assert_eq!(forwarded.proto(), Some("https")); - /// assert_eq!(forwarded.forwarded_for(), vec!["192.0.2.43", "[2001:db8:cafe::17]", "unknown"]); + /// //assert_eq!(forwarded.to_string(), r#"for=192.0.2.43, for="[2001:db8:cafe::17]", for=unknown;proto=https"#); /// # Ok(()) } /// ``` /// /// ```rust - /// # fn main() -> http_types_rs::Result<()> { - /// use http_types_rs::{headers::Header, Request}; - /// use http_types_rs::proxies::Forwarded; + /// # use http_types_rs::{proxies::Forwarded, Method::Get, Request, Url, Result}; + /// # fn main() -> Result<()> { + /// let mut request = Request::new(Get, Url::parse("http://_/")?); + /// request.insert_header("X-Forwarded-For", "192.0.2.43, 2001:db8:cafe::17, unknown"); + /// let forwarded = Forwarded::from_headers(&request)?.unwrap(); + /// assert_eq!(forwarded.to_string(), r#"for=192.0.2.43, for="[2001:db8:cafe::17]", for=unknown"#); + /// # Ok(()) } + /// ``` /// - /// let mut request = Request::get("http://_/"); + /// ```rust + /// # use http_types_rs::{proxies::{Forwarded, ForwardedError}, Method::Get, Request, Url, Result}; + /// # fn main() -> Result<()> { + /// let mut request = Request::new(Get, Url::parse("http://_/")?); /// request.insert_header("X-Forwarded-For", "192.0.2.43, 2001:db8:cafe::17, unknown"); /// request.insert_header("X-Forwarded-Proto", "https"); - /// let forwarded = Forwarded::from_headers(&request)?.unwrap(); - /// assert_eq!(forwarded.forwarded_for(), vec!["192.0.2.43", "[2001:db8:cafe::17]", "unknown"]); - /// assert_eq!(forwarded.proto(), Some("https")); /// assert_eq!( - /// forwarded.header_value(), - /// r#"for=192.0.2.43, for="[2001:db8:cafe::17]", for=unknown;proto=https"# + /// Forwarded::from_headers(&request), + /// Err(ForwardedError::MultipleXForwardedHeaders(vec![ + /// "x-forwarded-for".to_string(), + /// "x-forwarded-proto".to_string() + /// ])), /// ); /// # Ok(()) } /// ``` - - pub fn from_headers(headers: &'a impl AsRef) -> Result, ParseError> { + pub fn from_headers( + headers: &'input impl AsRef, + ) -> Result, ForwardedError> { if let Some(forwarded) = Self::from_forwarded_header(headers)? { Ok(Some(forwarded)) } else { - Self::from_x_headers(headers) + Self::from_x_forwarded_headers(headers) } } - /// Parse a borrowed Forwarded from the Forwarded header, without x-forwarded-{for,by,proto} fallback - /// - /// # Examples - /// ```rust - /// # use http_types_rs::{proxies::Forwarded, Method::Get, Request, Url, Result}; - /// # fn main() -> Result<()> { - /// let mut request = Request::new(Get, Url::parse("http://_/")?); - /// request.insert_header( - /// "Forwarded", - /// r#"for=192.0.2.43, for="[2001:db8:cafe::17]", for=unknown;proto=https"# - /// ); - /// let forwarded = Forwarded::from_forwarded_header(&request)?.unwrap(); - /// assert_eq!(forwarded.proto(), Some("https")); - /// assert_eq!(forwarded.forwarded_for(), vec!["192.0.2.43", "[2001:db8:cafe::17]", "unknown"]); - /// # Ok(()) } - /// ``` - /// ```rust - /// # use http_types_rs::{proxies::Forwarded, Method::Get, Request, Url, Result}; - /// # fn main() -> Result<()> { - /// let mut request = Request::new(Get, Url::parse("http://_/")?); - /// request.insert_header("X-Forwarded-For", "192.0.2.43, 2001:db8:cafe::17"); - /// assert!(Forwarded::from_forwarded_header(&request)?.is_none()); - /// # Ok(()) } - /// ``` - pub fn from_forwarded_header(headers: &'a impl AsRef) -> Result, ParseError> { - if let Some(headers) = headers.as_ref().get(FORWARDED) { - Ok(Some(Self::parse(headers.as_ref().as_str())?)) + /// Parses list of `Forwarded` HTTP headers into a borrowed `Forwarded` instance. + pub fn from_forwarded_header( + headers: &'input impl AsRef, + ) -> Result, ForwardedError> { + let headers = if let Some(headers) = headers.as_ref().get(FORWARDED) { + headers } else { - Ok(None) + return Ok(None); + }; + + let mut forwarded = Forwarded::new(); + for value in headers { + let rest = forwarded.parse(value.as_str())?; + if !rest.is_empty() { + return Err(ForwardedError::TrailingData(rest.to_string())); + } } + + Ok(Some(forwarded)) } - /// Parse a borrowed Forwarded from the historical - /// non-standardized x-forwarded-{for,by,proto} headers, without - /// support for the Forwarded header. + /// Attempt to parse non-standard `X-Forwarded-*` headers into a borrowed `Forwarded` instance. /// - /// # Examples - /// ```rust - /// # use http_types_rs::{proxies::Forwarded, Method::Get, Request, Url, Result}; - /// # fn main() -> Result<()> { - /// let mut request = Request::new(Get, Url::parse("http://_/")?); - /// request.insert_header("X-Forwarded-For", "192.0.2.43, 2001:db8:cafe::17"); - /// let forwarded = Forwarded::from_headers(&request)?.unwrap(); - /// assert_eq!(forwarded.forwarded_for(), vec!["192.0.2.43", "[2001:db8:cafe::17]"]); - /// # Ok(()) } - /// ``` - /// ```rust - /// # use http_types_rs::{proxies::Forwarded, Method::Get, Request, Url, Result}; - /// # fn main() -> Result<()> { - /// let mut request = Request::new(Get, Url::parse("http://_/")?); - /// request.insert_header( - /// "Forwarded", - /// r#"for=192.0.2.43, for="[2001:db8:cafe::17]", for=unknown;proto=https"# - /// ); - /// assert!(Forwarded::from_x_headers(&request)?.is_none()); - /// # Ok(()) } - /// ``` - pub fn from_x_headers(headers: &'a impl AsRef) -> Result, ParseError> { + /// This will only attempt to do the conversion if only one kind of `X-Forwarded-*` header was + /// specified since there is no way for us to know which order the headers were added in and at + /// which steps. This is in accordance with Section 7.4 of RFC 7239. + /// + /// # Supported headers + /// + /// - `X-Forwarded-By` + /// - `X-Forwarded-For` + /// - `X-Forwarded-Host` + /// - `X-Forwarded-Proto` + pub fn from_x_forwarded_headers( + headers: &'input impl AsRef, + ) -> Result, ForwardedError> { let headers = headers.as_ref(); - let forwarded_for: Vec> = headers - .get(X_FORWARDED_FOR) - .map(|hv| { - hv.as_str() - .split(',') - .map(|v| { - let v = v.trim(); - match v.parse::().ok() { - Some(IpAddr::V6(v6)) => Cow::Owned(format!(r#"[{}]"#, v6)), - _ => Cow::Borrowed(v), - } - }) - .collect() + let mut found_headers = Vec::new(); + for header in [ + &X_FORWARDED_BY, + &X_FORWARDED_FOR, + &X_FORWARDED_HOST, + &X_FORWARDED_PROTO, + ] { + if let Some(found) = headers.names().find(|h| h == &header) { + found_headers.push(found.as_str().to_string()); + } + } + + match found_headers.len() { + 0 => return Ok(None), + 1 => {} + // If there were more than one kind of `X-Forwarded-*` header we shouldn't try to parse + // them since there is no way to know in which order they were added and by which + // proxies. C.f. Section 7.4 of RFC 7239. + _ => return Err(ForwardedError::MultipleXForwardedHeaders(found_headers)), + } + + let mut forwarded = Forwarded::new(); + + if let Some(values) = headers.get(X_FORWARDED_BY) { + values.as_str().split(',').for_each(|value| { + let value = value.trim(); + let value = match value.parse::().ok() { + Some(IpAddr::V6(v6)) => format!("[{}]", v6).into(), + _ => value.into(), + }; + forwarded.elements.push(ForwardedElement { + by: Some(value), + ..Default::default() + }); }) - .unwrap_or_default(); + } - let by = headers.get(X_FORWARDED_BY).map(|hv| Cow::Borrowed(hv.as_str())); + if let Some(values) = headers.get(X_FORWARDED_FOR) { + values.as_str().split(',').for_each(|value| { + let value = value.trim(); + let value = match value.parse::().ok() { + Some(IpAddr::V6(v6)) => format!("[{}]", v6).into(), + _ => value.into(), + }; + forwarded.elements.push(ForwardedElement { + r#for: Some(value), + ..Default::default() + }); + }) + } - let proto = headers.get(X_FORWARDED_PROTO).map(|p| Cow::Borrowed(p.as_str())); + if let Some(values) = headers.get(X_FORWARDED_HOST) { + values.as_str().split(',').for_each(|value| { + let value = value.trim(); + forwarded.elements.push(ForwardedElement { + host: Some(value.into()), + ..Default::default() + }); + }) + } - let host = headers.get(X_FORWARDED_HOST).map(|h| Cow::Borrowed(h.as_str())); + if let Some(values) = headers.get(X_FORWARDED_PROTO) { + values.as_str().split(',').for_each(|value| { + let value = value.trim(); + forwarded.elements.push(ForwardedElement { + proto: Some(value.into()), + ..Default::default() + }); + }) + } - if !forwarded_for.is_empty() || by.is_some() || proto.is_some() || host.is_some() { - Ok(Some(Self { - forwarded_for, - by, - proto, - host, - })) - } else { - Ok(None) + Ok(Some(forwarded)) + } + + /// Parses a `Forwarded` HTTP header value into a borrowed `Forwarded` instance. + pub fn parse(&mut self, input: &'input str) -> Result<&'input str, ForwardedError> { + let (mut element, mut rest) = ForwardedElement::parse(input)?; + self.elements.push(element); + + while rest.starts_with(',') { + (element, rest) = ForwardedElement::parse(&rest[1..])?; + self.elements.push(element); } + + Ok(rest) } - /// parse a &str into a borrowed Forwarded - /// - /// # Examples - /// ```rust - /// # fn main() -> http_types_rs::Result<()> { - /// use http_types_rs::headers::Header; - /// use http_types_rs::proxies::Forwarded; - /// - /// let forwarded = Forwarded::parse( - /// r#"for=192.0.2.43, for="[2001:db8:cafe::17]", FOR=unknown;proto=https"# - /// )?; - /// assert_eq!(forwarded.forwarded_for(), vec!["192.0.2.43", "[2001:db8:cafe::17]", "unknown"]); - /// assert_eq!( - /// forwarded.header_value(), - /// r#"for=192.0.2.43, for="[2001:db8:cafe::17]", for=unknown;proto=https"# - /// ); - /// # Ok(()) } - /// ``` - pub fn parse(input: &'a str) -> Result { - let mut input = input; - let mut forwarded = Forwarded::new(); + /// Transform a borrowed `Forwarded` into an owned `Forwarded`. + pub fn into_owned(self) -> Forwarded<'static> { + Forwarded { + elements: self + .elements + .into_iter() + .map(|element| element.into_owned()) + .collect(), + } + } +} + +impl<'fe> std::fmt::Display for Forwarded<'fe> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let elements: Vec<_> = self + .elements + .iter() + .map(ForwardedElement::to_string) + .collect(); + + write!(f, "{}", elements.join(", ")) + } +} + +impl<'input> Header for Forwarded<'input> { + fn header_name(&self) -> HeaderName { + FORWARDED + } + + fn header_value(&self) -> HeaderValue { + let output = self.to_string(); + + // SAFETY: the internal strings are validated to be ASCII. + unsafe { HeaderValue::from_bytes_unchecked(output.into()) } + } +} - while !input.is_empty() { - input = if starts_with_ignore_case("for=", input) { - forwarded.parse_for(input)? - } else { - forwarded.parse_forwarded_pair(input)? +/// Error type for parsing `ForwardedElement`s. +#[derive(Debug, PartialEq, Eq)] +pub enum ForwardedElementError { + /// Returned when parsing a parameter name failed. + ParameterParseError, + /// Returned when parsing a parameter value failed. + ValueParseError, + /// Returned when the parser expected a specific character and got another one. + UnexpectedCharacter(char, char), + /// Returned when a `forwarded-element` contained the same parameter more than once. + DuplicateParameter(String), + /// Returned when trying to set a parameter key that isn't a valid token. + NonTokenParameter, + /// Returned when trying to set or parse a non-ASCII parameter value. + NonAsciiValue, +} + +impl std::error::Error for ForwardedElementError {} + +impl std::fmt::Display for ForwardedElementError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ForwardedElementError::ParameterParseError => write!(f, "Failed to parse parameter"), + ForwardedElementError::ValueParseError => write!(f, "Failed to parse value"), + ForwardedElementError::UnexpectedCharacter(expected, found) => { + write!(f, "Expected character {expected:?}, found {found:?}") + } + ForwardedElementError::DuplicateParameter(parameter) => { + write!(f, "Same parameter was found multiple times: {parameter:?}") + } + ForwardedElementError::NonTokenParameter => { + write!(f, "Tried to set a parameter that wasn't a valid token") + } + ForwardedElementError::NonAsciiValue => { + write!(f, "Failed set parameter to non-ASCII value") + } + } + } +} + +/// A Rust representation of the [RFC 7329](https://www.rfc-editor.org/rfc/rfc7239#section-4) +/// `forwarded-element` production. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct ForwardedElement<'fe> { + /// Identifies the user-agent facing interface of the proxy. + by: Option>, + /// Identifies the node making the request to the proxy. + r#for: Option>, + /// The host request header field as received by the proxy. + host: Option>, + /// Indicates what protocol was used to make the request. + proto: Option>, + /// Map of `Forwarded` header extension parameters. + extensions: HashMap, Cow<'fe, str>>, +} + +impl<'fe, 'input: 'fe> ForwardedElement<'fe> { + /// Parses a string conforming to the [RFC + /// 7329](https://www.rfc-editor.org/rfc/rfc7239#section-4) `forwarded-element` ABNF production + /// into a `ForwardedElement` + pub fn parse(input: &'input str) -> Result<(Self, &'input str), ForwardedElementError> { + let mut element = ForwardedElement::default(); + + // Skip any potential whitespace ahead of the whole forwarded-element. + let mut rest = skip_whitespace(input); + + rest = element.parse_forwarded_pair(rest)?; + loop { + match rest.chars().next() { + Some(';') => rest = element.parse_forwarded_pair(&rest[1..])?, + Some(',') => break, + Some(c) => return Err(ForwardedElementError::UnexpectedCharacter(';', c)), + None => break, } } - Ok(forwarded) + Ok((element, rest)) } - fn parse_forwarded_pair(&mut self, input: &'a str) -> Result<&'a str, ParseError> { - let (key, value, rest) = match parse_token(input) { - (Some(key), rest) if rest.starts_with('=') => match parse_value(&rest[1..]) { - (Some(value), rest) => Some((key, value, rest)), - (None, _) => None, - }, - _ => None, + fn parse_forwarded_pair( + &mut self, + input: &'input str, + ) -> Result<&'input str, ForwardedElementError> { + let (parameter, rest) = + parse_token(input).ok_or(ForwardedElementError::ParameterParseError)?; + + let rest = match rest.chars().next() { + Some('=') => &rest[1..], + Some(c) => return Err(ForwardedElementError::UnexpectedCharacter('=', c)), + None => return Err(ForwardedElementError::ParameterParseError), + }; + + let (value, rest) = parse_value(rest).ok_or(ForwardedElementError::ValueParseError)?; + // SAFETY: all internal strings have to be valid ASCII. + if !value.is_ascii() { + return Err(ForwardedElementError::NonAsciiValue); } - .ok_or_else(|| ParseError::new("parse error in forwarded-pair"))?; - match key { + match rest.chars().next() { + Some(',' | ';') => {} + None => {} + _ => return Err(ForwardedElementError::ValueParseError), + } + + match parameter.to_ascii_lowercase().as_str() { "by" => { if self.by.is_some() { - return Err(ParseError::new("parse error, duplicate `by` key")); + return Err(ForwardedElementError::DuplicateParameter("by".into())); } self.by = Some(value); } - + "for" => { + if self.r#for.is_some() { + return Err(ForwardedElementError::DuplicateParameter("for".into())); + } + self.r#for = Some(value); + } "host" => { if self.host.is_some() { - return Err(ParseError::new("parse error, duplicate `host` key")); + return Err(ForwardedElementError::DuplicateParameter("host".into())); } self.host = Some(value); } - "proto" => { if self.proto.is_some() { - return Err(ParseError::new("parse error, duplicate `proto` key")); + return Err(ForwardedElementError::DuplicateParameter("proto".into())); } self.proto = Some(value); } - - _ => { /* extensions are allowed in the spec */ } + _ => { + if self.extensions.contains_key(¶meter) { + return Err(ForwardedElementError::DuplicateParameter(parameter.into())); + } + self.extensions.insert(parameter, value); + } } - match rest.strip_prefix(';') { - Some(rest) => Ok(rest), - None => Ok(rest), - } + Ok(rest) } - fn parse_for(&mut self, input: &'a str) -> Result<&'a str, ParseError> { - let mut rest = input; - - loop { - rest = match match_ignore_case("for=", rest) { - (true, rest) => rest, - (false, _) => return Err(ParseError::new("http list must start with for=")), - }; - - let (value, rest_) = parse_value(rest); - rest = rest_; - - if let Some(value) = value { - // add a successful for= value - self.forwarded_for.push(value); - } else { - return Err(ParseError::new("for= without valid value")); - } - - match rest.chars().next() { - // we have another for= - Some(',') => { - rest = rest[1..].trim_start(); - } - - // we have reached the end of the for= section - Some(';') => return Ok(&rest[1..]), - - // reached the end of the input - None => return Ok(rest), - - // bail - _ => return Err(ParseError::new("unexpected character after for= section")), - } + /// Transforms a borrowed `ForwardedElement` into an owned `ForwardedElement. + pub fn into_owned(self) -> ForwardedElement<'static> { + ForwardedElement { + by: self.by.map(|by| Cow::Owned(by.into_owned())), + r#for: self.r#for.map(|r#for| Cow::Owned(r#for.into_owned())), + host: self.host.map(|host| Cow::Owned(host.into_owned())), + proto: self.proto.map(|proto| Cow::Owned(proto.into_owned())), + extensions: self + .extensions + .into_iter() + .map(|(property, value)| { + ( + Cow::Owned(property.into_owned()), + Cow::Owned(value.into_owned()), + ) + }) + .collect(), } } - /// Transform a borrowed Forwarded into an owned - /// Forwarded. This is a noop if the Forwarded is already owned. - pub fn into_owned(self) -> Forwarded<'static> { - Forwarded { - by: self.by.map(|by| Cow::Owned(by.into_owned())), - forwarded_for: self.forwarded_for.into_iter().map(|ff| Cow::Owned(ff.into_owned())).collect(), - host: self.host.map(|h| Cow::Owned(h.into_owned())), - proto: self.proto.map(|p| Cow::Owned(p.into_owned())), + /// Sets the `by` parameter value. + pub fn set_by(&mut self, by: impl Into>) -> Result<(), ForwardedElementError> { + let value = by.into(); + + // SAFETY: all internal strings have to be valid ASCII. + if !value.is_ascii() { + return Err(ForwardedElementError::NonAsciiValue); } + self.by = Some(value); + Ok(()) } - /// Builds a new empty Forwarded - pub fn new() -> Self { - Self::default() + /// Returns the `by` parameter value. + pub fn by(&self) -> Option<&str> { + self.by.as_deref() } - /// Adds a `for` section to this header - pub fn add_for(&mut self, forwarded_for: impl Into>) { - self.forwarded_for.push(forwarded_for.into()); + /// Sets the `for` parameter value. + pub fn set_for( + &mut self, + forwarded_for: impl Into>, + ) -> Result<(), ForwardedElementError> { + let value = forwarded_for.into(); + + // SAFETY: all internal strings have to be valid ASCII. + if !value.is_ascii() { + return Err(ForwardedElementError::NonAsciiValue); + } + self.r#for = Some(value); + Ok(()) } - /// Returns the `for` field of this header - pub fn forwarded_for(&self) -> Vec<&str> { - self.forwarded_for.iter().map(|x| x.as_ref()).collect() + /// Returns the `for` parameter value. + pub fn r#for(&self) -> Option<&str> { + self.r#for.as_deref() } - /// Sets the `host` field of this header - pub fn set_host(&mut self, host: impl Into>) { - self.host = Some(host.into()); + /// Sets the `host` parameter value + pub fn set_host( + &mut self, + host: impl Into>, + ) -> Result<(), ForwardedElementError> { + let value = host.into(); + + // SAFETY: all internal strings have to be valid ASCII. + if !value.is_ascii() { + return Err(ForwardedElementError::NonAsciiValue); + } + self.host = Some(value); + Ok(()) } - /// Returns the `host` field of this header + /// Returns the `host` parameter value. pub fn host(&self) -> Option<&str> { self.host.as_deref() } - /// Sets the `proto` field of this header - pub fn set_proto(&mut self, proto: impl Into>) { - self.proto = Some(proto.into()) + /// Sets the `proto` parameter value. + pub fn set_proto( + &mut self, + proto: impl Into>, + ) -> Result<(), ForwardedElementError> { + let value = proto.into(); + + // SAFETY: all internal strings have to be valid ASCII. + if !value.is_ascii() { + return Err(ForwardedElementError::NonAsciiValue); + } + self.proto = Some(value); + Ok(()) } - /// Returns the `proto` field of this header + /// Returns the `proto` parameter value. pub fn proto(&self) -> Option<&str> { self.proto.as_deref() } - /// Sets the `by` field of this header - pub fn set_by(&mut self, by: impl Into>) { - self.by = Some(by.into()); - } - - /// Returns the `by` field of this header - pub fn by(&self) -> Option<&str> { - self.by.as_deref() - } -} - -impl<'a> Header for Forwarded<'a> { - fn header_name(&self) -> HeaderName { - FORWARDED - } - fn header_value(&self) -> HeaderValue { - let mut output = String::new(); - if let Some(by) = self.by() { - write!(&mut output, "by={};", by).unwrap(); + /// Sets an extension parameter value. + pub fn set_extension( + &mut self, + parameter: impl Into>, + value: impl Into>, + ) -> Result>, ForwardedElementError> { + let parameter = parameter.into(); + if !parameter.chars().all(tchar) { + return Err(ForwardedElementError::NonTokenParameter); } - output.push_str( - &self - .forwarded_for - .iter() - .map(|f| format!("for={}", format_value(f))) - .collect::>() - .join(", "), - ); + // SAFETY: all internal strings have to be valid ASCII. + let value = value.into(); + if !value.is_ascii() { + return Err(ForwardedElementError::NonAsciiValue); + } - output.push(';'); + Ok(self.extensions.insert(parameter, value)) + } - if let Some(host) = self.host() { - write!(&mut output, "host={};", host).unwrap(); - } + /// Returns an extension parameter value, if set. + pub fn extension( + &'fe self, + parameter: impl Into>, + ) -> Result, ForwardedElementError> { + let parameter = parameter.into(); - if let Some(proto) = self.proto() { - write!(&mut output, "proto={};", proto).unwrap(); + // SAFETY: all internal strings have to be valid ASCII. + if !parameter.chars().all(tchar) { + return Err(ForwardedElementError::NonTokenParameter); } - // remove a trailing semicolon - output.pop(); - - // SAFETY: the internal string is validated to be ASCII. - unsafe { HeaderValue::from_bytes_unchecked(output.into()) } + Ok(self.extensions.get(¶meter).map(|value| value.deref())) } -} -fn parse_value(input: &str) -> (Option>, &str) { - match parse_token(input) { - (Some(token), rest) => (Some(Cow::Borrowed(token)), rest), - (None, rest) => parse_quoted_string(rest), + /// Returns the `HashMap` of extension parameters. + pub fn extensions(&self) -> &HashMap, Cow<'fe, str>> { + &self.extensions } } -fn format_value(input: &str) -> Cow<'_, str> { - match parse_token(input) { - (_, "") => input.into(), - _ => { - let mut string = String::from("\""); - for ch in input.chars() { - if let '\\' | '"' = ch { - string.push('\\'); +impl<'fe> std::fmt::Display for ForwardedElement<'fe> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut write_semicolon = { + let mut first = true; + move |f: &mut std::fmt::Formatter<'_>| -> std::fmt::Result { + if first { + first = false; + } else { + write!(f, ";")?; } - string.push(ch); + Ok(()) } - string.push('"'); - string.into() + }; + + if let Some(by) = &self.by { + write_semicolon(f)?; + write!(f, "by={}", format_value(by.as_ref()))?; } - } -} -fn match_ignore_case<'a>(start: &'static str, input: &'a str) -> (bool, &'a str) { - let len = start.len(); - if input[..len].eq_ignore_ascii_case(start) { - (true, &input[len..]) - } else { - (false, input) - } -} + if let Some(r#for) = &self.r#for { + write_semicolon(f)?; + write!(f, "for={}", format_value(r#for.as_ref()))?; + } -fn starts_with_ignore_case(start: &'static str, input: &str) -> bool { - if start.len() <= input.len() { - let len = start.len(); - input[..len].eq_ignore_ascii_case(start) - } else { - false - } -} + if let Some(host) = &self.host { + write_semicolon(f)?; + write!(f, "host={}", format_value(host.as_ref()))?; + } -impl std::fmt::Display for Forwarded<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.header_value().as_str()) + if let Some(proto) = &self.proto { + write_semicolon(f)?; + write!(f, "proto={}", format_value(proto.as_ref()))?; + } + + for (parameter, value) in self.extensions.iter() { + write_semicolon(f)?; + write!(f, "{}={}", parameter, format_value(value.as_ref()))?; + } + + Ok(()) } } -#[derive(Debug, Clone)] -pub struct ParseError(&'static str); -impl ParseError { - pub fn new(msg: &'static str) -> Self { - Self(msg) - } +fn parse_value(input: &str) -> Option<(Cow<'_, str>, &str)> { + let (value, rest) = parse_token(input).or_else(|| parse_quoted_string(input))?; + Some((value, skip_whitespace(rest))) } -impl std::error::Error for ParseError {} -impl std::fmt::Display for ParseError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "unable to parse forwarded header: {}", self.0) +fn format_value(input: &str) -> Cow<'_, str> { + if input.chars().all(tchar) { + // If the value fully consists of token characters, write it out as-is. + return input.into(); + } + + // Otherwise write out a quoted string. + let mut out = String::from("\""); + for c in input.chars() { + match c { + '"' | '\\' => { + out.push('\\'); + out.push(c); + } + _ => out.push(c), + } } + out.push('"'); + out.into() } -impl<'a> TryFrom<&'a str> for Forwarded<'a> { - type Error = ParseError; - fn try_from(value: &'a str) -> Result { - Self::parse(value) +fn skip_whitespace(input: &str) -> &str { + let mut rest = input; + while rest.starts_with(' ') { + rest = &rest[1..]; } + rest } #[cfg(test)] mod tests { use super::*; - use crate::{Method::Get, Request, Response, Result}; - use url::Url; - - #[test] - fn starts_with_ignore_case_can_handle_short_inputs() { - assert!(!starts_with_ignore_case("helloooooo", "h")); - } - - #[test] - fn parsing_for() -> Result<()> { - assert_eq!(Forwarded::parse(r#"for="_gazonk""#)?.forwarded_for(), vec!["_gazonk"]); - assert_eq!( - Forwarded::parse(r#"For="[2001:db8:cafe::17]:4711""#)?.forwarded_for(), - vec!["[2001:db8:cafe::17]:4711"] - ); - - assert_eq!( - Forwarded::parse("for=192.0.2.60;proto=http;by=203.0.113.43")?.forwarded_for(), - vec!["192.0.2.60"] - ); - - assert_eq!( - Forwarded::parse("for=192.0.2.43, for=198.51.100.17")?.forwarded_for(), - vec!["192.0.2.43", "198.51.100.17"] - ); - - assert_eq!( - Forwarded::parse(r#"for=192.0.2.43,for="[2001:db8:cafe::17]",for=unknown"#)?.forwarded_for(), - Forwarded::parse(r#"for=192.0.2.43, for="[2001:db8:cafe::17]", for=unknown"#)?.forwarded_for() - ); - - assert_eq!( - Forwarded::parse(r#"for=192.0.2.43,for="this is a valid quoted-string, \" \\",for=unknown"#)?.forwarded_for(), - vec!["192.0.2.43", r#"this is a valid quoted-string, " \"#, "unknown"] - ); - Ok(()) - } - - #[test] - fn basic_parse() -> Result<()> { - let forwarded = Forwarded::parse("for=client.com;by=proxy.com;host=host.com;proto=https")?; - - assert_eq!(forwarded.by(), Some("proxy.com")); - assert_eq!(forwarded.forwarded_for(), vec!["client.com"]); - assert_eq!(forwarded.host(), Some("host.com")); - assert_eq!(forwarded.proto(), Some("https")); - assert!(matches!(forwarded, Forwarded { .. })); - Ok(()) - } - - #[test] - fn bad_parse() { - let err = Forwarded::parse("by=proxy.com;for=client;host=example.com;host").unwrap_err(); - assert_eq!(err.to_string(), "unable to parse forwarded header: parse error in forwarded-pair"); - - let err = Forwarded::parse("by;for;host;proto").unwrap_err(); - assert_eq!(err.to_string(), "unable to parse forwarded header: parse error in forwarded-pair"); + mod forwarded { + use url::Url; + + use crate::{Method, Request}; + + use super::*; + + #[test] + fn two_values_same_string() { + let mut actual = Forwarded::default(); + + let rest = actual + .parse("by=192.0.2.2;for=192.0.2.1, for=192.0.2.3;by=192.0.2.4") + .expect("Forwarded header value didn't parse"); + assert_eq!(rest, ""); + + let expected = Forwarded { + elements: vec![ + ForwardedElement { + by: Some("192.0.2.2".into()), + r#for: Some("192.0.2.1".into()), + ..Default::default() + }, + ForwardedElement { + by: Some("192.0.2.4".into()), + r#for: Some("192.0.2.3".into()), + ..Default::default() + }, + ], + }; + assert_eq!(actual, expected); + } - let err = Forwarded::parse("for=for, key=value").unwrap_err(); - assert_eq!(err.to_string(), "unable to parse forwarded header: http list must start with for="); + #[test] + fn two_separate_strings() { + let mut actual = Forwarded::default(); + + let rest = actual + .parse("by=192.0.2.6;for=192.0.2.5;host=example.org;proto=https;something=another") + .expect("Forwarded header value didn't parse"); + assert_eq!(rest, ""); + + let rest = actual + .parse("by=192.0.2.8;for=192.0.2.7;host=example.com;proto=http;bar=baz") + .expect("Forwarded header value didn't parse"); + assert_eq!(rest, ""); + + let mut extensions1 = HashMap::new(); + extensions1.insert("something".into(), "another".into()); + let mut extensions2 = HashMap::new(); + extensions2.insert("bar".into(), "baz".into()); + let expected = Forwarded { + elements: vec![ + ForwardedElement { + by: Some("192.0.2.6".into()), + r#for: Some("192.0.2.5".into()), + host: Some("example.org".into()), + proto: Some("https".into()), + extensions: extensions1, + }, + ForwardedElement { + by: Some("192.0.2.8".into()), + r#for: Some("192.0.2.7".into()), + host: Some("example.com".into()), + proto: Some("http".into()), + extensions: extensions2, + }, + ], + }; + assert_eq!(actual, expected); + } - let err = Forwarded::parse(r#"for="unterminated string"#).unwrap_err(); - assert_eq!(err.to_string(), "unable to parse forwarded header: for= without valid value"); + #[test] + fn x_forwarded_for() { + let mut request = Request::new(Method::Get, Url::parse("http://_/").unwrap()); + request + .append_header(X_FORWARDED_FOR, "192.0.2.43, 2001:db8:cafe::17") + .unwrap(); + + let forwarded = Forwarded::from_x_forwarded_headers(&request) + .expect("Failed to parse headers") + .expect("Found no headers"); + + assert_eq!( + forwarded, + Forwarded { + elements: vec![ + ForwardedElement { + r#for: Some("192.0.2.43".into()), + ..Default::default() + }, + ForwardedElement { + r#for: Some("[2001:db8:cafe::17]".into()), + ..Default::default() + }, + ], + }, + ); + } - let err = Forwarded::parse(r#"for=, for=;"#).unwrap_err(); - assert_eq!(err.to_string(), "unable to parse forwarded header: for= without valid value"); - } + #[test] + fn multiple_x_forwarded_headers() { + let mut request = Request::new(Method::Get, Url::parse("http://_/").unwrap()); + request + .append_header(X_FORWARDED_FOR, "192.0.2.43, 2001:db8:cafe::17") + .unwrap(); + request.append_header(X_FORWARDED_PROTO, "gopher").unwrap(); + let res = + Forwarded::from_x_forwarded_headers(&request).expect_err("Parsing didn't fail"); + assert_eq!( + res, + ForwardedError::MultipleXForwardedHeaders(vec![ + X_FORWARDED_FOR.to_string(), + X_FORWARDED_PROTO.to_string(), + ]) + ); + } - #[test] - fn bad_parse_from_headers() -> Result<()> { - let mut response = Response::new(200); - response.append_header("forwarded", "uh oh").unwrap(); - assert_eq!( - Forwarded::from_headers(&response).unwrap_err().to_string(), - "unable to parse forwarded header: parse error in forwarded-pair" - ); + #[test] + fn to_string() { + let mut forwarded = Forwarded::default(); + forwarded + .parse("by=192.0.2.2;for=192.0.2.1, for=192.0.2.3;by=192.0.2.4") + .expect("Forwarded header value didn't parse"); + + assert_eq!( + forwarded.to_string(), + "by=192.0.2.2;for=192.0.2.1, by=192.0.2.4;for=192.0.2.3", + ); + } - let response = Response::new(200); - assert!(Forwarded::from_headers(&response)?.is_none()); - Ok(()) - } + #[test] + fn owned_can_outlive_request() { + let forwarded = { + let mut request = Request::new(Method::Get, Url::parse("http://_/").unwrap()); + request + .append_header("Forwarded", "for=for;by=by;host=host;proto=proto") + .unwrap(); + Forwarded::from_headers(&request) + .unwrap() + .unwrap() + .into_owned() + }; + assert_eq!(forwarded.elements[0].by, Some("by".into())); + } - #[test] - fn from_x_headers() -> Result<()> { - let mut request = Request::new(Get, Url::parse("http://_/")?); - request.append_header(X_FORWARDED_FOR, "192.0.2.43, 2001:db8:cafe::17").unwrap(); - request.append_header(X_FORWARDED_PROTO, "gopher").unwrap(); - request.append_header(X_FORWARDED_HOST, "example.com").unwrap(); - let forwarded = Forwarded::from_headers(&request)?.unwrap(); - assert_eq!( - forwarded.to_string(), - r#"for=192.0.2.43, for="[2001:db8:cafe::17]";host=example.com;proto=gopher"# - ); - Ok(()) + #[test] + fn all_rfc_examples() { + let examples = vec![ + ( + r#"for="_gazonk""#, + vec![ForwardedElement { + r#for: Some("_gazonk".into()), + ..Default::default() + }], + ), + ( + r#"For="[2001:db8:cafe::17]:4711""#, + vec![ForwardedElement { + r#for: Some("[2001:db8:cafe::17]:4711".into()), + ..Default::default() + }], + ), + ( + r#" for=192.0.2.60;proto=http;by=203.0.113.43"#, + vec![ForwardedElement { + by: Some("203.0.113.43".into()), + r#for: Some("192.0.2.60".into()), + proto: Some("http".into()), + ..Default::default() + }], + ), + ( + r#"for=192.0.2.43, for=198.51.100.17"#, + vec![ + ForwardedElement { + r#for: Some("192.0.2.43".into()), + ..Default::default() + }, + ForwardedElement { + r#for: Some("198.51.100.17".into()), + ..Default::default() + }, + ], + ), + ( + r#"for=_hidden, for=_SEVKISEK"#, + vec![ + ForwardedElement { + r#for: Some("_hidden".into()), + ..Default::default() + }, + ForwardedElement { + r#for: Some("_SEVKISEK".into()), + ..Default::default() + }, + ], + ), + ( + r#"for=192.0.2.43,for="[2001:db8:cafe::17]",for=unknown"#, + vec![ + ForwardedElement { + r#for: Some("192.0.2.43".into()), + ..Default::default() + }, + ForwardedElement { + r#for: Some("[2001:db8:cafe::17]".into()), + ..Default::default() + }, + ForwardedElement { + r#for: Some("unknown".into()), + ..Default::default() + }, + ], + ), + ( + r#"for=192.0.2.43, for="[2001:db8:cafe::17]", for=unknown"#, + vec![ + ForwardedElement { + r#for: Some("192.0.2.43".into()), + ..Default::default() + }, + ForwardedElement { + r#for: Some("[2001:db8:cafe::17]".into()), + ..Default::default() + }, + ForwardedElement { + r#for: Some("unknown".into()), + ..Default::default() + }, + ], + ), + ( + r#"for=192.0.2.43"#, + vec![ForwardedElement { + r#for: Some("192.0.2.43".into()), + ..Default::default() + }], + ), + ( + r#"for="[2001:db8:cafe::17]", for=unknown"#, + vec![ + ForwardedElement { + r#for: Some("[2001:db8:cafe::17]".into()), + ..Default::default() + }, + ForwardedElement { + r#for: Some("unknown".into()), + ..Default::default() + }, + ], + ), + ( + r#"for=192.0.2.43, for="[2001:db8:cafe::17]""#, + vec![ + ForwardedElement { + r#for: Some("192.0.2.43".into()), + ..Default::default() + }, + ForwardedElement { + r#for: Some("[2001:db8:cafe::17]".into()), + ..Default::default() + }, + ], + ), + ( + r#"for=192.0.2.43, for=198.51.100.17;by=203.0.113.60;proto=http;host=example.com"#, + vec![ + ForwardedElement { + r#for: Some("192.0.2.43".into()), + ..Default::default() + }, + ForwardedElement { + by: Some("203.0.113.60".into()), + r#for: Some("198.51.100.17".into()), + host: Some("example.com".into()), + proto: Some("http".into()), + ..Default::default() + }, + ], + ), + ]; + for (value, elements) in examples.into_iter() { + let mut headers = Headers::new(); + headers.append("Forwarded", value).unwrap(); + + let parsed = Forwarded::from_forwarded_header(&headers) + .expect(&format!("Failed while parsing {:?}", value)) + .expect(&format!("No headers found while parsing {:?}", value)); + assert_eq!(parsed.elements, elements); + } + } } - #[test] - fn formatting_edge_cases() { - let mut forwarded = Forwarded::new(); - forwarded.add_for(r#"quote: " backslash: \"#); - forwarded.add_for(";proto=https"); - assert_eq!(forwarded.to_string(), r#"for="quote: \" backslash: \\", for=";proto=https""#); - } + mod forwarded_element { + use super::*; - #[test] - fn parse_edge_cases() -> Result<()> { - let forwarded = Forwarded::parse(r#"for=";", for=",", for="\"", for=unquoted;by=";proto=https""#)?; - assert_eq!(forwarded.forwarded_for(), vec![";", ",", "\"", "unquoted"]); - assert_eq!(forwarded.by(), Some(";proto=https")); - assert!(forwarded.proto().is_none()); + #[test] + fn empty_input() { + let res = ForwardedElement::parse(""); + assert_eq!(res, Err(ForwardedElementError::ParameterParseError)); + } - let forwarded = Forwarded::parse("proto=https")?; - assert_eq!(forwarded.proto(), Some("https")); - Ok(()) - } + #[test] + fn duplicate_parameters() { + let res = ForwardedElement::parse("for=bar;for=baz"); + assert_eq!( + res, + Err(ForwardedElementError::DuplicateParameter("for".into())) + ); + } - #[test] - fn owned_parse() -> Result<()> { - let forwarded = Forwarded::parse("for=client;by=proxy.com;host=example.com;proto=https")?.into_owned(); + #[test] + fn invalid_value() { + let res = ForwardedElement::parse("for=bär"); + assert_eq!(res, Err(ForwardedElementError::ValueParseError)); + } - assert_eq!(forwarded.by(), Some("proxy.com")); - assert_eq!(forwarded.forwarded_for(), vec!["client"]); - assert_eq!(forwarded.host(), Some("example.com")); - assert_eq!(forwarded.proto(), Some("https")); - assert!(matches!(forwarded, Forwarded { .. })); - Ok(()) - } + #[test] + fn space_within_element_is_invalid() { + let res = ForwardedElement::parse("for=bar; by=baz"); + assert_eq!(res, Err(ForwardedElementError::ParameterParseError)); + } - #[test] - fn from_request() -> Result<()> { - let mut request = Request::new(Get, Url::parse("http://_/")?); - request.append_header("Forwarded", "for=for").unwrap(); + #[test] + fn all_parameters() { + let (res, rest) = ForwardedElement::parse( + "by=192.0.2.1;for=192.0.2.2;host=example.org;proto=https;something=another", + ) + .expect("String didn't parse as ForwardedElement"); + + let mut extensions = HashMap::new(); + extensions.insert("something".into(), "another".into()); + assert_eq!( + res, + ForwardedElement { + by: Some("192.0.2.1".into()), + r#for: Some("192.0.2.2".into()), + host: Some("example.org".into()), + proto: Some("https".into()), + extensions, + } + ); - let forwarded = Forwarded::from_headers(&request)?.unwrap(); - assert_eq!(forwarded.forwarded_for(), vec!["for"]); + assert_eq!(rest, ""); + } - Ok(()) - } + #[test] + fn to_string() { + let mut extensions = HashMap::new(); + extensions.insert("something".into(), r#"some "thing""#.into()); + let element = ForwardedElement { + by: Some("[2001:db8:cafe::17]:4711".into()), + r#for: Some("[2001:db8:cafe::16]:2342".into()), + host: Some("example.org".into()), + proto: Some("https".into()), + extensions, + }; - #[test] - fn owned_can_outlive_request() -> Result<()> { - let forwarded = { - let mut request = Request::new(Get, Url::parse("http://_/")?); - request.append_header("Forwarded", "for=for;by=by;host=host;proto=proto").unwrap(); - Forwarded::from_headers(&request)?.unwrap().into_owned() - }; - assert_eq!(forwarded.by(), Some("by")); - Ok(()) - } + println!("{}", &element.to_string()); + assert_eq!( + element.to_string(), + r#"by="[2001:db8:cafe::17]:4711";for="[2001:db8:cafe::16]:2342";host=example.org;proto=https;something="some \"thing\"""# + ); + } - #[test] - fn round_trip() -> Result<()> { - let inputs = [ - "for=client,for=b,for=c;by=proxy.com;host=example.com;proto=https", - "by=proxy.com;proto=https;host=example.com;for=a,for=b", - ]; - for input in inputs { - let forwarded = Forwarded::parse(input).map_err(|_| crate::Error::new_adhoc(input))?; - let header = forwarded.header_value(); - let parsed = Forwarded::parse(header.as_str())?; - assert_eq!(forwarded, parsed); + #[test] + fn non_ascii_value() { + let element = ForwardedElement::parse(r#"for=for;by=by;host=host;proto="pröto""#) + .expect_err("Didn't fail to parse non-ASCII parameter value"); + assert_eq!(element, ForwardedElementError::NonAsciiValue,); } - Ok(()) } } diff --git a/src/proxies/mod.rs b/src/proxies/mod.rs index bbef0654..29cfc3b0 100644 --- a/src/proxies/mod.rs +++ b/src/proxies/mod.rs @@ -1,3 +1,3 @@ //! Headers that are set by proxies mod forwarded; -pub use forwarded::Forwarded; +pub use forwarded::{Forwarded, ForwardedElement, ForwardedElementError, ForwardedError};