1use sofie_db::Context as _;
2use std::net::SocketAddr;
3use std::pin::Pin;
4use std::task::{Context, Poll};
5
6use bytes::Bytes;
7use http::header::{
8 CACHE_CONTROL, CONTENT_DISPOSITION, CONTENT_LENGTH, CONTENT_SECURITY_POLICY, CONTENT_TYPE, ETAG,
9};
10use http::{HeaderValue, Method, StatusCode};
11use http_body_util::{Either, Full};
12use hyper::body::{Body, Frame, Incoming};
13use sofie_core::{Error, Result};
14use sofie_storage::{Content, SERVED_PATH, Served, Storage, content_disposition};
15use tokio::sync::mpsc;
16
17use crate::answer::{self, Response};
18use crate::caller::Caller;
19use crate::pipeline::Pipeline;
20use crate::{Area, RequestContext, Serving};
21use sofie_db::Resolve;
22
23const UNSIGNED: &str = "This link is invalid or has expired.";
24const READ_AHEAD: usize = 4;
25
26#[cfg(feature = "live")]
27type Others = crate::live::Body;
28#[cfg(not(feature = "live"))]
29type Others = Full<Bytes>;
30
31pub(crate) type Streamed = http::Response<Either<Others, Chunks>>;
32
33impl Serving {
34 #[must_use]
37 pub fn storage(mut self, storage: Storage) -> Self {
38 self.storage = Some(storage);
39 self
40 }
41}
42
43impl<A: Area, M> Resolve<RequestContext<A, M>> for Storage {
44 fn resolve(context: &mut RequestContext<A, M>) -> impl Future<Output = Result<Self>> + Send {
45 let storage = context
46 .matched
47 .storage
48 .clone()
49 .map(|storage| storage.with_clock(context.clock()));
50 std::future::ready(storage.ok_or_else(|| {
51 Error::internal_message(
52 "a handler takes `Storage`, but the app has none; see `Serving::storage`",
53 )
54 }))
55 }
56}
57
58pub(crate) async fn answer(
61 pipeline: &Pipeline,
62 request: http::Request<Incoming>,
63 peer: SocketAddr,
64) -> Streamed {
65 let storage = pipeline.endpoints.serving.storage.as_ref();
66 let Some(storage) = storage.filter(|_| is_download(&request)) else {
67 #[cfg(feature = "live")]
68 let others = crate::live::answer(pipeline, request, peer).await;
69 #[cfg(not(feature = "live"))]
70 let others = pipeline.handle(request, peer).await;
71 return others.map(Either::Left);
72 };
73 let (request, _) = request.into_parts();
74 #[cfg(feature = "shield")]
75 if let Some(refusal) = pipeline.shielded(&request, peer) {
76 return whole(refusal);
77 }
78 download(storage, &request).await
79}
80
81fn is_download(request: &http::Request<Incoming>) -> bool {
82 (request.method() == Method::GET || request.method() == Method::HEAD)
83 && request.uri().path().starts_with(SERVED_PATH)
84}
85
86async fn download(storage: &Storage, request: &http::request::Parts) -> Streamed {
87 let caller = Caller::of(&request.headers);
88 let path_and_query = request
89 .uri
90 .path_and_query()
91 .map_or("", http::uri::PathAndQuery::as_str);
92 let Some(served) = storage.served(path_and_query) else {
93 return whole(answer::refusal(StatusCode::FORBIDDEN, UNSIGNED, caller));
94 };
95 let tag = format!("\"{}\"", served.key);
97 if answer::is_held(&request.headers, &tag) {
98 let mut response = answer::response(StatusCode::NOT_MODIFIED, None, Bytes::new());
99 file_headers(response.headers_mut(), &served, &tag);
100 return whole(response);
101 }
102 let found = if request.method == Method::HEAD {
103 let exists = storage.exists(&served.key).await;
104 exists.map(|exists| exists.then_some(None))
105 } else {
106 storage
107 .get(&served.key)
108 .await
109 .map(|content| content.map(Some))
110 };
111 let content = match found {
112 Ok(Some(content)) => content,
113 Ok(None) => return whole(answer::failure(Error::NotFound, caller)),
114 Err(error) => return whole(answer::failure(error, caller)),
115 };
116 let mut head = answer::response(StatusCode::OK, None, Bytes::new());
117 let headers = head.headers_mut();
118 file_headers(headers, &served, &tag);
119 let (content_type, disposition) = served.content_type.served();
120 if let Ok(value) = HeaderValue::from_str(content_type) {
121 headers.insert(CONTENT_TYPE, value);
122 }
123 let disposition = content_disposition(disposition, served.filename.as_deref());
124 if let Ok(value) = HeaderValue::from_str(&disposition) {
125 headers.insert(CONTENT_DISPOSITION, value);
126 }
127 if let Some(size) = content.as_ref().and_then(Content::size) {
128 headers.insert(CONTENT_LENGTH, HeaderValue::from(size));
129 }
130 let (parts, empty) = answer::secure(head).into_parts();
131 let body = match content {
132 Some(content) => Either::Right(Chunks::reading(content)),
133 None => Either::Left(whole_body(empty)),
134 };
135 http::Response::from_parts(parts, body)
136}
137
138fn file_headers(headers: &mut http::HeaderMap, served: &Served, tag: &str) {
140 let cache = format!("private, max-age={}, immutable", served.fresh_for.as_secs());
141 if let Ok(value) = HeaderValue::from_str(&cache) {
142 headers.insert(CACHE_CONTROL, value);
143 }
144 if let Ok(value) = HeaderValue::from_str(tag) {
145 headers.insert(ETAG, value);
146 }
147 headers.insert(
148 CONTENT_SECURITY_POLICY,
149 HeaderValue::from_static("default-src 'none'; sandbox"),
150 );
151}
152
153fn whole(response: Response) -> Streamed {
154 answer::secure(response).map(|body| Either::Left(whole_body(body)))
155}
156
157fn whole_body(body: Full<Bytes>) -> Others {
158 #[cfg(feature = "live")]
159 return Either::Left(body);
160 #[cfg(not(feature = "live"))]
161 body
162}
163
164pub(crate) struct Chunks {
166 receiver: mpsc::Receiver<Result<Bytes>>,
167}
168
169impl Chunks {
170 fn reading(mut content: Content) -> Self {
171 let (sender, receiver) = mpsc::channel(READ_AHEAD);
172 tokio::spawn(async move {
173 while let Some(chunk) = content.chunk().await.transpose() {
174 if let Err(error) = &chunk {
175 tracing::warn!(error.kind = "storage", error = %error, "reading a download failed");
176 }
177 let failed = chunk.is_err();
178 if sender.send(chunk).await.is_err() || failed {
179 return;
180 }
181 }
182 });
183 Self { receiver }
184 }
185}
186
187impl Body for Chunks {
188 type Data = Bytes;
189 type Error = Error;
190
191 fn poll_frame(
192 mut self: Pin<&mut Self>,
193 context: &mut Context<'_>,
194 ) -> Poll<Option<Result<Frame<Bytes>>>> {
195 self.receiver
196 .poll_recv(context)
197 .map(|chunk| chunk.map(|chunk| chunk.map(Frame::data)))
198 }
199}