mirror of
https://github.com/fafhrd91/actix-web
synced 2024-11-27 17:52:56 +01:00
files: file path filtering closure (#2274)
Co-authored-by: Rob Ede <robjtede@icloud.com>
This commit is contained in:
parent
539697292a
commit
09afd033fc
@ -1,6 +1,9 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2021-xx-xx
|
## 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
|
## 0.6.0-beta.5 - 2021-06-17
|
||||||
|
@ -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_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
|
||||||
use actix_utils::future::ok;
|
use actix_utils::future::ok;
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
dev::{AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse},
|
dev::{
|
||||||
|
AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest,
|
||||||
|
ServiceResponse,
|
||||||
|
},
|
||||||
error::Error,
|
error::Error,
|
||||||
guard::Guard,
|
guard::Guard,
|
||||||
http::header::DispositionType,
|
http::header::DispositionType,
|
||||||
@ -13,7 +21,7 @@ use futures_core::future::LocalBoxFuture;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
directory_listing, named, Directory, DirectoryRenderer, FilesService, HttpNewService,
|
directory_listing, named, Directory, DirectoryRenderer, FilesService, HttpNewService,
|
||||||
MimeOverride,
|
MimeOverride, PathFilter,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Static files handling service.
|
/// Static files handling service.
|
||||||
@ -36,6 +44,7 @@ pub struct Files {
|
|||||||
default: Rc<RefCell<Option<Rc<HttpNewService>>>>,
|
default: Rc<RefCell<Option<Rc<HttpNewService>>>>,
|
||||||
renderer: Rc<DirectoryRenderer>,
|
renderer: Rc<DirectoryRenderer>,
|
||||||
mime_override: Option<Rc<MimeOverride>>,
|
mime_override: Option<Rc<MimeOverride>>,
|
||||||
|
path_filter: Option<Rc<PathFilter>>,
|
||||||
file_flags: named::Flags,
|
file_flags: named::Flags,
|
||||||
use_guards: Option<Rc<dyn Guard>>,
|
use_guards: Option<Rc<dyn Guard>>,
|
||||||
guards: Vec<Rc<dyn Guard>>,
|
guards: Vec<Rc<dyn Guard>>,
|
||||||
@ -60,6 +69,7 @@ impl Clone for Files {
|
|||||||
file_flags: self.file_flags,
|
file_flags: self.file_flags,
|
||||||
path: self.path.clone(),
|
path: self.path.clone(),
|
||||||
mime_override: self.mime_override.clone(),
|
mime_override: self.mime_override.clone(),
|
||||||
|
path_filter: self.path_filter.clone(),
|
||||||
use_guards: self.use_guards.clone(),
|
use_guards: self.use_guards.clone(),
|
||||||
guards: self.guards.clone(),
|
guards: self.guards.clone(),
|
||||||
hidden_files: self.hidden_files,
|
hidden_files: self.hidden_files,
|
||||||
@ -104,6 +114,7 @@ impl Files {
|
|||||||
default: Rc::new(RefCell::new(None)),
|
default: Rc::new(RefCell::new(None)),
|
||||||
renderer: Rc::new(directory_listing),
|
renderer: Rc::new(directory_listing),
|
||||||
mime_override: None,
|
mime_override: None,
|
||||||
|
path_filter: None,
|
||||||
file_flags: named::Flags::default(),
|
file_flags: named::Flags::default(),
|
||||||
use_guards: None,
|
use_guards: None,
|
||||||
guards: Vec::new(),
|
guards: Vec::new(),
|
||||||
@ -149,6 +160,38 @@ impl Files {
|
|||||||
self
|
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<F>(mut self, f: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(&Path, &RequestHead) -> bool + 'static,
|
||||||
|
{
|
||||||
|
self.path_filter = Some(Rc::new(f));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set index file
|
/// Set index file
|
||||||
///
|
///
|
||||||
/// Shows specific index file for directories instead of
|
/// Shows specific index file for directories instead of
|
||||||
@ -318,6 +361,7 @@ impl ServiceFactory<ServiceRequest> for Files {
|
|||||||
default: None,
|
default: None,
|
||||||
renderer: self.renderer.clone(),
|
renderer: self.renderer.clone(),
|
||||||
mime_override: self.mime_override.clone(),
|
mime_override: self.mime_override.clone(),
|
||||||
|
path_filter: self.path_filter.clone(),
|
||||||
file_flags: self.file_flags,
|
file_flags: self.file_flags,
|
||||||
guards: self.use_guards.clone(),
|
guards: self.use_guards.clone(),
|
||||||
hidden_files: self.hidden_files,
|
hidden_files: self.hidden_files,
|
||||||
|
@ -16,11 +16,12 @@
|
|||||||
|
|
||||||
use actix_service::boxed::{BoxService, BoxServiceFactory};
|
use actix_service::boxed::{BoxService, BoxServiceFactory};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
dev::{ServiceRequest, ServiceResponse},
|
dev::{RequestHead, ServiceRequest, ServiceResponse},
|
||||||
error::Error,
|
error::Error,
|
||||||
http::header::DispositionType,
|
http::header::DispositionType,
|
||||||
};
|
};
|
||||||
use mime_guess::from_ext;
|
use mime_guess::from_ext;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
mod chunked;
|
mod chunked;
|
||||||
mod directory;
|
mod directory;
|
||||||
@ -56,6 +57,8 @@ pub fn file_extension_to_mime(ext: &str) -> mime::Mime {
|
|||||||
|
|
||||||
type MimeOverride = dyn Fn(&mime::Name<'_>) -> DispositionType;
|
type MimeOverride = dyn Fn(&mime::Name<'_>) -> DispositionType;
|
||||||
|
|
||||||
|
type PathFilter = dyn Fn(&Path, &RequestHead) -> bool;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{
|
use std::{
|
||||||
@ -901,4 +904,40 @@ mod tests {
|
|||||||
let bytes = test::read_body(resp).await;
|
let bytes = test::read_body(resp).await;
|
||||||
assert!(format!("{:?}", bytes).contains("/tests/test.png"));
|
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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ use futures_core::future::LocalBoxFuture;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, NamedFile,
|
named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, NamedFile,
|
||||||
PathBufWrap,
|
PathBufWrap, PathFilter,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Assembled file serving service.
|
/// Assembled file serving service.
|
||||||
@ -25,6 +25,7 @@ pub struct FilesService {
|
|||||||
pub(crate) default: Option<HttpService>,
|
pub(crate) default: Option<HttpService>,
|
||||||
pub(crate) renderer: Rc<DirectoryRenderer>,
|
pub(crate) renderer: Rc<DirectoryRenderer>,
|
||||||
pub(crate) mime_override: Option<Rc<MimeOverride>>,
|
pub(crate) mime_override: Option<Rc<MimeOverride>>,
|
||||||
|
pub(crate) path_filter: Option<Rc<PathFilter>>,
|
||||||
pub(crate) file_flags: named::Flags,
|
pub(crate) file_flags: named::Flags,
|
||||||
pub(crate) guards: Option<Rc<dyn Guard>>,
|
pub(crate) guards: Option<Rc<dyn Guard>>,
|
||||||
pub(crate) hidden_files: bool,
|
pub(crate) hidden_files: bool,
|
||||||
@ -82,6 +83,18 @@ impl Service<ServiceRequest> for FilesService {
|
|||||||
Err(e) => return Box::pin(ok(req.error_response(e))),
|
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
|
// full file path
|
||||||
let path = self.directory.join(&real_path);
|
let path = self.directory.join(&real_path);
|
||||||
if let Err(err) = path.canonicalize() {
|
if let Err(err) = path.canonicalize() {
|
||||||
|
Loading…
Reference in New Issue
Block a user