From d43b3ac0389632bb2813dc149679f2c92b2e2099 Mon Sep 17 00:00:00 2001 From: Tpt Date: Fri, 9 Apr 2021 13:23:25 +0200 Subject: [PATCH] Fixes HTTP content negotiation The implementation in http_types is buggy Bug: #91 --- server/src/main.rs | 98 ++++++++++++++++++++++++++++++++++++-------- wikibase/src/main.rs | 65 +++++++++++++++++++++-------- 2 files changed, 131 insertions(+), 32 deletions(-) diff --git a/server/src/main.rs b/server/src/main.rs index 5c6aeef6..4892c039 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -15,9 +15,10 @@ use async_std::io::Read; use async_std::net::{TcpListener, TcpStream}; use async_std::prelude::*; use async_std::task::{block_on, spawn}; -use http_types::content::{Accept, ContentType}; +use http_types::content::ContentType; use http_types::{ - bail_status, headers, Error, Method, Mime, Request, Response, Result, StatusCode, + bail_status, format_err_status, headers, Error, Method, Mime, Request, Response, Result, + StatusCode, }; use oxigraph::io::{DatasetFormat, GraphFormat}; use oxigraph::model::{GraphName, GraphNameRef, NamedNode, NamedOrBlankNode}; @@ -618,21 +619,53 @@ fn content_negotiation( supported: &[&str], parse: impl Fn(&str) -> Option, ) -> Result { - if let Some(mut accept) = Accept::from_headers(request)? { - let supported: Vec = supported - .iter() - .map(|h| Mime::from_str(h).unwrap()) - .collect(); - parse(accept.negotiate(&supported)?.value().as_str()) - } else { - parse(supported.first().ok_or_else(|| { - Error::from_str( - StatusCode::InternalServerError, - "No default MIME type provided", - ) - })?) + let header = request + .header(headers::ACCEPT) + .map(|h| h.last().as_str().trim()) + .unwrap_or(""); + let supported_mime: Vec = supported + .iter() + .map(|h| Mime::from_str(h).unwrap()) + .collect(); + + if header.is_empty() { + return parse(supported.first().unwrap()) + .ok_or_else(|| Error::from_str(StatusCode::InternalServerError, "Unknown mime type")); + } + let mut result = None; + let mut result_score = 0f32; + + for possible in header.split(',') { + let possible = Mime::from_str(possible.trim())?; + let score = if let Some(q) = possible.param("q") { + f32::from_str(&q.to_string())? + } else { + 1. + }; + if score <= result_score { + continue; + } + for candidate in &supported_mime { + if (possible.basetype() == candidate.basetype() || possible.basetype() == "*") + && (possible.subtype() == candidate.subtype() || possible.subtype() == "*") + { + result = Some(candidate); + result_score = score; + break; + } + } } - .ok_or_else(|| Error::from_str(StatusCode::InternalServerError, "Unknown mime type")) + + let result = result.ok_or_else(|| { + format_err_status!( + 406, + "The available Content-Types are {}", + supported.join(", ") + ) + })?; + + parse(result.essence()) + .ok_or_else(|| Error::from_str(StatusCode::InternalServerError, "Unknown mime type")) } fn bad_request(e: impl Into) -> Error { @@ -711,6 +744,39 @@ mod tests { ); } + #[test] + fn get_query_accept_star() { + let mut request = Request::new( + Method::Get, + Url::parse("http://localhost/query?query=SELECT%20*%20WHERE%20{%20?s%20?p%20?o%20}") + .unwrap(), + ); + request.insert_header("Accept", "*/*"); + ServerTest::new().test_status(request, StatusCode::Ok); + } + + #[test] + fn get_query_accept_good() { + let mut request = Request::new( + Method::Get, + Url::parse("http://localhost/query?query=SELECT%20*%20WHERE%20{%20?s%20?p%20?o%20}") + .unwrap(), + ); + request.insert_header("Accept", "application/sparql-results+json;charset=utf-8"); + ServerTest::new().test_status(request, StatusCode::Ok); + } + + #[test] + fn get_query_accept_bad() { + let mut request = Request::new( + Method::Get, + Url::parse("http://localhost/query?query=SELECT%20*%20WHERE%20{%20?s%20?p%20?o%20}") + .unwrap(), + ); + request.insert_header("Accept", "application/foo"); + ServerTest::new().test_status(request, StatusCode::NotAcceptable); + } + #[test] fn get_bad_query() { ServerTest::new().test_status( diff --git a/wikibase/src/main.rs b/wikibase/src/main.rs index 787bd9cf..021fd589 100644 --- a/wikibase/src/main.rs +++ b/wikibase/src/main.rs @@ -15,9 +15,10 @@ use async_std::future::Future; use async_std::net::{TcpListener, TcpStream}; use async_std::prelude::*; use async_std::task::spawn; -use http_types::content::{Accept, ContentType}; +use http_types::content::ContentType; use http_types::{ - bail_status, headers, Error, Method, Mime, Request, Response, Result, StatusCode, + bail_status, format_err_status, headers, Error, Method, Mime, Request, Response, Result, + StatusCode, }; use oxigraph::io::GraphFormat; use oxigraph::model::{GraphName, NamedNode, NamedOrBlankNode}; @@ -301,19 +302,51 @@ fn content_negotiation( supported: &[&str], parse: impl Fn(&str) -> Option, ) -> Result { - if let Some(mut accept) = Accept::from_headers(request)? { - let supported: Vec = supported - .iter() - .map(|h| Mime::from_str(h).unwrap()) - .collect(); - parse(accept.negotiate(&supported)?.value().as_str()) - } else { - parse(supported.first().ok_or_else(|| { - Error::from_str( - StatusCode::InternalServerError, - "No default MIME type provided", - ) - })?) + let header = request + .header(headers::ACCEPT) + .map(|h| h.last().as_str().trim()) + .unwrap_or(""); + let supported_mime: Vec = supported + .iter() + .map(|h| Mime::from_str(h).unwrap()) + .collect(); + + if header.is_empty() { + return parse(supported.first().unwrap()) + .ok_or_else(|| Error::from_str(StatusCode::InternalServerError, "Unknown mime type")); } - .ok_or_else(|| Error::from_str(StatusCode::InternalServerError, "Unknown mime type")) + let mut result = None; + let mut result_score = 0f32; + + for possible in header.split(',') { + let possible = Mime::from_str(possible.trim())?; + let score = if let Some(q) = possible.param("q") { + f32::from_str(&q.to_string())? + } else { + 1. + }; + if score <= result_score { + continue; + } + for candidate in &supported_mime { + if (possible.basetype() == candidate.basetype() || possible.basetype() == "*") + && (possible.subtype() == candidate.subtype() || possible.subtype() == "*") + { + result = Some(candidate); + result_score = score; + break; + } + } + } + + let result = result.ok_or_else(|| { + format_err_status!( + 406, + "The available Content-Types are {}", + supported.join(", ") + ) + })?; + + parse(result.essence()) + .ok_or_else(|| Error::from_str(StatusCode::InternalServerError, "Unknown mime type")) }