Skip to main content

parse_net/
origin.rs

1//! `Origin` (RFC 6454 section 7): `null` or `scheme "://" host [ ":" port ]`.
2//!
3//! # Examples
4//!
5//! ```
6//! use parse_net::{self as pn, OriginValue};
7//!
8//! let OriginValue::Origin(a) = pn::parse_origin(b"https://app.example.com")? else { unreachable!() };
9//! let OriginValue::Origin(b) = pn::parse_origin(b"HTTPS://APP.example.com:443")? else { unreachable!() };
10//! assert!(a.same_origin(&b));
11//! let OriginValue::Origin(evil) = pn::parse_origin(b"https://app.example.com.evil.test")? else { unreachable!() };
12//! assert!(!a.same_origin(&evil));
13//! assert_eq!(pn::parse_origin(b"https://app.example.com/").unwrap_err().kind(), pn::ErrorKind::InvalidOrigin);
14//! # Ok::<(), pn::ParseError>(())
15//! ```
16
17use crate::{
18    error::{ErrorKind, ParseError, Result, WriteError, err},
19    host::{Host, HostPort, host_port},
20    text::{skip_ows, trim_ows},
21    write::{CountSink, Sink, reserve, write_decimal_u64},
22};
23
24/// An `Origin` value.
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
26pub enum OriginValue<'a> {
27    /// `null` (an opaque origin: a sandboxed frame, a `data:` URL, a
28    /// privacy-sensitive redirect).
29    Null,
30    /// A serialized origin.
31    Origin(Origin<'a>),
32}
33
34/// A serialized origin.
35#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
36pub struct Origin<'a> {
37    /// The scheme as sent (compare it ASCII case-insensitively).
38    pub scheme: &'a [u8],
39    /// The host.
40    pub host: Host<'a>,
41    /// The port, when one was given.
42    pub port: Option<u16>,
43}
44
45impl Origin<'_> {
46    /// The port, or the scheme's default: 80 for `http` and `ws`, 443 for
47    /// `https` and `wss`; `None` for another scheme without a port.
48    ///
49    /// # Examples
50    ///
51    /// ```
52    /// use parse_net::{parse_origin, OriginValue};
53    ///
54    /// let OriginValue::Origin(o) = parse_origin(b"wss://chat.example")? else { unreachable!() };
55    /// assert_eq!(o.effective_port(), Some(443));
56    /// # Ok::<(), parse_net::ParseError>(())
57    /// ```
58    #[must_use]
59    pub fn effective_port(&self) -> Option<u16> {
60        self.port.or_else(|| {
61            let s = self.scheme;
62            if s.eq_ignore_ascii_case(b"http") || s.eq_ignore_ascii_case(b"ws") {
63                Some(80)
64            } else if s.eq_ignore_ascii_case(b"https") || s.eq_ignore_ascii_case(b"wss") {
65                Some(443)
66            } else {
67                None
68            }
69        })
70    }
71
72    /// Whether two origins are the same: schemes and hosts equal as whole
73    /// values (ASCII case-insensitive, IP literals by value), effective ports
74    /// equal. Never a prefix or suffix comparison.
75    #[must_use]
76    pub fn same_origin(&self, other: &Origin<'_>) -> bool {
77        self.scheme.eq_ignore_ascii_case(other.scheme)
78            && self.host.eq_ignore_ascii_case(&other.host)
79            && self.effective_port() == other.effective_port()
80    }
81
82    /// Writes the origin with a lowercase scheme and without a default port.
83    ///
84    /// # Errors
85    ///
86    /// [`WriteError::Full`]; nothing is written when the sink reports its
87    /// room.
88    ///
89    /// # Examples
90    ///
91    /// ```
92    /// use parse_net::{parse_origin, OriginValue, SliceSink};
93    ///
94    /// let OriginValue::Origin(o) = parse_origin(b"HTTPS://a.example:443")? else { unreachable!() };
95    /// let mut buf = [0u8; 64];
96    /// let mut out = SliceSink::new(&mut buf);
97    /// o.write_to(&mut out).expect("room");
98    /// assert_eq!(out.written(), b"https://a.example");
99    /// # Ok::<(), parse_net::ParseError>(())
100    /// ```
101    pub fn write_to(&self, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
102        let mut count = CountSink::new();
103        self.put(&mut count)?;
104        reserve(out, count.count())?;
105        self.put(out)
106    }
107
108    fn put(&self, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
109        let mut scheme = [0u8; 32];
110        for chunk in self.scheme.chunks(scheme.len()) {
111            let dst = scheme.get_mut(..chunk.len()).unwrap_or(&mut []);
112            for (d, &c) in dst.iter_mut().zip(chunk) {
113                *d = c.to_ascii_lowercase();
114            }
115            out.put(dst)?;
116        }
117        out.put(b"://")?;
118        HostPort {
119            host: self.host,
120            port: None,
121            empty_port: false,
122        }
123        .write_to(out)?;
124        let default = Origin {
125            port: None,
126            ..*self
127        }
128        .effective_port();
129        if let Some(p) = self.port.filter(|&p| Some(p) != default) {
130            out.put(b":")?;
131            write_decimal_u64(u64::from(p), out)?;
132        }
133        Ok(())
134    }
135}
136
137/// Parses an `Origin` value.
138///
139/// `null` is case-sensitive. The scheme is a letter followed by letters,
140/// digits, `+`, `-`, or `.`; the host follows the `Host` rules and may not
141/// be empty; a path, query, fragment, trailing `/`, userinfo, or empty port
142/// is refused.
143///
144/// # Errors
145///
146/// [`ErrorKind::InvalidOrigin`]; the `Host` errors for the host part.
147///
148/// # Examples
149///
150/// ```
151/// use parse_net::{parse_origin, OriginValue};
152///
153/// assert_eq!(parse_origin(b"null")?, OriginValue::Null);
154/// assert!(parse_origin(b"Null").is_err());
155/// # Ok::<(), parse_net::ParseError>(())
156/// ```
157pub fn parse_origin(value: &[u8]) -> Result<OriginValue<'_>> {
158    let lead = skip_ows(value);
159    let v = trim_ows(value);
160    if v == b"null" {
161        return Ok(OriginValue::Null);
162    }
163    let scheme_len = v
164        .iter()
165        .take_while(|&&b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'-' | b'.'))
166        .count();
167    if scheme_len == 0 || !v.first().is_some_and(u8::is_ascii_alphabetic) {
168        return err(ErrorKind::InvalidOrigin, lead);
169    }
170    let scheme = v.get(..scheme_len).unwrap_or(&[]);
171    if v.get(scheme_len..scheme_len + 3) != Some(b"://") {
172        return err(ErrorKind::InvalidOrigin, lead + scheme_len);
173    }
174    let authority_at = scheme_len + 3;
175    let authority = v.get(authority_at..).unwrap_or(&[]);
176    if let Some(i) = authority
177        .iter()
178        .position(|&b| matches!(b, b'/' | b'?' | b'#'))
179    {
180        return err(ErrorKind::InvalidOrigin, lead + authority_at + i);
181    }
182    let hp = host_port(authority).map_err(|e: ParseError| e.shifted(lead + authority_at))?;
183    if hp.host.is_empty() || hp.empty_port {
184        return err(ErrorKind::InvalidOrigin, lead + authority_at);
185    }
186    Ok(OriginValue::Origin(Origin {
187        scheme,
188        host: hp.host,
189        port: hp.port,
190    }))
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    fn o(v: &[u8]) -> Origin<'_> {
198        match parse_origin(v).unwrap() {
199            OriginValue::Origin(o) => o,
200            OriginValue::Null => panic!("null"),
201        }
202    }
203
204    #[test]
205    fn test_origin_edge_cases() {
206        let k = |v: &[u8]| parse_origin(v).unwrap_err().kind();
207        assert_eq!(k(b"Null"), ErrorKind::InvalidOrigin);
208        assert_eq!(o(b"https://a.com").port, None);
209        assert_eq!(o(b"https://a.com:443").port, Some(443));
210        assert_eq!(k(b"https://a.com/"), ErrorKind::InvalidOrigin);
211        assert_eq!(k(b"https://a.com:"), ErrorKind::InvalidOrigin);
212        assert_eq!(k(b"https://user@a.com"), ErrorKind::InvalidHost);
213        assert_eq!(o(b"HTTPS://A.COM").scheme, b"HTTPS");
214        assert_eq!(k(b"file://"), ErrorKind::InvalidOrigin);
215        assert_eq!(k(b"https:a.com"), ErrorKind::InvalidOrigin);
216        assert_eq!(k(b"1https://a.com"), ErrorKind::InvalidOrigin);
217        assert_eq!(k(b"https://a.com?x"), ErrorKind::InvalidOrigin);
218        assert_eq!(k(b""), ErrorKind::InvalidOrigin);
219        assert!(matches!(o(b"http://[::1]:8080").host, Host::V6(_)));
220        let long = [b"https://".as_slice(), &[b'a'; 4096]].concat();
221        assert!(parse_origin(&long).is_ok());
222    }
223
224    #[test]
225    fn test_origin_suffix_attack_not_same() {
226        let a = o(b"https://example.com");
227        assert!(!a.same_origin(&o(b"https://example.com.evil.test")));
228        assert!(!a.same_origin(&o(b"https://evilexample.com")));
229        assert!(!a.same_origin(&o(b"http://example.com")));
230        assert!(!a.same_origin(&o(b"https://example.com:8443")));
231        assert!(a.same_origin(&o(b"https://EXAMPLE.com:443")));
232    }
233
234    #[test]
235    fn test_origin_trailing_slash_refused() {
236        assert_eq!(
237            parse_origin(b"https://example.com/").unwrap_err().kind(),
238            ErrorKind::InvalidOrigin
239        );
240    }
241
242    #[test]
243    fn test_origin_write() {
244        let mut buf = [0u8; 64];
245        let mut out = crate::SliceSink::new(&mut buf);
246        o(b"HTTP://[::1]:8080").write_to(&mut out).unwrap();
247        assert_eq!(out.written(), b"http://[::1]:8080");
248        assert_eq!(o(b"x-app://a").effective_port(), None);
249    }
250}