From 532cb50773e32f079a172b5fc928a007ce852416 Mon Sep 17 00:00:00 2001 From: Eduardo Trujillo <ed@chromabits.com> Date: Tue, 15 Nov 2022 18:20:00 -0800 Subject: [PATCH] test(files): Enable tests in files service again --- Cargo.lock | 101 ++++ Cargo.toml | 3 + src/files/mod.rs | 1360 ++++++++++++++++++---------------------------- 3 files changed, 624 insertions(+), 840 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a47aacf..4142925 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -181,6 +181,32 @@ dependencies = [ "zstd", ] +[[package]] +name = "actix-http-test" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40511826540d084fbcd68ee65b75b1849961c1760a193b09180a4851f20075b" +dependencies = [ + "actix-codec 0.5.0", + "actix-rt 2.7.0", + "actix-server", + "actix-service 2.0.2", + "actix-tls", + "actix-utils 3.0.1", + "awc", + "base64 0.13.1", + "bytes 1.2.1", + "futures-core", + "http", + "log", + "serde", + "serde_json", + "serde_urlencoded 0.7.1", + "slab", + "socket2 0.4.7", + "tokio 1.21.2", +] + [[package]] name = "actix-macros" version = "0.1.3" @@ -279,6 +305,29 @@ dependencies = [ "pin-project-lite 0.2.9", ] +[[package]] +name = "actix-test" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "546b075f2ee13e081a040b60b95a08f0eceaac6bc759309026611234dc80abfe" +dependencies = [ + "actix-codec 0.5.0", + "actix-http 3.2.2", + "actix-http-test", + "actix-rt 2.7.0", + "actix-service 2.0.2", + "actix-utils 3.0.1", + "actix-web", + "awc", + "futures-core", + "futures-util", + "log", + "serde", + "serde_json", + "serde_urlencoded 0.7.1", + "tokio 1.21.2", +] + [[package]] name = "actix-threadpool" version = "0.3.3" @@ -294,6 +343,23 @@ dependencies = [ "threadpool", ] +[[package]] +name = "actix-tls" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fde0cf292f7cdc7f070803cb9a0d45c018441321a78b1042ffbbb81ec333297" +dependencies = [ + "actix-codec 0.5.0", + "actix-rt 2.7.0", + "actix-service 2.0.2", + "actix-utils 3.0.1", + "futures-core", + "http", + "log", + "pin-project-lite 0.2.9", + "tokio-util 0.7.4", +] + [[package]] name = "actix-utils" version = "1.0.6" @@ -539,6 +605,40 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "awc" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80ca7ff88063086d2e2c70b9f3b29b2fcd999bac68ac21731e66781970d68519" +dependencies = [ + "actix-codec 0.5.0", + "actix-http 3.2.2", + "actix-rt 2.7.0", + "actix-service 2.0.2", + "actix-tls", + "actix-utils 3.0.1", + "ahash", + "base64 0.13.1", + "bytes 1.2.1", + "cfg-if 1.0.0", + "cookie", + "derive_more", + "futures-core", + "futures-util", + "h2 0.3.15", + "http", + "itoa 1.0.4", + "log", + "mime", + "percent-encoding", + "pin-project-lite 0.2.9", + "rand 0.8.5", + "serde", + "serde_json", + "serde_urlencoded 0.7.1", + "tokio 1.21.2", +] + [[package]] name = "axum" version = "0.5.17" @@ -1101,6 +1201,7 @@ dependencies = [ "actix-http 1.0.1", "actix-rt 2.7.0", "actix-service 2.0.2", + "actix-test", "actix-web", "anyhow", "async-compression", diff --git a/Cargo.toml b/Cargo.toml index b79716b..4657a66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,5 +62,8 @@ rev = "1bd30fbd1a219e8982571da48eb68f34317d1e15" version = "0.3" features = ["gzip", "tokio"] +[dev-dependencies] +actix-test = "0.1.0" + [features] console-subscriber = ["dep:console-subscriber", "tokio/tracing"] \ No newline at end of file diff --git a/src/files/mod.rs b/src/files/mod.rs index 78e48b6..026d17d 100644 --- a/src/files/mod.rs +++ b/src/files/mod.rs @@ -69,7 +69,8 @@ pub struct Files { path_contexts: Arc<Vec<PathContext>>, redirect_to_slash: bool, default: Rc<RefCell<Option<Rc<HttpNewService>>>>, - guards: Option<Rc<dyn Guard>>, + use_guards: Option<Rc<dyn Guard>>, + guards: Vec<Rc<dyn Guard>>, } impl Clone for Files { @@ -79,9 +80,10 @@ impl Clone for Files { redirect_to_slash: self.redirect_to_slash, default: self.default.clone(), path: self.path.clone(), - guards: self.guards.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(), } } } @@ -109,11 +111,12 @@ impl Files { // }; Files { - path: path.to_string(), + path: path.trim_end_matches('/').to_string(), directory, redirect_to_slash: false, default: Rc::new(RefCell::new(None)), - guards: None, + use_guards: None, + guards: vec![], root_path_context, path_contexts, } @@ -127,12 +130,50 @@ impl Files { self } + /// Adds a routing guard. + /// + /// Use this to allow multiple chained file services that respond to strictly different + /// properties of a request. Due to the way routing works, if a guard check returns true and the + /// request starts being handled by the file service, it will not be able to back-out and try + /// the next service, you will simply get a 404 (or 405) error response. + /// + /// To allow `POST` requests to retrieve files, see [`Files::use_guards`]. + /// + /// # Examples + /// ``` + /// use std::{ + /// convert::TryInto, + /// sync::Arc, + /// }; + /// use actix_web::{guard::Header, App}; + /// use espresso::{ + /// files::Files, + /// config::ServerConfig, + /// }; + /// use tokio::sync::RwLock; + /// + /// let mut server_config = ServerConfig::default(); + /// + /// let serve_dir = Arc::new(RwLock::new(Some(".".into()))); + /// let root_path_context = Arc::new((&server_config).try_into().unwrap()); + /// let path_contexts = Arc::new(vec![]); + /// + /// App::new().service( + /// Files::new("/", serve_dir, root_path_context, path_contexts) + /// .guard(Header("Host", "example.com")) + /// ); + /// ``` + pub fn guard<G: Guard + 'static>(mut self, guard: G) -> Self { + self.guards.push(Rc::new(guard)); + self + } + /// Specifies custom guards to use for directory listings and files. /// /// Default behaviour allows GET and HEAD. #[inline] - pub fn use_guards<G: Guard + 'static>(mut self, guards: G) -> Self { - self.guards = Some(Rc::new(guards)); + pub fn method_guard<G: Guard + 'static>(mut self, guards: G) -> Self { + self.use_guards = Some(Rc::new(guards)); self } @@ -157,7 +198,19 @@ impl Files { } impl HttpServiceFactory for Files { - fn register(self, config: &mut AppService) { + 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()); } @@ -168,7 +221,7 @@ impl HttpServiceFactory for Files { ResourceDef::prefix(&self.path) }; - config.register_service(rdef, None, self, None) + config.register_service(rdef, guards, self, None) } } @@ -187,7 +240,7 @@ impl ServiceFactory<ServiceRequest> for Files { None, self.root_path_context.clone(), self.path_contexts.clone(), - self.guards.clone(), + self.use_guards.clone(), ); if let Some(ref default) = *self.default.borrow() { @@ -210,835 +263,462 @@ impl ServiceFactory<ServiceRequest> for Files { #[cfg(test)] mod tests { - // use std::fs; - // use std::ops::Add; - // use std::{ - // convert::TryInto, - // time::{Duration, SystemTime}, - // }; - - // use super::*; - // use crate::config::{ContentDispositionConfig, IndexStrategyConfig, ServerConfig}; - // use actix_files::NamedFile; - // use actix_web::guard; - // use actix_web::http::header::{self, ContentDisposition, DispositionParam, DispositionType}; - // use actix_web::http::{Method, StatusCode}; - // use actix_web::middleware::Compress; - // use actix_web::test::{self, TestRequest}; - // use actix_web::{web, App, HttpResponse, Responder}; - // use bytes::Bytes; - // use fs::File; - - // 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_if_modified_since_without_if_none_match() { - // let file = NamedFile::open("Cargo.toml").unwrap(); - // let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); - - // let req = TestRequest::default() - // .header(header::IF_MODIFIED_SINCE, since) - // .to_http_request(); - // let resp = file.respond_to(&req).await.unwrap(); - // assert_eq!(resp.status(), StatusCode::NOT_MODIFIED); - // } - - // #[actix_rt::test] - // async fn test_if_modified_since_with_if_none_match() { - // let file = NamedFile::open("Cargo.toml").unwrap(); - // let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); - - // let req = TestRequest::default() - // .header(header::IF_NONE_MATCH, "miss_etag") - // .header(header::IF_MODIFIED_SINCE, since) - // .to_http_request(); - // let resp = file.respond_to(&req).await.unwrap(); - // assert_ne!(resp.status(), StatusCode::NOT_MODIFIED); - // } - - // #[actix_rt::test] - // async fn test_named_file_text() { - // assert!(NamedFile::open("test--").is_err()); - // let mut file = NamedFile::open("Cargo.toml").unwrap(); - // { - // file.file(); - // let _f: &File = &file; - // } - // { - // let _f: &mut File = &mut file; - // } - - // let req = TestRequest::default().to_http_request(); - // let resp = file.respond_to(&req).await.unwrap(); - // assert_eq!( - // resp.headers().get(header::CONTENT_TYPE).unwrap(), - // "text/x-toml" - // ); - // assert_eq!( - // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - // "inline; filename=\"Cargo.toml\"" - // ); - // } - - // #[actix_rt::test] - // async fn test_named_file_content_disposition() { - // assert!(NamedFile::open("test--").is_err()); - // let mut file = NamedFile::open("Cargo.toml").unwrap(); - // { - // file.file(); - // let _f: &File = &file; - // } - // { - // let _f: &mut File = &mut file; - // } - - // let req = TestRequest::default().to_http_request(); - // let resp = file.respond_to(&req).await.unwrap(); - // assert_eq!( - // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - // "inline; filename=\"Cargo.toml\"" - // ); - - // let file = NamedFile::open("Cargo.toml") - // .unwrap() - // .disable_content_disposition(); - // let req = TestRequest::default().to_http_request(); - // let resp = file.respond_to(&req).await.unwrap(); - // assert!(resp.headers().get(header::CONTENT_DISPOSITION).is_none()); - // } - - // #[actix_rt::test] - // async fn test_named_file_non_ascii_file_name() { - // let mut file = NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml").unwrap(); - // { - // file.file(); - // let _f: &File = &file; - // } - // { - // let _f: &mut File = &mut file; - // } - - // let req = TestRequest::default().to_http_request(); - // let resp = file.respond_to(&req).await.unwrap(); - // assert_eq!( - // resp.headers().get(header::CONTENT_TYPE).unwrap(), - // "text/x-toml" - // ); - // assert_eq!( - // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - // "inline; filename=\"貨物.toml\"; filename*=UTF-8''%E8%B2%A8%E7%89%A9.toml" - // ); - // } - - // #[actix_rt::test] - // async fn test_named_file_set_content_type() { - // let mut file = NamedFile::open("Cargo.toml") - // .unwrap() - // .set_content_type(mime::TEXT_XML); - // { - // file.file(); - // let _f: &File = &file; - // } - // { - // let _f: &mut File = &mut file; - // } - - // let req = TestRequest::default().to_http_request(); - // let resp = file.respond_to(&req).await.unwrap(); - // assert_eq!( - // resp.headers().get(header::CONTENT_TYPE).unwrap(), - // "text/xml" - // ); - // assert_eq!( - // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - // "inline; filename=\"Cargo.toml\"" - // ); - // } - - // #[actix_rt::test] - // async fn test_named_file_image() { - // let mut file = NamedFile::open("tests/test.png").unwrap(); - // { - // file.file(); - // let _f: &File = &file; - // } - // { - // let _f: &mut File = &mut file; - // } - - // let req = TestRequest::default().to_http_request(); - // let resp = file.respond_to(&req).await.unwrap(); - // assert_eq!( - // resp.headers().get(header::CONTENT_TYPE).unwrap(), - // "image/png" - // ); - // assert_eq!( - // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - // "inline; filename=\"test.png\"" - // ); - // } - - // #[actix_rt::test] - // async fn test_named_file_image_attachment() { - // let cd = ContentDisposition { - // disposition: DispositionType::Attachment, - // parameters: vec![DispositionParam::Filename(String::from("test.png"))], - // }; - // let mut file = NamedFile::open("tests/test.png") - // .unwrap() - // .set_content_disposition(cd); - // { - // file.file(); - // let _f: &File = &file; - // } - // { - // let _f: &mut File = &mut file; - // } - - // let req = TestRequest::default().to_http_request(); - // let resp = file.respond_to(&req).await.unwrap(); - // assert_eq!( - // resp.headers().get(header::CONTENT_TYPE).unwrap(), - // "image/png" - // ); - // assert_eq!( - // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - // "attachment; filename=\"test.png\"" - // ); - // } - - // #[actix_rt::test] - // async fn test_named_file_binary() { - // let mut file = NamedFile::open("tests/test.binary").unwrap(); - // { - // file.file(); - // let _f: &File = &file; - // } - // { - // let _f: &mut File = &mut file; - // } - - // let req = TestRequest::default().to_http_request(); - // let resp = file.respond_to(&req).await.unwrap(); - // assert_eq!( - // resp.headers().get(header::CONTENT_TYPE).unwrap(), - // "application/octet-stream" - // ); - // assert_eq!( - // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - // "attachment; filename=\"test.binary\"" - // ); - // } - - // #[actix_rt::test] - // async fn test_named_file_status_code_text() { - // let mut file = NamedFile::open("Cargo.toml") - // .unwrap() - // .set_status_code(StatusCode::NOT_FOUND); - // { - // file.file(); - // let _f: &File = &file; - // } - // { - // let _f: &mut File = &mut file; - // } - - // let req = TestRequest::default().to_http_request(); - // let resp = file.respond_to(&req).await.unwrap(); - // assert_eq!( - // resp.headers().get(header::CONTENT_TYPE).unwrap(), - // "text/x-toml" - // ); - // assert_eq!( - // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - // "inline; filename=\"Cargo.toml\"" - // ); - // assert_eq!(resp.status(), StatusCode::NOT_FOUND); - // } - - // #[actix_rt::test] - // async fn test_mime_override() { - // let mut server_config = ServerConfig::default(); - - // server_config.index_strategy = Some(IndexStrategyConfig::IndexFiles { - // filenames: ["Cargo.toml".to_owned()].iter().cloned().collect(), - // }); - // server_config.mime_disposition = Some( - // vec![( - // "text/x-toml".to_owned(), - // ContentDispositionConfig::Attachment, - // )] - // .iter() - // .cloned() - // .collect(), - // ); - - // let root_path_context = Arc::new((&server_config).try_into().unwrap()); - // let path_contexts = Arc::new(vec![]); - - // let mut 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(&mut 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 mut server_config = ServerConfig::default(); - - // server_config.index_strategy = Some(IndexStrategyConfig::IndexFiles { - // filenames: ["Cargo.toml".to_owned()].iter().cloned().collect(), - // }); - - // let root_path_context = Arc::new((&server_config).try_into().unwrap()); - // let path_contexts = Arc::new(vec![]); - - // let mut srv = test::init_service(App::new().service(Files::new( - // "/test", - // serve_dir("."), - // root_path_context, - // path_contexts, - // ))) - // .await; - - // // Valid range header - // let request = TestRequest::get() - // .uri("/t%65st/Cargo.toml") - // .header(header::RANGE, "bytes=10-20") - // .to_request(); - // let response = test::call_service(&mut srv, request).await; - // assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); - - // // Invalid range header - // let request = TestRequest::get() - // .uri("/t%65st/Cargo.toml") - // .header(header::RANGE, "bytes=1-0") - // .to_request(); - // let response = test::call_service(&mut srv, request).await; - - // assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); - // } - - // #[actix_rt::test] - // async fn test_named_file_content_range_headers() { - // let srv = test::init_service(|| { - // 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, - // )) - // }); - - // // Valid range header - // let response = srv - // .get("/tests/test.binary") - // .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"); - - // // Invalid range header - // let response = srv - // .get("/tests/test.binary") - // .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 = test::init_service(|| { - // 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, - // )) - // }); - - // // Valid range header - // let response = srv - // .get("/tests/test.binary") - // .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"); - - // // Valid range header, starting from 0 - // let response = srv - // .get("/tests/test.binary") - // .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"); - - // // Without range header - // 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"); - - // // Should be no transfer-encoding - // let transfer_encoding = response.headers().get(header::TRANSFER_ENCODING); - // assert!(transfer_encoding.is_none()); - - // // Check file contents - // 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 = test::init_service(|| { - // 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 mut server_config = ServerConfig::default(); - - // server_config.index_strategy = Some(IndexStrategyConfig::IndexFiles { - // filenames: ["Cargo.toml".to_owned()].iter().cloned().collect(), - // }); - - // let root_path_context = Arc::new((&server_config).try_into().unwrap()); - // let path_contexts = Arc::new(vec![]); - - // let mut 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(&mut 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 mut 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(&mut 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 mut 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(&mut 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 mut srv = test::init_service(App::new().service( - // Files::new("/", serve_dir("."), root_path_context, path_contexts).use_guards(guard::Post()), - // )) - // .await; - - // let req = TestRequest::default() - // .uri("/Cargo.toml") - // .method(Method::POST) - // .to_request(); - - // let resp = test::call_service(&mut srv, req).await; - // assert_eq!(resp.status(), StatusCode::OK); - // } - - // #[actix_rt::test] - // async fn test_named_file_content_encoding() { - // let mut srv = test::init_service(App::new().wrap(Compress::default()).service( - // web::resource("/").to(|| async { - // NamedFile::open("Cargo.toml") - // .unwrap() - // .set_content_encoding(header::ContentEncoding::Identity) - // }), - // )) - // .await; - - // let request = TestRequest::get() - // .uri("/") - // .header(header::ACCEPT_ENCODING, "gzip") - // .to_request(); - // let res = test::call_service(&mut srv, request).await; - // assert_eq!(res.status(), StatusCode::OK); - // assert!(!res.headers().contains_key(header::CONTENT_ENCODING)); - // } - - // #[actix_rt::test] - // async fn test_named_file_content_encoding_gzip() { - // let mut srv = test::init_service(App::new().wrap(Compress::default()).service( - // web::resource("/").to(|| async { - // NamedFile::open("Cargo.toml") - // .unwrap() - // .set_content_encoding(header::ContentEncoding::Gzip) - // }), - // )) - // .await; - - // let request = TestRequest::get() - // .uri("/") - // .header(header::ACCEPT_ENCODING, "gzip") - // .to_request(); - // let res = test::call_service(&mut srv, request).await; - // assert_eq!(res.status(), StatusCode::OK); - // assert_eq!( - // res - // .headers() - // .get(header::CONTENT_ENCODING) - // .unwrap() - // .to_str() - // .unwrap(), - // "gzip" - // ); - // } - - // #[actix_rt::test] - // async fn test_named_file_allowed_method() { - // let req = TestRequest::default().method(Method::GET).to_http_request(); - // let file = NamedFile::open("Cargo.toml").unwrap(); - // let resp = file.respond_to(&req).await.unwrap(); - // assert_eq!(resp.status(), StatusCode::OK); - // } - - // #[actix_rt::test] - // async fn test_static_files() { - // let mut server_config = ServerConfig::default(); - - // server_config.index_strategy = Some(IndexStrategyConfig::AlwaysShowListing); - - // let root_path_context = Arc::new((&server_config).try_into().unwrap()); - // let path_contexts = Arc::new(vec![]); - - // let mut 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(&mut srv, req).await; - // assert_eq!(resp.status(), StatusCode::NOT_FOUND); - - // // Without index strategy. - // let root_path_context = Arc::new((&ServerConfig::default()).try_into().unwrap()); - // let path_contexts = Arc::new(vec![]); - - // let mut 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(&mut srv, req).await; - // assert_eq!(resp.status(), StatusCode::NOT_FOUND); - - // // With default renderer. - // server_config.index_strategy = Some(IndexStrategyConfig::AlwaysShowListing); - - // let root_path_context = Arc::new((&server_config).try_into().unwrap()); - // let path_contexts = Arc::new(vec![]); - - // let mut srv = test::init_service(App::new().service(Files::new( - // "/", - // serve_dir("."), - // root_path_context, - // path_contexts, - // ))) - // .await; - // let req = TestRequest::with_uri("/tests").to_request(); - // let resp = test::call_service(&mut srv, req).await; - // 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 mut server_config = ServerConfig::default(); - - // server_config.index_strategy = Some(IndexStrategyConfig::IndexFiles { - // filenames: ["test.png".to_owned()].iter().cloned().collect(), - // }); - - // let root_path_context = Arc::new((&server_config).try_into().unwrap()); - // let path_contexts = Arc::new(vec![]); - - // // should not redirect if no index - // // let mut srv = test::init_service( - // // App::new().service(Files::new("/", serve_dir(".")).redirect_to_slash_directory()), - // // ) - // // .await; - // // let req = TestRequest::with_uri("/tests").to_request(); - // // let resp = test::call_service(&mut srv, req).await; - // // assert_eq!(resp.status(), StatusCode::NOT_FOUND); - - // // should redirect if index present - // let mut 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(&mut srv, req).await; - // assert_eq!(resp.status(), StatusCode::FOUND); - - // // should not redirect if the path is wrong - // let req = TestRequest::with_uri("/not_existing").to_request(); - // let resp = test::call_service(&mut 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, - // ); - // } - - // #[actix_rt::test] - // async fn test_default_handler_file_missing() { - // let root_path_context = Arc::new((&ServerConfig::default()).try_into().unwrap()); - // let path_contexts = Arc::new(vec![]); - - // let mut st = Files::new("/", serve_dir("."), root_path_context, path_contexts) - // .default_handler(|req: ServiceRequest| { - // ok(req.into_response(HttpResponse::Ok().body("default content"))) - // }) - // .new_service(()) - // .await - // .unwrap(); - // let req = TestRequest::with_uri("/missing").to_srv_request(); - - // let resp = test::call_service(&mut st, req).await; - // assert_eq!(resp.status(), StatusCode::OK); - // let bytes = test::read_body(resp).await; - // assert_eq!(bytes, Bytes::from_static(b"default content")); - // } - - // #[actix_rt::test] - // async fn test_serve_index() { - // let st = Files::new(".").index_file("test.binary"); - // let req = TestRequest::default().uri("/tests").finish(); - - // let resp = st.handle(&req).respond_to(&req).unwrap(); - // let resp = resp.as_msg(); - // assert_eq!(resp.status(), StatusCode::OK); - // assert_eq!( - // resp.headers() - // .get(header::CONTENT_TYPE) - // .expect("content type"), - // "application/octet-stream" - // ); - // assert_eq!( - // resp.headers() - // .get(header::CONTENT_DISPOSITION) - // .expect("content disposition"), - // "attachment; filename=\"test.binary\"" - // ); - - // let req = TestRequest::default().uri("/tests/").finish(); - // let resp = st.handle(&req).respond_to(&req).unwrap(); - // let resp = resp.as_msg(); - // assert_eq!(resp.status(), StatusCode::OK); - // assert_eq!( - // resp.headers().get(header::CONTENT_TYPE).unwrap(), - // "application/octet-stream" - // ); - // assert_eq!( - // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - // "attachment; filename=\"test.binary\"" - // ); - - // // nonexistent index file - // let req = TestRequest::default().uri("/tests/unknown").finish(); - // let resp = st.handle(&req).respond_to(&req).unwrap(); - // let resp = resp.as_msg(); - // assert_eq!(resp.status(), StatusCode::NOT_FOUND); - - // let req = TestRequest::default().uri("/tests/unknown/").finish(); - // let resp = st.handle(&req).respond_to(&req).unwrap(); - // let resp = resp.as_msg(); - // assert_eq!(resp.status(), StatusCode::NOT_FOUND); - // } - - // #[actix_rt::test] - // async fn test_serve_index_nested() { - // let st = Files::new(".").index_file("mod.rs"); - // let req = TestRequest::default().uri("/src/client").finish(); - // let resp = st.handle(&req).respond_to(&req).unwrap(); - // let resp = resp.as_msg(); - // assert_eq!(resp.status(), StatusCode::OK); - // assert_eq!( - // resp.headers().get(header::CONTENT_TYPE).unwrap(), - // "text/x-rust" - // ); - // assert_eq!( - // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - // "inline; filename=\"mod.rs\"" - // ); - // } - - // #[actix_rt::test] - // fn integration_serve_index() { - // let mut srv = test::TestServer::with_factory(|| { - // App::new().handler( - // "test", - // Files::new(".").index_file("Cargo.toml"), - // ) - // }); - - // let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - // let response = srv.execute(request.send()).unwrap(); - // assert_eq!(response.status(), StatusCode::OK); - // let bytes = srv.execute(response.body()).unwrap(); - // let data = Bytes::from(fs::read("Cargo.toml").unwrap()); - // assert_eq!(bytes, data); - - // let request = srv.get().uri(srv.url("/test/")).finish().unwrap(); - // let response = srv.execute(request.send()).unwrap(); - // assert_eq!(response.status(), StatusCode::OK); - // let bytes = srv.execute(response.body()).unwrap(); - // let data = Bytes::from(fs::read("Cargo.toml").unwrap()); - // assert_eq!(bytes, data); - - // // nonexistent index file - // let request = srv.get().uri(srv.url("/test/unknown")).finish().unwrap(); - // let response = srv.execute(request.send()).unwrap(); - // assert_eq!(response.status(), StatusCode::NOT_FOUND); - - // let request = srv.get().uri(srv.url("/test/unknown/")).finish().unwrap(); - // let response = srv.execute(request.send()).unwrap(); - // assert_eq!(response.status(), StatusCode::NOT_FOUND); - // } - - // #[actix_rt::test] - // fn integration_percent_encoded() { - // let mut srv = test::TestServer::with_factory(|| { - // App::new().handler( - // "test", - // Files::new(".").index_file("Cargo.toml"), - // ) - // }); - - // let request = srv - // .get() - // .uri(srv.url("/test/%43argo.toml")) - // .finish() - // .unwrap(); - // let response = srv.execute(request.send()).unwrap(); - // assert_eq!(response.status(), StatusCode::OK); - // } + 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 mut 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(&mut 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 mut server_config = ServerConfig::default(); + + server_config.index_strategy = Some(IndexStrategyConfig::IndexFiles { + filenames: ["Cargo.toml".to_owned()].iter().cloned().collect(), + }); + + let root_path_context = Arc::new((&server_config).try_into().unwrap()); + let path_contexts = Arc::new(vec![]); + + let mut srv = test::init_service(App::new().service(Files::new( + "/test", + serve_dir("."), + root_path_context, + path_contexts, + ))) + .await; + + // Valid range header + let request = TestRequest::get() + .uri("/t%65st/Cargo.toml") + .append_header((header::RANGE, "bytes=10-20")) + .to_request(); + let response = test::call_service(&mut srv, request).await; + assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); + + // Invalid range header + let request = TestRequest::get() + .uri("/t%65st/Cargo.toml") + .append_header((header::RANGE, "bytes=1-0")) + .to_request(); + let response = test::call_service(&mut 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, + )) + }); + + // Valid range header + 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"); + + // Invalid range header + 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, + )) + }); + + // Valid range header + 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"); + + // Valid range header, starting from 0 + 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"); + + // Without range header + 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"); + + // Should be no transfer-encoding + let transfer_encoding = response.headers().get(header::TRANSFER_ENCODING); + assert!(transfer_encoding.is_none()); + + // Check file contents + 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 mut server_config = ServerConfig::default(); + + server_config.index_strategy = Some(IndexStrategyConfig::IndexFiles { + filenames: ["Cargo.toml".to_owned()].iter().cloned().collect(), + }); + + let root_path_context = Arc::new((&server_config).try_into().unwrap()); + let path_contexts = Arc::new(vec![]); + + let mut 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(&mut 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 mut 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(&mut 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 mut 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(&mut 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 mut 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(&mut 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 mut 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(&mut 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 mut 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(&mut 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 mut 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(&mut 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 mut 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(&mut 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 mut server_config = ServerConfig::default(); + + server_config.index_strategy = Some(IndexStrategyConfig::IndexFiles { + filenames: ["test.png".to_owned()].iter().cloned().collect(), + }); + + let root_path_context: Arc<PathContext> = Arc::new((&server_config).try_into().unwrap()); + let path_contexts = Arc::new(vec![]); + + // should redirect if index present + let mut 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(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::FOUND); + + // should not redirect if the path is wrong + let req = TestRequest::with_uri("/not_existing").to_request(); + let resp = test::call_service(&mut 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, + ); + } } -- GitLab