#![allow(clippy::borrow_interior_mutable_const, clippy::type_complexity)]
use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::Arc;
use actix_service::boxed::{self, BoxServiceFactory};
use actix_service::{IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
use actix_web::dev::{
AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse,
};
use actix_web::error::Error as ActixError;
use actix_web::guard::Guard;
use futures_util::future::LocalBoxFuture;
use path_context::PathContext;
use service::FilesService;
use tokio::sync::RwLock;
use self::service::FilesServiceInner;
pub mod directory;
mod named_ext;
mod pages;
mod path;
pub mod path_context;
mod pathbuf;
mod service;
type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, ActixError, ()>;
pub struct Files {
path: String,
directory: Arc<RwLock<Option<PathBuf>>>,
root_path_context: Arc<PathContext>,
#[allow(clippy::rc_buffer)]
path_contexts: Arc<Vec<PathContext>>,
redirect_to_slash: bool,
default: Rc<RefCell<Option<Rc<HttpNewService>>>>,
use_guards: Option<Rc<dyn Guard>>,
guards: Vec<Rc<dyn Guard>>,
}
impl Clone for Files {
fn clone(&self) -> Self {
Self {
directory: self.directory.clone(),
redirect_to_slash: self.redirect_to_slash,
default: self.default.clone(),
path: self.path.clone(),
use_guards: self.use_guards.clone(),
root_path_context: self.root_path_context.clone(),
path_contexts: self.path_contexts.clone(),
guards: self.guards.clone(),
}
}
}
impl Files {
#[allow(clippy::rc_buffer)]
pub fn new(
path: &str,
directory: Arc<RwLock<Option<PathBuf>>>,
root_path_context: Arc<PathContext>,
path_contexts: Arc<Vec<PathContext>>,
) -> Files {
Files {
path: path.trim_end_matches('/').to_string(),
directory,
redirect_to_slash: false,
default: Rc::new(RefCell::new(None)),
use_guards: None,
guards: vec![],
root_path_context,
path_contexts,
}
}
pub fn redirect_to_slash_directory(mut self) -> Self {
self.redirect_to_slash = true;
self
}
pub fn guard<G: Guard + 'static>(mut self, guard: G) -> Self {
self.guards.push(Rc::new(guard));
self
}
#[inline]
pub fn method_guard<G: Guard + 'static>(mut self, guards: G) -> Self {
self.use_guards = Some(Rc::new(guards));
self
}
pub fn default_handler<F, U>(mut self, f: F) -> Self
where
F: IntoServiceFactory<U, ServiceRequest>,
U: ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse,
Error = actix_web::error::Error,
> + 'static,
{
self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory(
f.into_factory().map_init_err(|_| ()),
)))));
self
}
}
impl HttpServiceFactory for Files {
fn register(mut self, config: &mut AppService) {
let guards = if self.guards.is_empty() {
None
} else {
let guards = std::mem::take(&mut self.guards);
Some(
guards
.into_iter()
.map(|guard| -> Box<dyn Guard> { Box::new(guard) })
.collect::<Vec<_>>(),
)
};
if self.default.borrow().is_none() {
*self.default.borrow_mut() = Some(config.default_service());
}
let rdef = if config.is_root() {
ResourceDef::root_prefix(&self.path)
} else {
ResourceDef::prefix(&self.path)
};
config.register_service(rdef, guards, self, None)
}
}
impl ServiceFactory<ServiceRequest> for Files {
type Response = ServiceResponse;
type Error = ActixError;
type Config = ();
type Service = FilesService;
type InitError = ();
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future {
let mut inner = FilesServiceInner::new(
self.directory.clone(),
self.redirect_to_slash,
None,
self.root_path_context.clone(),
self.path_contexts.clone(),
self.use_guards.clone(),
);
if let Some(ref default) = *self.default.borrow() {
let fut = default.new_service(());
Box::pin(async {
match fut.await {
Ok(default) => {
inner.default = Some(default);
Ok(FilesService(Rc::new(inner)))
}
Err(_) => Err(()),
}
})
} else {
Box::pin(async move { Ok(FilesService(Rc::new(inner))) })
}
}
}
#[cfg(test)]
mod tests {
use std::collections::{HashMap, HashSet};
use std::convert::TryInto;
use std::fs;
use super::*;
use crate::config::{ContentDispositionConfig, IndexStrategyConfig, ServerConfig};
use actix_web::guard;
use actix_web::http::{header, Method, StatusCode};
use actix_web::test::{self, TestRequest};
use actix_web::App;
use bytes::Bytes;
fn serve_dir<T: Into<PathBuf>>(path: T) -> Arc<RwLock<Option<PathBuf>>> {
Arc::new(RwLock::new(Some(path.into())))
}
#[actix_rt::test]
async fn test_mime_override() {
let server_config = ServerConfig {
index_strategy: Some(IndexStrategyConfig::IndexFiles {
filenames: HashSet::from(["Cargo.toml".to_owned()]),
}),
mime_disposition: Some(HashMap::from([(
"text/x-toml".to_owned(),
ContentDispositionConfig::Attachment,
)])),
..ServerConfig::default()
};
let root_path_context = Arc::new((&server_config).try_into().unwrap());
let path_contexts = Arc::new(vec![]);
let srv = test::init_service(App::new().service(Files::new(
"/",
serve_dir("."),
root_path_context,
path_contexts,
)))
.await;
let request = TestRequest::get().uri("/").to_request();
let response = test::call_service(&srv, request).await;
assert_eq!(response.status(), StatusCode::OK);
let content_disposition = response
.headers()
.get(header::CONTENT_DISPOSITION)
.expect("To have CONTENT_DISPOSITION");
let content_disposition = content_disposition
.to_str()
.expect("Convert CONTENT_DISPOSITION to str");
assert_eq!(content_disposition, "attachment; filename=\"Cargo.toml\"");
}
#[actix_rt::test]
async fn test_named_file_ranges_status_code() {
let server_config = ServerConfig {
index_strategy: Some(IndexStrategyConfig::IndexFiles {
filenames: ["Cargo.toml".to_owned()].iter().cloned().collect(),
}),
..ServerConfig::default()
};
let root_path_context = Arc::new((&server_config).try_into().unwrap());
let path_contexts = Arc::new(vec![]);
let srv = test::init_service(App::new().service(Files::new(
"/test",
serve_dir("."),
root_path_context,
path_contexts,
)))
.await;
let request = TestRequest::get()
.uri("/t%65st/Cargo.toml")
.append_header((header::RANGE, "bytes=10-20"))
.to_request();
let response = test::call_service(&srv, request).await;
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
let request = TestRequest::get()
.uri("/t%65st/Cargo.toml")
.append_header((header::RANGE, "bytes=1-0"))
.to_request();
let response = test::call_service(&srv, request).await;
assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
}
#[actix_rt::test]
async fn test_named_file_content_range_headers() {
let srv = actix_test::start(|| {
let root_path_context = Arc::new((&ServerConfig::default()).try_into().unwrap());
let path_contexts = Arc::new(vec![]);
App::new().service(Files::new(
"/",
serve_dir("."),
root_path_context,
path_contexts,
))
});
let response = srv
.get("/tests/test.binary")
.append_header((header::RANGE, "bytes=10-20"))
.send()
.await
.unwrap();
let content_range = response.headers().get(header::CONTENT_RANGE).unwrap();
assert_eq!(content_range.to_str().unwrap(), "bytes 10-20/100");
let response = srv
.get("/tests/test.binary")
.append_header((header::RANGE, "bytes=10-5"))
.send()
.await
.unwrap();
let content_range = response.headers().get(header::CONTENT_RANGE).unwrap();
assert_eq!(content_range.to_str().unwrap(), "bytes */100");
}
#[actix_rt::test]
async fn test_named_file_content_length_headers() {
let srv = actix_test::start(|| {
let root_path_context = Arc::new((&ServerConfig::default()).try_into().unwrap());
let path_contexts = Arc::new(vec![]);
App::new().service(Files::new(
"/",
serve_dir("."),
root_path_context,
path_contexts,
))
});
let response = srv
.get("/tests/test.binary")
.append_header((header::RANGE, "bytes=10-20"))
.send()
.await
.unwrap();
let content_length = response.headers().get(header::CONTENT_LENGTH).unwrap();
assert_eq!(content_length.to_str().unwrap(), "11");
let response = srv
.get("/tests/test.binary")
.append_header((header::RANGE, "bytes=0-20"))
.send()
.await
.unwrap();
let content_length = response.headers().get(header::CONTENT_LENGTH).unwrap();
assert_eq!(content_length.to_str().unwrap(), "21");
let mut response = srv.get("/tests/test.binary").send().await.unwrap();
let content_length = response.headers().get(header::CONTENT_LENGTH).unwrap();
assert_eq!(content_length.to_str().unwrap(), "100");
let transfer_encoding = response.headers().get(header::TRANSFER_ENCODING);
assert!(transfer_encoding.is_none());
let bytes = response.body().await.unwrap();
let data = Bytes::from(fs::read("tests/test.binary").unwrap());
assert_eq!(bytes, data);
}
#[actix_rt::test]
async fn test_head_content_length_headers() {
let srv = actix_test::start(|| {
let root_path_context = Arc::new((&ServerConfig::default()).try_into().unwrap());
let path_contexts = Arc::new(vec![]);
App::new().service(Files::new(
"/",
serve_dir("."),
root_path_context,
path_contexts,
))
});
let response = srv.head("/tests/test.binary").send().await.unwrap();
let content_length = response
.headers()
.get(header::CONTENT_LENGTH)
.unwrap()
.to_str()
.unwrap();
assert_eq!(content_length, "100");
}
#[actix_rt::test]
async fn test_static_files_with_spaces() {
let server_config = ServerConfig {
index_strategy: Some(IndexStrategyConfig::IndexFiles {
filenames: ["Cargo.toml".to_owned()].iter().cloned().collect(),
}),
..ServerConfig::default()
};
let root_path_context = Arc::new((&server_config).try_into().unwrap());
let path_contexts = Arc::new(vec![]);
let srv = test::init_service(App::new().service(Files::new(
"/",
serve_dir("."),
root_path_context,
path_contexts,
)))
.await;
let request = TestRequest::get()
.uri("/tests/test%20space.binary")
.to_request();
let response = test::call_service(&srv, request).await;
assert_eq!(response.status(), StatusCode::OK);
let bytes = test::read_body(response).await;
let data = Bytes::from(fs::read("tests/test space.binary").unwrap());
assert_eq!(bytes, data);
}
#[actix_rt::test]
async fn test_files_not_allowed() {
let root_path_context = Arc::new((&ServerConfig::default()).try_into().unwrap());
let path_contexts = Arc::new(vec![]);
let srv = test::init_service(App::new().default_service(Files::new(
"/",
serve_dir("."),
root_path_context,
path_contexts,
)))
.await;
let req = TestRequest::default()
.uri("/Cargo.toml")
.method(Method::POST)
.to_request();
let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
let root_path_context = Arc::new((&ServerConfig::default()).try_into().unwrap());
let path_contexts = Arc::new(vec![]);
let srv = test::init_service(App::new().default_service(Files::new(
"/",
serve_dir("."),
root_path_context,
path_contexts,
)))
.await;
let req = TestRequest::default()
.method(Method::PUT)
.uri("/Cargo.toml")
.to_request();
let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
}
#[actix_rt::test]
async fn test_files_guards() {
let root_path_context = Arc::new((&ServerConfig::default()).try_into().unwrap());
let path_contexts = Arc::new(vec![]);
let srv = test::init_service(App::new().service(
Files::new("/", serve_dir("."), root_path_context, path_contexts).method_guard(guard::Post()),
))
.await;
let req = TestRequest::default()
.uri("/Cargo.toml")
.method(Method::POST)
.to_request();
let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn test_static_files() {
let server_config = ServerConfig {
index_strategy: Some(IndexStrategyConfig::AlwaysShowListing),
..ServerConfig::default()
};
let root_path_context = Arc::new((&server_config).try_into().unwrap());
let path_contexts = Arc::new(vec![]);
let srv = test::init_service(App::new().service(Files::new(
"/",
serve_dir("."),
root_path_context,
path_contexts,
)))
.await;
let req = TestRequest::with_uri("/tests/test.png").to_request();
let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let bytes = test::read_body(resp).await;
let data = Bytes::from(fs::read("tests/test.png").unwrap());
assert_eq!(bytes, data);
}
#[actix_rt::test]
async fn test_static_files_percent_encoded() {
let server_config = ServerConfig {
index_strategy: Some(IndexStrategyConfig::AlwaysShowListing),
..ServerConfig::default()
};
let root_path_context = Arc::new((&server_config).try_into().unwrap());
let path_contexts = Arc::new(vec![]);
let srv = test::init_service(App::new().service(Files::new(
"/",
serve_dir("."),
root_path_context,
path_contexts,
)))
.await;
let req = TestRequest::with_uri("/%43argo.toml").to_request();
let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn test_static_files_with_missing_path() {
let server_config = ServerConfig {
index_strategy: Some(IndexStrategyConfig::AlwaysShowListing),
..ServerConfig::default()
};
let root_path_context = Arc::new((&server_config).try_into().unwrap());
let path_contexts = Arc::new(vec![]);
let srv = test::init_service(App::new().service(Files::new(
"/",
serve_dir("."),
root_path_context,
path_contexts,
)))
.await;
let req = TestRequest::with_uri("/missing").to_request();
let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[actix_rt::test]
async fn test_static_files_without_index_strategy() {
let root_path_context = Arc::new((&ServerConfig::default()).try_into().unwrap());
let path_contexts = Arc::new(vec![]);
let srv = test::init_service(App::new().service(Files::new(
"/",
serve_dir("."),
root_path_context,
path_contexts,
)))
.await;
let req = TestRequest::default().to_request();
let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[actix_rt::test]
async fn test_static_files_with_listing_index_strategy() {
let server_config = ServerConfig {
index_strategy: Some(IndexStrategyConfig::AlwaysShowListing),
..ServerConfig::default()
};
let root_path_context = Arc::new((&server_config).try_into().unwrap());
let path_contexts = Arc::new(vec![]);
let srv = test::init_service(App::new().default_service(Files::new(
"/",
serve_dir("."),
root_path_context,
path_contexts,
)))
.await;
let req = TestRequest::with_uri("/tests").to_request();
let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"text/html; charset=utf-8"
);
let bytes = test::read_body(resp).await;
assert!(format!("{:?}", bytes).contains("/tests/test.png"));
}
#[actix_rt::test]
async fn test_redirect_to_slash_directory() {
let server_config = ServerConfig {
index_strategy: Some(IndexStrategyConfig::IndexFiles {
filenames: ["test.png".to_owned()].iter().cloned().collect(),
}),
..ServerConfig::default()
};
let root_path_context: Arc<PathContext> = Arc::new((&server_config).try_into().unwrap());
let path_contexts = Arc::new(vec![]);
let srv = test::init_service(
App::new().service(
Files::new("/", serve_dir("."), root_path_context, path_contexts)
.redirect_to_slash_directory(),
),
)
.await;
let req = TestRequest::with_uri("/tests").to_request();
let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::FOUND);
let req = TestRequest::with_uri("/not_existing").to_request();
let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[actix_rt::test]
async fn test_static_files_bad_directory() {
let root_path_context = Arc::new((&ServerConfig::default()).try_into().unwrap());
let path_contexts = Arc::new(vec![]);
let _st: Files = Files::new("/", serve_dir("missing"), root_path_context, path_contexts);
let root_path_context = Arc::new((&ServerConfig::default()).try_into().unwrap());
let path_contexts = Arc::new(vec![]);
let _st: Files = Files::new(
"/",
serve_dir("Cargo.toml"),
root_path_context,
path_contexts,
);
}
}