api_tools/server/axum/layers/
cors.rs1use axum::http::{HeaderName, HeaderValue, Method};
4use tower_http::cors::{AllowOrigin, Any, CorsLayer};
5
6pub struct CorsConfig<'a> {
21 pub allow_origin: &'a str,
22 pub allow_methods: Vec<Method>,
23 pub allow_headers: Vec<HeaderName>,
24}
25
26pub fn cors(config: CorsConfig) -> CorsLayer {
45 let allow_origin = config.allow_origin;
46
47 let layer = CorsLayer::new()
48 .allow_methods(config.allow_methods)
49 .allow_headers(config.allow_headers);
50
51 if allow_origin == "*" {
52 layer.allow_origin(Any)
53 } else {
54 let origins = allow_origin
55 .split(',')
56 .filter(|url| *url != "*" && !url.is_empty())
57 .filter_map(|url| url.parse().ok())
58 .collect::<Vec<HeaderValue>>();
59
60 if origins.is_empty() {
61 layer.allow_origin(Any)
62 } else {
63 layer
64 .allow_origin(AllowOrigin::predicate(move |origin: &HeaderValue, _| {
65 origins.contains(origin)
66 }))
67 .allow_credentials(true)
68 }
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use axum::body::Body;
76 use axum::http::{Request, StatusCode, header};
77 use axum::response::Response;
78 use std::convert::Infallible;
79 use tower::{ServiceBuilder, ServiceExt};
80
81 fn ok_response() -> Response {
82 Response::builder().status(StatusCode::OK).body(Body::empty()).unwrap()
83 }
84
85 fn default_methods() -> Vec<Method> {
86 vec![Method::GET, Method::POST]
87 }
88
89 fn default_headers() -> Vec<HeaderName> {
90 vec![header::CONTENT_TYPE]
91 }
92
93 #[tokio::test]
94 async fn cors_with_wildcard_origin_allows_any() {
95 let layer = cors(CorsConfig {
96 allow_origin: "*",
97 allow_methods: default_methods(),
98 allow_headers: default_headers(),
99 });
100 let svc = ServiceBuilder::new()
101 .layer(layer)
102 .service(tower::service_fn(|_req: Request<Body>| async {
103 Ok::<_, Infallible>(ok_response())
104 }));
105
106 let req = Request::builder()
107 .method(Method::GET)
108 .uri("/")
109 .header(header::ORIGIN, "https://example.com")
110 .body(Body::empty())
111 .unwrap();
112 let resp = svc.oneshot(req).await.unwrap();
113
114 assert_eq!(resp.headers().get(header::ACCESS_CONTROL_ALLOW_ORIGIN).unwrap(), "*");
115 }
116
117 #[tokio::test]
118 async fn cors_whitelist_allows_authorized_origin() {
119 let layer = cors(CorsConfig {
120 allow_origin: "https://allowed.com,https://other.com",
121 allow_methods: default_methods(),
122 allow_headers: default_headers(),
123 });
124 let svc = ServiceBuilder::new()
125 .layer(layer)
126 .service(tower::service_fn(|_req: Request<Body>| async {
127 Ok::<_, Infallible>(ok_response())
128 }));
129
130 let req = Request::builder()
131 .method(Method::GET)
132 .uri("/")
133 .header(header::ORIGIN, "https://allowed.com")
134 .body(Body::empty())
135 .unwrap();
136 let resp = svc.oneshot(req).await.unwrap();
137
138 assert_eq!(
139 resp.headers().get(header::ACCESS_CONTROL_ALLOW_ORIGIN).unwrap(),
140 "https://allowed.com",
141 );
142 assert_eq!(
143 resp.headers().get(header::ACCESS_CONTROL_ALLOW_CREDENTIALS).unwrap(),
144 "true",
145 );
146 }
147
148 #[tokio::test]
149 async fn cors_whitelist_omits_allow_origin_for_unauthorized_origin() {
150 let layer = cors(CorsConfig {
151 allow_origin: "https://allowed.com",
152 allow_methods: default_methods(),
153 allow_headers: default_headers(),
154 });
155 let svc = ServiceBuilder::new()
156 .layer(layer)
157 .service(tower::service_fn(|_req: Request<Body>| async {
158 Ok::<_, Infallible>(ok_response())
159 }));
160
161 let req = Request::builder()
162 .method(Method::GET)
163 .uri("/")
164 .header(header::ORIGIN, "https://forbidden.com")
165 .body(Body::empty())
166 .unwrap();
167 let resp = svc.oneshot(req).await.unwrap();
168
169 assert!(resp.headers().get(header::ACCESS_CONTROL_ALLOW_ORIGIN).is_none());
172 }
173
174 #[tokio::test]
177 async fn cors_empty_origin_string_falls_back_to_any() {
178 let layer = cors(CorsConfig {
179 allow_origin: "",
180 allow_methods: default_methods(),
181 allow_headers: default_headers(),
182 });
183 let svc = ServiceBuilder::new()
184 .layer(layer)
185 .service(tower::service_fn(|_req: Request<Body>| async {
186 Ok::<_, Infallible>(ok_response())
187 }));
188
189 let req = Request::builder()
190 .method(Method::GET)
191 .uri("/")
192 .header(header::ORIGIN, "https://anywhere.com")
193 .body(Body::empty())
194 .unwrap();
195 let resp = svc.oneshot(req).await.unwrap();
196
197 assert_eq!(resp.headers().get(header::ACCESS_CONTROL_ALLOW_ORIGIN).unwrap(), "*");
198 }
199}