1#[cfg(not(target_arch = "wasm32"))]
31use chrono::{DateTime, Utc};
32use http::header::{self, HeaderMap, HeaderName};
33use http::Method;
34use std::fmt::Display;
35#[cfg(not(target_arch = "wasm32"))]
36use std::time::Duration;
37#[cfg(not(target_arch = "wasm32"))]
38use url::Url;
39
40#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
43pub enum CorsError {
44 MissingAllowOrigin,
46 OriginMismatch,
49 WildcardWithCredentials,
51 CredentialsNotAllowed,
53 SameOriginMode,
56 UnsafeMethodForNoCors,
59 UnsafeHeaderForNoCors,
62 PreflightStatus,
64 PreflightInvalidResponse,
66 PreflightMethodRejected,
68 PreflightHeaderRejected,
70 CredentialedRedirect,
73}
74
75impl Display for CorsError {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 let s = match self {
78 Self::MissingAllowOrigin => "no Access-Control-Allow-Origin header",
79 Self::OriginMismatch => "Access-Control-Allow-Origin does not match the origin",
80 Self::WildcardWithCredentials => {
81 "Access-Control-Allow-Origin '*' cannot authorize a credentialed request"
82 }
83 Self::CredentialsNotAllowed => "Access-Control-Allow-Credentials is not 'true'",
84 Self::SameOriginMode => "same-origin mode request targeted another origin",
85 Self::UnsafeMethodForNoCors => "method not allowed for a cross-origin no-cors request",
86 Self::UnsafeHeaderForNoCors => "header not allowed for a cross-origin no-cors request",
87 Self::PreflightStatus => "preflight response status was not ok",
88 Self::PreflightInvalidResponse => "preflight response headers could not be parsed",
89 Self::PreflightMethodRejected => "method not allowed by preflight response",
90 Self::PreflightHeaderRejected => "header not allowed by preflight response",
91 Self::CredentialedRedirect => "redirect URL with embedded credentials",
92 };
93 f.write_str(s)
94 }
95}
96
97#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
107pub enum ResponseTainting {
108 #[default]
110 Basic,
111 Cors,
114 Opaque,
116}
117
118#[cfg(not(target_arch = "wasm32"))]
122pub(crate) fn serialize_origin(origin: &url::Origin, tainted: bool) -> String {
123 if tainted {
124 "null".to_string()
125 } else {
126 origin.ascii_serialization()
127 }
128}
129
130pub fn is_cors_safelisted_method(method: &Method) -> bool {
134 matches!(*method, Method::GET | Method::HEAD | Method::POST)
135}
136
137fn is_cors_unsafe_header_byte(b: u8) -> bool {
139 matches!(b,
140 0x00..=0x08 | 0x0A..=0x1F | 0x22 | 0x28 | 0x29 | 0x3A | 0x3C |
141 0x3E | 0x3F | 0x40 | 0x5B..=0x5D | 0x7B | 0x7D | 0x7F)
142}
143
144pub fn is_cors_safelisted_request_header(name: &HeaderName, value: &[u8]) -> bool {
149 if value.len() > 128 {
150 return false;
151 }
152 match name.as_str() {
153 "accept" => !value.iter().copied().any(is_cors_unsafe_header_byte),
154 "accept-language" | "content-language" => value.iter().all(|b| {
155 matches!(b,
156 0x30..=0x39 | 0x41..=0x5A | 0x61..=0x7A |
157 0x20 | 0x2A | 0x2C | 0x2D | 0x2E | 0x3B | 0x3D)
158 }),
159 "content-type" => {
160 if value.iter().copied().any(is_cors_unsafe_header_byte) {
161 return false;
162 }
163 let Ok(s) = std::str::from_utf8(value) else {
164 return false;
165 };
166 let essence = s
167 .split(';')
168 .next()
169 .unwrap_or("")
170 .trim_matches([' ', '\t'])
171 .to_ascii_lowercase();
172 matches!(
173 essence.as_str(),
174 "application/x-www-form-urlencoded" | "multipart/form-data" | "text/plain"
175 )
176 }
177 "range" => {
179 let Some(rest) = value.strip_prefix(b"bytes=") else {
180 return false;
181 };
182 let Ok(rest) = std::str::from_utf8(rest) else {
183 return false;
184 };
185 match rest.split_once('-') {
186 Some((start, end)) => {
187 !start.is_empty()
188 && start.bytes().all(|b| b.is_ascii_digit())
189 && end.bytes().all(|b| b.is_ascii_digit())
190 }
191 None => false,
192 }
193 }
194 _ => false,
195 }
196}
197
198pub fn is_forbidden_request_header(name: &HeaderName) -> bool {
204 let n = name.as_str();
205 n.starts_with("proxy-")
206 || n.starts_with("sec-")
207 || matches!(
208 n,
209 "accept-charset"
210 | "accept-encoding"
211 | "access-control-request-headers"
212 | "access-control-request-method"
213 | "connection"
214 | "content-length"
215 | "cookie"
216 | "cookie2"
217 | "date"
218 | "dnt"
219 | "expect"
220 | "host"
221 | "keep-alive"
222 | "origin"
223 | "referer"
224 | "set-cookie"
225 | "te"
226 | "trailer"
227 | "transfer-encoding"
228 | "upgrade"
229 | "via"
230 )
231}
232
233pub(crate) fn unsafe_request_header_names(headers: &HeaderMap) -> Vec<String> {
237 let mut names: Vec<String> = headers
238 .iter()
239 .filter(|(name, value)| {
240 !is_forbidden_request_header(name)
241 && !is_cors_safelisted_request_header(name, value.as_bytes())
242 })
243 .map(|(name, _)| name.as_str().to_string())
244 .collect();
245 names.sort_unstable();
246 names.dedup();
247 names
248}
249
250pub fn preflight_needed(method: &Method, headers: &HeaderMap) -> bool {
253 !is_cors_safelisted_method(method) || !unsafe_request_header_names(headers).is_empty()
254}
255
256#[cfg(not(target_arch = "wasm32"))]
265pub(crate) fn cors_check(
266 origin: &url::Origin,
267 tainted: bool,
268 credentials_include: bool,
269 response: &HeaderMap,
270) -> Result<(), CorsError> {
271 let mut values = response.get_all(header::ACCESS_CONTROL_ALLOW_ORIGIN).iter();
272 let Some(allow) = values.next() else {
273 return Err(CorsError::MissingAllowOrigin);
274 };
275 if values.next().is_some() {
277 return Err(CorsError::OriginMismatch);
278 }
279 if allow.as_bytes() == b"*" {
280 return if credentials_include {
281 Err(CorsError::WildcardWithCredentials)
282 } else {
283 Ok(())
284 };
285 }
286 if allow.as_bytes() != serialize_origin(origin, tainted).as_bytes() {
287 return Err(CorsError::OriginMismatch);
288 }
289 if !credentials_include {
290 return Ok(());
291 }
292 match response.get(header::ACCESS_CONTROL_ALLOW_CREDENTIALS) {
293 Some(v) if v.as_bytes() == b"true" => Ok(()),
294 _ => Err(CorsError::CredentialsNotAllowed),
295 }
296}
297
298#[cfg(not(target_arch = "wasm32"))]
303#[derive(Debug, Clone)]
304pub struct PreflightAllows {
305 methods: Vec<String>,
306 methods_wildcard: bool,
307 headers: Vec<String>,
308 headers_wildcard: bool,
309 pub max_age: Duration,
311}
312
313#[cfg(not(target_arch = "wasm32"))]
317pub const DEFAULT_PREFLIGHT_MAX_AGE: Duration = Duration::from_secs(5);
318#[cfg(not(target_arch = "wasm32"))]
321pub const MAX_PREFLIGHT_MAX_AGE: Duration = Duration::from_secs(2 * 60 * 60);
322
323#[cfg(not(target_arch = "wasm32"))]
324impl PreflightAllows {
325 pub fn permits(
331 &self,
332 method: &Method,
333 unsafe_header_names: &[String],
334 credentials_include: bool,
335 ) -> Result<(), CorsError> {
336 let allowed = self.methods.iter().any(|m| m == method.as_str())
337 || (self.methods_wildcard && !credentials_include)
338 || is_cors_safelisted_method(method);
339 if !allowed {
340 return Err(CorsError::PreflightMethodRejected);
341 }
342 for name in unsafe_header_names {
343 let listed = self.headers.iter().any(|h| h == name);
344 let wildcard_ok =
345 self.headers_wildcard && !credentials_include && name != "authorization";
346 if !listed && !wildcard_ok {
347 return Err(CorsError::PreflightHeaderRejected);
348 }
349 }
350 Ok(())
351 }
352}
353
354fn parse_token_list(
358 response: &HeaderMap,
359 name: HeaderName,
360 lowercase: bool,
361) -> Result<(Vec<String>, bool), CorsError> {
362 let mut items = Vec::new();
363 let mut wildcard = false;
364 for value in response.get_all(&name) {
365 let s = value
366 .to_str()
367 .map_err(|_| CorsError::PreflightInvalidResponse)?;
368 for item in s.split(',') {
369 let item = item.trim_matches([' ', '\t']);
370 if item.is_empty() {
371 continue;
372 }
373 if item == "*" {
374 wildcard = true;
375 continue;
376 }
377 let is_token = item.bytes().all(|b| {
378 b.is_ascii_alphanumeric()
379 || matches!(
380 b,
381 b'!' | b'#'
382 | b'$'
383 | b'%'
384 | b'&'
385 | b'\''
386 | b'*'
387 | b'+'
388 | b'-'
389 | b'.'
390 | b'^'
391 | b'_'
392 | b'`'
393 | b'|'
394 | b'~'
395 )
396 });
397 if !is_token {
398 return Err(CorsError::PreflightInvalidResponse);
399 }
400 items.push(if lowercase {
401 item.to_ascii_lowercase()
402 } else {
403 item.to_string()
404 });
405 }
406 }
407 Ok((items, wildcard))
408}
409
410#[cfg(not(target_arch = "wasm32"))]
414pub(crate) fn validate_preflight_response(
415 status: u16,
416 response: &HeaderMap,
417 origin: &url::Origin,
418 tainted: bool,
419 credentials_include: bool,
420) -> Result<PreflightAllows, CorsError> {
421 if !(200..300).contains(&status) {
422 return Err(CorsError::PreflightStatus);
423 }
424 cors_check(origin, tainted, credentials_include, response)?;
425 let (methods, methods_wildcard) =
426 parse_token_list(response, header::ACCESS_CONTROL_ALLOW_METHODS, false)?;
427 let (headers, headers_wildcard) =
428 parse_token_list(response, header::ACCESS_CONTROL_ALLOW_HEADERS, true)?;
429 let max_age = response
430 .get(header::ACCESS_CONTROL_MAX_AGE)
431 .and_then(|v| v.to_str().ok())
432 .and_then(|s| s.trim().parse::<u64>().ok())
433 .map_or(DEFAULT_PREFLIGHT_MAX_AGE, Duration::from_secs)
434 .min(MAX_PREFLIGHT_MAX_AGE);
435 Ok(PreflightAllows {
436 methods,
437 methods_wildcard,
438 headers,
439 headers_wildcard,
440 max_age,
441 })
442}
443
444#[cfg(not(target_arch = "wasm32"))]
447pub(crate) fn preflight_request_headers(
448 method: &Method,
449 unsafe_header_names: &[String],
450) -> HeaderMap {
451 let mut headers = HeaderMap::new();
452 headers.insert(header::ACCEPT, http::HeaderValue::from_static("*/*"));
453 if let Ok(v) = method.as_str().parse() {
454 headers.insert(header::ACCESS_CONTROL_REQUEST_METHOD, v);
455 }
456 if !unsafe_header_names.is_empty() {
457 if let Ok(v) = unsafe_header_names.join(",").parse() {
458 headers.insert(header::ACCESS_CONTROL_REQUEST_HEADERS, v);
459 }
460 }
461 headers
462}
463
464const CORS_SAFELISTED_RESPONSE_HEADERS: [&str; 7] = [
467 "cache-control",
468 "content-language",
469 "content-length",
470 "content-type",
471 "expires",
472 "last-modified",
473 "pragma",
474];
475
476pub fn readable_headers(
485 tainting: ResponseTainting,
486 headers: &HeaderMap,
487 credentials_include: bool,
488) -> HeaderMap {
489 let keep_all_but_cookies = |headers: &HeaderMap| {
490 let mut out = HeaderMap::new();
491 for (name, value) in headers {
492 if name != header::SET_COOKIE && name.as_str() != "set-cookie2" {
493 out.append(name.clone(), value.clone());
494 }
495 }
496 out
497 };
498 match tainting {
499 ResponseTainting::Basic => keep_all_but_cookies(headers),
500 ResponseTainting::Opaque => HeaderMap::new(),
501 ResponseTainting::Cors => {
502 let (exposed, wildcard) =
503 parse_token_list(headers, header::ACCESS_CONTROL_EXPOSE_HEADERS, true)
504 .unwrap_or((Vec::new(), false));
505 if wildcard && !credentials_include {
506 return keep_all_but_cookies(headers);
507 }
508 let mut out = HeaderMap::new();
509 for (name, value) in headers {
510 let n = name.as_str();
511 if CORS_SAFELISTED_RESPONSE_HEADERS.contains(&n)
512 || (exposed.iter().any(|e| e == n) && n != "set-cookie" && n != "set-cookie2")
513 {
514 out.append(name.clone(), value.clone());
515 }
516 }
517 out
518 }
519 }
520}
521
522#[cfg(not(target_arch = "wasm32"))]
532pub trait CorsPreflightCache: Send + Sync {
533 fn get(
536 &self,
537 origin: &str,
538 url: &Url,
539 credentials: bool,
540 now: DateTime<Utc>,
541 ) -> Option<PreflightAllows>;
542
543 fn put(
546 &self,
547 origin: &str,
548 url: &Url,
549 credentials: bool,
550 allows: PreflightAllows,
551 now: DateTime<Utc>,
552 );
553}
554
555#[cfg(not(target_arch = "wasm32"))]
558#[derive(Default)]
559pub struct InMemoryPreflightCache {
560 entries: parking_lot::RwLock<std::collections::HashMap<PreflightKey, PreflightEntry>>,
561}
562
563#[cfg(not(target_arch = "wasm32"))]
565type PreflightKey = (String, String, bool);
566#[cfg(not(target_arch = "wasm32"))]
568type PreflightEntry = (PreflightAllows, DateTime<Utc>);
569
570#[cfg(not(target_arch = "wasm32"))]
571impl InMemoryPreflightCache {
572 pub fn new() -> Self {
574 Self::default()
575 }
576
577 fn key(origin: &str, url: &Url, credentials: bool) -> PreflightKey {
579 let mut url = url.clone();
580 url.set_fragment(None);
581 (origin.to_string(), url.to_string(), credentials)
582 }
583}
584
585#[cfg(not(target_arch = "wasm32"))]
586impl CorsPreflightCache for InMemoryPreflightCache {
587 fn get(
588 &self,
589 origin: &str,
590 url: &Url,
591 credentials: bool,
592 now: DateTime<Utc>,
593 ) -> Option<PreflightAllows> {
594 let entries = self.entries.read();
595 let (allows, expires) = entries.get(&Self::key(origin, url, credentials))?;
596 (*expires > now).then(|| allows.clone())
597 }
598
599 fn put(
600 &self,
601 origin: &str,
602 url: &Url,
603 credentials: bool,
604 allows: PreflightAllows,
605 now: DateTime<Utc>,
606 ) {
607 let expires =
610 now + chrono::TimeDelta::from_std(allows.max_age).unwrap_or(chrono::TimeDelta::zero());
611 let mut entries = self.entries.write();
612 entries.retain(|_, (_, exp)| *exp > now);
613 entries.insert(Self::key(origin, url, credentials), (allows, expires));
614 }
615}
616
617#[cfg(test)]
618mod tests {
619 use super::*;
620 use http::HeaderValue;
621
622 fn origin(s: &str) -> url::Origin {
623 Url::parse(s).unwrap().origin()
624 }
625
626 fn headers(pairs: &[(&str, &str)]) -> HeaderMap {
627 let mut h = HeaderMap::new();
628 for (name, value) in pairs {
629 h.append(
630 HeaderName::from_bytes(name.as_bytes()).unwrap(),
631 HeaderValue::from_str(value).unwrap(),
632 );
633 }
634 h
635 }
636
637 #[test]
640 fn safelisted_methods() {
641 assert!(is_cors_safelisted_method(&Method::GET));
642 assert!(is_cors_safelisted_method(&Method::HEAD));
643 assert!(is_cors_safelisted_method(&Method::POST));
644 assert!(!is_cors_safelisted_method(&Method::PUT));
645 assert!(!is_cors_safelisted_method(&Method::DELETE));
646 assert!(!is_cors_safelisted_method(&Method::PATCH));
647 }
648
649 #[test]
650 fn safelisted_headers_by_name_and_value() {
651 let n = |s: &str| HeaderName::from_bytes(s.as_bytes()).unwrap();
652 assert!(is_cors_safelisted_request_header(
653 &n("accept"),
654 b"text/html,application/xhtml+xml;q=0.9,*/*;q=0.8"
655 ));
656 assert!(!is_cors_safelisted_request_header(&n("accept"), b"a\"b"));
658 assert!(is_cors_safelisted_request_header(
659 &n("accept-language"),
660 b"en-US,en;q=0.9"
661 ));
662 assert!(!is_cors_safelisted_request_header(
664 &n("accept-language"),
665 b"en/US"
666 ));
667 assert!(is_cors_safelisted_request_header(
668 &n("content-type"),
669 b"text/plain;charset=UTF-8"
670 ));
671 assert!(is_cors_safelisted_request_header(
672 &n("content-type"),
673 b"MULTIPART/FORM-DATA; boundary=x"
674 ));
675 assert!(!is_cors_safelisted_request_header(
676 &n("content-type"),
677 b"application/json"
678 ));
679 assert!(is_cors_safelisted_request_header(&n("range"), b"bytes=0-"));
680 assert!(is_cors_safelisted_request_header(
681 &n("range"),
682 b"bytes=200-1000"
683 ));
684 assert!(!is_cors_safelisted_request_header(
685 &n("range"),
686 b"bytes=-500"
687 ));
688 assert!(!is_cors_safelisted_request_header(
689 &n("range"),
690 b"bytes=0-50,100-150"
691 ));
692 assert!(!is_cors_safelisted_request_header(&n("x-custom"), b"1"));
693 let long = vec![b'a'; 129];
695 assert!(!is_cors_safelisted_request_header(&n("accept"), &long));
696 }
697
698 #[test]
699 fn unsafe_names_skip_forbidden_and_safelisted() {
700 let h = headers(&[
701 ("accept", "*/*"),
702 ("cookie", "a=1"),
703 ("referer", "https://a.example/"),
704 ("sec-fetch-mode", "cors"),
705 ("x-custom", "1"),
706 ("authorization", "Bearer t"),
707 ("content-type", "application/json"),
708 ]);
709 assert_eq!(
710 unsafe_request_header_names(&h),
711 vec!["authorization", "content-type", "x-custom"]
712 );
713 }
714
715 #[test]
716 fn preflight_needed_on_method_or_header() {
717 let plain = headers(&[("accept", "*/*")]);
718 assert!(!preflight_needed(&Method::GET, &plain));
719 assert!(!preflight_needed(&Method::POST, &plain));
720 assert!(preflight_needed(&Method::PUT, &plain));
721 assert!(preflight_needed(
722 &Method::GET,
723 &headers(&[("x-custom", "1")])
724 ));
725 assert!(preflight_needed(
726 &Method::POST,
727 &headers(&[("content-type", "application/json")])
728 ));
729 }
730
731 #[test]
734 fn cors_check_matches_origin() {
735 let o = origin("https://a.example");
736 let ok = headers(&[("access-control-allow-origin", "https://a.example")]);
737 assert_eq!(cors_check(&o, false, false, &ok), Ok(()));
738 assert_eq!(
739 cors_check(&o, false, false, &HeaderMap::new()),
740 Err(CorsError::MissingAllowOrigin)
741 );
742 let wrong = headers(&[("access-control-allow-origin", "https://b.example")]);
743 assert_eq!(
744 cors_check(&o, false, false, &wrong),
745 Err(CorsError::OriginMismatch)
746 );
747 let http = headers(&[("access-control-allow-origin", "http://a.example")]);
749 assert_eq!(
750 cors_check(&o, false, false, &http),
751 Err(CorsError::OriginMismatch)
752 );
753 }
754
755 #[test]
756 fn cors_check_wildcard_only_without_credentials() {
757 let o = origin("https://a.example");
758 let star = headers(&[("access-control-allow-origin", "*")]);
759 assert_eq!(cors_check(&o, false, false, &star), Ok(()));
760 assert_eq!(
761 cors_check(&o, false, true, &star),
762 Err(CorsError::WildcardWithCredentials)
763 );
764 }
765
766 #[test]
767 fn cors_check_credentials_require_allow_credentials_true() {
768 let o = origin("https://a.example");
769 let no_cred = headers(&[("access-control-allow-origin", "https://a.example")]);
770 assert_eq!(
771 cors_check(&o, false, true, &no_cred),
772 Err(CorsError::CredentialsNotAllowed)
773 );
774 let ok = headers(&[
775 ("access-control-allow-origin", "https://a.example"),
776 ("access-control-allow-credentials", "true"),
777 ]);
778 assert_eq!(cors_check(&o, false, true, &ok), Ok(()));
779 let bad_case = headers(&[
781 ("access-control-allow-origin", "https://a.example"),
782 ("access-control-allow-credentials", "True"),
783 ]);
784 assert_eq!(
785 cors_check(&o, false, true, &bad_case),
786 Err(CorsError::CredentialsNotAllowed)
787 );
788 }
789
790 #[test]
791 fn cors_check_tainted_origin_matches_null() {
792 let o = origin("https://a.example");
793 let null = headers(&[("access-control-allow-origin", "null")]);
794 assert_eq!(cors_check(&o, true, false, &null), Ok(()));
795 let real = headers(&[("access-control-allow-origin", "https://a.example")]);
796 assert_eq!(
797 cors_check(&o, true, false, &real),
798 Err(CorsError::OriginMismatch)
799 );
800 }
801
802 #[test]
803 fn cors_check_duplicate_allow_origin_fails() {
804 let mut h = headers(&[("access-control-allow-origin", "https://a.example")]);
805 h.append(
806 header::ACCESS_CONTROL_ALLOW_ORIGIN,
807 HeaderValue::from_static("https://a.example"),
808 );
809 assert_eq!(
810 cors_check(&origin("https://a.example"), false, false, &h),
811 Err(CorsError::OriginMismatch)
812 );
813 }
814
815 fn ok_preflight(extra: &[(&str, &str)]) -> HeaderMap {
818 let mut h = headers(&[("access-control-allow-origin", "https://a.example")]);
819 for (name, value) in extra {
820 h.append(
821 HeaderName::from_bytes(name.as_bytes()).unwrap(),
822 HeaderValue::from_str(value).unwrap(),
823 );
824 }
825 h
826 }
827
828 #[test]
829 fn preflight_rejects_non_ok_status() {
830 let o = origin("https://a.example");
831 assert_eq!(
832 validate_preflight_response(403, &ok_preflight(&[]), &o, false, false).unwrap_err(),
833 CorsError::PreflightStatus
834 );
835 assert_eq!(
836 validate_preflight_response(301, &ok_preflight(&[]), &o, false, false).unwrap_err(),
837 CorsError::PreflightStatus
838 );
839 }
840
841 #[test]
842 fn preflight_allows_listed_method_and_headers() {
843 let o = origin("https://a.example");
844 let resp = ok_preflight(&[
845 ("access-control-allow-methods", "PUT, DELETE"),
846 ("access-control-allow-headers", "X-Custom, Content-Type"),
847 ]);
848 let allows = validate_preflight_response(204, &resp, &o, false, false).unwrap();
849 assert_eq!(
850 allows.permits(&Method::PUT, &["x-custom".into()], false),
851 Ok(())
852 );
853 assert_eq!(
854 allows.permits(&Method::PATCH, &[], false),
855 Err(CorsError::PreflightMethodRejected)
856 );
857 assert_eq!(
858 allows.permits(&Method::PUT, &["x-other".into()], false),
859 Err(CorsError::PreflightHeaderRejected)
860 );
861 assert_eq!(allows.permits(&Method::POST, &[], false), Ok(()));
863 }
864
865 #[test]
866 fn preflight_wildcard_rules() {
867 let o = origin("https://a.example");
868 let resp = ok_preflight(&[
869 ("access-control-allow-methods", "*"),
870 ("access-control-allow-headers", "*"),
871 ]);
872 let allows = validate_preflight_response(200, &resp, &o, false, false).unwrap();
873 assert_eq!(
874 allows.permits(&Method::DELETE, &["x-custom".into()], false),
875 Ok(())
876 );
877 assert_eq!(
879 allows.permits(&Method::GET, &["authorization".into()], false),
880 Err(CorsError::PreflightHeaderRejected)
881 );
882 assert_eq!(
884 allows.permits(&Method::DELETE, &[], true),
885 Err(CorsError::PreflightMethodRejected)
886 );
887 }
888
889 #[test]
890 fn preflight_invalid_token_fails_parse() {
891 let o = origin("https://a.example");
892 let resp = ok_preflight(&[("access-control-allow-methods", "PUT, DEL ETE")]);
893 assert_eq!(
894 validate_preflight_response(200, &resp, &o, false, false).unwrap_err(),
895 CorsError::PreflightInvalidResponse
896 );
897 }
898
899 #[test]
900 fn preflight_max_age_defaulted_and_capped() {
901 let o = origin("https://a.example");
902 let allows =
903 validate_preflight_response(200, &ok_preflight(&[]), &o, false, false).unwrap();
904 assert_eq!(allows.max_age, DEFAULT_PREFLIGHT_MAX_AGE);
905 let resp = ok_preflight(&[("access-control-max-age", "600")]);
906 let allows = validate_preflight_response(200, &resp, &o, false, false).unwrap();
907 assert_eq!(allows.max_age, Duration::from_secs(600));
908 let resp = ok_preflight(&[("access-control-max-age", "999999999")]);
909 let allows = validate_preflight_response(200, &resp, &o, false, false).unwrap();
910 assert_eq!(allows.max_age, MAX_PREFLIGHT_MAX_AGE);
911 }
912
913 #[test]
914 fn preflight_request_headers_shape() {
915 let h = preflight_request_headers(&Method::PUT, &["x-custom".into(), "x-other".into()]);
916 assert_eq!(h.get(header::ACCEPT).unwrap(), "*/*");
917 assert_eq!(h.get(header::ACCESS_CONTROL_REQUEST_METHOD).unwrap(), "PUT");
918 assert_eq!(
919 h.get(header::ACCESS_CONTROL_REQUEST_HEADERS).unwrap(),
920 "x-custom,x-other"
921 );
922 let h = preflight_request_headers(&Method::PUT, &[]);
923 assert!(h.get(header::ACCESS_CONTROL_REQUEST_HEADERS).is_none());
924 }
925
926 #[test]
929 fn readable_headers_by_tainting() {
930 let resp = headers(&[
931 ("content-type", "text/html"),
932 ("x-request-id", "42"),
933 ("set-cookie", "session=s"),
934 ]);
935 let basic = readable_headers(ResponseTainting::Basic, &resp, false);
936 assert!(basic.get("content-type").is_some());
937 assert!(basic.get("x-request-id").is_some());
938 assert!(basic.get("set-cookie").is_none());
939
940 let cors = readable_headers(ResponseTainting::Cors, &resp, false);
941 assert!(cors.get("content-type").is_some());
942 assert!(cors.get("x-request-id").is_none());
943 assert!(cors.get("set-cookie").is_none());
944
945 let opaque = readable_headers(ResponseTainting::Opaque, &resp, false);
946 assert!(opaque.is_empty());
947 }
948
949 #[test]
950 fn expose_headers_extends_cors_view() {
951 let resp = headers(&[
952 ("x-request-id", "42"),
953 ("x-secret", "s"),
954 ("access-control-expose-headers", "X-Request-Id"),
955 ]);
956 let cors = readable_headers(ResponseTainting::Cors, &resp, false);
957 assert!(cors.get("x-request-id").is_some());
958 assert!(cors.get("x-secret").is_none());
959 }
960
961 #[test]
962 fn expose_headers_wildcard_only_without_credentials() {
963 let resp = headers(&[
964 ("x-request-id", "42"),
965 ("set-cookie", "session=s"),
966 ("access-control-expose-headers", "*"),
967 ]);
968 let no_creds = readable_headers(ResponseTainting::Cors, &resp, false);
969 assert!(no_creds.get("x-request-id").is_some());
970 assert!(no_creds.get("set-cookie").is_none());
971 let creds = readable_headers(ResponseTainting::Cors, &resp, true);
972 assert!(creds.get("x-request-id").is_none());
973 }
974
975 #[test]
978 fn cache_roundtrip_and_expiry() {
979 let cache = InMemoryPreflightCache::new();
980 let url = Url::parse("https://api.example/data").unwrap();
981 let o = origin("https://a.example");
982 let resp = ok_preflight(&[
983 ("access-control-allow-methods", "PUT"),
984 ("access-control-max-age", "60"),
985 ]);
986 let allows = validate_preflight_response(200, &resp, &o, false, false).unwrap();
987 let now = Utc::now();
988 cache.put("https://a.example", &url, false, allows, now);
989
990 let hit = cache.get("https://a.example", &url, false, now).unwrap();
991 assert_eq!(hit.permits(&Method::PUT, &[], false), Ok(()));
992 assert!(cache.get("https://b.example", &url, false, now).is_none());
994 assert!(cache.get("https://a.example", &url, true, now).is_none());
995 let frag = Url::parse("https://api.example/data#frag").unwrap();
997 assert!(cache.get("https://a.example", &frag, false, now).is_some());
998 let later = now + chrono::TimeDelta::seconds(61);
1000 assert!(cache.get("https://a.example", &url, false, later).is_none());
1001 }
1002
1003 #[test]
1004 fn origin_serialization() {
1005 assert_eq!(
1006 serialize_origin(&origin("https://a.example"), false),
1007 "https://a.example"
1008 );
1009 assert_eq!(serialize_origin(&origin("https://a.example"), true), "null");
1010 assert_eq!(
1012 serialize_origin(&origin("https://a.example:8443"), false),
1013 "https://a.example:8443"
1014 );
1015 }
1016}