got content negotiation working, wip

This commit is contained in:
Alex Kreidler 2020-09-30 16:23:28 -04:00
parent aa11231ee5
commit ee2a7c50f8
4 changed files with 172 additions and 2 deletions

View File

@ -34,6 +34,7 @@ members = [
"actix-multipart", "actix-multipart",
"actix-web-actors", "actix-web-actors",
"actix-web-codegen", "actix-web-codegen",
"actix-example-server",
"test-server", "test-server",
] ]

View File

@ -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"

View File

@ -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
}

View File

@ -10,15 +10,34 @@ use actix_web::{
dev::{ServiceRequest, ServiceResponse}, dev::{ServiceRequest, ServiceResponse},
error::Error, error::Error,
guard::Guard, guard::Guard,
http::header::{Accept, Header},
http::{header, Method}, http::{header, Method},
HttpResponse, HttpResponse,
}; };
use futures_util::future::{ok, Either, LocalBoxFuture, Ready}; use futures_util::future::{ok, Either, LocalBoxFuture, Ready};
use log::debug;
use crate::{ use crate::{
named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride,
NamedFile, PathBufWrap, 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. /// Assembled file serving service.
pub struct FilesService { pub struct FilesService {
@ -39,7 +58,11 @@ type FilesServiceFuture = Either<
>; >;
impl FilesService { impl FilesService {
fn handle_err(&mut self, e: io::Error, req: ServiceRequest) -> FilesServiceFuture { fn handle_err<T: std::fmt::Display + std::convert::Into<actix_web::Error>>(
&mut self,
e: T,
req: ServiceRequest,
) -> FilesServiceFuture {
log::debug!("Failed to handle {}: {}", req.path(), e); log::debug!("Failed to handle {}: {}", req.path(), e);
if let Some(ref mut default) = self.default { if let Some(ref mut default) = self.default {
@ -89,7 +112,105 @@ impl Service for FilesService {
}; };
// full file path // 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, Ok(path) => path,
Err(e) => return self.handle_err(e, req), Err(e) => return self.handle_err(e, req),
}; };