1#![allow(
2 type_alias_bounds,
3 clippy::borrow_interior_mutable_const,
4 clippy::type_complexity,
5 clippy::result_unit_err,
6 clippy::enum_variant_names
7)]
8
9use std::fs::{DirEntry, File};
11use std::path::{Path, PathBuf};
12use std::{
13 cmp, fmt::Write, io, io::Read, io::Seek, pin::Pin, rc::Rc, task::Context, task::Poll,
14};
15
16use futures::future::{FutureExt, LocalBoxFuture};
17use futures::{Future, Stream};
18use mime_guess::from_ext;
19use ntex::http::error::BlockingError;
20use ntex::http::{Method, Payload, Uri};
21use ntex::router::{ResourceDef, ResourcePath};
22use ntex::service::boxed::{self, BoxService, BoxServiceFactory};
23use ntex::service::{IntoServiceFactory, Service, ServiceCtx, ServiceFactory};
24use ntex::web::dev::{WebServiceConfig, WebServiceFactory};
25use ntex::web::error::ErrorRenderer;
26use ntex::web::guard::Guard;
27use ntex::web::{self, FromRequest, HttpRequest, HttpResponse, WebRequest, WebResponse};
28use ntex::{SharedCfg, util::Bytes};
29use percent_encoding::{CONTROLS, utf8_percent_encode};
30use v_htmlescape::escape as escape_html_entity;
31
32mod error;
33pub mod header;
34mod named;
35mod range;
36
37use self::error::{FilesError, UriSegmentError};
38pub use crate::named::NamedFile;
39pub use crate::range::HttpRange;
40
41type HttpService<Err: ErrorRenderer> = BoxService<WebRequest<Err>, WebResponse, Err::Container>;
42type HttpServiceFactory<Err: ErrorRenderer> =
43 BoxServiceFactory<SharedCfg, WebRequest<Err>, WebResponse, Err::Container, ()>;
44
45#[inline]
49pub fn file_extension_to_mime(ext: &str) -> mime::Mime {
50 from_ext(ext).first_or_octet_stream()
51}
52
53#[doc(hidden)]
54pub struct ChunkedReadFile {
57 size: u64,
58 offset: u64,
59 file: Option<File>,
60 fut: Option<LocalBoxFuture<'static, Result<(File, Bytes), BlockingError<io::Error>>>>,
61 counter: u64,
62}
63
64impl Stream for ChunkedReadFile {
65 type Item = Result<Bytes, std::io::Error>;
66
67 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
68 if let Some(ref mut fut) = self.fut {
69 return match Pin::new(fut).poll(cx) {
70 Poll::Ready(Ok((file, bytes))) => {
71 self.fut.take();
72 self.file = Some(file);
73 self.offset += bytes.len() as u64;
74 self.counter += bytes.len() as u64;
75 Poll::Ready(Some(Ok(bytes)))
76 }
77 Poll::Ready(Err(e)) => {
78 let e = match e {
79 BlockingError::Error(e) => e,
80 BlockingError::Canceled => io::Error::other("Operation is canceled"),
81 };
82 Poll::Ready(Some(Err(e)))
83 }
84 Poll::Pending => Poll::Pending,
85 };
86 }
87
88 let size = self.size;
89 let offset = self.offset;
90 let counter = self.counter;
91
92 if size == counter {
93 Poll::Ready(None)
94 } else {
95 let mut file = self.file.take().expect("Use after completion");
96 self.fut = Some(
97 web::block(move || {
98 let max_bytes: usize =
99 cmp::min(size.saturating_sub(counter), 65_536) as usize;
100 let mut buf = Vec::with_capacity(max_bytes);
101 file.seek(io::SeekFrom::Start(offset))?;
102 let nbytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
103 if nbytes == 0 {
104 return Err(io::ErrorKind::UnexpectedEof.into());
105 }
106 Ok((file, Bytes::from(buf)))
107 })
108 .boxed_local(),
109 );
110 self.poll_next(cx)
111 }
112 }
113}
114
115type DirectoryRenderer = dyn Fn(&Directory, &HttpRequest) -> Result<WebResponse, io::Error>;
116
117#[derive(Debug)]
119pub struct Directory {
120 pub base: PathBuf,
122 pub path: PathBuf,
124}
125
126impl Directory {
127 pub fn new(base: PathBuf, path: PathBuf) -> Directory {
129 Directory { base, path }
130 }
131
132 pub fn is_visible(&self, entry: &io::Result<DirEntry>) -> bool {
134 if let Ok(ref entry) = *entry {
135 if let Some(name) = entry.file_name().to_str()
136 && name.starts_with('.')
137 {
138 return false;
139 }
140 if let Ok(ref md) = entry.metadata() {
141 let ft = md.file_type();
142 return ft.is_dir() || ft.is_file() || ft.is_symlink();
143 }
144 }
145 false
146 }
147}
148
149macro_rules! encode_file_url {
151 ($path:ident) => {
152 utf8_percent_encode(&$path, CONTROLS)
153 };
154}
155
156macro_rules! encode_file_name {
158 ($entry:ident) => {
159 escape_html_entity(&$entry.file_name().to_string_lossy())
160 };
161}
162
163fn directory_listing(dir: &Directory, req: &HttpRequest) -> Result<WebResponse, io::Error> {
164 let index_of = format!("Index of {}", req.path());
165 let mut body = String::new();
166 let base = Path::new(req.path());
167
168 for entry in dir.path.read_dir()? {
169 if dir.is_visible(&entry) {
170 let entry = entry.unwrap();
171 let p = match entry.path().strip_prefix(&dir.path) {
172 Ok(p) if cfg!(windows) => base.join(p).to_string_lossy().replace('\\', "/"),
173 Ok(p) => base.join(p).to_string_lossy().into_owned(),
174 Err(_) => continue,
175 };
176
177 if let Ok(metadata) = entry.metadata() {
179 if metadata.is_dir() {
180 let _ = write!(
181 body,
182 "<li><a href=\"{}\">{}/</a></li>",
183 encode_file_url!(p),
184 encode_file_name!(entry),
185 );
186 } else {
187 let _ = write!(
188 body,
189 "<li><a href=\"{}\">{}</a></li>",
190 encode_file_url!(p),
191 encode_file_name!(entry),
192 );
193 }
194 } else {
195 continue;
196 }
197 }
198 }
199
200 let html = format!(
201 "<html>\
202 <head><title>{}</title></head>\
203 <body><h1>{}</h1>\
204 <ul>\
205 {}\
206 </ul></body>\n</html>",
207 index_of, index_of, body
208 );
209 Ok(WebResponse::new(
210 HttpResponse::Ok().content_type("text/html; charset=utf-8").body(html),
211 req.clone(),
212 ))
213}
214
215type MimeOverride = dyn Fn(&mime::Name) -> header::DispositionType;
216
217pub struct Files<Err: ErrorRenderer> {
229 path: String,
230 directory: PathBuf,
231 index: Option<String>,
232 show_index: bool,
233 redirect_to_slash: bool,
234 default: Option<Rc<HttpServiceFactory<Err>>>,
235 renderer: Rc<DirectoryRenderer>,
236 mime_override: Option<Rc<MimeOverride>>,
237 file_flags: named::Flags,
238 guards: Option<Rc<dyn Guard>>,
239}
240
241impl<Err: ErrorRenderer> Clone for Files<Err> {
242 fn clone(&self) -> Self {
243 Self {
244 directory: self.directory.clone(),
245 index: self.index.clone(),
246 show_index: self.show_index,
247 redirect_to_slash: self.redirect_to_slash,
248 default: self.default.clone(),
249 renderer: self.renderer.clone(),
250 file_flags: self.file_flags.clone(),
251 path: self.path.clone(),
252 mime_override: self.mime_override.clone(),
253 guards: self.guards.clone(),
254 }
255 }
256}
257
258impl<Err: ErrorRenderer> Files<Err> {
259 pub fn new<T: Into<PathBuf>>(path: &str, dir: T) -> Self {
265 let orig_dir = dir.into();
266 let dir = match orig_dir.canonicalize() {
267 Ok(canon_dir) => canon_dir,
268 Err(_) => {
269 log::error!("Specified path is not a directory: {:?}", orig_dir);
270 PathBuf::new()
271 }
272 };
273
274 Files {
275 path: path.to_string(),
276 directory: dir,
277 index: None,
278 show_index: false,
279 redirect_to_slash: false,
280 default: None,
281 renderer: Rc::new(directory_listing),
282 mime_override: None,
283 file_flags: named::Flags::default(),
284 guards: None,
285 }
286 }
287
288 pub fn show_files_listing(mut self) -> Self {
292 self.show_index = true;
293 self
294 }
295
296 pub fn redirect_to_slash_directory(mut self) -> Self {
300 self.redirect_to_slash = true;
301 self
302 }
303
304 pub fn files_listing_renderer<F>(mut self, f: F) -> Self
306 where
307 for<'r, 's> F:
308 Fn(&'r Directory, &'s HttpRequest) -> Result<WebResponse, io::Error> + 'static,
309 {
310 self.renderer = Rc::new(f);
311 self
312 }
313
314 pub fn mime_override<F>(mut self, f: F) -> Self
316 where
317 F: Fn(&mime::Name) -> header::DispositionType + 'static,
318 {
319 self.mime_override = Some(Rc::new(f));
320 self
321 }
322
323 pub fn index_file<T: Into<String>>(mut self, index: T) -> Self {
328 self.index = Some(index.into());
329 self
330 }
331
332 #[inline]
333 pub fn use_etag(mut self, value: bool) -> Self {
337 self.file_flags.set(named::Flags::ETAG, value);
338 self
339 }
340
341 #[inline]
342 pub fn use_last_modified(mut self, value: bool) -> Self {
346 self.file_flags.set(named::Flags::LAST_MD, value);
347 self
348 }
349
350 #[inline]
354 pub fn use_guards<G: Guard + 'static>(mut self, guards: G) -> Self {
355 self.guards = Some(Rc::new(guards));
356 self
357 }
358
359 #[inline]
363 pub fn disable_content_disposition(mut self) -> Self {
364 self.file_flags.remove(named::Flags::CONTENT_DISPOSITION);
365 self
366 }
367
368 pub fn default_handler<F, U>(mut self, f: F) -> Self
370 where
371 F: IntoServiceFactory<U, WebRequest<Err>, SharedCfg>,
372 U: ServiceFactory<
373 WebRequest<Err>,
374 SharedCfg,
375 Response = WebResponse,
376 Error = Err::Container,
377 > + 'static,
378 {
379 self.default = Some(Rc::new(boxed::factory(f.into_factory().map_init_err(|_| ()))));
381
382 self
383 }
384}
385
386impl<Err> WebServiceFactory<Err> for Files<Err>
387where
388 Err: ErrorRenderer,
389 Err::Container: From<FilesError>,
390{
391 fn register(mut self, config: &mut WebServiceConfig<Err>) {
392 if self.default.is_none() {
393 self.default = Some(config.default_service());
394 }
395 let rdef = if config.is_root() {
396 ResourceDef::root_prefix(&self.path)
397 } else {
398 ResourceDef::prefix(&self.path)
399 };
400 config.register_service(rdef, None, self, None)
401 }
402}
403
404impl<Err> ServiceFactory<WebRequest<Err>, SharedCfg> for Files<Err>
405where
406 Err: ErrorRenderer,
407 Err::Container: From<FilesError>,
408{
409 type Response = WebResponse;
410 type Error = Err::Container;
411 type Service = FilesService<Err>;
412 type InitError = ();
413
414 async fn create(&self, cfg: SharedCfg) -> Result<Self::Service, Self::InitError> {
415 let mut srv = FilesService {
416 directory: self.directory.clone(),
417 index: self.index.clone(),
418 show_index: self.show_index,
419 redirect_to_slash: self.redirect_to_slash,
420 default: None,
421 renderer: self.renderer.clone(),
422 mime_override: self.mime_override.clone(),
423 file_flags: self.file_flags.clone(),
424 guards: self.guards.clone(),
425 };
426
427 if let Some(default) = self.default.as_ref() {
428 default
429 .create(cfg)
430 .map(move |result| match result {
431 Ok(default) => {
432 srv.default = Some(default);
433 Ok(srv)
434 }
435 Err(_) => Err(()),
436 })
437 .await
438 } else {
439 Ok(srv)
440 }
441 }
442}
443
444pub struct FilesService<Err: ErrorRenderer> {
445 directory: PathBuf,
446 index: Option<String>,
447 show_index: bool,
448 redirect_to_slash: bool,
449 default: Option<HttpService<Err>>,
450 renderer: Rc<DirectoryRenderer>,
451 mime_override: Option<Rc<MimeOverride>>,
452 file_flags: named::Flags,
453 guards: Option<Rc<dyn Guard>>,
454}
455
456impl<Err: ErrorRenderer> FilesService<Err>
457where
458 Err::Container: From<FilesError>,
459{
460 async fn handle_io_error(
461 &self,
462 e: io::Error,
463 req: WebRequest<Err>,
464 ctx: ServiceCtx<'_, Self>,
465 ) -> Result<WebResponse, Err::Container> {
466 log::debug!("Files: Failed to handle {}: {}", req.path(), e);
467 if let Some(ref default) = self.default {
468 ctx.call(default, req).await
469 } else {
470 Ok(req.error_response(FilesError::from(e)))
471 }
472 }
473}
474
475impl<Err> Service<WebRequest<Err>> for FilesService<Err>
476where
477 Err: ErrorRenderer,
478 Err::Container: From<FilesError>,
479{
480 type Response = WebResponse;
481 type Error = Err::Container;
482
483 async fn call(
484 &self,
485 req: WebRequest<Err>,
486 ctx: ServiceCtx<'_, Self>,
487 ) -> Result<Self::Response, Self::Error> {
488 let is_method_valid = if let Some(guard) = &self.guards {
489 (**guard).check(req.head())
491 } else {
492 matches!(*req.method(), Method::HEAD | Method::GET)
494 };
495
496 if !is_method_valid {
497 return Ok(req.error_response(FilesError::MethodNotAllowed));
498 }
499
500 let real_path = match PathBufWrp::get_pathbuf(req.match_info().path()) {
501 Ok(item) => item,
502 Err(e) => return Ok(req.error_response(FilesError::from(e))),
503 };
504
505 let path = match self.directory.join(real_path.0).canonicalize() {
507 Ok(path) => path,
508 Err(e) => return self.handle_io_error(e, req, ctx).await,
509 };
510
511 if path.is_dir() {
512 if let Some(ref redir_index) = self.index {
513 if self.redirect_to_slash && !req.path().ends_with('/') {
514 let redirect_to = format!("{}/", req.path());
515 return Ok(req.into_response(
516 HttpResponse::Found()
517 .header(http::header::LOCATION, redirect_to)
518 .body("")
519 .into_body(),
520 ));
521 }
522
523 let path = path.join(redir_index);
524
525 match NamedFile::open(path) {
526 Ok(mut named_file) => {
527 if let Some(ref mime_override) = self.mime_override {
528 let new_disposition =
529 mime_override(&named_file.content_type.type_());
530 named_file.content_disposition.disposition = new_disposition;
531 }
532
533 named_file.flags = self.file_flags.clone();
534 let (req, _) = req.into_parts();
535 Ok(WebResponse::new(named_file.into_response(&req), req))
536 }
537 Err(e) => self.handle_io_error(e, req, ctx).await,
538 }
539 } else if self.show_index {
540 let dir = Directory::new(self.directory.clone(), path);
541 let (req, _) = req.into_parts();
542 let x = (self.renderer)(&dir, &req);
543 match x {
544 Ok(resp) => Ok(resp),
545 Err(e) => Ok(WebResponse::from_err::<Err, _>(FilesError::from(e), req)),
546 }
547 } else {
548 Ok(WebResponse::from_err::<Err, _>(FilesError::IsDirectory, req.into_parts().0))
549 }
550 } else {
551 match NamedFile::open(path) {
552 Ok(mut named_file) => {
553 if let Some(ref mime_override) = self.mime_override {
554 let new_disposition = mime_override(&named_file.content_type.type_());
555 named_file.content_disposition.disposition = new_disposition;
556 }
557
558 named_file.flags = self.file_flags.clone();
559 let (req, _) = req.into_parts();
560 Ok(WebResponse::new(named_file.into_response(&req), req))
561 }
562 Err(e) => self.handle_io_error(e, req, ctx).await,
563 }
564 }
565 }
566}
567
568#[derive(Debug)]
569struct PathBufWrp(PathBuf);
570
571impl PathBufWrp {
572 fn get_pathbuf(path: &str) -> Result<Self, UriSegmentError> {
573 let mut buf = PathBuf::new();
574 for segment in path.split('/') {
575 if segment == ".." {
576 buf.pop();
577 } else if segment.starts_with('.') {
578 return Err(UriSegmentError::BadStart('.'));
579 } else if segment.starts_with('*') {
580 return Err(UriSegmentError::BadStart('*'));
581 } else if segment.ends_with(':') {
582 return Err(UriSegmentError::BadEnd(':'));
583 } else if segment.ends_with('>') {
584 return Err(UriSegmentError::BadEnd('>'));
585 } else if segment.ends_with('<') {
586 return Err(UriSegmentError::BadEnd('<'));
587 } else if segment.is_empty() {
588 continue;
589 } else if cfg!(windows) && segment.contains('\\') {
590 return Err(UriSegmentError::BadChar('\\'));
591 } else {
592 buf.push(Uri::unquote(segment).as_ref())
593 }
594 }
595
596 Ok(PathBufWrp(buf))
597 }
598}
599
600impl<Err> FromRequest<Err> for PathBufWrp {
601 type Error = UriSegmentError;
602
603 async fn from_request(req: &HttpRequest, _: &mut Payload) -> Result<Self, Self::Error> {
604 PathBufWrp::get_pathbuf(req.match_info().path())
605 }
606}
607
608#[cfg(test)]
609mod tests {
610 use std::fs;
611 use std::iter::FromIterator;
612 use std::ops::Add;
613 use std::time::{Duration, SystemTime};
614
615 use super::*;
616 use ntex::http::{self, Method, StatusCode};
617 use ntex::web::middleware::Compress;
618 use ntex::web::test::{self, TestRequest};
619 use ntex::web::{App, DefaultError, guard};
620
621 #[ntex::test]
622 async fn test_file_extension_to_mime() {
623 let m = file_extension_to_mime("jpg");
624 assert_eq!(m, mime::IMAGE_JPEG);
625
626 let m = file_extension_to_mime("invalid extension!!");
627 assert_eq!(m, mime::APPLICATION_OCTET_STREAM);
628
629 let m = file_extension_to_mime("");
630 assert_eq!(m, mime::APPLICATION_OCTET_STREAM);
631 }
632
633 #[ntex::test]
634 async fn test_if_modified_since_without_if_none_match() {
635 let file = NamedFile::open("Cargo.toml").unwrap();
636 let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
637
638 let req = TestRequest::default()
639 .header(http::header::IF_MODIFIED_SINCE, since.to_string())
640 .to_http_request();
641 let resp = test::respond_to(file, &req).await;
642 assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
643 }
644
645 #[ntex::test]
646 async fn test_if_modified_since_with_if_none_match() {
647 let file = NamedFile::open("Cargo.toml").unwrap();
648 let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
649
650 let req = TestRequest::default()
651 .header(http::header::IF_NONE_MATCH, "miss_etag")
652 .header(http::header::IF_MODIFIED_SINCE, since.to_string())
653 .to_http_request();
654 let resp = test::respond_to(file, &req).await;
655 assert_ne!(resp.status(), StatusCode::NOT_MODIFIED);
656 }
657
658 #[ntex::test]
659 async fn test_named_file_text() {
660 assert!(NamedFile::open("test--").is_err());
661 let mut file = NamedFile::open("Cargo.toml").unwrap();
662 {
663 file.file();
664 let _f: &File = &file;
665 }
666 {
667 let _f: &mut File = &mut file;
668 }
669
670 let req = TestRequest::default().to_http_request();
671 let resp = test::respond_to(file, &req).await;
672 assert_eq!(resp.headers().get(http::header::CONTENT_TYPE).unwrap(), "text/x-toml");
673 assert_eq!(
674 resp.headers().get(http::header::CONTENT_DISPOSITION).unwrap(),
675 "inline; filename=\"Cargo.toml\""
676 );
677 }
678
679 #[ntex::test]
680 async fn test_named_file_content_disposition() {
681 assert!(NamedFile::open("test--").is_err());
682 let mut file = NamedFile::open("Cargo.toml").unwrap();
683 {
684 file.file();
685 let _f: &File = &file;
686 }
687 {
688 let _f: &mut File = &mut file;
689 }
690
691 let req = TestRequest::default().to_http_request();
692 let resp = test::respond_to(file, &req).await;
693 assert_eq!(
694 resp.headers().get(http::header::CONTENT_DISPOSITION).unwrap(),
695 "inline; filename=\"Cargo.toml\""
696 );
697
698 let file = NamedFile::open("Cargo.toml").unwrap().disable_content_disposition();
699 let req = TestRequest::default().to_http_request();
700 let resp = test::respond_to(file, &req).await;
701 assert!(resp.headers().get(http::header::CONTENT_DISPOSITION).is_none());
702 }
703
704 #[ntex::test]
732 async fn test_named_file_set_content_type() {
733 let mut file = NamedFile::open("Cargo.toml").unwrap().set_content_type(mime::TEXT_XML);
734 {
735 file.file();
736 let _f: &File = &file;
737 }
738 {
739 let _f: &mut File = &mut file;
740 }
741
742 let req = TestRequest::default().to_http_request();
743 let resp = test::respond_to(file, &req).await;
744 assert_eq!(resp.headers().get(http::header::CONTENT_TYPE).unwrap(), "text/xml");
745 assert_eq!(
746 resp.headers().get(http::header::CONTENT_DISPOSITION).unwrap(),
747 "inline; filename=\"Cargo.toml\""
748 );
749 }
750
751 #[ntex::test]
752 async fn test_named_file_image() {
753 let mut file = NamedFile::open("tests/test.png").unwrap();
754 {
755 file.file();
756 let _f: &File = &file;
757 }
758 {
759 let _f: &mut File = &mut file;
760 }
761
762 let req = TestRequest::default().to_http_request();
763 let resp = test::respond_to(file, &req).await;
764 assert_eq!(resp.headers().get(http::header::CONTENT_TYPE).unwrap(), "image/png");
765 assert_eq!(
766 resp.headers().get(http::header::CONTENT_DISPOSITION).unwrap(),
767 "inline; filename=\"test.png\""
768 );
769 }
770
771 #[ntex::test]
772 async fn test_named_file_image_attachment() {
773 use crate::header::{ContentDisposition, DispositionParam, DispositionType};
774
775 let cd = ContentDisposition {
776 disposition: DispositionType::Attachment,
777 parameters: vec![DispositionParam::Filename(String::from("test.png"))],
778 };
779 let mut file = NamedFile::open("tests/test.png").unwrap().set_content_disposition(cd);
780 {
781 file.file();
782 let _f: &File = &file;
783 }
784 {
785 let _f: &mut File = &mut file;
786 }
787
788 let req = TestRequest::default().to_http_request();
789 let resp = test::respond_to(file, &req).await;
790 assert_eq!(resp.headers().get(http::header::CONTENT_TYPE).unwrap(), "image/png");
791 assert_eq!(
792 resp.headers().get(http::header::CONTENT_DISPOSITION).unwrap(),
793 "attachment; filename=\"test.png\""
794 );
795 }
796
797 #[ntex::test]
798 async fn test_named_file_binary() {
799 let mut file = NamedFile::open("tests/test.binary").unwrap();
800 {
801 file.file();
802 let _f: &File = &file;
803 }
804 {
805 let _f: &mut File = &mut file;
806 }
807
808 let req = TestRequest::default().to_http_request();
809 let resp = test::respond_to(file, &req).await;
810 assert_eq!(
811 resp.headers().get(http::header::CONTENT_TYPE).unwrap(),
812 "application/octet-stream"
813 );
814 assert_eq!(
815 resp.headers().get(http::header::CONTENT_DISPOSITION).unwrap(),
816 "attachment; filename=\"test.binary\""
817 );
818 }
819
820 #[ntex::test]
821 async fn test_named_file_status_code_text() {
822 let mut file =
823 NamedFile::open("Cargo.toml").unwrap().set_status_code(StatusCode::NOT_FOUND);
824 {
825 file.file();
826 let _f: &File = &file;
827 }
828 {
829 let _f: &mut File = &mut file;
830 }
831
832 let req = TestRequest::default().to_http_request();
833 let resp = test::respond_to(file, &req).await;
834 assert_eq!(resp.headers().get(http::header::CONTENT_TYPE).unwrap(), "text/x-toml");
835 assert_eq!(
836 resp.headers().get(http::header::CONTENT_DISPOSITION).unwrap(),
837 "inline; filename=\"Cargo.toml\""
838 );
839 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
840 }
841
842 #[ntex::test]
843 async fn test_mime_override() {
844 fn all_attachment(_: &mime::Name) -> header::DispositionType {
845 header::DispositionType::Attachment
846 }
847
848 let srv = test::init_service(App::new().service(
849 Files::new("/", ".").mime_override(all_attachment).index_file("Cargo.toml"),
850 ))
851 .await;
852
853 let request = TestRequest::get().uri("/").to_request();
854 let response = test::call_service(&srv, request).await;
855 assert_eq!(response.status(), StatusCode::OK);
856
857 let content_disposition = response
858 .headers()
859 .get(http::header::CONTENT_DISPOSITION)
860 .expect("To have CONTENT_DISPOSITION");
861 let content_disposition =
862 content_disposition.to_str().expect("Convert CONTENT_DISPOSITION to str");
863 assert_eq!(content_disposition, "attachment; filename=\"Cargo.toml\"");
864 }
865
866 #[ntex::test]
867 async fn test_named_file_ranges_status_code() {
868 let srv = test::init_service(
869 App::new().service(Files::new("/test", ".").index_file("Cargo.toml")),
870 )
871 .await;
872
873 let request = TestRequest::get()
875 .uri("/t%65st/Cargo.toml")
876 .header(http::header::RANGE, "bytes=10-20")
877 .to_request();
878 let response = test::call_service(&srv, request).await;
879 assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
880
881 let request = TestRequest::get()
883 .uri("/t%65st/Cargo.toml")
884 .header(http::header::RANGE, "bytes=1-0")
885 .to_request();
886 let response = test::call_service(&srv, request).await;
887
888 assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
889 }
890
891 #[ntex::test]
892 async fn test_named_file_content_range_headers() {
893 let srv = test::init_service(
894 App::new().service(Files::new("/test", ".").index_file("tests/test.binary")),
895 )
896 .await;
897
898 let request = TestRequest::get()
900 .uri("/t%65st/tests/test.binary")
901 .header(http::header::RANGE, "bytes=10-20")
902 .to_request();
903
904 let response = test::call_service(&srv, request).await;
905 let contentrange =
906 response.headers().get(http::header::CONTENT_RANGE).unwrap().to_str().unwrap();
907
908 assert_eq!(contentrange, "bytes 10-20/100");
909
910 let request = TestRequest::get()
912 .uri("/t%65st/tests/test.binary")
913 .header(http::header::RANGE, "bytes=10-5")
914 .to_request();
915 let response = test::call_service(&srv, request).await;
916
917 let contentrange =
918 response.headers().get(http::header::CONTENT_RANGE).unwrap().to_str().unwrap();
919
920 assert_eq!(contentrange, "bytes */100");
921 }
922
923 #[ntex::test]
924 async fn test_named_file_content_length_headers() {
925 let srv = test::init_service(
926 App::new().service(Files::new("test", ".").index_file("tests/test.binary")),
927 )
928 .await;
929
930 let request = TestRequest::get()
932 .uri("/t%65st/tests/test.binary")
933 .header(http::header::RANGE, "bytes=10-20")
934 .to_request();
935 let _response = test::call_service(&srv, request).await;
936
937 let request = TestRequest::get()
947 .uri("/t%65st/tests/test.binary")
948 .header(http::header::RANGE, "bytes=10-8")
949 .to_request();
950 let response = test::call_service(&srv, request).await;
951 assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
952
953 let request = TestRequest::get()
955 .uri("/t%65st/tests/test.binary")
956 .to_request();
958 let _response = test::call_service(&srv, request).await;
959
960 let request = TestRequest::get().uri("/t%65st/tests/test.binary").to_request();
970 let response = test::call_service(&srv, request).await;
971
972 let bytes = test::read_body(response).await;
984 let data = Bytes::from(fs::read("tests/test.binary").unwrap());
985 assert_eq!(bytes, data);
986 }
987
988 #[ntex::test]
989 async fn test_head_content_length_headers() {
990 let srv = test::init_service(
991 App::new().service(Files::new("test", ".").index_file("tests/test.binary")),
992 )
993 .await;
994
995 let request = TestRequest::default()
997 .method(Method::HEAD)
998 .uri("/t%65st/tests/test.binary")
999 .to_request();
1000 let _response = test::call_service(&srv, request).await;
1001
1002 }
1011
1012 #[ntex::test]
1013 async fn test_static_files_with_spaces() {
1014 let srv = test::init_service(
1015 App::new().service(Files::new("/", ".").index_file("Cargo.toml")),
1016 )
1017 .await;
1018 let request = TestRequest::get().uri("/tests/test%20space.binary").to_request();
1019 let response = test::call_service(&srv, request).await;
1020 assert_eq!(response.status(), StatusCode::OK);
1021
1022 let bytes = test::read_body(response).await;
1023 let data = Bytes::from(fs::read("tests/test space.binary").unwrap());
1024 assert_eq!(bytes, data);
1025 }
1026
1027 #[ntex::test]
1028 async fn test_files_not_allowed() {
1029 let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
1030
1031 let req = TestRequest::default().uri("/Cargo.toml").method(Method::POST).to_request();
1032
1033 let resp = test::call_service(&srv, req).await;
1034 assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
1035
1036 let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
1037 let req = TestRequest::default().method(Method::PUT).uri("/Cargo.toml").to_request();
1038 let resp = test::call_service(&srv, req).await;
1039 assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
1040 }
1041
1042 #[ntex::test]
1043 async fn test_files_guards() {
1044 let srv = test::init_service(
1045 App::new().service(Files::new("/", ".").use_guards(guard::Post())),
1046 )
1047 .await;
1048
1049 let req = TestRequest::default().uri("/Cargo.toml").method(Method::POST).to_request();
1050
1051 let resp = test::call_service(&srv, req).await;
1052 assert_eq!(resp.status(), StatusCode::OK);
1053 }
1054
1055 #[ntex::test]
1056 async fn test_named_file_content_encoding() {
1057 let srv = test::init_service(App::new().middleware(Compress::default()).service(
1058 web::resource("/").to(|| async {
1059 NamedFile::open("Cargo.toml")
1060 .unwrap()
1061 .set_content_encoding(http::header::ContentEncoding::Identity)
1062 }),
1063 ))
1064 .await;
1065
1066 let request = TestRequest::get()
1067 .uri("/")
1068 .header(http::header::ACCEPT_ENCODING, "gzip")
1069 .to_request();
1070 let res = test::call_service(&srv, request).await;
1071 assert_eq!(res.status(), StatusCode::OK);
1072 assert!(!res.headers().contains_key(http::header::CONTENT_ENCODING));
1073 }
1074
1075 #[ntex::test]
1076 async fn test_named_file_content_encoding_gzip() {
1077 let srv = test::init_service(App::new().middleware(Compress::default()).service(
1078 web::resource("/").to(|| async {
1079 NamedFile::open("Cargo.toml")
1080 .unwrap()
1081 .set_content_encoding(http::header::ContentEncoding::Gzip)
1082 }),
1083 ))
1084 .await;
1085
1086 let request = TestRequest::get()
1087 .uri("/")
1088 .header(http::header::ACCEPT_ENCODING, "gzip")
1089 .to_request();
1090 let res = test::call_service(&srv, request).await;
1091 assert_eq!(res.status(), StatusCode::OK);
1092 assert_eq!(
1093 res.headers().get(http::header::CONTENT_ENCODING).unwrap().to_str().unwrap(),
1094 "gzip"
1095 );
1096 }
1097
1098 #[ntex::test]
1099 async fn test_named_file_allowed_method() {
1100 let req = TestRequest::default().method(Method::GET).to_http_request();
1101 let file = NamedFile::open("Cargo.toml").unwrap();
1102 let resp = test::respond_to(file, &req).await;
1103 assert_eq!(resp.status(), StatusCode::OK);
1104 }
1105
1106 #[ntex::test]
1107 async fn test_static_files() {
1108 let srv =
1109 test::init_service(App::new().service(Files::new("/", ".").show_files_listing()))
1110 .await;
1111 let req = TestRequest::with_uri("/missing").to_request();
1112
1113 let resp = test::call_service(&srv, req).await;
1114 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1115
1116 let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
1117
1118 let req = TestRequest::default().to_request();
1119 let resp = test::call_service(&srv, req).await;
1120 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1121
1122 let srv =
1123 test::init_service(App::new().service(Files::new("/", ".").show_files_listing()))
1124 .await;
1125 let req = TestRequest::with_uri("/tests").to_request();
1126 let resp = test::call_service(&srv, req).await;
1127 assert_eq!(
1128 resp.headers().get(http::header::CONTENT_TYPE).unwrap(),
1129 "text/html; charset=utf-8"
1130 );
1131
1132 let bytes = test::read_body(resp).await;
1133 assert!(format!("{:?}", bytes).contains("/tests/test.png"));
1134 }
1135
1136 #[ntex::test]
1137 async fn test_redirect_to_slash_directory() {
1138 let srv = test::init_service(
1140 App::new().service(Files::new("/", ".").redirect_to_slash_directory()),
1141 )
1142 .await;
1143 let req = TestRequest::with_uri("/tests").to_request();
1144 let resp = test::call_service(&srv, req).await;
1145 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1146
1147 let srv = test::init_service(App::new().service(
1149 Files::new("/", ".").index_file("test.png").redirect_to_slash_directory(),
1150 ))
1151 .await;
1152 let req = TestRequest::with_uri("/tests").to_request();
1153 let resp = test::call_service(&srv, req).await;
1154 assert_eq!(resp.status(), StatusCode::FOUND);
1155
1156 let req = TestRequest::with_uri("/not_existing").to_request();
1158 let resp = test::call_service(&srv, req).await;
1159 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1160 }
1161
1162 #[ntex::test]
1163 async fn test_static_files_bad_directory() {
1164 let _st: Files<DefaultError> = Files::new("/", "missing");
1165 let _st: Files<DefaultError> = Files::new("/", "Cargo.toml");
1166 }
1167
1168 #[ntex::test]
1169 async fn test_default_handler_file_missing() {
1170 let st = Files::new("/", ".")
1171 .default_handler(|req: WebRequest<DefaultError>| async move {
1172 Ok(req.into_response(HttpResponse::Ok().body("default content")))
1173 })
1174 .pipeline(SharedCfg::default())
1175 .await
1176 .unwrap();
1177 let req = TestRequest::with_uri("/missing").to_srv_request();
1178
1179 let resp = test::call_service(&st, req).await;
1180 assert_eq!(resp.status(), StatusCode::OK);
1181 let bytes = test::read_body(resp).await;
1182 assert_eq!(bytes, Bytes::from_static(b"default content"));
1183 }
1184
1185 #[ntex::test]
1300 async fn test_path_buf() {
1301 assert_eq!(
1302 PathBufWrp::get_pathbuf("/test/.tt").map(|t| t.0),
1303 Err(UriSegmentError::BadStart('.'))
1304 );
1305 assert_eq!(
1306 PathBufWrp::get_pathbuf("/test/*tt").map(|t| t.0),
1307 Err(UriSegmentError::BadStart('*'))
1308 );
1309 assert_eq!(
1310 PathBufWrp::get_pathbuf("/test/tt:").map(|t| t.0),
1311 Err(UriSegmentError::BadEnd(':'))
1312 );
1313 assert_eq!(
1314 PathBufWrp::get_pathbuf("/test/tt<").map(|t| t.0),
1315 Err(UriSegmentError::BadEnd('<'))
1316 );
1317 assert_eq!(
1318 PathBufWrp::get_pathbuf("/test/tt>").map(|t| t.0),
1319 Err(UriSegmentError::BadEnd('>'))
1320 );
1321 assert_eq!(
1322 PathBufWrp::get_pathbuf("/seg1/seg2/").unwrap().0,
1323 PathBuf::from_iter(vec!["seg1", "seg2"])
1324 );
1325 assert_eq!(
1326 PathBufWrp::get_pathbuf("/seg1/../seg2/").unwrap().0,
1327 PathBuf::from_iter(vec!["seg2"])
1328 );
1329 }
1330}