Skip to main content

siderite_core/middleware/
cors.rs

1//! CORS and response compression (thin, configurable wrappers over
2//! `tower-http`).
3
4use super::adapt::{BoxService, impl_layer, wrap_engine};
5use http::{HeaderName, HeaderValue, Method};
6use std::time::Duration;
7use tower_http::compression::CompressionLayer;
8use tower_http::cors::{AllowHeaders, AllowMethods, AllowOrigin, CorsLayer};
9
10#[derive(Debug, Clone, Default)]
11enum Allow<T> {
12    #[default]
13    Nothing,
14    Any,
15    List(Vec<T>),
16}
17
18/// Cross-origin resource sharing.
19///
20/// [`Cors::new`] allows nothing; add origins/methods/headers or start from
21/// [`Cors::permissive`]. With `allow_credentials(true)`, "any" is answered by
22/// mirroring the request (browsers reject a literal `*` together with
23/// credentials).
24#[derive(Debug, Clone, Default)]
25pub struct Cors {
26    origins: Allow<HeaderValue>,
27    methods: Allow<Method>,
28    headers: Allow<HeaderName>,
29    expose: Vec<HeaderName>,
30    credentials: bool,
31    max_age: Option<Duration>,
32}
33
34impl Cors {
35    /// A policy that allows no cross-origin access until configured.
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// Any origin, method and header; no credentials.
41    pub fn permissive() -> Self {
42        Self::new()
43            .allow_any_origin()
44            .allow_any_method()
45            .allow_any_header()
46    }
47
48    /// Allow one origin (e.g. `https://app.example.com`). Invalid values are
49    /// ignored with a warning.
50    #[must_use]
51    pub fn allow_origin(mut self, origin: &str) -> Self {
52        match HeaderValue::from_str(origin) {
53            Ok(value) => push(&mut self.origins, value),
54            Err(_) => tracing::warn!(origin, "ignoring invalid CORS origin"),
55        }
56        self
57    }
58
59    /// Allow every origin.
60    #[must_use]
61    pub fn allow_any_origin(mut self) -> Self {
62        self.origins = Allow::Any;
63        self
64    }
65
66    /// Allow one method.
67    #[must_use]
68    pub fn allow_method(mut self, method: Method) -> Self {
69        push(&mut self.methods, method);
70        self
71    }
72
73    /// Allow every method.
74    #[must_use]
75    pub fn allow_any_method(mut self) -> Self {
76        self.methods = Allow::Any;
77        self
78    }
79
80    /// Allow one request header. Invalid names are ignored with a warning.
81    #[must_use]
82    pub fn allow_header(mut self, name: &str) -> Self {
83        match HeaderName::from_bytes(name.as_bytes()) {
84            Ok(name) => push(&mut self.headers, name),
85            Err(_) => tracing::warn!(name, "ignoring invalid CORS header name"),
86        }
87        self
88    }
89
90    /// Allow every request header.
91    #[must_use]
92    pub fn allow_any_header(mut self) -> Self {
93        self.headers = Allow::Any;
94        self
95    }
96
97    /// Expose a response header to browser scripts.
98    #[must_use]
99    pub fn expose_header(mut self, name: &str) -> Self {
100        match HeaderName::from_bytes(name.as_bytes()) {
101            Ok(name) => self.expose.push(name),
102            Err(_) => tracing::warn!(name, "ignoring invalid CORS header name"),
103        }
104        self
105    }
106
107    /// Allow credentials (cookies, authorization headers).
108    #[must_use]
109    pub fn allow_credentials(mut self, allow: bool) -> Self {
110        self.credentials = allow;
111        self
112    }
113
114    /// How long browsers may cache preflight results.
115    #[must_use]
116    pub fn max_age(mut self, max_age: Duration) -> Self {
117        self.max_age = Some(max_age);
118        self
119    }
120
121    fn wrap(&self, inner: BoxService) -> BoxService {
122        let mirror = self.credentials;
123        let origin = match &self.origins {
124            Allow::Nothing => AllowOrigin::list([]),
125            Allow::Any if mirror => AllowOrigin::mirror_request(),
126            Allow::Any => AllowOrigin::any(),
127            Allow::List(v) => AllowOrigin::list(v.clone()),
128        };
129        let methods = match &self.methods {
130            Allow::Nothing => AllowMethods::list([]),
131            Allow::Any if mirror => AllowMethods::mirror_request(),
132            Allow::Any => AllowMethods::any(),
133            Allow::List(v) => AllowMethods::list(v.clone()),
134        };
135        let headers = match &self.headers {
136            Allow::Nothing => AllowHeaders::list([]),
137            Allow::Any if mirror => AllowHeaders::mirror_request(),
138            Allow::Any => AllowHeaders::any(),
139            Allow::List(v) => AllowHeaders::list(v.clone()),
140        };
141        let mut layer = CorsLayer::new()
142            .allow_origin(origin)
143            .allow_methods(methods)
144            .allow_headers(headers)
145            .expose_headers(self.expose.clone())
146            .allow_credentials(self.credentials);
147        if let Some(age) = self.max_age {
148            layer = layer.max_age(age);
149        }
150        wrap_engine(&layer, inner)
151    }
152}
153
154fn push<T>(slot: &mut Allow<T>, item: T) {
155    match slot {
156        Allow::List(items) => items.push(item),
157        other => *other = Allow::List(vec![item]),
158    }
159}
160
161/// Response compression (gzip and brotli, negotiated via `Accept-Encoding`).
162///
163/// Tiny bodies, images and already-compressed types are left alone.
164#[derive(Debug, Clone)]
165pub struct Compression {
166    gzip: bool,
167    brotli: bool,
168}
169
170impl Default for Compression {
171    fn default() -> Self {
172        Self {
173            gzip: true,
174            brotli: true,
175        }
176    }
177}
178
179impl Compression {
180    /// gzip and brotli enabled.
181    pub fn new() -> Self {
182        Self::default()
183    }
184
185    /// Enable or disable gzip.
186    #[must_use]
187    pub fn gzip(mut self, enabled: bool) -> Self {
188        self.gzip = enabled;
189        self
190    }
191
192    /// Enable or disable brotli.
193    #[must_use]
194    pub fn brotli(mut self, enabled: bool) -> Self {
195        self.brotli = enabled;
196        self
197    }
198
199    fn wrap(&self, inner: BoxService) -> BoxService {
200        let layer = CompressionLayer::new().gzip(self.gzip).br(self.brotli);
201        wrap_engine(&layer, inner)
202    }
203}
204
205impl_layer!(Cors, Compression);