From 72328aa30610486099106bd22c69bd0924981182 Mon Sep 17 00:00:00 2001 From: Molkars Date: Sun, 2 Jul 2023 01:19:58 -0600 Subject: [PATCH 1/3] implement warp::reply::file, warp::header::conditionals, and move warp::fs::Conditionals to warp::header --- src/filters/fs.rs | 94 ++++------------------------------------- src/filters/header.rs | 98 ++++++++++++++++++++++++++++++++++++++++++- src/reply.rs | 25 +++++++++++ 3 files changed, 129 insertions(+), 88 deletions(-) diff --git a/src/filters/fs.rs b/src/filters/fs.rs index 0949b66ec..463a58c39 100644 --- a/src/filters/fs.rs +++ b/src/filters/fs.rs @@ -1,7 +1,6 @@ //! File System Filters use std::cmp; -use std::convert::Infallible; use std::fs::Metadata; use std::future::Future; use std::io; @@ -14,8 +13,7 @@ use bytes::{Bytes, BytesMut}; use futures_util::future::Either; use futures_util::{future, ready, stream, FutureExt, Stream, StreamExt, TryFutureExt}; use headers::{ - AcceptRanges, ContentLength, ContentRange, ContentType, HeaderMapExt, IfModifiedSince, IfRange, - IfUnmodifiedSince, LastModified, Range, + AcceptRanges, ContentLength, ContentRange, ContentType, HeaderMapExt, LastModified, Range, }; use http::StatusCode; use hyper::Body; @@ -26,6 +24,7 @@ use tokio::io::AsyncSeekExt; use tokio_util::io::poll_read_buf; use crate::filter::{Filter, FilterClone, One}; +use crate::header::{Conditionals, conditionals}; use crate::reject::{self, Rejection}; use crate::reply::{Reply, Response}; @@ -135,84 +134,6 @@ fn sanitize_path(base: impl AsRef, tail: &str) -> Result, - if_unmodified_since: Option, - if_range: Option, - range: Option, -} - -enum Cond { - NoBody(Response), - WithBody(Option), -} - -impl Conditionals { - fn check(self, last_modified: Option) -> Cond { - if let Some(since) = self.if_unmodified_since { - let precondition = last_modified - .map(|time| since.precondition_passes(time.into())) - .unwrap_or(false); - - tracing::trace!( - "if-unmodified-since? {:?} vs {:?} = {}", - since, - last_modified, - precondition - ); - if !precondition { - let mut res = Response::new(Body::empty()); - *res.status_mut() = StatusCode::PRECONDITION_FAILED; - return Cond::NoBody(res); - } - } - - if let Some(since) = self.if_modified_since { - tracing::trace!( - "if-modified-since? header = {:?}, file = {:?}", - since, - last_modified - ); - let unmodified = last_modified - .map(|time| !since.is_modified(time.into())) - // no last_modified means its always modified - .unwrap_or(false); - if unmodified { - let mut res = Response::new(Body::empty()); - *res.status_mut() = StatusCode::NOT_MODIFIED; - return Cond::NoBody(res); - } - } - - if let Some(if_range) = self.if_range { - tracing::trace!("if-range? {:?} vs {:?}", if_range, last_modified); - let can_range = !if_range.is_modified(None, last_modified.as_ref()); - - if !can_range { - return Cond::WithBody(None); - } - } - - Cond::WithBody(self.range) - } -} - -fn conditionals() -> impl Filter, Error = Infallible> + Copy { - crate::header::optional2() - .and(crate::header::optional2()) - .and(crate::header::optional2()) - .and(crate::header::optional2()) - .map( - |if_modified_since, if_unmodified_since, if_range, range| Conditionals { - if_modified_since, - if_unmodified_since, - if_range, - range, - }, - ) -} - /// A file response. #[derive(Debug)] pub struct File { @@ -247,7 +168,7 @@ impl File { // Silly wrapper since Arc doesn't implement AsRef ;_; #[derive(Clone, Debug)] -struct ArcPath(Arc); +pub(crate) struct ArcPath(pub(crate) Arc); impl AsRef for ArcPath { fn as_ref(&self) -> &Path { @@ -261,7 +182,7 @@ impl Reply for File { } } -fn file_reply( +pub(crate) fn file_reply( path: ArcPath, conditionals: Conditionals, ) -> impl Future> + Send { @@ -310,9 +231,10 @@ fn file_conditional( let mut len = meta.len(); let modified = meta.modified().ok().map(LastModified::from); + use crate::header::ConditionalBody; let resp = match conditionals.check(modified) { - Cond::NoBody(resp) => resp, - Cond::WithBody(range) => { + ConditionalBody::NoBody(resp) => resp, + ConditionalBody::WithBody(range) => { bytes_range(range, len) .map(|(start, end)| { let sub_len = end - start; @@ -343,7 +265,7 @@ fn file_conditional( resp }) - .unwrap_or_else(|BadRange| { + .unwrap_or_else(|_: BadRange| { // bad byte range let mut resp = Response::new(Body::empty()); *resp.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE; diff --git a/src/filters/header.rs b/src/filters/header.rs index 0c535a38b..267c35b00 100644 --- a/src/filters/header.rs +++ b/src/filters/header.rs @@ -8,12 +8,14 @@ use std::convert::Infallible; use std::str::FromStr; use futures_util::future; -use headers::{Header, HeaderMapExt}; +use headers::{Header, HeaderMapExt, IfModifiedSince, IfRange, IfUnmodifiedSince, LastModified, Range}; use http::header::HeaderValue; -use http::HeaderMap; +use http::{HeaderMap, StatusCode}; +use hyper::Body; use crate::filter::{filter_fn, filter_fn_one, Filter, One}; use crate::reject::{self, Rejection}; +use crate::reply::Response; /// Create a `Filter` that tries to parse the specified header. /// @@ -228,3 +230,95 @@ pub fn value( pub fn headers_cloned() -> impl Filter, Error = Infallible> + Copy { filter_fn_one(|route| future::ok(route.headers().clone())) } + +/// Create a `Filter` that returns a container for conditional headers +/// +/// # Example +/// ``` +/// use warp::Filter; +/// use warp::fs::Conditionals; +/// +/// let headers = warp::header::conditionals() +/// .and_then(|conditionals: Conditionals| { +/// warp::reply::file("index.html", conditionals) +/// }); +/// ``` +pub fn conditionals() -> impl Filter, Error = Infallible> + Copy { + optional2() + .and(optional2()) + .and(optional2()) + .and(optional2()) + .map( + |if_modified_since, if_unmodified_since, if_range, range| Conditionals { + if_modified_since, + if_unmodified_since, + if_range, + range, + }, + ) +} + + +/// Utilized conditional headers to determine response-content +#[derive(Debug, Default)] +pub struct Conditionals { + if_modified_since: Option, + if_unmodified_since: Option, + if_range: Option, + range: Option, +} + +pub(crate) enum ConditionalBody { + NoBody(Response), + WithBody(Option), +} + +impl Conditionals { + pub(crate) fn check(self, last_modified: Option) -> ConditionalBody { + if let Some(since) = self.if_unmodified_since { + let precondition = last_modified + .map(|time| since.precondition_passes(time.into())) + .unwrap_or(false); + + tracing::trace!( + "if-unmodified-since? {:?} vs {:?} = {}", + since, + last_modified, + precondition + ); + if !precondition { + let mut res = Response::new(Body::empty()); + *res.status_mut() = StatusCode::PRECONDITION_FAILED; + return ConditionalBody::NoBody(res); + } + } + + if let Some(since) = self.if_modified_since { + tracing::trace!( + "if-modified-since? header = {:?}, file = {:?}", + since, + last_modified + ); + let unmodified = last_modified + .map(|time| !since.is_modified(time.into())) + // no last_modified means its always modified + .unwrap_or(false); + if unmodified { + let mut res = Response::new(Body::empty()); + *res.status_mut() = StatusCode::NOT_MODIFIED; + return ConditionalBody::NoBody(res); + } + } + + if let Some(if_range) = self.if_range { + tracing::trace!("if-range? {:?} vs {:?}", if_range, last_modified); + let can_range = !if_range.is_modified(None, last_modified.as_ref()); + + if !can_range { + return ConditionalBody::WithBody(None); + } + } + + ConditionalBody::WithBody(self.range) + } +} diff --git a/src/reply.rs b/src/reply.rs index 74dee278d..7bcefedb2 100644 --- a/src/reply.rs +++ b/src/reply.rs @@ -37,6 +37,9 @@ use std::borrow::Cow; use std::convert::TryFrom; use std::error::Error as StdError; use std::fmt; +use std::future::Future; +use std::path::PathBuf; +use std::sync::Arc; use crate::generic::{Either, One}; use http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; @@ -392,6 +395,28 @@ impl Reply for WithHeader { } } + +/// Serve a file as an `impl Reply` +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// use warp::fs::Conditionals; +/// use std::path::PathBuf; +/// +/// let route = warp::any() +/// .and(warp::path::param::()) +/// .and(warp::header::conditionals()) +/// .and_then(|file: String, conditionals: Conditionals| { +/// warp::reply::file(PathBuf::from(file), conditionals) +/// }); +/// ``` +pub fn file(path: impl Into, conditionals: crate::header::Conditionals) -> impl Future> + Send { + let path = Arc::new(path.into()); + crate::fs::file_reply(crate::fs::ArcPath(path), conditionals) +} + impl Reply for ::http::Response where Body: From, From a983cbe2a228b2e5f40525fdb1b5f07244d79317 Mon Sep 17 00:00:00 2001 From: Molkars Date: Sun, 2 Jul 2023 01:20:50 -0600 Subject: [PATCH 2/3] add example for warp::reply::file in examples/file.rs --- examples/file.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/examples/file.rs b/examples/file.rs index a0cf2afa4..94905f448 100644 --- a/examples/file.rs +++ b/examples/file.rs @@ -1,6 +1,7 @@ #![deny(warnings)] use warp::Filter; +use warp::header::Conditionals; #[tokio::main] async fn main() { @@ -10,12 +11,22 @@ async fn main() { .and(warp::path::end()) .and(warp::fs::file("./README.md")); + // try GET /dyn/Cargo.toml or GET /dyn/README.md + let dynamic_file = warp::get() + .and(warp::path::path("dyn")) + .and(warp::path::param::()) + .and(warp::header::conditionals()) + .and_then(|file_name: String, conditionals: Conditionals| { + warp::reply::file(file_name, conditionals) + }); + // dir already requires GET... let examples = warp::path("ex").and(warp::fs::dir("./examples/")); // GET / => README.md + // Get /dyn/{file} => ./{file} // GET /ex/... => ./examples/.. - let routes = readme.or(examples); + let routes = readme.or(dynamic_file).or(examples); warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; } From c98c734bd56d162f6bd6d11eff8130b990429c98 Mon Sep 17 00:00:00 2001 From: Molkars Date: Tue, 11 Jul 2023 08:45:59 -0500 Subject: [PATCH 3/3] appease cargo fmt --- examples/file.rs | 2 +- src/filters/fs.rs | 5 +++-- src/filters/header.rs | 7 ++++--- src/reply.rs | 6 ++++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/examples/file.rs b/examples/file.rs index 94905f448..2b5963628 100644 --- a/examples/file.rs +++ b/examples/file.rs @@ -1,7 +1,7 @@ #![deny(warnings)] -use warp::Filter; use warp::header::Conditionals; +use warp::Filter; #[tokio::main] async fn main() { diff --git a/src/filters/fs.rs b/src/filters/fs.rs index 463a58c39..a8e751cf5 100644 --- a/src/filters/fs.rs +++ b/src/filters/fs.rs @@ -24,7 +24,7 @@ use tokio::io::AsyncSeekExt; use tokio_util::io::poll_read_buf; use crate::filter::{Filter, FilterClone, One}; -use crate::header::{Conditionals, conditionals}; +use crate::header::{conditionals, Conditionals}; use crate::reject::{self, Rejection}; use crate::reply::{Reply, Response}; @@ -424,9 +424,10 @@ unit_error! { #[cfg(test)] mod tests { - use super::sanitize_path; use bytes::BytesMut; + use super::sanitize_path; + #[test] fn test_sanitize_path() { let base = "/var/www"; diff --git a/src/filters/header.rs b/src/filters/header.rs index 267c35b00..51bcba4dd 100644 --- a/src/filters/header.rs +++ b/src/filters/header.rs @@ -8,7 +8,9 @@ use std::convert::Infallible; use std::str::FromStr; use futures_util::future; -use headers::{Header, HeaderMapExt, IfModifiedSince, IfRange, IfUnmodifiedSince, LastModified, Range}; +use headers::{ + Header, HeaderMapExt, IfModifiedSince, IfRange, IfUnmodifiedSince, LastModified, Range, +}; use http::header::HeaderValue; use http::{HeaderMap, StatusCode}; use hyper::Body; @@ -243,7 +245,7 @@ pub fn headers_cloned() -> impl Filter, Error = Infalli /// warp::reply::file("index.html", conditionals) /// }); /// ``` -pub fn conditionals() -> impl Filter, Error = Infallible> + Copy { +pub fn conditionals() -> impl Filter, Error = Infallible> + Copy { optional2() .and(optional2()) .and(optional2()) @@ -258,7 +260,6 @@ pub fn conditionals() -> impl Filter, Error = Infallib ) } - /// Utilized conditional headers to determine response-content #[derive(Debug, Default)] pub struct Conditionals { diff --git a/src/reply.rs b/src/reply.rs index 7bcefedb2..edd3e64d7 100644 --- a/src/reply.rs +++ b/src/reply.rs @@ -395,7 +395,6 @@ impl Reply for WithHeader { } } - /// Serve a file as an `impl Reply` /// /// # Example @@ -412,7 +411,10 @@ impl Reply for WithHeader { /// warp::reply::file(PathBuf::from(file), conditionals) /// }); /// ``` -pub fn file(path: impl Into, conditionals: crate::header::Conditionals) -> impl Future> + Send { +pub fn file( + path: impl Into, + conditionals: crate::header::Conditionals, +) -> impl Future> + Send { let path = Arc::new(path.into()); crate::fs::file_reply(crate::fs::ArcPath(path), conditionals) }