Skip to main content

salvo_cors/
lib.rs

1#![cfg_attr(test, allow(clippy::unwrap_used))]
2//! CORS (Cross-Origin Resource Sharing) protection for Salvo web framework.
3//!
4//! # Important
5//!
6//! The CORS handler must be added to [`Service`](salvo_core::Service) via `.hoop()`,
7//! **not** to [`Router`](salvo_core::Router). This is because browsers send
8//! `OPTIONS` preflight requests that don't match any route, and only
9//! `Service`-level middleware can intercept them.
10//!
11//! ```no_run
12//! use salvo_core::http::Method;
13//! use salvo_core::prelude::*;
14//! use salvo_cors::Cors;
15//!
16//! #[handler]
17//! async fn hello() -> &'static str {
18//!     "Hello World"
19//! }
20//!
21//! fn main() {
22//!     let cors = Cors::new()
23//!         .allow_origin("http://localhost:3000")
24//!         .allow_methods([Method::GET, Method::POST, Method::DELETE])
25//!         .allow_headers("authorization")
26//!         .into_handler();
27//!
28//!     let router = Router::new().get(hello);
29//!     // CORS must be on Service, NOT on Router
30//!     let _service = Service::new(router).hoop(cors);
31//! }
32//! ```
33//!
34//! [CORS]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
35#![doc(html_favicon_url = "https://salvo.rs/favicon-32x32.png")]
36#![doc(html_logo_url = "https://salvo.rs/images/logo.svg")]
37#![cfg_attr(docsrs, feature(doc_cfg))]
38
39use bytes::{BufMut, BytesMut};
40use salvo_core::http::header::{self, HeaderMap, HeaderName, HeaderValue};
41use salvo_core::http::{Method, Request, Response, StatusCode};
42use salvo_core::{Depot, FlowCtrl, Handler, async_trait};
43
44mod allow_credentials;
45mod allow_headers;
46mod allow_methods;
47mod allow_origin;
48mod allow_private_network;
49mod expose_headers;
50mod max_age;
51mod vary;
52
53pub use self::allow_credentials::AllowCredentials;
54pub use self::allow_headers::AllowHeaders;
55pub use self::allow_methods::AllowMethods;
56pub use self::allow_origin::AllowOrigin;
57pub use self::allow_private_network::AllowPrivateNetwork;
58pub use self::expose_headers::ExposeHeaders;
59pub use self::max_age::MaxAge;
60pub use self::vary::Vary;
61
62static WILDCARD: HeaderValue = HeaderValue::from_static("*");
63
64/// Represents a wildcard value (`*`) used with some CORS headers such as
65/// [`Cors::allow_methods`].
66#[derive(Debug, Clone, Copy)]
67#[must_use]
68pub struct Any;
69
70fn separated_by_commas<I>(mut iter: I) -> Option<HeaderValue>
71where
72    I: Iterator<Item = HeaderValue>,
73{
74    match iter.next() {
75        Some(fst) => {
76            let mut result = BytesMut::from(fst.as_bytes());
77            for val in iter {
78                result.reserve(val.len() + 1);
79                result.put_u8(b',');
80                result.extend_from_slice(val.as_bytes());
81            }
82
83            HeaderValue::from_maybe_shared(result.freeze()).ok()
84        }
85        None => None,
86    }
87}
88
89/// [`Cors`] middleware which adds headers for [CORS][mdn].
90///
91/// After building, call [`.into_handler()`](Cors::into_handler) and add the
92/// resulting handler to [`Service`](salvo_core::Service) via `.hoop()`.
93/// Do **not** add it to `Router` — preflight `OPTIONS` requests won't reach
94/// router-level middleware.
95///
96/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
97#[derive(Clone, Debug)]
98pub struct Cors {
99    allow_credentials: AllowCredentials,
100    allow_headers: AllowHeaders,
101    allow_methods: AllowMethods,
102    allow_origin: AllowOrigin,
103    allow_private_network: AllowPrivateNetwork,
104    expose_headers: ExposeHeaders,
105    max_age: MaxAge,
106    vary: Vary,
107}
108impl Default for Cors {
109    #[inline]
110    fn default() -> Self {
111        Self::new()
112    }
113}
114
115impl Cors {
116    /// Create new `Cors`.
117    #[inline]
118    #[must_use]
119    pub fn new() -> Self {
120        Self {
121            allow_credentials: Default::default(),
122            allow_headers: Default::default(),
123            allow_methods: Default::default(),
124            allow_origin: Default::default(),
125            allow_private_network: Default::default(),
126            expose_headers: Default::default(),
127            max_age: Default::default(),
128            vary: Default::default(),
129        }
130    }
131
132    /// A permissive configuration:
133    ///
134    /// - All request headers allowed.
135    /// - All methods allowed.
136    /// - All origins allowed.
137    /// - All headers exposed.
138    ///
139    /// # Security Warning
140    ///
141    /// **This configuration allows any website to make requests to your API.**
142    /// Only use this for:
143    /// - Public APIs that don't require authentication
144    /// - Development/testing environments
145    ///
146    /// For production APIs that require authentication, configure CORS explicitly
147    /// with specific allowed origins.
148    #[must_use]
149    pub fn permissive() -> Self {
150        Self::new()
151            .allow_headers(Any)
152            .allow_methods(Any)
153            .allow_origin(Any)
154            .expose_headers(Any)
155    }
156
157    /// A very permissive configuration:
158    ///
159    /// - **Credentials allowed.**
160    /// - The method received in `Access-Control-Request-Method` is sent back as an allowed method.
161    /// - The origin of the preflight request is sent back as an allowed origin.
162    /// - The header names received in `Access-Control-Request-Headers` are sent back as allowed
163    ///   headers.
164    /// - No headers are currently exposed, but this may change in the future.
165    ///
166    /// # Security Warning
167    ///
168    /// **⚠️ DANGER: This configuration essentially disables CORS protection!**
169    ///
170    /// By enabling credentials AND mirroring the request origin, you are allowing
171    /// ANY website to:
172    /// - Make authenticated requests to your API
173    /// - Read response data including sensitive information
174    /// - Perform actions on behalf of logged-in users (CSRF attacks)
175    ///
176    /// **This should NEVER be used in production with authentication.**
177    ///
178    /// Only use this for:
179    /// - Local development where security is not a concern
180    /// - Internal tools on trusted networks
181    ///
182    /// For production, always configure explicit allowed origins:
183    /// ```ignore
184    /// Cors::new()
185    ///     .allow_origin("https://your-frontend.com")
186    ///     .allow_credentials(true)
187    /// ```
188    #[must_use]
189    pub fn very_permissive() -> Self {
190        tracing::warn!(
191            "Using Cors::very_permissive() - this disables CORS security and should not be used in production!"
192        );
193        Self::new()
194            .allow_credentials(true)
195            .allow_headers(AllowHeaders::mirror_request())
196            .allow_methods(AllowMethods::mirror_request())
197            .allow_origin(AllowOrigin::mirror_request())
198    }
199
200    /// Sets whether to add the `Access-Control-Allow-Credentials` header.
201    #[inline]
202    #[must_use]
203    pub fn allow_credentials(mut self, allow_credentials: impl Into<AllowCredentials>) -> Self {
204        self.allow_credentials = allow_credentials.into();
205        self
206    }
207
208    /// Adds multiple headers to the list of allowed request headers.
209    ///
210    /// **Note**: These should match the values the browser sends via
211    /// `Access-Control-Request-Headers`, e.g.`content-type`.
212    ///
213    /// # Panics
214    ///
215    /// Panics if any of the headers are not a valid `http::header::HeaderName`.
216    #[inline]
217    #[must_use]
218    pub fn allow_headers(mut self, headers: impl Into<AllowHeaders>) -> Self {
219        self.allow_headers = headers.into();
220        self
221    }
222
223    /// Sets the `Access-Control-Max-Age` header.
224    ///
225    /// # Example
226    ///
227    /// ```
228    /// use std::time::Duration;
229    ///
230    /// use salvo_core::prelude::*;
231    /// use salvo_cors::Cors;
232    ///
233    /// let cors = Cors::new().max_age(30); // 30 seconds
234    /// let cors = Cors::new().max_age(Duration::from_secs(30)); // or a Duration
235    /// ```
236    #[inline]
237    #[must_use]
238    pub fn max_age(mut self, max_age: impl Into<MaxAge>) -> Self {
239        self.max_age = max_age.into();
240        self
241    }
242
243    /// Adds multiple methods to the existing list of allowed request methods.
244    ///
245    /// # Panics
246    ///
247    /// Panics if the provided argument is not a valid `http::Method`.
248    #[inline]
249    #[must_use]
250    pub fn allow_methods<I>(mut self, methods: I) -> Self
251    where
252        I: Into<AllowMethods>,
253    {
254        self.allow_methods = methods.into();
255        self
256    }
257
258    /// Set the value of the [`Access-Control-Allow-Origin`][mdn] header.
259    /// ```
260    /// use salvo_core::http::HeaderValue;
261    /// use salvo_cors::Cors;
262    ///
263    /// let cors = Cors::new().allow_origin("http://example.com".parse::<HeaderValue>().unwrap());
264    /// ```
265    ///
266    /// Multiple origins can be allowed with
267    ///
268    /// ```
269    /// use salvo_cors::Cors;
270    ///
271    /// let origins = ["http://example.com", "http://api.example.com"];
272    ///
273    /// let cors = Cors::new().allow_origin(origins);
274    /// ```
275    ///
276    /// All origins can be allowed with
277    ///
278    /// ```
279    /// use salvo_cors::{Any, Cors};
280    ///
281    /// let cors = Cors::new().allow_origin(Any);
282    /// ```
283    ///
284    /// You can also use a closure
285    ///
286    /// ```
287    /// use salvo_core::http::HeaderValue;
288    /// use salvo_core::{Depot, Request};
289    /// use salvo_cors::{AllowOrigin, Cors};
290    ///
291    /// let cors = Cors::new().allow_origin(AllowOrigin::dynamic(
292    ///     |origin: Option<&HeaderValue>, _req: &Request, _depot: &Depot| {
293    ///         if origin?.as_bytes().ends_with(b".rust-lang.org") {
294    ///             origin.cloned()
295    ///         } else {
296    ///             None
297    ///         }
298    ///     },
299    /// ));
300    /// ```
301    ///
302    /// You can also use an async closure, make sure all the values are owned
303    /// before passing into the future:
304    ///
305    /// ```
306    /// # #[derive(Clone)]
307    /// # struct Client;
308    /// # fn get_api_client() -> Client {
309    /// #     Client
310    /// # }
311    /// # impl Client {
312    /// #     async fn fetch_allowed_origins(&self) -> Vec<HeaderValue> {
313    /// #         vec![HeaderValue::from_static("http://example.com")]
314    /// #     }
315    /// #     async fn fetch_allowed_origins_for_path(&self, _path: String) -> Vec<HeaderValue> {
316    /// #         vec![HeaderValue::from_static("http://example.com")]
317    /// #     }
318    /// # }
319    /// use salvo_core::http::header::HeaderValue;
320    /// use salvo_core::{Depot, Request};
321    /// use salvo_cors::{AllowOrigin, Cors};
322    ///
323    /// let cors = Cors::new().allow_origin(AllowOrigin::dynamic_async(
324    ///     |origin: Option<&HeaderValue>, _req: &Request, _depot: &Depot| {
325    ///         let origin = origin.cloned();
326    ///         async move {
327    ///             let client = get_api_client();
328    ///             // fetch list of origins that are allowed
329    ///             let origins = client.fetch_allowed_origins().await;
330    ///             if origins.contains(origin.as_ref()?) {
331    ///                 origin
332    ///             } else {
333    ///                 None
334    ///             }
335    ///         }
336    ///     },
337    /// ));
338    /// ```
339    ///
340    /// **Note** that multiple calls to this method will override any previous
341    /// calls.
342    ///
343    /// **Note** origin must contain http or https protocol name.
344    ///
345    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
346    #[inline]
347    #[must_use]
348    pub fn allow_origin(mut self, origin: impl Into<AllowOrigin>) -> Self {
349        self.allow_origin = origin.into();
350        self
351    }
352
353    /// Set the value of the [`Access-Control-Expose-Headers`][mdn] header.
354    ///
355    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers
356    #[inline]
357    #[must_use]
358    pub fn expose_headers(mut self, headers: impl Into<ExposeHeaders>) -> Self {
359        self.expose_headers = headers.into();
360        self
361    }
362
363    /// Set the value of the [`Access-Control-Allow-Private-Network`][wicg] header.
364    ///
365    /// ```
366    /// use salvo_cors::Cors;
367    ///
368    /// let cors = Cors::new().allow_private_network(true);
369    /// ```
370    ///
371    /// [wicg]: https://wicg.github.io/private-network-access/
372    #[must_use]
373    pub fn allow_private_network<T>(mut self, allow_private_network: T) -> Self
374    where
375        T: Into<AllowPrivateNetwork>,
376    {
377        self.allow_private_network = allow_private_network.into();
378        self
379    }
380
381    /// Set the value(s) of the [`Vary`][mdn] header.
382    ///
383    /// In contrast to the other headers, this one has a non-empty default of
384    /// [`preflight_request_headers()`].
385    ///
386    /// You only need to set this is you want to remove some of these defaults,
387    /// or if you use a closure for one of the other headers and want to add a
388    /// vary header accordingly.
389    ///
390    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary
391    #[must_use]
392    pub fn vary<T>(mut self, headers: impl Into<Vary>) -> Self {
393        self.vary = headers.into();
394        self
395    }
396
397    /// Returns a new `CorsHandler` using current cors settings.
398    pub fn into_handler(self) -> CorsHandler {
399        self.ensure_usable_cors_rules();
400        CorsHandler::new(self, CallNext::default())
401    }
402
403    fn ensure_usable_cors_rules(&self) {
404        if self.allow_credentials.is_true() {
405            assert!(
406                !self.allow_headers.is_wildcard(),
407                "Invalid CORS configuration: Cannot combine `Access-Control-Allow-Credentials: true` \
408                 with `Access-Control-Allow-Headers: *`"
409            );
410
411            assert!(
412                !self.allow_methods.is_wildcard(),
413                "Invalid CORS configuration: Cannot combine `Access-Control-Allow-Credentials: true` \
414                 with `Access-Control-Allow-Methods: *`"
415            );
416
417            assert!(
418                !self.allow_origin.is_wildcard(),
419                "Invalid CORS configuration: Cannot combine `Access-Control-Allow-Credentials: true` \
420                 with `Access-Control-Allow-Origin: *`"
421            );
422
423            assert!(
424                !self.expose_headers.is_wildcard(),
425                "Invalid CORS configuration: Cannot combine `Access-Control-Allow-Credentials: true` \
426                 with `Access-Control-Expose-Headers: *`"
427            );
428        }
429    }
430}
431
432/// Enum to control when to call next handler.
433#[non_exhaustive]
434#[derive(Default, Clone, Copy, Eq, PartialEq, Debug)]
435pub enum CallNext {
436    /// Call next handlers before [`CorsHandler`] write data to response.
437    #[default]
438    Before,
439    /// Call next handlers after [`CorsHandler`] write data to response.
440    After,
441}
442
443/// CorsHandler
444#[derive(Clone, Debug)]
445pub struct CorsHandler {
446    cors: Cors,
447    call_next: CallNext,
448}
449impl CorsHandler {
450    /// Create a new `CorsHandler`.
451    pub fn new(cors: Cors, call_next: CallNext) -> Self {
452        Self { cors, call_next }
453    }
454}
455
456#[async_trait]
457impl Handler for CorsHandler {
458    async fn handle(
459        &self,
460        req: &mut Request,
461        depot: &mut Depot,
462        res: &mut Response,
463        ctrl: &mut FlowCtrl,
464    ) {
465        if self.call_next == CallNext::Before {
466            ctrl.call_next(req, depot, res).await;
467        }
468
469        let origin = req.headers().get(&header::ORIGIN);
470        let mut headers = HeaderMap::new();
471
472        // These headers are applied to both preflight and subsequent regular CORS requests:
473        // https://fetch.spec.whatwg.org/#http-responses
474        headers.extend(self.cors.allow_origin.to_header(origin, req, depot).await);
475        headers.extend(
476            self.cors
477                .allow_credentials
478                .to_header(origin, req, depot)
479                .await,
480        );
481        headers.extend(
482            self.cors
483                .allow_private_network
484                .to_header(origin, req, depot)
485                .await,
486        );
487
488        let mut vary_headers = self.cors.vary.values();
489        if let Some(first) = vary_headers.next() {
490            let mut header = match headers.entry(header::VARY) {
491                header::Entry::Occupied(_) => {
492                    unreachable!("no vary header inserted up to this point")
493                }
494                header::Entry::Vacant(v) => v.insert_entry(first),
495            };
496
497            for val in vary_headers {
498                header.append(val);
499            }
500        }
501
502        // Return results immediately upon preflight request
503        if req.method() == Method::OPTIONS {
504            // These headers are applied only to preflight requests
505            headers.extend(self.cors.allow_methods.to_header(origin, req, depot).await);
506            headers.extend(self.cors.allow_headers.to_header(origin, req, depot).await);
507            headers.extend(self.cors.max_age.to_header(origin, req, depot).await);
508            res.status_code = Some(StatusCode::NO_CONTENT);
509        } else {
510            // This header is applied only to non-preflight requests
511            headers.extend(self.cors.expose_headers.to_header(origin, req, depot).await);
512        }
513        res.headers_mut().extend(headers);
514
515        if self.call_next == CallNext::After {
516            ctrl.call_next(req, depot, res).await;
517        }
518    }
519}
520
521/// Iterator over the three request headers that may be involved in a CORS preflight request.
522///
523/// This is the default set of header names returned in the `vary` header
524pub fn preflight_request_headers() -> impl Iterator<Item = HeaderName> {
525    [
526        header::ORIGIN,
527        header::ACCESS_CONTROL_REQUEST_METHOD,
528        header::ACCESS_CONTROL_REQUEST_HEADERS,
529    ]
530    .into_iter()
531}
532
533#[cfg(test)]
534mod tests {
535    use salvo_core::http::header::*;
536    use salvo_core::prelude::*;
537    use salvo_core::test::TestClient;
538
539    use super::*;
540
541    #[tokio::test]
542    async fn test_cors() {
543        let cors_handler = Cors::new()
544            .allow_origin("https://salvo.rs")
545            .allow_methods(vec![Method::GET, Method::POST, Method::OPTIONS])
546            .allow_headers(vec![
547                "CONTENT-TYPE",
548                "Access-Control-Request-Method",
549                "Access-Control-Allow-Origin",
550                "Access-Control-Allow-Headers",
551                "Access-Control-Max-Age",
552            ])
553            .into_handler();
554
555        #[handler]
556        async fn hello() -> &'static str {
557            "hello"
558        }
559
560        let router = Router::new()
561            .hoop(cors_handler)
562            .push(Router::with_path("hello").goal(hello));
563        let service = Service::new(router);
564
565        async fn options_access(service: &Service, origin: &str) -> Response {
566            TestClient::options("http://127.0.0.1:5801/hello")
567                .add_header("Origin", origin, true)
568                .add_header("Access-Control-Request-Method", "POST", true)
569                .add_header("Access-Control-Request-Headers", "Content-Type", true)
570                .send(service)
571                .await
572        }
573
574        let res = TestClient::options("https://salvo.rs").send(&service).await;
575        assert!(res.headers().get(ACCESS_CONTROL_ALLOW_METHODS).is_none());
576
577        let res = options_access(&service, "https://salvo.rs").await;
578        let headers = res.headers();
579        assert!(headers.get(ACCESS_CONTROL_ALLOW_METHODS).is_some());
580        assert!(headers.get(ACCESS_CONTROL_ALLOW_HEADERS).is_some());
581
582        let res = TestClient::options("https://google.com")
583            .send(&service)
584            .await;
585        let headers = res.headers();
586        assert!(
587            headers.get(ACCESS_CONTROL_ALLOW_METHODS).is_none(),
588            "POST, GET, DELETE, OPTIONS"
589        );
590        assert!(headers.get(ACCESS_CONTROL_ALLOW_HEADERS).is_none());
591    }
592}