Skip to main content

sofie_http/
step.rs

1use std::net::IpAddr;
2use std::time::Duration;
3
4use http::header::{
5    CONTENT_SECURITY_POLICY, REFERRER_POLICY, SET_COOKIE, STRICT_TRANSPORT_SECURITY,
6    X_CONTENT_TYPE_OPTIONS, X_FRAME_OPTIONS,
7};
8use http::request::Parts;
9use http::{HeaderMap, HeaderName, HeaderValue};
10use sofie_core::{Error, Result, Uuid};
11use sofie_db::BoxFuture;
12
13use crate::admission::Visit;
14use crate::answer::{self, Response};
15use crate::pipeline::Asked;
16use crate::target::Target;
17
18/// `check` can only refuse and runs after CSRF, the guard and rate limits, so a
19/// step cannot let through what they refused.
20pub trait PipelineStep: Send + Sync + 'static {
21    /// Before the body is read.
22    fn check<'a>(&'a self, _request: &'a RequestHead<'a>) -> BoxFuture<'a, Result<()>> {
23        Box::pin(std::future::ready(Ok(())))
24    }
25
26    /// Before compression and the framework's security and CORS headers.
27    fn answered(&self, _request: &RequestHead<'_>, _answer: &mut ResponseHead<'_>) {}
28}
29
30pub struct RequestHead<'r> {
31    parts: &'r Parts,
32    client: IpAddr,
33    principal: Option<Uuid>,
34}
35
36impl RequestHead<'_> {
37    #[must_use]
38    pub fn method(&self) -> &str {
39        self.parts.method.as_str()
40    }
41
42    #[must_use]
43    pub fn path(&self) -> &str {
44        self.parts.uri.path()
45    }
46
47    #[must_use]
48    pub fn header(&self, name: &str) -> Option<&str> {
49        self.parts.headers.get(name)?.to_str().ok()
50    }
51
52    /// Read through trusted proxies only.
53    #[must_use]
54    pub fn client(&self) -> IpAddr {
55        self.client
56    }
57
58    #[must_use]
59    pub fn principal(&self) -> Option<Uuid> {
60        self.principal
61    }
62}
63
64/// The status and body are the handler's and the framework's; a step changes headers only.
65pub struct ResponseHead<'r> {
66    status: u16,
67    headers: &'r mut HeaderMap,
68    elapsed: Duration,
69}
70
71const FRAMEWORK_HEADERS: [HeaderName; 6] = [
72    SET_COOKIE,
73    CONTENT_SECURITY_POLICY,
74    X_CONTENT_TYPE_OPTIONS,
75    REFERRER_POLICY,
76    X_FRAME_OPTIONS,
77    STRICT_TRANSPORT_SECURITY,
78];
79
80impl ResponseHead<'_> {
81    #[must_use]
82    pub fn status(&self) -> u16 {
83        self.status
84    }
85
86    /// Since the request was routed, until its answer was built.
87    #[must_use]
88    pub fn elapsed(&self) -> Duration {
89        self.elapsed
90    }
91
92    #[must_use]
93    pub fn header(&self, name: &str) -> Option<&str> {
94        self.headers.get(name)?.to_str().ok()
95    }
96
97    /// The session cookie, security and CORS headers are the framework's and refused.
98    pub fn set_header(&mut self, name: &str, value: &str) -> Result<()> {
99        let header = HeaderName::try_from(name)
100            .map_err(|error| Error::internal(format!("header name `{name}`"), error))?;
101        if is_framework_header(&header) {
102            return Err(Error::internal_message(format!(
103                "a pipeline step may not set `{header}`, which the framework sets"
104            )));
105        }
106        let value = HeaderValue::try_from(value)
107            .map_err(|error| Error::internal(format!("the value of header `{header}`"), error))?;
108        self.headers.insert(header, value);
109        Ok(())
110    }
111}
112
113fn is_framework_header(name: &HeaderName) -> bool {
114    FRAMEWORK_HEADERS.contains(name)
115        || name.as_str() == answer::PERMISSIONS_POLICY_HEADER
116        || name.as_str().starts_with("access-control-")
117}
118
119fn request_head(asked: Asked<'_>, principal: Option<Uuid>) -> RequestHead<'_> {
120    RequestHead {
121        parts: asked.request,
122        client: asked.client,
123        principal,
124    }
125}
126
127pub(crate) async fn refusal(
128    everywhere: &[Box<dyn PipelineStep>],
129    area: &[Box<dyn PipelineStep>],
130    visit: Visit<'_>,
131) -> Option<Response> {
132    let head = request_head(visit.asked, visit.principal);
133    for step in everywhere.iter().chain(area) {
134        if let Err(error) = step.check(&head).await {
135            return Some(answer::failure(error, visit.asked.caller));
136        }
137    }
138    None
139}
140
141/// In reverse of `check`'s order, so the outermost step sees the answer last.
142pub(crate) fn answered(
143    everywhere: &[Box<dyn PipelineStep>],
144    target: Option<&Target>,
145    asked: Asked<'_>,
146    principal: Option<Uuid>,
147    mut response: Response,
148    elapsed: Duration,
149) -> Response {
150    let area = target.map_or(&[][..], |target| &target.admission.area.steps[..]);
151    if everywhere.is_empty() && area.is_empty() {
152        return response;
153    }
154    let request = request_head(asked, principal);
155    let status = response.status().as_u16();
156    let mut head = ResponseHead {
157        status,
158        headers: response.headers_mut(),
159        elapsed,
160    };
161    for step in area.iter().rev().chain(everywhere.iter().rev()) {
162        step.answered(&request, &mut head);
163    }
164    response
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn a_step_may_not_replace_the_content_security_policy() {
173        let mut headers = HeaderMap::new();
174        let mut head = ResponseHead {
175            status: 200,
176            headers: &mut headers,
177            elapsed: Duration::ZERO,
178        };
179
180        assert!(
181            head.set_header("Content-Security-Policy", "script-src *")
182                .is_err()
183        );
184        assert!(head.set_header("x-answered-by", "step").is_ok());
185    }
186}