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