Skip to main content

sofie_http/
storage.rs

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    /// Where uploads go and handlers keep files; the signed links of a `Local` store are served
35    /// at `/_sofie/files/`.
36    #[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
58/// A download is streamed from the store, so it is answered before the pipeline, whose
59/// answers are whole.
60pub(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    // A key names one content for good, so the key itself is a strong tag.
96    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
138/// A stored PDF opens on the app's own origin: sandboxed, it can run nothing there.
139fn 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
164/// Read by a task of its own, so the body only waits on a channel.
165pub(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}