reinhardt_admin/server/
origin_guard.rs1use async_trait::async_trait;
32use reinhardt_http::{Handler, Middleware, Request, Response, Result};
33use std::sync::Arc;
34
35pub struct AdminOriginGuardMiddleware;
49
50#[async_trait]
51impl Middleware for AdminOriginGuardMiddleware {
52 async fn process(&self, request: Request, next: Arc<dyn Handler>) -> Result<Response> {
53 if is_safe_method(&request.method) {
55 return next.handle(request).await;
56 }
57
58 if !is_same_origin(&request.headers) {
60 tracing::warn!(
61 method = %request.method,
62 uri = %request.uri,
63 "Admin origin guard: cross-origin or missing origin"
64 );
65 return Ok(Response::new(hyper::StatusCode::FORBIDDEN)
66 .with_header("Content-Type", "application/json")
67 .with_body(
68 r#"{"error":"Forbidden: cross-origin admin requests are not allowed"}"#,
69 ));
70 }
71
72 next.handle(request).await
73 }
74}
75
76fn is_safe_method(method: &hyper::Method) -> bool {
78 matches!(
79 *method,
80 hyper::Method::GET | hyper::Method::HEAD | hyper::Method::OPTIONS
81 )
82}
83
84fn is_same_origin(headers: &hyper::HeaderMap) -> bool {
96 let host = match headers
97 .get(hyper::header::HOST)
98 .and_then(|v| v.to_str().ok())
99 {
100 Some(h) => h,
101 None => return false,
103 };
104
105 if let Some(origin) = headers
107 .get(hyper::header::ORIGIN)
108 .and_then(|v| v.to_str().ok())
109 {
110 return origin_matches_host(origin, host);
111 }
112
113 if let Some(referer) = headers
115 .get(hyper::header::REFERER)
116 .and_then(|v| v.to_str().ok())
117 {
118 return referer_matches_host(referer, host);
119 }
120
121 false
125}
126
127fn origin_matches_host(origin: &str, host: &str) -> bool {
129 let origin_host = origin.split("://").nth(1).unwrap_or(origin);
131 let origin_host = origin_host.trim_end_matches('/');
132 origin_host == host
133}
134
135fn referer_matches_host(referer: &str, host: &str) -> bool {
137 let after_scheme = referer.split("://").nth(1).unwrap_or(referer);
139 let referer_host = after_scheme.split('/').next().unwrap_or(after_scheme);
140 referer_host == host
141}
142
143#[cfg(all(test, server))]
144mod tests {
145 use super::*;
146 use bytes::Bytes;
147 use hyper::{HeaderMap, Method, StatusCode, Version};
148
149 struct PassthroughHandler;
150
151 #[async_trait]
152 impl Handler for PassthroughHandler {
153 async fn handle(&self, _request: Request) -> Result<Response> {
154 Ok(Response::new(StatusCode::OK).with_body("ok"))
155 }
156 }
157
158 fn make_request(method: Method, headers: HeaderMap) -> Request {
159 Request::builder()
160 .method(method)
161 .uri("/api/server_fn/get_list")
162 .version(Version::HTTP_11)
163 .headers(headers)
164 .body(Bytes::new())
165 .build()
166 .unwrap()
167 }
168
169 #[tokio::test]
170 async fn test_get_request_passes_through() {
171 let mw = AdminOriginGuardMiddleware;
172 let next = Arc::new(PassthroughHandler);
173 let req = make_request(Method::GET, HeaderMap::new());
174 let resp = mw.process(req, next).await.unwrap();
175 assert_eq!(resp.status, StatusCode::OK);
176 }
177
178 #[tokio::test]
179 async fn test_post_without_origin_returns_403() {
180 let mw = AdminOriginGuardMiddleware;
181 let next = Arc::new(PassthroughHandler);
182 let mut headers = HeaderMap::new();
183 headers.insert(hyper::header::HOST, "localhost:8000".parse().unwrap());
184 let req = make_request(Method::POST, headers);
185 let resp = mw.process(req, next).await.unwrap();
186 assert_eq!(resp.status, StatusCode::FORBIDDEN);
187 }
188
189 #[tokio::test]
190 async fn test_post_without_host_returns_403() {
191 let mw = AdminOriginGuardMiddleware;
192 let next = Arc::new(PassthroughHandler);
193 let mut headers = HeaderMap::new();
194 headers.insert(
195 hyper::header::ORIGIN,
196 "http://localhost:8000".parse().unwrap(),
197 );
198 let req = make_request(Method::POST, headers);
199 let resp = mw.process(req, next).await.unwrap();
200 assert_eq!(resp.status, StatusCode::FORBIDDEN);
201 }
202
203 #[tokio::test]
204 async fn test_post_same_origin_passes() {
205 let mw = AdminOriginGuardMiddleware;
206 let next = Arc::new(PassthroughHandler);
207 let mut headers = HeaderMap::new();
208 headers.insert(hyper::header::HOST, "localhost:8000".parse().unwrap());
209 headers.insert(
210 hyper::header::ORIGIN,
211 "http://localhost:8000".parse().unwrap(),
212 );
213 let req = make_request(Method::POST, headers);
214 let resp = mw.process(req, next).await.unwrap();
215 assert_eq!(resp.status, StatusCode::OK);
216 }
217
218 #[tokio::test]
219 async fn test_post_different_origin_returns_403() {
220 let mw = AdminOriginGuardMiddleware;
221 let next = Arc::new(PassthroughHandler);
222 let mut headers = HeaderMap::new();
223 headers.insert(hyper::header::HOST, "localhost:8000".parse().unwrap());
224 headers.insert(hyper::header::ORIGIN, "http://evil.com".parse().unwrap());
225 let req = make_request(Method::POST, headers);
226 let resp = mw.process(req, next).await.unwrap();
227 assert_eq!(resp.status, StatusCode::FORBIDDEN);
228 }
229
230 #[tokio::test]
231 async fn test_post_referer_same_origin_passes() {
232 let mw = AdminOriginGuardMiddleware;
233 let next = Arc::new(PassthroughHandler);
234 let mut headers = HeaderMap::new();
235 headers.insert(hyper::header::HOST, "example.com".parse().unwrap());
236 headers.insert(
237 hyper::header::REFERER,
238 "https://example.com/admin/".parse().unwrap(),
239 );
240 let req = make_request(Method::POST, headers);
241 let resp = mw.process(req, next).await.unwrap();
242 assert_eq!(resp.status, StatusCode::OK);
243 }
244
245 #[tokio::test]
246 async fn test_post_referer_different_origin_returns_403() {
247 let mw = AdminOriginGuardMiddleware;
248 let next = Arc::new(PassthroughHandler);
249 let mut headers = HeaderMap::new();
250 headers.insert(hyper::header::HOST, "example.com".parse().unwrap());
251 headers.insert(
252 hyper::header::REFERER,
253 "https://evil.com/admin/".parse().unwrap(),
254 );
255 let req = make_request(Method::POST, headers);
256 let resp = mw.process(req, next).await.unwrap();
257 assert_eq!(resp.status, StatusCode::FORBIDDEN);
258 }
259
260 #[tokio::test]
261 async fn test_options_request_passes_through() {
262 let mw = AdminOriginGuardMiddleware;
263 let next = Arc::new(PassthroughHandler);
264 let req = make_request(Method::OPTIONS, HeaderMap::new());
265 let resp = mw.process(req, next).await.unwrap();
266 assert_eq!(resp.status, StatusCode::OK);
267 }
268
269 #[test]
270 fn test_origin_matches_host() {
271 assert!(origin_matches_host(
272 "http://localhost:8000",
273 "localhost:8000"
274 ));
275 assert!(origin_matches_host("https://example.com", "example.com"));
276 assert!(!origin_matches_host("http://evil.com", "example.com"));
277 assert!(!origin_matches_host(
278 "http://localhost:9000",
279 "localhost:8000"
280 ));
281 }
282
283 #[test]
284 fn test_referer_matches_host() {
285 assert!(referer_matches_host(
286 "http://localhost:8000/admin/",
287 "localhost:8000"
288 ));
289 assert!(referer_matches_host(
290 "https://example.com/admin/model/",
291 "example.com"
292 ));
293 assert!(!referer_matches_host(
294 "http://evil.com/admin/",
295 "example.com"
296 ));
297 }
298}