diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index c4d56010f..86e5e6563 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -3,6 +3,12 @@ ## Unreleased - 2020-xx-xx +## 0.5.0 - 2020-12-26 +* Optionally support hidden files/directories. [#1811] + +[#1811]: https://github.com/actix/actix-web/pull/1811 + + ## 0.4.1 - 2020-11-24 * Clarify order of parameters in `Files::new` and improve docs. diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index f7d32f8ec..b67abb1c1 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.4.1" +version = "0.5.0" authors = ["Nikolay Kim "] description = "Static file serving for Actix Web" readme = "README.md" @@ -23,13 +23,13 @@ bitflags = "1" bytes = "0.5.3" futures-core = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false } -derive_more = "0.99.2" +derive_more = "0.99.5" log = "0.4" mime = "0.3" mime_guess = "2.0.1" percent-encoding = "2.1" -v_htmlescape = "0.11" +v_htmlescape = "0.12" [dev-dependencies] actix-rt = "1.0.0" -actix-web = { version = "3.0.0", features = ["openssl"] } +actix-web = "3.0.0" diff --git a/actix-files/README.md b/actix-files/README.md index 685e5dbe5..2953b4458 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -3,11 +3,11 @@ > Static file serving for Actix Web [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) -[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.4.1)](https://docs.rs/actix-files/0.4.1) +[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.5.0)](https://docs.rs/actix-files/0.5.0) [![Version](https://img.shields.io/badge/rustc-1.42+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html) ![License](https://img.shields.io/crates/l/actix-files.svg)
-[![dependency status](https://deps.rs/crate/actix-files/0.4.1/status.svg)](https://deps.rs/crate/actix-files/0.4.1) +[![dependency status](https://deps.rs/crate/actix-files/0.5.0/status.svg)](https://deps.rs/crate/actix-files/0.5.0) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs index a99b4699e..d0cac6aa4 100644 --- a/actix-files/src/files.rs +++ b/actix-files/src/files.rs @@ -39,6 +39,7 @@ pub struct Files { mime_override: Option>, file_flags: named::Flags, guards: Option>, + hidden_files: bool, } impl fmt::Debug for Files { @@ -60,6 +61,7 @@ impl Clone for Files { path: self.path.clone(), mime_override: self.mime_override.clone(), guards: self.guards.clone(), + hidden_files: self.hidden_files, } } } @@ -103,6 +105,7 @@ impl Files { mime_override: None, file_flags: named::Flags::default(), guards: None, + hidden_files: false, } } @@ -213,6 +216,13 @@ impl Files { self } + + /// Enables serving hidden files and directories, allowing a leading dots in url fragments. + #[inline] + pub fn use_hidden_files(mut self) -> Self { + self.hidden_files = true; + self + } } impl HttpServiceFactory for Files { @@ -251,6 +261,7 @@ impl ServiceFactory for Files { mime_override: self.mime_override.clone(), file_flags: self.file_flags, guards: self.guards.clone(), + hidden_files: self.hidden_files, }; if let Some(ref default) = *self.default.borrow() { diff --git a/actix-files/src/path_buf.rs b/actix-files/src/path_buf.rs index 2f3ae84d4..dd8e5b503 100644 --- a/actix-files/src/path_buf.rs +++ b/actix-files/src/path_buf.rs @@ -15,12 +15,19 @@ impl FromStr for PathBufWrap { type Err = UriSegmentError; fn from_str(path: &str) -> Result { + Self::parse_path(path, false) + } +} + +impl PathBufWrap { + /// Parse a path, giving the choice of allowing hidden files to be considered valid segments. + pub fn parse_path(path: &str, hidden_files: bool) -> Result { let mut buf = PathBuf::new(); for segment in path.split('/') { if segment == ".." { buf.pop(); - } else if segment.starts_with('.') { + } else if !hidden_files && segment.starts_with('.') { return Err(UriSegmentError::BadStart('.')); } else if segment.starts_with('*') { return Err(UriSegmentError::BadStart('*')); @@ -96,4 +103,17 @@ mod tests { PathBuf::from_iter(vec!["seg2"]) ); } + + #[test] + fn test_parse_path() { + assert_eq!( + PathBufWrap::parse_path("/test/.tt", false).map(|t| t.0), + Err(UriSegmentError::BadStart('.')) + ); + + assert_eq!( + PathBufWrap::parse_path("/test/.tt", true).unwrap().0, + PathBuf::from_iter(vec!["test", ".tt"]) + ); + } } diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs index cbf4c2d3b..dc4f2bd2c 100644 --- a/actix-files/src/service.rs +++ b/actix-files/src/service.rs @@ -31,6 +31,7 @@ pub struct FilesService { pub(crate) mime_override: Option>, pub(crate) file_flags: named::Flags, pub(crate) guards: Option>, + pub(crate) hidden_files: bool, } type FilesServiceFuture = Either< @@ -83,10 +84,11 @@ impl Service for FilesService { ))); } - let real_path: PathBufWrap = match req.match_info().path().parse() { - Ok(item) => item, - Err(e) => return Either::Left(ok(req.error_response(e))), - }; + let real_path = + match PathBufWrap::parse_path(req.match_info().path(), self.hidden_files) { + Ok(item) => item, + Err(e) => return Either::Left(ok(req.error_response(e))), + }; // full file path let path = match self.directory.join(&real_path).canonicalize() { diff --git a/actix-http/src/h1/mod.rs b/actix-http/src/h1/mod.rs index 0c85f076a..3d5dea5d6 100644 --- a/actix-http/src/h1/mod.rs +++ b/actix-http/src/h1/mod.rs @@ -17,7 +17,7 @@ pub use self::codec::Codec; pub use self::dispatcher::Dispatcher; pub use self::expect::ExpectHandler; pub use self::payload::Payload; -pub use self::service::{H1Service, H1ServiceHandler, OneRequest}; +pub use self::service::{H1Service, H1ServiceHandler}; pub use self::upgrade::UpgradeHandler; pub use self::utils::SendResponse; diff --git a/actix-http/src/h1/service.rs b/actix-http/src/h1/service.rs index 5008791c0..cbba0609c 100644 --- a/actix-http/src/h1/service.rs +++ b/actix-http/src/h1/service.rs @@ -9,12 +9,12 @@ use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_rt::net::TcpStream; use actix_service::{pipeline_factory, IntoServiceFactory, Service, ServiceFactory}; use futures_core::ready; -use futures_util::future::{ok, Ready}; +use futures_util::future::ready; use crate::body::MessageBody; use crate::cloneable::CloneableService; use crate::config::ServiceConfig; -use crate::error::{DispatchError, Error, ParseError}; +use crate::error::{DispatchError, Error}; use crate::helpers::DataFactory; use crate::request::Request; use crate::response::Response; @@ -22,7 +22,7 @@ use crate::{ConnectCallback, Extensions}; use super::codec::Codec; use super::dispatcher::Dispatcher; -use super::{ExpectHandler, Message, UpgradeHandler}; +use super::{ExpectHandler, UpgradeHandler}; /// `ServiceFactory` implementation for HTTP1 transport pub struct H1Service> { @@ -90,7 +90,7 @@ where > { pipeline_factory(|io: TcpStream| { let peer_addr = io.peer_addr().ok(); - ok((io, peer_addr)) + ready(Ok((io, peer_addr))) }) .and_then(self) } @@ -139,7 +139,7 @@ mod openssl { ) .and_then(|io: SslStream| { let peer_addr = io.get_ref().peer_addr().ok(); - ok((io, peer_addr)) + ready(Ok((io, peer_addr))) }) .and_then(self.map_err(TlsError::Service)) } @@ -189,7 +189,7 @@ mod rustls { ) .and_then(|io: TlsStream| { let peer_addr = io.get_ref().0.peer_addr().ok(); - ok((io, peer_addr)) + ready(Ok((io, peer_addr))) }) .and_then(self.map_err(TlsError::Service)) } @@ -500,103 +500,3 @@ where ) } } - -/// `ServiceFactory` implementation for `OneRequestService` service -#[derive(Default)] -pub struct OneRequest { - config: ServiceConfig, - _t: PhantomData, -} - -impl OneRequest -where - T: AsyncRead + AsyncWrite + Unpin, -{ - /// Create new `H1SimpleService` instance. - pub fn new() -> Self { - OneRequest { - config: ServiceConfig::default(), - _t: PhantomData, - } - } -} - -impl ServiceFactory for OneRequest -where - T: AsyncRead + AsyncWrite + Unpin, -{ - type Config = (); - type Request = T; - type Response = (Request, Framed); - type Error = ParseError; - type InitError = (); - type Service = OneRequestService; - type Future = Ready>; - - fn new_service(&self, _: ()) -> Self::Future { - ok(OneRequestService { - _t: PhantomData, - config: self.config.clone(), - }) - } -} - -/// `Service` implementation for HTTP1 transport. Reads one request and returns -/// request and framed object. -pub struct OneRequestService { - _t: PhantomData, - config: ServiceConfig, -} - -impl Service for OneRequestService -where - T: AsyncRead + AsyncWrite + Unpin, -{ - type Request = T; - type Response = (Request, Framed); - type Error = ParseError; - type Future = OneRequestServiceResponse; - - fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - OneRequestServiceResponse { - framed: Some(Framed::new(req, Codec::new(self.config.clone()))), - } - } -} - -#[doc(hidden)] -#[pin_project::pin_project] -pub struct OneRequestServiceResponse -where - T: AsyncRead + AsyncWrite + Unpin, -{ - #[pin] - framed: Option>, -} - -impl Future for OneRequestServiceResponse -where - T: AsyncRead + AsyncWrite + Unpin, -{ - type Output = Result<(Request, Framed), ParseError>; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.as_mut().project(); - - match ready!(this.framed.as_pin_mut().unwrap().next_item(cx)) { - Some(Ok(req)) => match req { - Message::Item(req) => { - let mut this = self.as_mut().project(); - Poll::Ready(Ok((req, this.framed.take().unwrap()))) - } - Message::Chunk(_) => unreachable!("Something is wrong"), - }, - Some(Err(err)) => Poll::Ready(Err(err)), - None => Poll::Ready(Err(ParseError::Incomplete)), - } - } -}