//! Static files support use std::cell::RefCell; use std::fmt::Write; use std::fs::{DirEntry, File}; use std::io::{Read, Seek}; use std::marker::PhantomData; use std::path::{Path, PathBuf}; use std::rc::Rc; use std::{cmp, io}; use bytes::Bytes; use futures::{Async, Future, Poll, Stream}; use mime; use mime_guess::get_mime_type; use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; use v_htmlescape::escape as escape_html_entity; use actix_http::error::{Error, ErrorInternalServerError}; use actix_service::{boxed::BoxedNewService, NewService, Service}; use actix_web::dev::{self, HttpServiceFactory, ResourceDef, Url}; use actix_web::{ blocking, FromRequest, HttpRequest, HttpResponse, Responder, ServiceFromRequest, ServiceRequest, ServiceResponse, }; use futures::future::{ok, FutureResult}; mod config; mod error; mod named; use self::error::{StaticFilesError, UriSegmentError}; pub use crate::config::{DefaultConfig, StaticFileConfig}; pub use crate::named::NamedFile; type HttpNewService

= BoxedNewService<(), ServiceRequest

, ServiceResponse, (), ()>; /// Return the MIME type associated with a filename extension (case-insensitive). /// If `ext` is empty or no associated type for the extension was found, returns /// the type `application/octet-stream`. #[inline] pub fn file_extension_to_mime(ext: &str) -> mime::Mime { get_mime_type(ext) } #[doc(hidden)] /// A helper created from a `std::fs::File` which reads the file /// chunk-by-chunk on a `ThreadPool`. pub struct ChunkedReadFile { size: u64, offset: u64, file: Option, fut: Option>, counter: u64, } fn handle_error(err: blocking::BlockingError) -> Error { match err { blocking::BlockingError::Error(err) => err.into(), blocking::BlockingError::Canceled => { ErrorInternalServerError("Unexpected error").into() } } } impl Stream for ChunkedReadFile { type Item = Bytes; type Error = Error; fn poll(&mut self) -> Poll, Error> { if self.fut.is_some() { return match self.fut.as_mut().unwrap().poll().map_err(handle_error)? { Async::Ready((file, bytes)) => { self.fut.take(); self.file = Some(file); self.offset += bytes.len() as u64; self.counter += bytes.len() as u64; Ok(Async::Ready(Some(bytes))) } Async::NotReady => Ok(Async::NotReady), }; } let size = self.size; let offset = self.offset; let counter = self.counter; if size == counter { Ok(Async::Ready(None)) } else { let mut file = self.file.take().expect("Use after completion"); self.fut = Some(blocking::run(move || { let max_bytes: usize; max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; let mut buf = Vec::with_capacity(max_bytes); file.seek(io::SeekFrom::Start(offset))?; let nbytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?; if nbytes == 0 { return Err(io::ErrorKind::UnexpectedEof.into()); } Ok((file, Bytes::from(buf))) })); self.poll() } } } type DirectoryRenderer = Fn(&Directory, &HttpRequest) -> Result; /// A directory; responds with the generated directory listing. #[derive(Debug)] pub struct Directory { /// Base directory pub base: PathBuf, /// Path of subdirectory to generate listing for pub path: PathBuf, } impl Directory { /// Create a new directory pub fn new(base: PathBuf, path: PathBuf) -> Directory { Directory { base, path } } /// Is this entry visible from this directory? pub fn is_visible(&self, entry: &io::Result) -> bool { if let Ok(ref entry) = *entry { if let Some(name) = entry.file_name().to_str() { if name.starts_with('.') { return false; } } if let Ok(ref md) = entry.metadata() { let ft = md.file_type(); return ft.is_dir() || ft.is_file() || ft.is_symlink(); } } false } } // show file url as relative to static path macro_rules! encode_file_url { ($path:ident) => { utf8_percent_encode(&$path.to_string_lossy(), DEFAULT_ENCODE_SET) }; } // " -- " & -- & ' -- ' < -- < > -- > / -- / macro_rules! encode_file_name { ($entry:ident) => { escape_html_entity(&$entry.file_name().to_string_lossy()) }; } fn directory_listing( dir: &Directory, req: &HttpRequest, ) -> Result { let index_of = format!("Index of {}", req.path()); let mut body = String::new(); let base = Path::new(req.path()); for entry in dir.path.read_dir()? { if dir.is_visible(&entry) { let entry = entry.unwrap(); let p = match entry.path().strip_prefix(&dir.path) { Ok(p) => base.join(p), Err(_) => continue, }; // if file is a directory, add '/' to the end of the name if let Ok(metadata) = entry.metadata() { if metadata.is_dir() { let _ = write!( body, "

  • {}/
  • ", encode_file_url!(p), encode_file_name!(entry), ); } else { let _ = write!( body, "
  • {}
  • ", encode_file_url!(p), encode_file_name!(entry), ); } } else { continue; } } } let html = format!( "\ {}\

    {}

    \ \n", index_of, index_of, body ); Ok(ServiceResponse::new( req.clone(), HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body(html), )) } /// Static files handling /// /// `StaticFile` handler must be registered with `App::handler()` method, /// because `StaticFile` handler requires access sub-path information. /// /// ```rust,ignore /// # extern crate actix_web; /// use actix_web::{fs, App}; /// /// fn main() { /// let app = App::new() /// .handler("/static", fs::StaticFiles::new(".").unwrap()) /// .finish(); /// } /// ``` pub struct StaticFiles { path: ResourceDef, directory: PathBuf, index: Option, show_index: bool, default: Rc>>>>, renderer: Rc, _chunk_size: usize, _follow_symlinks: bool, _cd_map: PhantomData, } impl StaticFiles { /// Create new `StaticFiles` instance for specified base directory. /// /// `StaticFile` uses `ThreadPool` for blocking filesystem operations. /// By default pool with 5x threads of available cpus is used. /// Pool size can be changed by setting ACTIX_CPU_POOL environment variable. pub fn new>(path: &str, dir: T) -> Result, Error> { Self::with_config(path, dir, DefaultConfig) } } impl StaticFiles { /// Create new `StaticFiles` instance for specified base directory. /// /// Identical with `new` but allows to specify configiration to use. pub fn with_config>( path: &str, dir: T, _: C, ) -> Result, Error> { let dir = dir.into().canonicalize()?; if !dir.is_dir() { return Err(StaticFilesError::IsNotDirectory.into()); } Ok(StaticFiles { path: ResourceDef::root_prefix(path), directory: dir, index: None, show_index: false, default: Rc::new(RefCell::new(None)), renderer: Rc::new(directory_listing), _chunk_size: 0, _follow_symlinks: false, _cd_map: PhantomData, }) } /// Show files listing for directories. /// /// By default show files listing is disabled. pub fn show_files_listing(mut self) -> Self { self.show_index = true; self } /// Set custom directory renderer pub fn files_listing_renderer(mut self, f: F) -> Self where for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) -> Result + 'static, { self.renderer = Rc::new(f); self } /// Set index file /// /// Shows specific index file for directory "/" instead of /// showing files listing. pub fn index_file>(mut self, index: T) -> StaticFiles { self.index = Some(index.into()); self } } impl HttpServiceFactory

    for StaticFiles { type Factory = Self; fn rdef(&self) -> &ResourceDef { &self.path } fn create(self) -> Self { self } } impl NewService> for StaticFiles { type Response = ServiceResponse; type Error = (); type Service = StaticFilesService; type InitError = (); type Future = FutureResult; fn new_service(&self, _: &()) -> Self::Future { ok(StaticFilesService { directory: self.directory.clone(), index: self.index.clone(), show_index: self.show_index, default: self.default.clone(), renderer: self.renderer.clone(), _chunk_size: self._chunk_size, _follow_symlinks: self._follow_symlinks, _cd_map: self._cd_map, }) } } pub struct StaticFilesService { directory: PathBuf, index: Option, show_index: bool, default: Rc>>>>, renderer: Rc, _chunk_size: usize, _follow_symlinks: bool, _cd_map: PhantomData, } impl Service> for StaticFilesService { type Response = ServiceResponse; type Error = (); type Future = FutureResult; fn poll_ready(&mut self) -> Poll<(), Self::Error> { Ok(Async::Ready(())) } fn call(&mut self, req: ServiceRequest

    ) -> Self::Future { let (req, _) = req.into_parts(); let real_path = match PathBufWrp::get_pathbuf(req.match_info()) { Ok(item) => item, Err(e) => return ok(ServiceResponse::from_err(e, req.clone())), }; // full filepath let path = match self.directory.join(&real_path.0).canonicalize() { Ok(path) => path, Err(e) => return ok(ServiceResponse::from_err(e, req.clone())), }; if path.is_dir() { if let Some(ref redir_index) = self.index { let path = path.join(redir_index); match NamedFile::open_with_config(path, C::default()) { Ok(named_file) => match named_file.respond_to(&req) { Ok(item) => ok(ServiceResponse::new(req.clone(), item)), Err(e) => ok(ServiceResponse::from_err(e, req.clone())), }, Err(e) => ok(ServiceResponse::from_err(e, req.clone())), } } else if self.show_index { let dir = Directory::new(self.directory.clone(), path); let x = (self.renderer)(&dir, &req); match x { Ok(resp) => ok(resp), Err(e) => ok(ServiceResponse::from_err(e, req.clone())), } } else { ok(ServiceResponse::from_err( StaticFilesError::IsDirectory, req.clone(), )) } } else { match NamedFile::open_with_config(path, C::default()) { Ok(named_file) => match named_file.respond_to(&req) { Ok(item) => ok(ServiceResponse::new(req.clone(), item)), Err(e) => ok(ServiceResponse::from_err(e, req.clone())), }, Err(e) => ok(ServiceResponse::from_err(e, req.clone())), } } } } struct PathBufWrp(PathBuf); impl PathBufWrp { fn get_pathbuf(path: &dev::Path) -> Result { let path_str = path.path(); let mut buf = PathBuf::new(); for segment in path_str.split('/') { if segment == ".." { buf.pop(); } else if segment.starts_with('.') { return Err(UriSegmentError::BadStart('.')); } else if segment.starts_with('*') { return Err(UriSegmentError::BadStart('*')); } else if segment.ends_with(':') { return Err(UriSegmentError::BadEnd(':')); } else if segment.ends_with('>') { return Err(UriSegmentError::BadEnd('>')); } else if segment.ends_with('<') { return Err(UriSegmentError::BadEnd('<')); } else if segment.is_empty() { continue; } else if cfg!(windows) && segment.contains('\\') { return Err(UriSegmentError::BadChar('\\')); } else { buf.push(segment) } } Ok(PathBufWrp(buf)) } } impl

    FromRequest

    for PathBufWrp { type Error = UriSegmentError; type Future = Result; type Config = (); fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { PathBufWrp::get_pathbuf(req.match_info()) } } /// HTTP Range header representation. #[derive(Debug, Clone, Copy)] struct HttpRange { pub start: u64, pub length: u64, } static PREFIX: &'static str = "bytes="; const PREFIX_LEN: usize = 6; impl HttpRange { /// Parses Range HTTP header string as per RFC 2616. /// /// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`). /// `size` is full size of response (file). fn parse(header: &str, size: u64) -> Result, ()> { if header.is_empty() { return Ok(Vec::new()); } if !header.starts_with(PREFIX) { return Err(()); } let size_sig = size as i64; let mut no_overlap = false; let all_ranges: Vec> = header[PREFIX_LEN..] .split(',') .map(|x| x.trim()) .filter(|x| !x.is_empty()) .map(|ra| { let mut start_end_iter = ra.split('-'); let start_str = start_end_iter.next().ok_or(())?.trim(); let end_str = start_end_iter.next().ok_or(())?.trim(); if start_str.is_empty() { // If no start is specified, end specifies the // range start relative to the end of the file. let mut length: i64 = end_str.parse().map_err(|_| ())?; if length > size_sig { length = size_sig; } Ok(Some(HttpRange { start: (size_sig - length) as u64, length: length as u64, })) } else { let start: i64 = start_str.parse().map_err(|_| ())?; if start < 0 { return Err(()); } if start >= size_sig { no_overlap = true; return Ok(None); } let length = if end_str.is_empty() { // If no end is specified, range extends to end of the file. size_sig - start } else { let mut end: i64 = end_str.parse().map_err(|_| ())?; if start > end { return Err(()); } if end >= size_sig { end = size_sig - 1; } end - start + 1 }; Ok(Some(HttpRange { start: start as u64, length: length as u64, })) } }) .collect::>()?; let ranges: Vec = all_ranges.into_iter().filter_map(|x| x).collect(); if no_overlap && ranges.is_empty() { return Err(()); } Ok(ranges) } } #[cfg(test)] mod tests { use std::fs; use std::ops::Add; use std::time::{Duration, SystemTime}; use bytes::BytesMut; use super::*; use actix_web::http::{header, header::DispositionType, Method, StatusCode}; use actix_web::test::{self, TestRequest}; use actix_web::App; #[test] fn test_file_extension_to_mime() { let m = file_extension_to_mime("jpg"); assert_eq!(m, mime::IMAGE_JPEG); let m = file_extension_to_mime("invalid extension!!"); assert_eq!(m, mime::APPLICATION_OCTET_STREAM); let m = file_extension_to_mime(""); assert_eq!(m, mime::APPLICATION_OCTET_STREAM); } #[test] fn test_if_modified_since_without_if_none_match() { let file = NamedFile::open("Cargo.toml").unwrap(); let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); let req = TestRequest::default() .header(header::IF_MODIFIED_SINCE, since) .to_http_request(); let resp = file.respond_to(&req).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_MODIFIED); } #[test] fn test_if_modified_since_with_if_none_match() { let file = NamedFile::open("Cargo.toml").unwrap(); let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); let req = TestRequest::default() .header(header::IF_NONE_MATCH, "miss_etag") .header(header::IF_MODIFIED_SINCE, since) .to_http_request(); let resp = file.respond_to(&req).unwrap(); assert_ne!(resp.status(), StatusCode::NOT_MODIFIED); } #[test] fn test_named_file_text() { assert!(NamedFile::open("test--").is_err()); let mut file = NamedFile::open("Cargo.toml").unwrap(); { file.file(); let _f: &File = &file; } { let _f: &mut File = &mut file; } let req = TestRequest::default().to_http_request(); let resp = file.respond_to(&req).unwrap(); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), "text/x-toml" ); assert_eq!( resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), "inline; filename=\"Cargo.toml\"" ); } #[test] fn test_named_file_set_content_type() { let mut file = NamedFile::open("Cargo.toml") .unwrap() .set_content_type(mime::TEXT_XML); { file.file(); let _f: &File = &file; } { let _f: &mut File = &mut file; } let req = TestRequest::default().to_http_request(); let resp = file.respond_to(&req).unwrap(); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), "text/xml" ); assert_eq!( resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), "inline; filename=\"Cargo.toml\"" ); } #[test] fn test_named_file_image() { let mut file = NamedFile::open("tests/test.png").unwrap(); { file.file(); let _f: &File = &file; } { let _f: &mut File = &mut file; } let req = TestRequest::default().to_http_request(); let resp = file.respond_to(&req).unwrap(); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), "image/png" ); assert_eq!( resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), "inline; filename=\"test.png\"" ); } #[test] fn test_named_file_image_attachment() { use header::{ContentDisposition, DispositionParam, DispositionType}; let cd = ContentDisposition { disposition: DispositionType::Attachment, parameters: vec![DispositionParam::Filename(String::from("test.png"))], }; let mut file = NamedFile::open("tests/test.png") .unwrap() .set_content_disposition(cd); { file.file(); let _f: &File = &file; } { let _f: &mut File = &mut file; } let req = TestRequest::default().to_http_request(); let resp = file.respond_to(&req).unwrap(); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), "image/png" ); assert_eq!( resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), "attachment; filename=\"test.png\"" ); } #[derive(Default)] pub struct AllAttachmentConfig; impl StaticFileConfig for AllAttachmentConfig { fn content_disposition_map(_typ: mime::Name) -> DispositionType { DispositionType::Attachment } } #[derive(Default)] pub struct AllInlineConfig; impl StaticFileConfig for AllInlineConfig { fn content_disposition_map(_typ: mime::Name) -> DispositionType { DispositionType::Inline } } #[test] fn test_named_file_image_attachment_and_custom_config() { let file = NamedFile::open_with_config("tests/test.png", AllAttachmentConfig).unwrap(); let req = TestRequest::default().to_http_request(); let resp = file.respond_to(&req).unwrap(); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), "image/png" ); assert_eq!( resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), "attachment; filename=\"test.png\"" ); let file = NamedFile::open_with_config("tests/test.png", AllInlineConfig).unwrap(); let req = TestRequest::default().to_http_request(); let resp = file.respond_to(&req).unwrap(); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), "image/png" ); assert_eq!( resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), "inline; filename=\"test.png\"" ); } #[test] fn test_named_file_binary() { let mut file = NamedFile::open("tests/test.binary").unwrap(); { file.file(); let _f: &File = &file; } { let _f: &mut File = &mut file; } let req = TestRequest::default().to_http_request(); let resp = file.respond_to(&req).unwrap(); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), "application/octet-stream" ); assert_eq!( resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), "attachment; filename=\"test.binary\"" ); } #[test] fn test_named_file_status_code_text() { let mut file = NamedFile::open("Cargo.toml") .unwrap() .set_status_code(StatusCode::NOT_FOUND); { file.file(); let _f: &File = &file; } { let _f: &mut File = &mut file; } let req = TestRequest::default().to_http_request(); let resp = file.respond_to(&req).unwrap(); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), "text/x-toml" ); assert_eq!( resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), "inline; filename=\"Cargo.toml\"" ); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[test] fn test_named_file_ranges_status_code() { let mut srv = test::init_service( App::new().service( StaticFiles::new("/test", ".") .unwrap() .index_file("Cargo.toml"), ), ); // Valid range header let request = TestRequest::get() .uri("/t%65st/Cargo.toml") .header(header::RANGE, "bytes=10-20") .to_request(); let response = test::call_success(&mut srv, request); assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); // Invalid range header let request = TestRequest::get() .uri("/t%65st/Cargo.toml") .header(header::RANGE, "bytes=1-0") .to_request(); let response = test::call_success(&mut srv, request); assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); } #[test] fn test_named_file_content_range_headers() { let mut srv = test::init_service( App::new().service( StaticFiles::new("/test", ".") .unwrap() .index_file("tests/test.binary"), ), ); // Valid range header let request = TestRequest::get() .uri("/t%65st/tests/test.binary") .header(header::RANGE, "bytes=10-20") .to_request(); let response = test::call_success(&mut srv, request); let contentrange = response .headers() .get(header::CONTENT_RANGE) .unwrap() .to_str() .unwrap(); assert_eq!(contentrange, "bytes 10-20/100"); // Invalid range header let request = TestRequest::get() .uri("/t%65st/tests/test.binary") .header(header::RANGE, "bytes=10-5") .to_request(); let response = test::call_success(&mut srv, request); let contentrange = response .headers() .get(header::CONTENT_RANGE) .unwrap() .to_str() .unwrap(); assert_eq!(contentrange, "bytes */100"); } #[test] fn test_named_file_content_length_headers() { let mut srv = test::init_service( App::new().service( StaticFiles::new("test", ".") .unwrap() .index_file("tests/test.binary"), ), ); // Valid range header let request = TestRequest::get() .uri("/t%65st/tests/test.binary") .header(header::RANGE, "bytes=10-20") .to_request(); let response = test::call_success(&mut srv, request); let contentlength = response .headers() .get(header::CONTENT_LENGTH) .unwrap() .to_str() .unwrap(); assert_eq!(contentlength, "11"); // Invalid range header let request = TestRequest::get() .uri("/t%65st/tests/test.binary") .header(header::RANGE, "bytes=10-8") .to_request(); let response = test::call_success(&mut srv, request); assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); // Without range header let request = TestRequest::get() .uri("/t%65st/tests/test.binary") // .no_default_headers() .to_request(); let response = test::call_success(&mut srv, request); let contentlength = response .headers() .get(header::CONTENT_LENGTH) .unwrap() .to_str() .unwrap(); assert_eq!(contentlength, "100"); // chunked let request = TestRequest::get() .uri("/t%65st/tests/test.binary") .to_request(); let mut response = test::call_success(&mut srv, request); // with enabled compression // { // let te = response // .headers() // .get(header::TRANSFER_ENCODING) // .unwrap() // .to_str() // .unwrap(); // assert_eq!(te, "chunked"); // } let bytes = test::block_on(response.take_body().fold(BytesMut::new(), |mut b, c| { b.extend(c); Ok::<_, Error>(b) })) .unwrap(); let data = Bytes::from(fs::read("tests/test.binary").unwrap()); assert_eq!(bytes.freeze(), data); } #[test] fn test_static_files_with_spaces() { let mut srv = test::init_service( App::new() .service(StaticFiles::new("/", ".").unwrap().index_file("Cargo.toml")), ); let request = TestRequest::get() .uri("/tests/test%20space.binary") .to_request(); let mut response = test::call_success(&mut srv, request); assert_eq!(response.status(), StatusCode::OK); let bytes = test::block_on(response.take_body().fold(BytesMut::new(), |mut b, c| { b.extend(c); Ok::<_, Error>(b) })) .unwrap(); let data = Bytes::from(fs::read("tests/test space.binary").unwrap()); assert_eq!(bytes.freeze(), data); } #[derive(Default)] pub struct OnlyMethodHeadConfig; impl StaticFileConfig for OnlyMethodHeadConfig { fn is_method_allowed(method: &Method) -> bool { match *method { Method::HEAD => true, _ => false, } } } #[test] fn test_named_file_not_allowed() { let file = NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); let req = TestRequest::default() .method(Method::POST) .to_http_request(); let resp = file.respond_to(&req).unwrap(); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); let file = NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); let req = TestRequest::default().method(Method::PUT).to_http_request(); let resp = file.respond_to(&req).unwrap(); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); let file = NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); let req = TestRequest::default().method(Method::GET).to_http_request(); let resp = file.respond_to(&req).unwrap(); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); } // #[test] // fn test_named_file_content_encoding() { // let req = TestRequest::default().method(Method::GET).finish(); // let file = NamedFile::open("Cargo.toml").unwrap(); // assert!(file.encoding.is_none()); // let resp = file // .set_content_encoding(ContentEncoding::Identity) // .respond_to(&req) // .unwrap(); // assert!(resp.content_encoding().is_some()); // assert_eq!(resp.content_encoding().unwrap().as_str(), "identity"); // } #[test] fn test_named_file_any_method() { let req = TestRequest::default() .method(Method::POST) .to_http_request(); let file = NamedFile::open("Cargo.toml").unwrap(); let resp = file.respond_to(&req).unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[test] fn test_static_files() { let mut srv = test::init_service( App::new().service(StaticFiles::new("/", ".").unwrap().show_files_listing()), ); let req = TestRequest::with_uri("/missing").to_request(); let resp = test::call_success(&mut srv, req); assert_eq!(resp.status(), StatusCode::NOT_FOUND); let mut srv = test::init_service(App::new().service(StaticFiles::new("/", ".").unwrap())); let req = TestRequest::default().to_request(); let resp = test::call_success(&mut srv, req); assert_eq!(resp.status(), StatusCode::NOT_FOUND); let mut srv = test::init_service( App::new().service(StaticFiles::new("/", ".").unwrap().show_files_listing()), ); let req = TestRequest::with_uri("/tests").to_request(); let mut resp = test::call_success(&mut srv, req); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), "text/html; charset=utf-8" ); let bytes = test::block_on(resp.take_body().fold(BytesMut::new(), |mut b, c| { b.extend(c); Ok::<_, Error>(b) })) .unwrap(); assert!(format!("{:?}", bytes).contains("/tests/test.png")); } #[test] fn test_static_files_bad_directory() { let st: Result, Error> = StaticFiles::new("/", "missing"); assert!(st.is_err()); let st: Result, Error> = StaticFiles::new("/", "Cargo.toml"); assert!(st.is_err()); } // #[test] // fn test_default_handler_file_missing() { // let st = StaticFiles::new(".") // .unwrap() // .default_handler(|_: &_| "default content"); // let req = TestRequest::with_uri("/missing") // .param("tail", "missing") // .finish(); // let resp = st.handle(&req).respond_to(&req).unwrap(); // let resp = resp.as_msg(); // assert_eq!(resp.status(), StatusCode::OK); // assert_eq!( // resp.body(), // &Body::Binary(Binary::Slice(b"default content")) // ); // } // #[test] // fn test_serve_index() { // let st = StaticFiles::new(".").unwrap().index_file("test.binary"); // let req = TestRequest::default().uri("/tests").finish(); // let resp = st.handle(&req).respond_to(&req).unwrap(); // let resp = resp.as_msg(); // assert_eq!(resp.status(), StatusCode::OK); // assert_eq!( // resp.headers() // .get(header::CONTENT_TYPE) // .expect("content type"), // "application/octet-stream" // ); // assert_eq!( // resp.headers() // .get(header::CONTENT_DISPOSITION) // .expect("content disposition"), // "attachment; filename=\"test.binary\"" // ); // let req = TestRequest::default().uri("/tests/").finish(); // let resp = st.handle(&req).respond_to(&req).unwrap(); // let resp = resp.as_msg(); // assert_eq!(resp.status(), StatusCode::OK); // assert_eq!( // resp.headers().get(header::CONTENT_TYPE).unwrap(), // "application/octet-stream" // ); // assert_eq!( // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), // "attachment; filename=\"test.binary\"" // ); // // nonexistent index file // let req = TestRequest::default().uri("/tests/unknown").finish(); // let resp = st.handle(&req).respond_to(&req).unwrap(); // let resp = resp.as_msg(); // assert_eq!(resp.status(), StatusCode::NOT_FOUND); // let req = TestRequest::default().uri("/tests/unknown/").finish(); // let resp = st.handle(&req).respond_to(&req).unwrap(); // let resp = resp.as_msg(); // assert_eq!(resp.status(), StatusCode::NOT_FOUND); // } // #[test] // fn test_serve_index_nested() { // let st = StaticFiles::new(".").unwrap().index_file("mod.rs"); // let req = TestRequest::default().uri("/src/client").finish(); // let resp = st.handle(&req).respond_to(&req).unwrap(); // let resp = resp.as_msg(); // assert_eq!(resp.status(), StatusCode::OK); // assert_eq!( // resp.headers().get(header::CONTENT_TYPE).unwrap(), // "text/x-rust" // ); // assert_eq!( // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), // "inline; filename=\"mod.rs\"" // ); // } // #[test] // fn integration_serve_index() { // let mut srv = test::TestServer::with_factory(|| { // App::new().handler( // "test", // StaticFiles::new(".").unwrap().index_file("Cargo.toml"), // ) // }); // let request = srv.get().uri(srv.url("/test")).finish().unwrap(); // let response = srv.execute(request.send()).unwrap(); // assert_eq!(response.status(), StatusCode::OK); // let bytes = srv.execute(response.body()).unwrap(); // let data = Bytes::from(fs::read("Cargo.toml").unwrap()); // assert_eq!(bytes, data); // let request = srv.get().uri(srv.url("/test/")).finish().unwrap(); // let response = srv.execute(request.send()).unwrap(); // assert_eq!(response.status(), StatusCode::OK); // let bytes = srv.execute(response.body()).unwrap(); // let data = Bytes::from(fs::read("Cargo.toml").unwrap()); // assert_eq!(bytes, data); // // nonexistent index file // let request = srv.get().uri(srv.url("/test/unknown")).finish().unwrap(); // let response = srv.execute(request.send()).unwrap(); // assert_eq!(response.status(), StatusCode::NOT_FOUND); // let request = srv.get().uri(srv.url("/test/unknown/")).finish().unwrap(); // let response = srv.execute(request.send()).unwrap(); // assert_eq!(response.status(), StatusCode::NOT_FOUND); // } // #[test] // fn integration_percent_encoded() { // let mut srv = test::TestServer::with_factory(|| { // App::new().handler( // "test", // StaticFiles::new(".").unwrap().index_file("Cargo.toml"), // ) // }); // let request = srv // .get() // .uri(srv.url("/test/%43argo.toml")) // .finish() // .unwrap(); // let response = srv.execute(request.send()).unwrap(); // assert_eq!(response.status(), StatusCode::OK); // } struct T(&'static str, u64, Vec); #[test] fn test_parse() { let tests = vec![ T("", 0, vec![]), T("", 1000, vec![]), T("foo", 0, vec![]), T("bytes=", 0, vec![]), T("bytes=7", 10, vec![]), T("bytes= 7 ", 10, vec![]), T("bytes=1-", 0, vec![]), T("bytes=5-4", 10, vec![]), T("bytes=0-2,5-4", 10, vec![]), T("bytes=2-5,4-3", 10, vec![]), T("bytes=--5,4--3", 10, vec![]), T("bytes=A-", 10, vec![]), T("bytes=A- ", 10, vec![]), T("bytes=A-Z", 10, vec![]), T("bytes= -Z", 10, vec![]), T("bytes=5-Z", 10, vec![]), T("bytes=Ran-dom, garbage", 10, vec![]), T("bytes=0x01-0x02", 10, vec![]), T("bytes= ", 10, vec![]), T("bytes= , , , ", 10, vec![]), T( "bytes=0-9", 10, vec![HttpRange { start: 0, length: 10, }], ), T( "bytes=0-", 10, vec![HttpRange { start: 0, length: 10, }], ), T( "bytes=5-", 10, vec![HttpRange { start: 5, length: 5, }], ), T( "bytes=0-20", 10, vec![HttpRange { start: 0, length: 10, }], ), T( "bytes=15-,0-5", 10, vec![HttpRange { start: 0, length: 6, }], ), T( "bytes=1-2,5-", 10, vec![ HttpRange { start: 1, length: 2, }, HttpRange { start: 5, length: 5, }, ], ), T( "bytes=-2 , 7-", 11, vec![ HttpRange { start: 9, length: 2, }, HttpRange { start: 7, length: 4, }, ], ), T( "bytes=0-0 ,2-2, 7-", 11, vec![ HttpRange { start: 0, length: 1, }, HttpRange { start: 2, length: 1, }, HttpRange { start: 7, length: 4, }, ], ), T( "bytes=-5", 10, vec![HttpRange { start: 5, length: 5, }], ), T( "bytes=-15", 10, vec![HttpRange { start: 0, length: 10, }], ), T( "bytes=0-499", 10000, vec![HttpRange { start: 0, length: 500, }], ), T( "bytes=500-999", 10000, vec![HttpRange { start: 500, length: 500, }], ), T( "bytes=-500", 10000, vec![HttpRange { start: 9500, length: 500, }], ), T( "bytes=9500-", 10000, vec![HttpRange { start: 9500, length: 500, }], ), T( "bytes=0-0,-1", 10000, vec![ HttpRange { start: 0, length: 1, }, HttpRange { start: 9999, length: 1, }, ], ), T( "bytes=500-600,601-999", 10000, vec![ HttpRange { start: 500, length: 101, }, HttpRange { start: 601, length: 399, }, ], ), T( "bytes=500-700,601-999", 10000, vec![ HttpRange { start: 500, length: 201, }, HttpRange { start: 601, length: 399, }, ], ), // Match Apache laxity: T( "bytes= 1 -2 , 4- 5, 7 - 8 , ,,", 11, vec![ HttpRange { start: 1, length: 2, }, HttpRange { start: 4, length: 2, }, HttpRange { start: 7, length: 2, }, ], ), ]; for t in tests { let header = t.0; let size = t.1; let expected = t.2; let res = HttpRange::parse(header, size); if res.is_err() { if expected.is_empty() { continue; } else { assert!( false, "parse({}, {}) returned error {:?}", header, size, res.unwrap_err() ); } } let got = res.unwrap(); if got.len() != expected.len() { assert!( false, "len(parseRange({}, {})) = {}, want {}", header, size, got.len(), expected.len() ); continue; } for i in 0..expected.len() { if got[i].start != expected[i].start { assert!( false, "parseRange({}, {})[{}].start = {}, want {}", header, size, i, got[i].start, expected[i].start ) } if got[i].length != expected[i].length { assert!( false, "parseRange({}, {})[{}].length = {}, want {}", header, size, i, got[i].length, expected[i].length ) } } } } }