Skip to main content

tower_http/services/fs/serve_dir/
future.rs

1use super::{
2    open_file::{FileOpened, FileRequestExtent, OpenFileOutput},
3    DefaultServeDirFallback, ResponseBody,
4};
5use crate::{
6    body::UnsyncBoxBody, content_encoding::Encoding, services::fs::AsyncReadBody, BoxError,
7};
8use bytes::Bytes;
9use futures_core::future::BoxFuture;
10use futures_util::future::{FutureExt, TryFutureExt};
11use http::{
12    header::{self, ALLOW},
13    HeaderValue, Request, Response, StatusCode,
14};
15use http_body_util::{BodyExt, Empty, Full};
16use pin_project_lite::pin_project;
17use std::{
18    convert::Infallible,
19    future::Future,
20    io,
21    pin::Pin,
22    task::{ready, Context, Poll},
23};
24use tower_service::Service;
25
26pin_project! {
27    /// Response future of [`ServeDir::try_call()`][`super::ServeDir::try_call()`].
28    pub struct ResponseFuture<ReqBody, F = DefaultServeDirFallback> {
29        #[pin]
30        pub(super) inner: ResponseFutureInner<ReqBody, F>,
31    }
32}
33
34impl<ReqBody, F> ResponseFuture<ReqBody, F> {
35    pub(super) fn open_file_future(
36        future: BoxFuture<'static, io::Result<OpenFileOutput>>,
37        fallback_and_request: Option<(F, Request<ReqBody>)>,
38    ) -> Self {
39        Self {
40            inner: ResponseFutureInner::OpenFileFuture {
41                future,
42                fallback_and_request,
43            },
44        }
45    }
46
47    pub(super) fn invalid_path(fallback_and_request: Option<(F, Request<ReqBody>)>) -> Self {
48        Self {
49            inner: ResponseFutureInner::InvalidPath {
50                fallback_and_request,
51            },
52        }
53    }
54
55    pub(super) fn method_not_allowed() -> Self {
56        Self {
57            inner: ResponseFutureInner::MethodNotAllowed,
58        }
59    }
60}
61
62pin_project! {
63    #[project = ResponseFutureInnerProj]
64    pub(super) enum ResponseFutureInner<ReqBody, F> {
65        OpenFileFuture {
66            #[pin]
67            future: BoxFuture<'static, io::Result<OpenFileOutput>>,
68            fallback_and_request: Option<(F, Request<ReqBody>)>,
69        },
70        FallbackFuture {
71            future: BoxFuture<'static, Result<Response<ResponseBody>, Infallible>>,
72        },
73        InvalidPath {
74            fallback_and_request: Option<(F, Request<ReqBody>)>,
75        },
76        MethodNotAllowed,
77    }
78}
79
80impl<F, ReqBody, ResBody> Future for ResponseFuture<ReqBody, F>
81where
82    F: Service<Request<ReqBody>, Response = Response<ResBody>, Error = Infallible> + Clone,
83    F::Future: Send + 'static,
84    ResBody: http_body::Body<Data = Bytes> + Send + 'static,
85    ResBody::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
86{
87    type Output = io::Result<Response<ResponseBody>>;
88
89    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
90        loop {
91            let mut this = self.as_mut().project();
92
93            let new_state = match this.inner.as_mut().project() {
94                ResponseFutureInnerProj::OpenFileFuture {
95                    future: open_file_future,
96                    fallback_and_request,
97                } => match ready!(open_file_future.poll(cx)) {
98                    Ok(OpenFileOutput::FileOpened(file_output)) => {
99                        break Poll::Ready(Ok(build_response(*file_output)));
100                    }
101
102                    Ok(OpenFileOutput::Redirect { location }) => {
103                        let mut res = response_with_status(StatusCode::TEMPORARY_REDIRECT);
104                        res.headers_mut().insert(http::header::LOCATION, location);
105                        break Poll::Ready(Ok(res));
106                    }
107
108                    Ok(OpenFileOutput::FileNotFound | OpenFileOutput::InvalidFilename) => {
109                        if let Some((mut fallback, request)) = fallback_and_request.take() {
110                            call_fallback(&mut fallback, request)
111                        } else {
112                            break Poll::Ready(Ok(not_found()));
113                        }
114                    }
115
116                    Ok(OpenFileOutput::PreconditionFailed) => {
117                        break Poll::Ready(Ok(response_with_status(
118                            StatusCode::PRECONDITION_FAILED,
119                        )));
120                    }
121
122                    Ok(OpenFileOutput::NotModified {
123                        etag,
124                        last_modified,
125                    }) => {
126                        let mut res = response_with_status(StatusCode::NOT_MODIFIED);
127                        if let Some(etag) = etag {
128                            res.headers_mut()
129                                .insert(header::ETAG, etag.into_header_value());
130                        }
131                        if let Some(last_modified) = last_modified {
132                            res.headers_mut().insert(
133                                header::LAST_MODIFIED,
134                                HeaderValue::from_str(&last_modified.0.to_string()).unwrap(),
135                            );
136                        }
137                        break Poll::Ready(Ok(res));
138                    }
139
140                    Ok(OpenFileOutput::InvalidRedirectUri) => {
141                        break Poll::Ready(Ok(response_with_status(
142                            StatusCode::INTERNAL_SERVER_ERROR,
143                        )));
144                    }
145
146                    Err(err) => {
147                        #[cfg(unix)]
148                        // 20 = libc::ENOTDIR => "not a directory
149                        // when `io_error_more` landed, this can be changed
150                        // to checking for `io::ErrorKind::NotADirectory`.
151                        // https://github.com/rust-lang/rust/issues/86442
152                        let error_is_not_a_directory = err.raw_os_error() == Some(20);
153                        #[cfg(not(unix))]
154                        let error_is_not_a_directory = false;
155
156                        if matches!(
157                            err.kind(),
158                            io::ErrorKind::NotFound | io::ErrorKind::PermissionDenied
159                        ) || error_is_not_a_directory
160                        {
161                            if let Some((mut fallback, request)) = fallback_and_request.take() {
162                                call_fallback(&mut fallback, request)
163                            } else {
164                                break Poll::Ready(Ok(not_found()));
165                            }
166                        } else {
167                            break Poll::Ready(Err(err));
168                        }
169                    }
170                },
171
172                ResponseFutureInnerProj::FallbackFuture { future } => {
173                    break Pin::new(future).poll(cx).map_err(|err| match err {})
174                }
175
176                ResponseFutureInnerProj::InvalidPath {
177                    fallback_and_request,
178                } => {
179                    if let Some((mut fallback, request)) = fallback_and_request.take() {
180                        call_fallback(&mut fallback, request)
181                    } else {
182                        break Poll::Ready(Ok(not_found()));
183                    }
184                }
185
186                ResponseFutureInnerProj::MethodNotAllowed => {
187                    let mut res = response_with_status(StatusCode::METHOD_NOT_ALLOWED);
188                    res.headers_mut()
189                        .insert(ALLOW, HeaderValue::from_static("GET,HEAD"));
190                    break Poll::Ready(Ok(res));
191                }
192            };
193
194            this.inner.set(new_state);
195        }
196    }
197}
198
199fn response_with_status(status: StatusCode) -> Response<ResponseBody> {
200    Response::builder()
201        .status(status)
202        .body(empty_body())
203        .unwrap()
204}
205
206fn not_found() -> Response<ResponseBody> {
207    response_with_status(StatusCode::NOT_FOUND)
208}
209
210pub(super) fn call_fallback<F, B, FResBody>(
211    fallback: &mut F,
212    req: Request<B>,
213) -> ResponseFutureInner<B, F>
214where
215    F: Service<Request<B>, Response = Response<FResBody>, Error = Infallible> + Clone,
216    F::Future: Send + 'static,
217    FResBody: http_body::Body<Data = Bytes> + Send + 'static,
218    FResBody::Error: Into<BoxError>,
219{
220    let future = fallback
221        .call(req)
222        .map_ok(|response| {
223            response
224                .map(|body| {
225                    UnsyncBoxBody::from_inner(
226                        body.map_err(|err| match err.into().downcast::<io::Error>() {
227                            Ok(err) => *err,
228                            Err(err) => io::Error::new(io::ErrorKind::Other, err),
229                        })
230                        .boxed_unsync(),
231                    )
232                })
233                .map(ResponseBody::new)
234        })
235        .boxed();
236
237    ResponseFutureInner::FallbackFuture { future }
238}
239
240fn build_response(output: FileOpened) -> Response<ResponseBody> {
241    let (maybe_file, size) = match output.extent {
242        FileRequestExtent::Full(file, size) => (Some(file), size),
243        FileRequestExtent::Head(size) => (None, size),
244    };
245
246    let mut builder = Response::builder()
247        .header(header::CONTENT_TYPE, output.mime_header_value)
248        .header(header::ACCEPT_RANGES, "bytes");
249
250    if let Some(encoding) = output
251        .maybe_encoding
252        .filter(|encoding| *encoding != Encoding::Identity)
253    {
254        builder = builder.header(header::CONTENT_ENCODING, encoding.into_header_value());
255    }
256
257    // Per RFC 9110 ยง12.5.3, Vary must be sent when the response could differ
258    // based on Accept-Encoding, even if this particular response is uncompressed.
259    if output.precompression_configured {
260        builder = builder.header(header::VARY, "accept-encoding");
261    }
262
263    if let Some(last_modified) = output.last_modified {
264        builder = builder.header(header::LAST_MODIFIED, last_modified.0.to_string());
265    }
266
267    if let Some(etag) = output.etag {
268        builder = builder.header(header::ETAG, etag.into_header_value());
269    }
270
271    match output.maybe_range {
272        Some(Ok(ranges)) => {
273            if let Some(range) = ranges.first() {
274                if ranges.len() > 1 {
275                    builder
276                        .header(header::CONTENT_RANGE, format!("bytes */{}", size))
277                        .status(StatusCode::RANGE_NOT_SATISFIABLE)
278                        .body(body_from_bytes(Bytes::from(
279                            "Cannot serve multipart range requests",
280                        )))
281                        .unwrap()
282                } else {
283                    let body = if let Some(file) = maybe_file {
284                        let range_size = range.end() - range.start() + 1;
285                        ResponseBody::new(UnsyncBoxBody::from_inner(
286                            AsyncReadBody::with_capacity_limited(
287                                file,
288                                output.chunk_size,
289                                range_size,
290                            )
291                            .boxed_unsync(),
292                        ))
293                    } else {
294                        empty_body()
295                    };
296
297                    let content_length = if size == 0 {
298                        0
299                    } else {
300                        range.end() - range.start() + 1
301                    };
302
303                    builder
304                        .header(
305                            header::CONTENT_RANGE,
306                            format!("bytes {}-{}/{}", range.start(), range.end(), size),
307                        )
308                        .header(header::CONTENT_LENGTH, content_length)
309                        .status(StatusCode::PARTIAL_CONTENT)
310                        .body(body)
311                        .unwrap()
312                }
313            } else {
314                builder
315                    .header(header::CONTENT_RANGE, format!("bytes */{}", size))
316                    .status(StatusCode::RANGE_NOT_SATISFIABLE)
317                    .body(body_from_bytes(Bytes::from(
318                        "No range found after parsing range header, please file an issue",
319                    )))
320                    .unwrap()
321            }
322        }
323
324        Some(Err(_)) => builder
325            .header(header::CONTENT_RANGE, format!("bytes */{}", size))
326            .status(StatusCode::RANGE_NOT_SATISFIABLE)
327            .body(empty_body())
328            .unwrap(),
329
330        // Not a range request
331        None => {
332            let body = if let Some(file) = maybe_file {
333                ResponseBody::new(UnsyncBoxBody::from_inner(
334                    AsyncReadBody::with_capacity(file, output.chunk_size).boxed_unsync(),
335                ))
336            } else {
337                empty_body()
338            };
339
340            builder
341                .header(header::CONTENT_LENGTH, size)
342                .body(body)
343                .unwrap()
344        }
345    }
346}
347
348fn body_from_bytes(bytes: Bytes) -> ResponseBody {
349    let body = Full::from(bytes).map_err(|err| match err {}).boxed_unsync();
350    ResponseBody::new(UnsyncBoxBody::from_inner(body))
351}
352
353fn empty_body() -> ResponseBody {
354    let body = Empty::new().map_err(|err| match err {}).boxed_unsync();
355    ResponseBody::new(UnsyncBoxBody::from_inner(body))
356}