Skip to main content

rama_http_headers/common/
origin.rs

1use std::borrow::Cow;
2use std::convert::TryFrom;
3use std::fmt;
4
5use rama_core::bytes::Bytes;
6use rama_core::error::{BoxError, ErrorContext as _};
7use rama_core::telemetry::tracing;
8use rama_http_types::HeaderValue;
9use rama_net::Protocol;
10use rama_net::address::Authority;
11use rama_net::uri::Uri;
12
13use crate::Error;
14use crate::util::{IterExt, TryFromValues};
15
16/// The `Origin` header.
17///
18/// The `Origin` header is a version of the `Referer` header that is used for all HTTP fetches and `POST`s whose CORS flag is set.
19/// This header is often used to inform recipients of the security context of where the request was initiated.
20///
21/// Following the spec, [https://fetch.spec.whatwg.org/#origin-header][url], the value of this header is composed of
22/// a String (scheme), Host (host/port)
23///
24/// [url]: https://fetch.spec.whatwg.org/#origin-header
25///
26/// # Examples
27///
28/// ```
29/// use rama_http_headers::Origin;
30///
31/// let origin = Origin::NULL;
32/// ```
33#[derive(Clone, Debug, PartialEq, Eq, Hash)]
34pub struct Origin(OriginOrNull);
35
36impl crate::TypedHeader for Origin {
37    fn name() -> &'static ::rama_http_types::header::HeaderName {
38        &::rama_http_types::header::ORIGIN
39    }
40}
41
42impl crate::HeaderDecode for Origin {
43    fn decode<'i, I>(values: &mut I) -> Result<Self, crate::Error>
44    where
45        I: Iterator<Item = &'i ::rama_http_types::header::HeaderValue>,
46    {
47        crate::util::TryFromValues::try_from_values(values).map(Origin)
48    }
49}
50
51impl crate::HeaderEncode for Origin {
52    fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
53        match HeaderValue::try_from(&self.0) {
54            Ok(value) => values.extend(::std::iter::once(value)),
55            Err(err) => {
56                tracing::debug!("failed to encode origin value as header: {err}");
57            }
58        }
59    }
60}
61
62#[derive(Clone, Debug, PartialEq, Eq, Hash)]
63enum OriginOrNull {
64    Origin(Protocol, Authority),
65    Null,
66}
67
68impl Origin {
69    /// The literal `null` Origin header.
70    pub const NULL: Self = Self(OriginOrNull::Null);
71
72    /// Checks if `Origin` is `null`.
73    #[inline]
74    pub fn is_null(&self) -> bool {
75        matches!(self.0, OriginOrNull::Null)
76    }
77
78    /// Get the "scheme" part of this origin.
79    #[inline]
80    pub fn scheme(&self) -> &str {
81        match self.0 {
82            OriginOrNull::Origin(ref scheme, _) => scheme.as_str(),
83            OriginOrNull::Null => "",
84        }
85    }
86
87    /// Get the "hostname" part of this origin.
88    #[inline]
89    pub fn hostname(&self) -> Cow<'_, str> {
90        match self.0 {
91            OriginOrNull::Origin(_, ref auth) => auth.address.host.to_str(),
92            OriginOrNull::Null => Cow::Borrowed(""),
93        }
94    }
95
96    /// Get the "port" part of this origin.
97    #[inline]
98    pub fn port(&self) -> Option<u16> {
99        match self.0 {
100            OriginOrNull::Origin(_, ref auth) => auth.address.port_u16(),
101            OriginOrNull::Null => None,
102        }
103    }
104
105    /// Tries to build a `Origin` from three parts, the scheme, the host and an optional port.
106    pub fn try_from_parts(
107        scheme: &str,
108        host: &str,
109        port: impl Into<Option<u16>>,
110    ) -> Result<Self, InvalidOrigin> {
111        struct MaybePort(Option<u16>);
112
113        impl fmt::Display for MaybePort {
114            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
115                if let Some(port) = self.0 {
116                    write!(f, ":{port}")
117                } else {
118                    Ok(())
119                }
120            }
121        }
122
123        let bytes = Bytes::from(format!("{}://{}{}", scheme, host, MaybePort(port.into())));
124        HeaderValue::from_maybe_shared(bytes)
125            .ok()
126            .and_then(|val| Self::try_from_header_value(&val))
127            .ok_or(InvalidOrigin)
128    }
129
130    /// Try to parse an `Origin` header value.
131    ///
132    /// Returns `None` for invalid origins, including values with a path other
133    /// than `/`, query, or fragment.
134    #[must_use]
135    pub fn try_from_header_value(value: &HeaderValue) -> Option<Self> {
136        OriginOrNull::try_from_value(value).map(Origin)
137    }
138}
139
140impl fmt::Display for Origin {
141    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
142        match self.0 {
143            OriginOrNull::Origin(ref scheme, ref auth) => write!(f, "{scheme}://{auth}"),
144            OriginOrNull::Null => f.write_str("null"),
145        }
146    }
147}
148
149rama_utils::macros::error::static_str_error! {
150    #[doc = "origin is not valid"]
151    pub struct InvalidOrigin;
152}
153
154impl OriginOrNull {
155    fn try_from_value(value: &HeaderValue) -> Option<Self> {
156        if value == "null" {
157            return Some(Self::Null);
158        }
159
160        let uri = Uri::try_from(value.as_bytes()).ok()?;
161
162        // An origin is `scheme://authority` with no query/fragment and at most
163        // a bare root path (`/`), e.g. `http://example.com` or
164        // `http://example.com/`.
165        let scheme = uri.scheme()?.clone();
166        let auth = uri.authority()?.into_owned();
167
168        let path_ok = uri.path_or_root() == "/";
169        if !path_ok || uri.query().is_some() || uri.fragment().is_some() {
170            return None;
171        }
172
173        Some(Self::Origin(scheme, auth))
174    }
175}
176
177impl TryFromValues for OriginOrNull {
178    fn try_from_values<'i, I>(values: &mut I) -> Result<Self, Error>
179    where
180        I: Iterator<Item = &'i HeaderValue>,
181    {
182        values
183            .just_one()
184            .and_then(Self::try_from_value)
185            .ok_or_else(Error::invalid)
186    }
187}
188
189impl<'a> TryFrom<&'a OriginOrNull> for HeaderValue {
190    type Error = BoxError;
191
192    fn try_from(origin: &'a OriginOrNull) -> Result<Self, Self::Error> {
193        match origin {
194            OriginOrNull::Origin(scheme, auth) => {
195                let s = format!("{scheme}://{auth}");
196                let bytes = Bytes::from(s);
197                Self::from_maybe_shared(bytes)
198                    .context("parse Scheme and Authority as a valid header value")
199            }
200            // Serialized as "null" per ASCII serialization of an origin
201            // https://html.spec.whatwg.org/multipage/browsers.html#ascii-serialisation-of-an-origin
202            OriginOrNull::Null => Ok(Self::from_static("null")),
203        }
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::super::{test_decode, test_encode};
210    use super::*;
211
212    #[test]
213    fn origin() {
214        let s = "http://web-platform.test:8000";
215        let origin = test_decode::<Origin>(&[s]).unwrap();
216        assert_eq!(origin.scheme(), "http");
217        assert_eq!(origin.hostname(), "web-platform.test");
218        assert_eq!(origin.port(), Some(8000));
219
220        let headers = test_encode(origin);
221        assert_eq!(headers["origin"], s);
222    }
223
224    #[test]
225    fn parse_from_header_value() {
226        let value = HeaderValue::from_static("http://localhost:8000");
227        let origin = Origin::try_from_header_value(&value).unwrap();
228
229        assert_eq!(origin.scheme(), "http");
230        assert_eq!(origin.hostname(), "localhost");
231        assert_eq!(origin.port(), Some(8000));
232
233        assert!(Origin::try_from_header_value(&HeaderValue::from_static("localhost")).is_none());
234    }
235
236    #[test]
237    fn null() {
238        assert_eq!(test_decode::<Origin>(&["null"]), Some(Origin::NULL),);
239
240        let headers = test_encode(Origin::NULL);
241        assert_eq!(headers["origin"], "null");
242    }
243}