1use std::sync::Arc;
15
16use axum::Router;
17use axum::extract::{Request, State};
18use axum::http::header::{HOST, ORIGIN};
19use axum::http::{HeaderName, HeaderValue, Method, StatusCode};
20use axum::middleware::{Next, from_fn_with_state};
21use axum::response::{IntoResponse, Response};
22use kernel::{AxCode, AxError};
23
24use crate::reception::{Arrival, Entry, ListenerOrigins, PageHeaders, Presented, decide_entry};
25
26use super::config::refusal_text;
27
28const SEC_FETCH_SITE: &str = "sec-fetch-site";
30
31pub(crate) type Entrance = (Arrival, Arc<ListenerOrigins>);
34
35pub fn entered<S>(routes: Router<S>, arrival: Arrival, origins: &ListenerOrigins) -> Router<S>
42where
43 S: Clone + Send + Sync + 'static,
44{
45 let names = Arc::new(origins.clone());
46 routes
47 .route_layer(from_fn_with_state((arrival, Arc::clone(&names)), enter))
48 .layer(from_fn_with_state(names, preflight))
49}
50
51pub(crate) async fn enter(
54 State((arrival, origins)): State<Entrance>,
55 request: Request,
56 next: Next,
57) -> Response {
58 let arrival = if request.method() == Method::OPTIONS {
59 Arrival::Preflight
60 } else {
61 arrival
62 };
63 match judged(&origins, &request, arrival) {
64 Some(refused) => refused,
65 None => next.run(request).await,
66 }
67}
68
69pub(crate) async fn preflight(
73 State(origins): State<Arc<ListenerOrigins>>,
74 request: Request,
75 next: Next,
76) -> Response {
77 if request.method() != Method::OPTIONS {
78 return next.run(request).await;
79 }
80 match judged(&origins, &request, Arrival::Preflight) {
81 Some(refused) => refused,
82 None => next.run(request).await,
83 }
84}
85
86fn judged(origins: &ListenerOrigins, request: &Request, arrival: Arrival) -> Option<Response> {
89 let headers = request.headers();
90 let read = |name: &str| headers.get(name).and_then(|value| value.to_str().ok());
91 let presented = Presented {
92 host: read(HOST.as_str()),
93 origin: read(ORIGIN.as_str()),
94 sec_fetch_site: read(SEC_FETCH_SITE),
95 };
96 match decide_entry(origins, presented, arrival) {
97 Entry::Refused(why) => {
98 Some((StatusCode::FORBIDDEN, refusal_text(&why.error())).into_response())
99 }
100 Entry::Page | Entry::Caller(_) => None,
101 }
102}
103
104#[derive(Debug, Clone)]
106pub struct Stamp(Vec<(HeaderName, HeaderValue)>);
107
108impl Stamp {
109 pub(crate) fn of(headers: &PageHeaders, stored: Stored) -> Result<Self, AxError> {
115 let extra = match stored {
116 Stored::AsPage => None,
117 Stored::Never => Some(("cache-control", "no-store".to_owned())),
118 };
119 headers
120 .lines()
121 .iter()
122 .cloned()
123 .chain(extra)
124 .map(|(name, value)| {
125 let name = HeaderName::try_from(name).map_err(|_| unspellable(name))?;
126 let value = HeaderValue::try_from(value).map_err(|_| unspellable(name.as_str()))?;
127 Ok((name, value))
128 })
129 .collect::<Result<Vec<_>, AxError>>()
130 .map(Self)
131 }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub(crate) enum Stored {
138 AsPage,
139 Never,
140}
141
142pub(crate) async fn stamp(
144 State(stamp): State<Arc<Stamp>>,
145 request: Request,
146 next: Next,
147) -> Response {
148 let mut response = next.run(request).await;
149 let headers = response.headers_mut();
150 for (name, value) in &stamp.0 {
151 headers.insert(name.clone(), value.clone());
152 }
153 response
154}
155
156fn unspellable(name: &str) -> AxError {
157 AxError::failure(
158 AxCode::ConfigInvalid,
159 "spell the listener's response headers",
160 format!("`{name}` is not a valid HTTP header"),
161 )
162 .with_recovery("report this: the headers are this build's own text")
163}