1use headers::{
12 AccessControlAllowHeaders, AccessControlAllowMethods, AccessControlExposeHeaders, HeaderMap,
13 HeaderMapExt, HeaderName, HeaderValue, Origin,
14};
15use http::header;
16use hyper::{Request, Response, StatusCode};
17use std::collections::HashSet;
18
19use crate::body::Body;
20use crate::{Error, error_page, handler::RequestHandlerOpts};
21
22#[derive(Clone, Debug)]
24pub struct Cors {
25 allowed_headers: HashSet<HeaderName>,
26 exposed_headers: HashSet<HeaderName>,
27 max_age: Option<u64>,
28 allowed_methods: HashSet<http::Method>,
29 origins: Option<HashSet<HeaderValue>>,
30}
31
32pub fn new(
34 origins_str: &str,
35 allow_headers_str: &str,
36 expose_headers_str: &str,
37) -> Option<Configured> {
38 let cors = Cors::new();
39 let cors = if origins_str.is_empty() {
40 None
41 } else {
42 let [allow_headers_vec, expose_headers_vec] =
43 [allow_headers_str, expose_headers_str].map(|s| {
44 if s.is_empty() {
45 vec!["origin", "content-type"]
46 } else {
47 s.split(',').map(|s| s.trim()).collect::<Vec<_>>()
48 }
49 });
50 let allow_headers_vec = validate_header_names("cors.allow_headers", &allow_headers_vec);
57 let expose_headers_vec = validate_header_names("cors.expose_headers", &expose_headers_vec);
58 let [allow_headers_str, expose_headers_str] =
59 [&allow_headers_vec, &expose_headers_vec].map(|v| v.join(","));
60
61 let cors_res = if origins_str == "*" {
62 match cors
63 .allow_any_origin()
64 .allow_headers(allow_headers_vec)
65 .and_then(|cors| cors.expose_headers(expose_headers_vec))
66 .and_then(|cors| cors.allow_methods(["GET", "HEAD", "OPTIONS"]))
67 {
68 Ok(cors) => Some(cors),
69 Err(err) => {
70 tracing::error!("cors: failed to build configuration: {err:?}");
71 None
72 }
73 }
74 } else {
75 let hosts = origins_str
76 .split(',')
77 .map(|s| s.trim())
78 .filter(|s| validate_origin_str("cors.allow_origins", s))
79 .collect::<Vec<_>>();
80 if hosts.is_empty() {
81 tracing::error!(
82 "cors: no valid origins found in `{origins_str}`; CORS will be disabled"
83 );
84 None
85 } else {
86 match cors
87 .allow_origins(hosts)
88 .and_then(|cors| cors.allow_headers(allow_headers_vec))
89 .and_then(|cors| cors.expose_headers(expose_headers_vec))
90 .and_then(|cors| cors.allow_methods(["GET", "HEAD", "OPTIONS"]))
91 {
92 Ok(cors) => Some(cors),
93 Err(err) => {
94 tracing::error!("cors: failed to build configuration: {err:?}");
95 None
96 }
97 }
98 }
99 };
100
101 if cors_res.is_some() {
102 tracing::info!(
103 "cors enabled=true, allow_methods=[GET,HEAD,OPTIONS], allow_origins={}, allow_headers=[{}], expose_headers=[{}]",
104 origins_str,
105 allow_headers_str,
106 expose_headers_str,
107 );
108 }
109 cors_res
110 };
111
112 Cors::build(cors)
113}
114
115fn validate_header_names<'a>(field: &str, names: &[&'a str]) -> Vec<&'a str> {
119 names
120 .iter()
121 .copied()
122 .filter(|h| {
123 if HeaderName::try_from(*h).is_ok() {
124 true
125 } else {
126 tracing::error!("{field}: ignoring invalid HTTP header name `{h}`");
127 false
128 }
129 })
130 .collect()
131}
132
133#[doc(hidden)]
136pub fn validate_origin_str(field: &str, origin: &str) -> bool {
137 let mut parts = origin.splitn(2, "://");
138 let scheme = parts.next();
139 let rest = parts.next();
140 match (scheme, rest) {
141 (Some(s), Some(r)) if !s.is_empty() && !r.is_empty() => {
142 if Origin::try_from_parts(s, r, None).is_ok() {
143 true
144 } else {
145 tracing::error!("{field}: ignoring invalid origin `{origin}`");
146 false
147 }
148 }
149 _ => {
150 tracing::error!(
151 "{field}: ignoring origin `{origin}` (expected `scheme://host[:port]`)"
152 );
153 false
154 }
155 }
156}
157
158impl Cors {
159 pub fn new() -> Self {
161 Self {
162 origins: None,
163 allowed_headers: HashSet::new(),
164 exposed_headers: HashSet::new(),
165 allowed_methods: HashSet::new(),
166 max_age: None,
167 }
168 }
169
170 pub fn allow_methods<I>(mut self, methods: I) -> Result<Self, Error>
172 where
173 I: IntoIterator,
174 http::Method: TryFrom<I::Item>,
175 {
176 for method in methods {
177 let method =
178 http::Method::try_from(method).map_err(|_| Error::msg("cors: illegal method"))?;
179 self.allowed_methods.insert(method);
180 }
181 Ok(self)
182 }
183
184 pub fn allow_any_origin(mut self) -> Self {
191 self.origins = None;
192 self
193 }
194
195 pub fn allow_origins<I>(mut self, origins: I) -> Result<Self, Error>
197 where
198 I: IntoIterator,
199 I::Item: IntoOrigin,
200 {
201 let allowed = self.origins.get_or_insert_with(HashSet::new);
202 for origin in origins {
203 let origin = origin.into_origin()?;
204 let value = HeaderValue::from_str(&origin.to_string())
205 .map_err(|err| Error::msg(format!("cors: invalid origin header value: {err}")))?;
206 allowed.insert(value);
207 }
208 Ok(self)
209 }
210
211 pub fn allow_headers<I>(mut self, headers: I) -> Result<Self, Error>
216 where
217 I: IntoIterator,
218 HeaderName: TryFrom<I::Item>,
219 {
220 for header in headers {
221 let header = HeaderName::try_from(header)
222 .map_err(|_| Error::msg("cors: illegal allow header"))?;
223 self.allowed_headers.insert(header);
224 }
225 Ok(self)
226 }
227
228 pub fn expose_headers<I>(mut self, headers: I) -> Result<Self, Error>
233 where
234 I: IntoIterator,
235 HeaderName: TryFrom<I::Item>,
236 {
237 for header in headers {
238 let header = HeaderName::try_from(header)
239 .map_err(|_| Error::msg("cors: illegal expose header"))?;
240 self.exposed_headers.insert(header);
241 }
242 Ok(self)
243 }
244
245 pub fn build(cors: Option<Cors>) -> Option<Configured> {
247 cors.as_ref()?;
248 let cors = cors?;
249
250 let allowed_headers = cors.allowed_headers.iter().cloned().collect();
251 let exposed_headers = cors.exposed_headers.iter().cloned().collect();
252 let methods_header = cors.allowed_methods.iter().cloned().collect();
253
254 Some(Configured {
255 cors,
256 allowed_headers,
257 exposed_headers,
258 methods_header,
259 })
260 }
261}
262
263impl Default for Cors {
264 fn default() -> Self {
265 Self::new()
266 }
267}
268
269#[derive(Clone, Debug)]
270pub struct Configured {
272 cors: Cors,
273 allowed_headers: AccessControlAllowHeaders,
274 exposed_headers: AccessControlExposeHeaders,
275 methods_header: AccessControlAllowMethods,
276}
277
278#[derive(Debug)]
279pub enum Validated {
281 Preflight(HeaderValue),
283 Simple(HeaderValue),
285 NotCors,
287}
288
289#[derive(Debug, Default)]
290pub enum Forbidden {
292 #[default]
294 Origin,
295 Method,
297 Header,
299}
300
301impl Configured {
302 pub fn check_request(
304 &self,
305 method: &http::Method,
306 headers: &HeaderMap,
307 ) -> Result<(HeaderMap, Validated), Forbidden> {
308 match (headers.get(header::ORIGIN), method) {
309 (Some(origin), &http::Method::OPTIONS) => {
310 if !self.is_origin_allowed(origin) {
313 return Err(Forbidden::Origin);
314 }
315
316 if let Some(req_method) = headers.get(header::ACCESS_CONTROL_REQUEST_METHOD) {
317 if !self.is_method_allowed(req_method) {
318 return Err(Forbidden::Method);
319 }
320 } else {
321 tracing::warn!(
322 "cors: preflight request missing `access-control-request-method` header"
323 );
324 return Err(Forbidden::Method);
325 }
326
327 if let Some(req_headers) = headers.get(header::ACCESS_CONTROL_REQUEST_HEADERS) {
328 let headers = match req_headers.to_str() {
329 Ok(val) => val,
330 Err(err) => {
331 tracing::error!(
332 "cors: error parsing header `access-control-request-headers` value: {:?}",
333 err,
334 );
335 return Err(Forbidden::Header);
336 }
337 };
338
339 for header in headers.split(',') {
340 let h = header.trim();
341 if !self.is_header_allowed(h) {
342 tracing::error!(
343 "cors: header `{}` is not allowed because is missing in `cors_allow_headers` server option",
344 h
345 );
346 return Err(Forbidden::Header);
347 }
348 }
349 }
350
351 let mut headers = HeaderMap::new();
352 self.append_preflight_headers(&mut headers);
353 headers.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin.into());
354
355 Ok((headers, Validated::Preflight(origin.clone())))
356 }
357 (Some(origin), _) => {
358 tracing::trace!("cors origin header: {:?}", origin);
360
361 if self.is_origin_allowed(origin) {
362 let mut headers = HeaderMap::new();
363 self.append_preflight_headers(&mut headers);
364 headers.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin.into());
365
366 Ok((headers, Validated::Simple(origin.clone())))
367 } else {
368 Err(Forbidden::Origin)
369 }
370 }
371 _ => {
372 Ok((HeaderMap::new(), Validated::NotCors))
374 }
375 }
376 }
377
378 fn is_method_allowed(&self, header: &HeaderValue) -> bool {
379 http::Method::from_bytes(header.as_bytes())
380 .map(|method| self.cors.allowed_methods.contains(&method))
381 .unwrap_or(false)
382 }
383
384 fn is_header_allowed(&self, header: &str) -> bool {
385 if header.is_empty() {
386 return false;
387 }
388 HeaderName::from_bytes(header.as_bytes())
389 .map(|header| self.cors.allowed_headers.contains(&header))
390 .unwrap_or(false)
391 }
392
393 fn is_origin_allowed(&self, origin: &HeaderValue) -> bool {
394 if origin.is_empty() {
395 return false;
396 }
397 if let Some(ref allowed) = self.cors.origins {
398 allowed.contains(origin)
399 } else {
400 true
401 }
402 }
403
404 fn append_preflight_headers(&self, headers: &mut HeaderMap) {
405 headers.typed_insert(self.allowed_headers.clone());
406 headers.typed_insert(self.exposed_headers.clone());
407 headers.typed_insert(self.methods_header.clone());
408
409 if let Some(max_age) = self.cors.max_age {
410 headers.insert(header::ACCESS_CONTROL_MAX_AGE, max_age.into());
411 }
412 }
413}
414
415pub trait IntoOrigin {
417 fn into_origin(self) -> Result<Origin, Error>;
419}
420
421impl IntoOrigin for &str {
422 fn into_origin(self) -> Result<Origin, Error> {
423 let (scheme, rest) = self
424 .split_once("://")
425 .ok_or_else(|| Error::msg("cors::into_origin: expected `scheme://host[:port]`"))?;
426 Origin::try_from_parts(scheme, rest, None)
427 .map_err(|_| Error::msg("cors::into_origin: invalid Origin"))
428 }
429}
430
431pub(crate) fn init(
433 cors_allow_origins: &str,
434 cors_allow_headers: &str,
435 cors_expose_headers: &str,
436 handler_opts: &mut RequestHandlerOpts,
437) {
438 handler_opts.cors = new(
439 cors_allow_origins.trim(),
440 cors_allow_headers.trim(),
441 cors_expose_headers.trim(),
442 );
443}
444
445#[derive(Clone)]
448pub(crate) struct CorsHeaders(pub(crate) HeaderMap);
449
450pub(crate) fn pre_process<T>(
452 opts: &RequestHandlerOpts,
453 req: &mut Request<T>,
454) -> Option<Result<Response<Body>, Error>> {
455 let cors = opts.cors.as_ref()?;
456 match cors.check_request(req.method(), req.headers()) {
457 Ok((headers, state)) => {
458 tracing::debug!("cors state: {:?}", state);
459 if !headers.is_empty() {
461 req.extensions_mut().insert(CorsHeaders(headers));
462 }
463 None
464 }
465 Err(err) => {
466 tracing::error!("cors error kind: {:?}", err);
467 Some(error_page::error_response(
468 req.uri(),
469 req.method(),
470 &StatusCode::FORBIDDEN,
471 &opts.page404,
472 &opts.page50x,
473 ))
474 }
475 }
476}
477
478pub(crate) fn post_process<T>(
480 opts: &RequestHandlerOpts,
481 req: &Request<T>,
482 mut resp: Response<Body>,
483) -> Result<Response<Body>, Error> {
484 if opts.cors.is_some()
485 && let Some(cors_headers) = req.extensions().get::<CorsHeaders>()
486 {
487 for (k, v) in cors_headers.0.iter() {
488 resp.headers_mut().insert(k, v.to_owned());
489 }
490 resp.headers_mut().insert(
491 http::header::VARY,
492 HeaderValue::from_name(http::header::ORIGIN),
493 );
494 resp.headers_mut().remove(http::header::ALLOW);
495 }
496 Ok(resp)
497}
498
499#[cfg(test)]
500mod tests {
501 use super::{Configured, Cors, post_process, pre_process};
502 use crate::body::Body;
503 use crate::{Error, handler::RequestHandlerOpts};
504 use hyper::{Request, Response, StatusCode};
505
506 fn make_request(method: &str, origin: &str) -> Request<Body> {
507 let mut builder = Request::builder();
508 if !origin.is_empty() {
509 builder = builder.header("Origin", origin);
510 }
511 builder
512 .method(method)
513 .uri("/")
514 .body(crate::body::empty())
515 .unwrap()
516 }
517
518 fn make_response() -> Response<Body> {
519 Response::builder().body(crate::body::empty()).unwrap()
520 }
521
522 fn make_cors_config() -> Option<Configured> {
523 let cors = Cors::new()
524 .allow_origins(vec!["https://example.com/"])
525 .unwrap()
526 .allow_headers(vec!["X-Allowed"])
527 .unwrap()
528 .allow_methods(vec!["GET", "HEAD"])
529 .unwrap();
530 Cors::build(Some(cors))
531 }
532
533 fn get_allowed_origin(resp: Response<Body>) -> Option<String> {
534 resp.headers()
535 .get("Access-Control-Allow-Origin")
536 .and_then(|v| v.to_str().ok())
537 .map(|s| s.to_owned())
538 }
539
540 fn is_403(result: Option<Result<Response<Body>, Error>>) -> bool {
541 if let Some(Ok(response)) = result {
542 response.status() == StatusCode::FORBIDDEN
543 } else {
544 false
545 }
546 }
547
548 #[test]
549 fn test_cors_disabled() -> Result<(), Error> {
550 let opts = RequestHandlerOpts {
551 cors: None,
552 ..Default::default()
553 };
554 let mut req = make_request("GET", "https://example.com/");
555
556 assert!(pre_process(&opts, &mut req).is_none());
557
558 let resp = post_process(&opts, &req, make_response())?;
559 assert_eq!(get_allowed_origin(resp), None);
560
561 Ok(())
562 }
563
564 #[test]
565 fn test_non_cors_request() -> Result<(), Error> {
566 let opts = RequestHandlerOpts {
567 cors: make_cors_config(),
568 ..Default::default()
569 };
570 let mut req = make_request("GET", "");
571
572 assert!(pre_process(&opts, &mut req).is_none());
573
574 let resp = post_process(&opts, &req, make_response())?;
575 assert_eq!(get_allowed_origin(resp), None);
576
577 Ok(())
578 }
579
580 #[test]
581 fn test_forbidden_request() {
582 let opts = RequestHandlerOpts {
583 cors: make_cors_config(),
584 ..Default::default()
585 };
586
587 assert!(is_403(pre_process(
588 &opts,
589 &mut make_request("GET", "https://example.info")
590 )));
591 assert!(is_403(pre_process(
592 &opts,
593 &mut make_request("OPTIONS", "https://example.com")
594 )));
595
596 let mut req = make_request("OPTIONS", "https://example.com");
597 req.headers_mut()
598 .insert("Access-Control-Request-Method", "POST".try_into().unwrap());
599 assert!(is_403(pre_process(&opts, &mut req)));
600
601 let mut req = make_request("OPTIONS", "https://example.com");
602 req.headers_mut()
603 .insert("Access-Control-Request-Method", "GET".try_into().unwrap());
604 req.headers_mut().insert(
605 "Access-Control-Request-Headers",
606 "X-Forbidden".try_into().unwrap(),
607 );
608 assert!(is_403(pre_process(&opts, &mut req)));
609 }
610
611 #[test]
612 fn test_allowed_request() -> Result<(), Error> {
613 let opts = RequestHandlerOpts {
614 cors: make_cors_config(),
615 ..Default::default()
616 };
617
618 let mut req = make_request("GET", "https://example.com");
619 assert!(pre_process(&opts, &mut req).is_none());
620
621 let resp = post_process(&opts, &req, make_response())?;
622 assert_eq!(get_allowed_origin(resp), Some("https://example.com".into()));
623
624 let mut req = make_request("GET", "https://example.com");
625 req.headers_mut()
626 .insert("Access-Control-Request-Method", "GET".try_into().unwrap());
627 req.headers_mut().insert(
628 "Access-Control-Request-Headers",
629 "X-Allowed".try_into().unwrap(),
630 );
631 assert!(pre_process(&opts, &mut req).is_none());
632
633 let resp = post_process(&opts, &req, make_response())?;
634 assert_eq!(get_allowed_origin(resp), Some("https://example.com".into()));
635
636 Ok(())
637 }
638
639 use super::{validate_header_names, validate_origin_str};
646 use headers::{HeaderName, Origin};
647 use proptest::prelude::*;
648
649 proptest! {
650 #![proptest_config(ProptestConfig {
651 cases: 256, ..ProptestConfig::default()
652 })]
653
654 #[test]
659 fn prop_validate_origin_str_never_panics(origin in "\\PC{0,128}") {
660 let ok = validate_origin_str("cors.allow_origins", &origin);
661 if ok {
662 let (scheme, rest) = origin.split_once("://").unwrap();
663 prop_assert!(
664 Origin::try_from_parts(scheme, rest, None).is_ok(),
665 "validator accepted `{origin}` but `Origin::try_from_parts` rejects it"
666 );
667 }
668 }
669
670 #[test]
673 fn prop_validate_origin_str_rejects_missing_scheme(s in "[^/:\\s]{0,32}") {
674 prop_assert!(
675 !validate_origin_str("cors.allow_origins", &s),
676 "validator accepted `{s}` which has no `scheme://` separator"
677 );
678 }
679
680 #[test]
683 fn prop_validate_header_names_retains_only_valid(
684 names in proptest::collection::vec("\\PC{0,32}", 0..16),
685 ) {
686 let slice: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
687 let kept = validate_header_names("cors.allow_headers", &slice);
688 for h in &kept {
689 prop_assert!(
690 HeaderName::try_from(*h).is_ok(),
691 "validator kept invalid header name `{h}`"
692 );
693 }
694 let kept2 = validate_header_names("cors.allow_headers", &kept);
696 prop_assert_eq!(kept, kept2);
697 }
698 }
699}