1use axum::extract::{Request, State};
10use axum::http::{Method, header};
11use axum::middleware::Next;
12use axum::response::{IntoResponse as _, Response};
13
14use crate::error::Error;
15use crate::state::AppState;
16
17pub async fn require_same_origin(
25 State(state): State<AppState>,
26 req: Request,
27 next: Next,
28) -> Response {
29 if is_safe_method(req.method()) {
30 return next.run(req).await;
31 }
32 let expected = state.config.public_base_url.trim_end_matches('/');
33 let headers = req.headers();
34 if let Some(origin) = header_str(headers, &header::ORIGIN) {
35 if origin.trim_end_matches('/') == expected {
36 return next.run(req).await;
37 }
38 return Error::Forbidden(String::from("origin mismatch")).into_response();
39 }
40 if let Some(referer) = header_str(headers, &header::REFERER) {
41 if origin_of(referer).is_some_and(|o| o == expected) {
42 return next.run(req).await;
43 }
44 return Error::Forbidden(String::from("referer origin mismatch")).into_response();
45 }
46 Error::Forbidden(String::from("missing Origin and Referer headers")).into_response()
47}
48
49const fn is_safe_method(method: &Method) -> bool {
51 matches!(*method, Method::GET | Method::HEAD | Method::OPTIONS)
52}
53
54fn header_str<'a>(
57 headers: &'a axum::http::HeaderMap,
58 name: &header::HeaderName,
59) -> Option<&'a str> {
60 headers.get(name).and_then(|v| v.to_str().ok())
61}
62
63fn origin_of(url: &str) -> Option<&str> {
67 let scheme_end = url.find("://")?;
68 let after_scheme = scheme_end.checked_add(3)?;
69 let rest = url.get(after_scheme..)?;
70 let host_end = rest.find('/').unwrap_or(rest.len());
71 let total_end = after_scheme.checked_add(host_end)?;
72 url.get(..total_end)
73}
74
75#[cfg(test)]
76mod tests {
77 use pretty_assertions::assert_eq;
78
79 use super::origin_of;
80
81 #[test]
82 fn origin_of_strips_path_and_query() {
83 assert_eq!(
84 origin_of("https://maps.example.org/library?x=1"),
85 Some("https://maps.example.org"),
86 );
87 }
88
89 #[test]
90 fn origin_of_keeps_port() {
91 assert_eq!(
92 origin_of("http://localhost:3000/foo"),
93 Some("http://localhost:3000"),
94 );
95 }
96
97 #[test]
98 fn origin_of_with_no_path() {
99 assert_eq!(
100 origin_of("https://maps.example.org"),
101 Some("https://maps.example.org"),
102 );
103 }
104
105 #[test]
106 fn origin_of_rejects_non_absolute() {
107 assert_eq!(origin_of("/library"), None);
108 }
109}