diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index bec67dd4e..54b0344a4 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx +* Added `Files::path_filter()`. [#2274] + +[#2274]: https://github.com/actix/actix-web/pull/2274 ## 0.6.0-beta.5 - 2021-06-17 diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs index c48cf59a7..49d81eb03 100644 --- a/actix-files/src/files.rs +++ b/actix-files/src/files.rs @@ -1,9 +1,17 @@ -use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc}; +use std::{ + cell::RefCell, + fmt, io, + path::{Path, PathBuf}, + rc::Rc, +}; use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt}; use actix_utils::future::ok; use actix_web::{ - dev::{AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse}, + dev::{ + AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest, + ServiceResponse, + }, error::Error, guard::Guard, http::header::DispositionType, @@ -13,7 +21,7 @@ use futures_core::future::LocalBoxFuture; use crate::{ directory_listing, named, Directory, DirectoryRenderer, FilesService, HttpNewService, - MimeOverride, + MimeOverride, PathFilter, }; /// Static files handling service. @@ -36,6 +44,7 @@ pub struct Files { default: Rc>>>, renderer: Rc, mime_override: Option>, + path_filter: Option>, file_flags: named::Flags, use_guards: Option>, guards: Vec>, @@ -60,6 +69,7 @@ impl Clone for Files { file_flags: self.file_flags, path: self.path.clone(), mime_override: self.mime_override.clone(), + path_filter: self.path_filter.clone(), use_guards: self.use_guards.clone(), guards: self.guards.clone(), hidden_files: self.hidden_files, @@ -104,6 +114,7 @@ impl Files { default: Rc::new(RefCell::new(None)), renderer: Rc::new(directory_listing), mime_override: None, + path_filter: None, file_flags: named::Flags::default(), use_guards: None, guards: Vec::new(), @@ -149,6 +160,38 @@ impl Files { self } + /// Sets path filtering closure. + /// + /// The path provided to the closure is relative to `serve_from` path. + /// You can safely join this path with the `serve_from` path to get the real path. + /// However, the real path may not exist since the filter is called before checking path existence. + /// + /// When a path doesn't pass the filter, [`Files::default_handler`] is called if set, otherwise, + /// `404 Not Found` is returned. + /// + /// # Examples + /// ``` + /// use std::path::Path; + /// use actix_files::Files; + /// + /// // prevent searching subdirectories and following symlinks + /// let files_service = Files::new("/", "./static").path_filter(|path, _| { + /// path.components().count() == 1 + /// && Path::new("./static") + /// .join(path) + /// .symlink_metadata() + /// .map(|m| !m.file_type().is_symlink()) + /// .unwrap_or(false) + /// }); + /// ``` + pub fn path_filter(mut self, f: F) -> Self + where + F: Fn(&Path, &RequestHead) -> bool + 'static, + { + self.path_filter = Some(Rc::new(f)); + self + } + /// Set index file /// /// Shows specific index file for directories instead of @@ -318,6 +361,7 @@ impl ServiceFactory for Files { default: None, renderer: self.renderer.clone(), mime_override: self.mime_override.clone(), + path_filter: self.path_filter.clone(), file_flags: self.file_flags, guards: self.use_guards.clone(), hidden_files: self.hidden_files, diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index c9cc79193..1eb091aaf 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -16,11 +16,12 @@ use actix_service::boxed::{BoxService, BoxServiceFactory}; use actix_web::{ - dev::{ServiceRequest, ServiceResponse}, + dev::{RequestHead, ServiceRequest, ServiceResponse}, error::Error, http::header::DispositionType, }; use mime_guess::from_ext; +use std::path::Path; mod chunked; mod directory; @@ -56,6 +57,8 @@ pub fn file_extension_to_mime(ext: &str) -> mime::Mime { type MimeOverride = dyn Fn(&mime::Name<'_>) -> DispositionType; +type PathFilter = dyn Fn(&Path, &RequestHead) -> bool; + #[cfg(test)] mod tests { use std::{ @@ -901,4 +904,40 @@ mod tests { let bytes = test::read_body(resp).await; assert!(format!("{:?}", bytes).contains("/tests/test.png")); } + + #[actix_rt::test] + async fn test_path_filter() { + // prevent searching subdirectories + let st = Files::new("/", ".") + .path_filter(|path, _| path.components().count() == 1) + .new_service(()) + .await + .unwrap(); + + let req = TestRequest::with_uri("/Cargo.toml").to_srv_request(); + let resp = test::call_service(&st, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/src/lib.rs").to_srv_request(); + let resp = test::call_service(&st, req).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[actix_rt::test] + async fn test_default_handler_filter() { + let st = Files::new("/", ".") + .default_handler(|req: ServiceRequest| { + ok(req.into_response(HttpResponse::Ok().body("default content"))) + }) + .path_filter(|path, _| path.extension() == Some("png".as_ref())) + .new_service(()) + .await + .unwrap(); + let req = TestRequest::with_uri("/Cargo.toml").to_srv_request(); + let resp = test::call_service(&st, req).await; + + assert_eq!(resp.status(), StatusCode::OK); + let bytes = test::read_body(resp).await; + assert_eq!(bytes, web::Bytes::from_static(b"default content")); + } } diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs index 64938e5ef..09122c63e 100644 --- a/actix-files/src/service.rs +++ b/actix-files/src/service.rs @@ -13,7 +13,7 @@ use futures_core::future::LocalBoxFuture; use crate::{ named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, NamedFile, - PathBufWrap, + PathBufWrap, PathFilter, }; /// Assembled file serving service. @@ -25,6 +25,7 @@ pub struct FilesService { pub(crate) default: Option, pub(crate) renderer: Rc, pub(crate) mime_override: Option>, + pub(crate) path_filter: Option>, pub(crate) file_flags: named::Flags, pub(crate) guards: Option>, pub(crate) hidden_files: bool, @@ -82,6 +83,18 @@ impl Service for FilesService { Err(e) => return Box::pin(ok(req.error_response(e))), }; + if let Some(filter) = &self.path_filter { + if !filter(real_path.as_ref(), req.head()) { + if let Some(ref default) = self.default { + return Box::pin(default.call(req)); + } else { + return Box::pin(ok( + req.into_response(actix_web::HttpResponse::NotFound().finish()) + )); + } + } + } + // full file path let path = self.directory.join(&real_path); if let Err(err) = path.canonicalize() {