1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
26pub enum OriginValue<'a> {
27 Null,
30 Origin(Origin<'a>),
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
36pub struct Origin<'a> {
37 pub scheme: &'a [u8],
39 pub host: Host<'a>,
41 pub port: Option<u16>,
43}
44
45impl Origin<'_> {
46 #[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 #[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 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
137pub 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}