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)]
36pub enum OriginValue<'a> {
37 Null,
40 Origin(Origin<'a>),
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
57pub struct Origin<'a> {
58 pub scheme: &'a [u8],
60 pub host: Host<'a>,
62 pub port: Option<u16>,
64}
65
66impl Origin<'_> {
67 #[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 #[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 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 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
181pub 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}