1use crate::auth_api::error;
23use crate::operator::Operator;
24use axum::extract::{Request, State};
25use axum::http::{HeaderMap, Method, StatusCode, header};
26use axum::middleware::Next;
27use axum::response::Response;
28use std::sync::Arc;
29
30pub async fn browser_rules(State(operator): State<Arc<Operator>>, req: Request, next: Next) -> Response {
31 let headers = req.headers();
32 let origin = headers.get(header::ORIGIN).map(|v| v.to_str().unwrap_or("\u{0}"));
34 let public_url = operator.public_url();
35 let expected = public_url.as_ref().map(|u| u.origin());
36 if is_state_changing(req.method()) {
37 if expected.is_none() {
38 return error(StatusCode::FORBIDDEN, "setup_required", "hennery is not set up yet");
39 }
40 if origin != expected {
41 tracing::debug!(?expected, received = ?origin, "request refused: origin_mismatch");
42 return error(
43 StatusCode::FORBIDDEN,
44 "origin_mismatch",
45 "state-changing requests must come from the public_url's origin",
46 );
47 }
48 if !is_json_or_empty(headers) {
49 return error(
50 StatusCode::UNSUPPORTED_MEDIA_TYPE,
51 "unsupported_media_type",
52 "the body must be application/json",
53 );
54 }
55 } else {
56 let site = headers.get("sec-fetch-site").map(|v| v.to_str().unwrap_or(""));
57 if site.is_some_and(|s| s != "same-origin" && s != "none") {
58 return error(StatusCode::FORBIDDEN, "cross_site", "cross-site requests are refused");
59 }
60 if origin.is_some() && origin != expected {
61 tracing::debug!(?expected, received = ?origin, "request refused: origin_mismatch");
62 return error(
63 StatusCode::FORBIDDEN,
64 "origin_mismatch",
65 "a request's Origin must be the public_url's",
66 );
67 }
68 }
69 next.run(req).await
70}
71
72fn is_state_changing(method: &Method) -> bool {
73 method != Method::GET && method != Method::HEAD
74}
75
76fn is_json_or_empty(headers: &HeaderMap) -> bool {
78 match headers.get(header::CONTENT_TYPE) {
79 Some(value) => value
80 .to_str()
81 .ok()
82 .and_then(|v| v.split(';').next())
83 .is_some_and(|essence| essence.trim().eq_ignore_ascii_case("application/json")),
84 None => {
85 !headers.contains_key(header::TRANSFER_ENCODING)
86 && headers
87 .get(header::CONTENT_LENGTH)
88 .is_none_or(|len| len.to_str().is_ok_and(|len| len.trim() == "0"))
89 }
90 }
91}