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, header};
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;
33mod file_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 if name.starts_with('.') {
137 return false;
138 }
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) -> file_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) -> file_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(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 = file_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 = file_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::file_header::{
774 Charset, ContentDisposition, DispositionParam, DispositionType,
775 };
776
777 let cd = ContentDisposition {
778 disposition: DispositionType::Attachment,
779 parameters: vec![DispositionParam::Filename(
780 Charset::Ext(String::from("UTF-8")),
781 None,
782 "test.png".to_string().into_bytes(),
783 )],
784 };
785 let mut file = NamedFile::open("tests/test.png").unwrap().set_content_disposition(cd);
786 {
787 file.file();
788 let _f: &File = &file;
789 }
790 {
791 let _f: &mut File = &mut file;
792 }
793
794 let req = TestRequest::default().to_http_request();
795 let resp = test::respond_to(file, &req).await;
796 assert_eq!(resp.headers().get(http::header::CONTENT_TYPE).unwrap(), "image/png");
797 assert_eq!(
798 resp.headers().get(http::header::CONTENT_DISPOSITION).unwrap(),
799 "attachment; filename=\"test.png\""
800 );
801 }
802
803 #[ntex::test]
804 async fn test_named_file_binary() {
805 let mut file = NamedFile::open("tests/test.binary").unwrap();
806 {
807 file.file();
808 let _f: &File = &file;
809 }
810 {
811 let _f: &mut File = &mut file;
812 }
813
814 let req = TestRequest::default().to_http_request();
815 let resp = test::respond_to(file, &req).await;
816 assert_eq!(
817 resp.headers().get(http::header::CONTENT_TYPE).unwrap(),
818 "application/octet-stream"
819 );
820 assert_eq!(
821 resp.headers().get(http::header::CONTENT_DISPOSITION).unwrap(),
822 "attachment; filename=\"test.binary\""
823 );
824 }
825
826 #[ntex::test]
827 async fn test_named_file_status_code_text() {
828 let mut file =
829 NamedFile::open("Cargo.toml").unwrap().set_status_code(StatusCode::NOT_FOUND);
830 {
831 file.file();
832 let _f: &File = &file;
833 }
834 {
835 let _f: &mut File = &mut file;
836 }
837
838 let req = TestRequest::default().to_http_request();
839 let resp = test::respond_to(file, &req).await;
840 assert_eq!(resp.headers().get(http::header::CONTENT_TYPE).unwrap(), "text/x-toml");
841 assert_eq!(
842 resp.headers().get(http::header::CONTENT_DISPOSITION).unwrap(),
843 "inline; filename=\"Cargo.toml\""
844 );
845 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
846 }
847
848 #[ntex::test]
849 async fn test_mime_override() {
850 fn all_attachment(_: &mime::Name) -> file_header::DispositionType {
851 file_header::DispositionType::Attachment
852 }
853
854 let srv = test::init_service(App::new().service(
855 Files::new("/", ".").mime_override(all_attachment).index_file("Cargo.toml"),
856 ))
857 .await;
858
859 let request = TestRequest::get().uri("/").to_request();
860 let response = test::call_service(&srv, request).await;
861 assert_eq!(response.status(), StatusCode::OK);
862
863 let content_disposition = response
864 .headers()
865 .get(http::header::CONTENT_DISPOSITION)
866 .expect("To have CONTENT_DISPOSITION");
867 let content_disposition =
868 content_disposition.to_str().expect("Convert CONTENT_DISPOSITION to str");
869 assert_eq!(content_disposition, "attachment; filename=\"Cargo.toml\"");
870 }
871
872 #[ntex::test]
873 async fn test_named_file_ranges_status_code() {
874 let srv = test::init_service(
875 App::new().service(Files::new("/test", ".").index_file("Cargo.toml")),
876 )
877 .await;
878
879 let request = TestRequest::get()
881 .uri("/t%65st/Cargo.toml")
882 .header(http::header::RANGE, "bytes=10-20")
883 .to_request();
884 let response = test::call_service(&srv, request).await;
885 assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
886
887 let request = TestRequest::get()
889 .uri("/t%65st/Cargo.toml")
890 .header(http::header::RANGE, "bytes=1-0")
891 .to_request();
892 let response = test::call_service(&srv, request).await;
893
894 assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
895 }
896
897 #[ntex::test]
898 async fn test_named_file_content_range_headers() {
899 let srv = test::init_service(
900 App::new().service(Files::new("/test", ".").index_file("tests/test.binary")),
901 )
902 .await;
903
904 let request = TestRequest::get()
906 .uri("/t%65st/tests/test.binary")
907 .header(http::header::RANGE, "bytes=10-20")
908 .to_request();
909
910 let response = test::call_service(&srv, request).await;
911 let contentrange =
912 response.headers().get(http::header::CONTENT_RANGE).unwrap().to_str().unwrap();
913
914 assert_eq!(contentrange, "bytes 10-20/100");
915
916 let request = TestRequest::get()
918 .uri("/t%65st/tests/test.binary")
919 .header(http::header::RANGE, "bytes=10-5")
920 .to_request();
921 let response = test::call_service(&srv, request).await;
922
923 let contentrange =
924 response.headers().get(http::header::CONTENT_RANGE).unwrap().to_str().unwrap();
925
926 assert_eq!(contentrange, "bytes */100");
927 }
928
929 #[ntex::test]
930 async fn test_named_file_content_length_headers() {
931 let srv = test::init_service(
932 App::new().service(Files::new("test", ".").index_file("tests/test.binary")),
933 )
934 .await;
935
936 let request = TestRequest::get()
938 .uri("/t%65st/tests/test.binary")
939 .header(http::header::RANGE, "bytes=10-20")
940 .to_request();
941 let _response = test::call_service(&srv, request).await;
942
943 let request = TestRequest::get()
953 .uri("/t%65st/tests/test.binary")
954 .header(http::header::RANGE, "bytes=10-8")
955 .to_request();
956 let response = test::call_service(&srv, request).await;
957 assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
958
959 let request = TestRequest::get()
961 .uri("/t%65st/tests/test.binary")
962 .to_request();
964 let _response = test::call_service(&srv, request).await;
965
966 let request = TestRequest::get().uri("/t%65st/tests/test.binary").to_request();
976 let response = test::call_service(&srv, request).await;
977
978 let bytes = test::read_body(response).await;
990 let data = Bytes::from(fs::read("tests/test.binary").unwrap());
991 assert_eq!(bytes, data);
992 }
993
994 #[ntex::test]
995 async fn test_head_content_length_headers() {
996 let srv = test::init_service(
997 App::new().service(Files::new("test", ".").index_file("tests/test.binary")),
998 )
999 .await;
1000
1001 let request = TestRequest::default()
1003 .method(Method::HEAD)
1004 .uri("/t%65st/tests/test.binary")
1005 .to_request();
1006 let _response = test::call_service(&srv, request).await;
1007
1008 }
1017
1018 #[ntex::test]
1019 async fn test_static_files_with_spaces() {
1020 let srv = test::init_service(
1021 App::new().service(Files::new("/", ".").index_file("Cargo.toml")),
1022 )
1023 .await;
1024 let request = TestRequest::get().uri("/tests/test%20space.binary").to_request();
1025 let response = test::call_service(&srv, request).await;
1026 assert_eq!(response.status(), StatusCode::OK);
1027
1028 let bytes = test::read_body(response).await;
1029 let data = Bytes::from(fs::read("tests/test space.binary").unwrap());
1030 assert_eq!(bytes, data);
1031 }
1032
1033 #[ntex::test]
1034 async fn test_files_not_allowed() {
1035 let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
1036
1037 let req = TestRequest::default().uri("/Cargo.toml").method(Method::POST).to_request();
1038
1039 let resp = test::call_service(&srv, req).await;
1040 assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
1041
1042 let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
1043 let req = TestRequest::default().method(Method::PUT).uri("/Cargo.toml").to_request();
1044 let resp = test::call_service(&srv, req).await;
1045 assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
1046 }
1047
1048 #[ntex::test]
1049 async fn test_files_guards() {
1050 let srv = test::init_service(
1051 App::new().service(Files::new("/", ".").use_guards(guard::Post())),
1052 )
1053 .await;
1054
1055 let req = TestRequest::default().uri("/Cargo.toml").method(Method::POST).to_request();
1056
1057 let resp = test::call_service(&srv, req).await;
1058 assert_eq!(resp.status(), StatusCode::OK);
1059 }
1060
1061 #[ntex::test]
1062 async fn test_named_file_content_encoding() {
1063 let srv = test::init_service(App::new().wrap(Compress::default()).service(
1064 web::resource("/").to(|| async {
1065 NamedFile::open("Cargo.toml")
1066 .unwrap()
1067 .set_content_encoding(http::header::ContentEncoding::Identity)
1068 }),
1069 ))
1070 .await;
1071
1072 let request = TestRequest::get()
1073 .uri("/")
1074 .header(http::header::ACCEPT_ENCODING, "gzip")
1075 .to_request();
1076 let res = test::call_service(&srv, request).await;
1077 assert_eq!(res.status(), StatusCode::OK);
1078 assert!(!res.headers().contains_key(http::header::CONTENT_ENCODING));
1079 }
1080
1081 #[ntex::test]
1082 async fn test_named_file_content_encoding_gzip() {
1083 let srv = test::init_service(App::new().wrap(Compress::default()).service(
1084 web::resource("/").to(|| async {
1085 NamedFile::open("Cargo.toml")
1086 .unwrap()
1087 .set_content_encoding(http::header::ContentEncoding::Gzip)
1088 }),
1089 ))
1090 .await;
1091
1092 let request = TestRequest::get()
1093 .uri("/")
1094 .header(http::header::ACCEPT_ENCODING, "gzip")
1095 .to_request();
1096 let res = test::call_service(&srv, request).await;
1097 assert_eq!(res.status(), StatusCode::OK);
1098 assert_eq!(
1099 res.headers().get(http::header::CONTENT_ENCODING).unwrap().to_str().unwrap(),
1100 "gzip"
1101 );
1102 }
1103
1104 #[ntex::test]
1105 async fn test_named_file_allowed_method() {
1106 let req = TestRequest::default().method(Method::GET).to_http_request();
1107 let file = NamedFile::open("Cargo.toml").unwrap();
1108 let resp = test::respond_to(file, &req).await;
1109 assert_eq!(resp.status(), StatusCode::OK);
1110 }
1111
1112 #[ntex::test]
1113 async fn test_static_files() {
1114 let srv =
1115 test::init_service(App::new().service(Files::new("/", ".").show_files_listing()))
1116 .await;
1117 let req = TestRequest::with_uri("/missing").to_request();
1118
1119 let resp = test::call_service(&srv, req).await;
1120 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1121
1122 let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
1123
1124 let req = TestRequest::default().to_request();
1125 let resp = test::call_service(&srv, req).await;
1126 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1127
1128 let srv =
1129 test::init_service(App::new().service(Files::new("/", ".").show_files_listing()))
1130 .await;
1131 let req = TestRequest::with_uri("/tests").to_request();
1132 let resp = test::call_service(&srv, req).await;
1133 assert_eq!(
1134 resp.headers().get(http::header::CONTENT_TYPE).unwrap(),
1135 "text/html; charset=utf-8"
1136 );
1137
1138 let bytes = test::read_body(resp).await;
1139 assert!(format!("{:?}", bytes).contains("/tests/test.png"));
1140 }
1141
1142 #[ntex::test]
1143 async fn test_redirect_to_slash_directory() {
1144 let srv = test::init_service(
1146 App::new().service(Files::new("/", ".").redirect_to_slash_directory()),
1147 )
1148 .await;
1149 let req = TestRequest::with_uri("/tests").to_request();
1150 let resp = test::call_service(&srv, req).await;
1151 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1152
1153 let srv = test::init_service(App::new().service(
1155 Files::new("/", ".").index_file("test.png").redirect_to_slash_directory(),
1156 ))
1157 .await;
1158 let req = TestRequest::with_uri("/tests").to_request();
1159 let resp = test::call_service(&srv, req).await;
1160 assert_eq!(resp.status(), StatusCode::FOUND);
1161
1162 let req = TestRequest::with_uri("/not_existing").to_request();
1164 let resp = test::call_service(&srv, req).await;
1165 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1166 }
1167
1168 #[ntex::test]
1169 async fn test_static_files_bad_directory() {
1170 let _st: Files<DefaultError> = Files::new("/", "missing");
1171 let _st: Files<DefaultError> = Files::new("/", "Cargo.toml");
1172 }
1173
1174 #[ntex::test]
1175 async fn test_default_handler_file_missing() {
1176 let st = Files::new("/", ".")
1177 .default_handler(|req: WebRequest<DefaultError>| async move {
1178 Ok(req.into_response(HttpResponse::Ok().body("default content")))
1179 })
1180 .pipeline(SharedCfg::default())
1181 .await
1182 .unwrap();
1183 let req = TestRequest::with_uri("/missing").to_srv_request();
1184
1185 let resp = test::call_service(&st, req).await;
1186 assert_eq!(resp.status(), StatusCode::OK);
1187 let bytes = test::read_body(resp).await;
1188 assert_eq!(bytes, Bytes::from_static(b"default content"));
1189 }
1190
1191 #[ntex::test]
1306 async fn test_path_buf() {
1307 assert_eq!(
1308 PathBufWrp::get_pathbuf("/test/.tt").map(|t| t.0),
1309 Err(UriSegmentError::BadStart('.'))
1310 );
1311 assert_eq!(
1312 PathBufWrp::get_pathbuf("/test/*tt").map(|t| t.0),
1313 Err(UriSegmentError::BadStart('*'))
1314 );
1315 assert_eq!(
1316 PathBufWrp::get_pathbuf("/test/tt:").map(|t| t.0),
1317 Err(UriSegmentError::BadEnd(':'))
1318 );
1319 assert_eq!(
1320 PathBufWrp::get_pathbuf("/test/tt<").map(|t| t.0),
1321 Err(UriSegmentError::BadEnd('<'))
1322 );
1323 assert_eq!(
1324 PathBufWrp::get_pathbuf("/test/tt>").map(|t| t.0),
1325 Err(UriSegmentError::BadEnd('>'))
1326 );
1327 assert_eq!(
1328 PathBufWrp::get_pathbuf("/seg1/seg2/").unwrap().0,
1329 PathBuf::from_iter(vec!["seg1", "seg2"])
1330 );
1331 assert_eq!(
1332 PathBufWrp::get_pathbuf("/seg1/../seg2/").unwrap().0,
1333 PathBuf::from_iter(vec!["seg2"])
1334 );
1335 }
1336}