use super::{ open_file::{FileOpened, FileRequestExtent, OpenFileOutput}, DefaultServeDirFallback, ResponseBody, }; use crate::{ body::UnsyncBoxBody, content_encoding::Encoding, services::fs::AsyncReadBody, BoxError, }; use bytes::Bytes; use futures_core::future::BoxFuture; use futures_util::future::{FutureExt, TryFutureExt}; use http::{ header::{self, ALLOW}, HeaderValue, Request, Response, StatusCode, }; use http_body_util::{BodyExt, Empty, Full}; use pin_project_lite::pin_project; use std::{ convert::Infallible, future::Future, io, pin::Pin, task::{ready, Context, Poll}, }; use tower_service::Service; pin_project! { /// Response future of [`ServeDir::try_call()`][`super::ServeDir::try_call()`]. pub struct ResponseFuture { #[pin] pub(super) inner: ResponseFutureInner, } } impl ResponseFuture { pub(super) fn open_file_future( future: BoxFuture<'static, io::Result>, fallback_and_request: Option<(F, Request)>, ) -> Self { Self { inner: ResponseFutureInner::OpenFileFuture { future, fallback_and_request, }, } } pub(super) fn invalid_path(fallback_and_request: Option<(F, Request)>) -> Self { Self { inner: ResponseFutureInner::InvalidPath { fallback_and_request, }, } } pub(super) fn method_not_allowed() -> Self { Self { inner: ResponseFutureInner::MethodNotAllowed, } } } pin_project! { #[project = ResponseFutureInnerProj] pub(super) enum ResponseFutureInner { OpenFileFuture { #[pin] future: BoxFuture<'static, io::Result>, fallback_and_request: Option<(F, Request)>, }, FallbackFuture { future: BoxFuture<'static, Result, Infallible>>, }, InvalidPath { fallback_and_request: Option<(F, Request)>, }, MethodNotAllowed, } } impl Future for ResponseFuture where F: Service, Response = Response, Error = Infallible> + Clone, F::Future: Send + 'static, ResBody: http_body::Body + Send + 'static, ResBody::Error: Into>, { type Output = io::Result>; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { loop { let mut this = self.as_mut().project(); let new_state = match this.inner.as_mut().project() { ResponseFutureInnerProj::OpenFileFuture { future: open_file_future, fallback_and_request, } => match ready!(open_file_future.poll(cx)) { Ok(OpenFileOutput::FileOpened(file_output)) => { break Poll::Ready(Ok(build_response(*file_output))); } Ok(OpenFileOutput::Redirect { location }) => { let mut res = response_with_status(StatusCode::TEMPORARY_REDIRECT); res.headers_mut().insert(http::header::LOCATION, location); break Poll::Ready(Ok(res)); } Ok(OpenFileOutput::FileNotFound | OpenFileOutput::InvalidFilename) => { if let Some((mut fallback, request)) = fallback_and_request.take() { call_fallback(&mut fallback, request) } else { break Poll::Ready(Ok(not_found())); } } Ok(OpenFileOutput::PreconditionFailed) => { break Poll::Ready(Ok(response_with_status( StatusCode::PRECONDITION_FAILED, ))); } Ok(OpenFileOutput::NotModified) => { break Poll::Ready(Ok(response_with_status(StatusCode::NOT_MODIFIED))); } Ok(OpenFileOutput::InvalidRedirectUri) => { break Poll::Ready(Ok(response_with_status( StatusCode::INTERNAL_SERVER_ERROR, ))); } Err(err) => { #[cfg(unix)] // 20 = libc::ENOTDIR => "not a directory // when `io_error_more` landed, this can be changed // to checking for `io::ErrorKind::NotADirectory`. // https://github.com/rust-lang/rust/issues/86442 let error_is_not_a_directory = err.raw_os_error() == Some(20); #[cfg(not(unix))] let error_is_not_a_directory = false; if matches!( err.kind(), io::ErrorKind::NotFound | io::ErrorKind::PermissionDenied ) || error_is_not_a_directory { if let Some((mut fallback, request)) = fallback_and_request.take() { call_fallback(&mut fallback, request) } else { break Poll::Ready(Ok(not_found())); } } else { break Poll::Ready(Err(err)); } } }, ResponseFutureInnerProj::FallbackFuture { future } => { break Pin::new(future).poll(cx).map_err(|err| match err {}) } ResponseFutureInnerProj::InvalidPath { fallback_and_request, } => { if let Some((mut fallback, request)) = fallback_and_request.take() { call_fallback(&mut fallback, request) } else { break Poll::Ready(Ok(not_found())); } } ResponseFutureInnerProj::MethodNotAllowed => { let mut res = response_with_status(StatusCode::METHOD_NOT_ALLOWED); res.headers_mut() .insert(ALLOW, HeaderValue::from_static("GET,HEAD")); break Poll::Ready(Ok(res)); } }; this.inner.set(new_state); } } } fn response_with_status(status: StatusCode) -> Response { Response::builder() .status(status) .body(empty_body()) .unwrap() } fn not_found() -> Response { response_with_status(StatusCode::NOT_FOUND) } pub(super) fn call_fallback( fallback: &mut F, req: Request, ) -> ResponseFutureInner where F: Service, Response = Response, Error = Infallible> + Clone, F::Future: Send + 'static, FResBody: http_body::Body + Send + 'static, FResBody::Error: Into, { let future = fallback .call(req) .map_ok(|response| { response .map(|body| { UnsyncBoxBody::new( body.map_err(|err| match err.into().downcast::() { Ok(err) => *err, Err(err) => io::Error::new(io::ErrorKind::Other, err), }) .boxed_unsync(), ) }) .map(ResponseBody::new) }) .boxed(); ResponseFutureInner::FallbackFuture { future } } fn build_response(output: FileOpened) -> Response { let (maybe_file, size) = match output.extent { FileRequestExtent::Full(file, meta) => (Some(file), meta.len()), FileRequestExtent::Head(meta) => (None, meta.len()), }; let mut builder = Response::builder() .header(header::CONTENT_TYPE, output.mime_header_value) .header(header::ACCEPT_RANGES, "bytes"); if let Some(encoding) = output .maybe_encoding .filter(|encoding| *encoding != Encoding::Identity) { builder = builder.header(header::CONTENT_ENCODING, encoding.into_header_value()); } if let Some(last_modified) = output.last_modified { builder = builder.header(header::LAST_MODIFIED, last_modified.0.to_string()); } match output.maybe_range { Some(Ok(ranges)) => { if let Some(range) = ranges.first() { if ranges.len() > 1 { builder .header(header::CONTENT_RANGE, format!("bytes */{}", size)) .status(StatusCode::RANGE_NOT_SATISFIABLE) .body(body_from_bytes(Bytes::from( "Cannot serve multipart range requests", ))) .unwrap() } else { let body = if let Some(file) = maybe_file { let range_size = range.end() - range.start() + 1; ResponseBody::new(UnsyncBoxBody::new( AsyncReadBody::with_capacity_limited( file, output.chunk_size, range_size, ) .boxed_unsync(), )) } else { empty_body() }; let content_length = if size == 0 { 0 } else { range.end() - range.start() + 1 }; builder .header( header::CONTENT_RANGE, format!("bytes {}-{}/{}", range.start(), range.end(), size), ) .header(header::CONTENT_LENGTH, content_length) .status(StatusCode::PARTIAL_CONTENT) .body(body) .unwrap() } } else { builder .header(header::CONTENT_RANGE, format!("bytes */{}", size)) .status(StatusCode::RANGE_NOT_SATISFIABLE) .body(body_from_bytes(Bytes::from( "No range found after parsing range header, please file an issue", ))) .unwrap() } } Some(Err(_)) => builder .header(header::CONTENT_RANGE, format!("bytes */{}", size)) .status(StatusCode::RANGE_NOT_SATISFIABLE) .body(empty_body()) .unwrap(), // Not a range request None => { let body = if let Some(file) = maybe_file { ResponseBody::new(UnsyncBoxBody::new( AsyncReadBody::with_capacity(file, output.chunk_size).boxed_unsync(), )) } else { empty_body() }; builder .header(header::CONTENT_LENGTH, size) .body(body) .unwrap() } } } fn body_from_bytes(bytes: Bytes) -> ResponseBody { let body = Full::from(bytes).map_err(|err| match err {}).boxed_unsync(); ResponseBody::new(UnsyncBoxBody::new(body)) } fn empty_body() -> ResponseBody { let body = Empty::new().map_err(|err| match err {}).boxed_unsync(); ResponseBody::new(UnsyncBoxBody::new(body)) }