From ee2a7c50f88c8253176e96cfc7df19b7d8907b7a Mon Sep 17 00:00:00 2001 From: Alex Kreidler Date: Wed, 30 Sep 2020 16:23:28 -0400 Subject: [PATCH] got content negotiation working, wip --- Cargo.toml | 1 + actix-example-server/Cargo.toml | 14 ++++ actix-example-server/main.rs | 34 +++++++++ actix-files/src/service.rs | 125 +++++++++++++++++++++++++++++++- 4 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 actix-example-server/Cargo.toml create mode 100644 actix-example-server/main.rs diff --git a/Cargo.toml b/Cargo.toml index 56158389c..a6e5f8191 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ members = [ "actix-multipart", "actix-web-actors", "actix-web-codegen", + "actix-example-server", "test-server", ] diff --git a/actix-example-server/Cargo.toml b/actix-example-server/Cargo.toml new file mode 100644 index 000000000..17c65cd1a --- /dev/null +++ b/actix-example-server/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "actix-example-server" +version = "0.1.0" +edition = "2018" + +[[bin]] +name = "actix-example-server" +path = "main.rs" + +[dependencies] +actix-web = { version = "3.0.0" } +actix-files = { version = "0.3.0"} +env_logger = "0.7.1" +mime = "0.3.16" \ No newline at end of file diff --git a/actix-example-server/main.rs b/actix-example-server/main.rs new file mode 100644 index 000000000..8a4b58d79 --- /dev/null +++ b/actix-example-server/main.rs @@ -0,0 +1,34 @@ +use actix_files as fs; +use actix_web::{http::header::DispositionType, middleware, App, HttpServer}; + +use mime; + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + std::env::set_var( + "RUST_LOG", + "actix_files=debug,actix_server=info,actix_web=info", + ); + env_logger::init(); + + fn all_inline(_: &mime::Name<'_>) -> DispositionType { + DispositionType::Inline + } + + HttpServer::new(|| { + App::new() + .wrap(middleware::DefaultHeaders::new().header("X-Version", "0.2")) + .wrap(middleware::Compress::default()) + .wrap(middleware::Logger::default()) + .service( + fs::Files::new("/static", "/home/alex/c2/ontorender/work") + .show_files_listing() + .use_last_modified(true) + .mime_override(all_inline), + ) + }) + .bind("127.0.0.1:8080")? + .workers(1) + .run() + .await +} diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs index cbf4c2d3b..a9f96f26f 100644 --- a/actix-files/src/service.rs +++ b/actix-files/src/service.rs @@ -10,15 +10,34 @@ use actix_web::{ dev::{ServiceRequest, ServiceResponse}, error::Error, guard::Guard, + http::header::{Accept, Header}, http::{header, Method}, HttpResponse, }; use futures_util::future::{ok, Either, LocalBoxFuture, Ready}; +use log::debug; + use crate::{ named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, NamedFile, PathBufWrap, }; +use mime::Mime; + +use derive_more::{Display, Error}; + +#[derive(Debug, Display, Error)] +enum FileServiceError { + #[display(fmt = "mime parsing error")] + MimeParsingError, + #[display(fmt = "Provided Mime type too broad")] + MimeTooBroad, + #[display(fmt = "Path manipulation error. Failed to add extension to path")] + PathManipulationError, +} + +// Use default implementation for `error_response()` method +impl actix_web::error::ResponseError for FileServiceError {} /// Assembled file serving service. pub struct FilesService { @@ -39,7 +58,11 @@ type FilesServiceFuture = Either< >; impl FilesService { - fn handle_err(&mut self, e: io::Error, req: ServiceRequest) -> FilesServiceFuture { + fn handle_err>( + &mut self, + e: T, + req: ServiceRequest, + ) -> FilesServiceFuture { log::debug!("Failed to handle {}: {}", req.path(), e); if let Some(ref mut default) = self.default { @@ -89,7 +112,105 @@ impl Service for FilesService { }; // full file path - let path = match self.directory.join(&real_path).canonicalize() { + let path = self.directory.join(&real_path); + debug!("Passed path, non-canonical: {:?}", path); + + // Here we do content negotiation if possible, otherwise we skip + // We can't do Conneg if the file already has an extension + // TODO: the Conneg mechanism as of right now does loop over every possible extension for every provided mime type. + // We should put restrictions in place so this is not a DOS opportunity. + if path.extension().is_none() { + match Accept::parse(&req) { + Ok(ac) => { + log::info!("Starting Content Negotiation processing"); + // Here we clone and sort the vector of MIME types by the provided quality, highest first. + let mut mc: actix_web::http::header::Accept = ac.clone(); + mc.sort_by(|a, b| b.quality.cmp(&a.quality)); + + for item in mc.0 { + let mval = &item.item.to_string(); + if mval == "*/*" { + continue; + } + let eres = mime_guess::get_mime_extensions_str(mval) + .ok_or_else(|| FileServiceError::MimeParsingError); + + match eres { + Ok(exts) => { + // If more than 5 file extensions for a mime type, it's too broad to test all extensions + if exts.len() > 5 { + debug!( + "Warning: more than 5 file exts for mime type" + ) + } + debug!("{:#?}", exts); + for extension in exts { + let mut pb = path.clone(); + + // The following entire section is simply to append the proper extension to the filename. A better way? + let res = pb.components().last().ok_or_else(|| { + FileServiceError::PathManipulationError + }); + match res { + Ok(lc) => { + let mut ls = lc.as_os_str().to_owned(); + ls.push("."); + ls.push(extension); + pb.pop(); + pb.push(ls); + } + Err(e) => { + continue; + // return self.handle_err(e, req); + } + } + + match NamedFile::open(pb) { + Ok(mut named_file) => { + if let Some(ref mime_override) = + self.mime_override + { + let new_disposition = mime_override( + &named_file.content_type.type_(), + ); + named_file + .content_disposition + .disposition = new_disposition; + } + named_file.flags = self.file_flags; + + let (req, _) = req.into_parts(); + return match named_file.into_response(&req) { + Ok(item) => Either::Left(ok( + ServiceResponse::new( + req.clone(), + item, + ), + )), + Err(e) => Either::Left(ok( + ServiceResponse::from_err(e, req), + )), + }; + } + Err(e) => continue, + } + } + // At this point we've tried all the extensions for a given type + // We will move on to other serializations/types + } + Err(_err) => { + debug!("Couldn't retrieve extensions based on the {:?} MIME type, so we skipped it", mval); + continue; + } + } + } + // TODO: return an error if we've tried all types with no result + } + Err(_) => {} + } + } + + let path = match path.canonicalize() { Ok(path) => path, Err(e) => return self.handle_err(e, req), };