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
18pub trait PipelineStep: Send + Sync + 'static {
21 fn check<'a>(&'a self, _request: &'a RequestHead<'a>) -> BoxFuture<'a, Result<()>> {
23 Box::pin(std::future::ready(Ok(())))
24 }
25
26 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 #[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
64pub 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 #[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 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
141pub(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}