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