diff --git a/db-tests/Cargo.lock b/db-tests/Cargo.lock index 17b09aec..1bb18192 100644 --- a/db-tests/Cargo.lock +++ b/db-tests/Cargo.lock @@ -650,7 +650,7 @@ dependencies = [ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syncstorage 0.1.0", + "syncstorage 0.1.2", ] [[package]] @@ -2405,7 +2405,7 @@ dependencies = [ [[package]] name = "syncstorage" -version = "0.1.0" +version = "0.1.2" dependencies = [ "actix-cors 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "actix-http 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/src/web/extractors.rs b/src/web/extractors.rs index 3436048e..f451506a 100644 --- a/src/web/extractors.rs +++ b/src/web/extractors.rs @@ -42,6 +42,9 @@ const BSO_MAX_TTL: u32 = 31_536_000; const BSO_MAX_SORTINDEX_VALUE: i32 = 999_999_999; const BSO_MIN_SORTINDEX_VALUE: i32 = -999_999_999; +const ACCEPTED_CONTENT_TYPES: [&str; 3] = + ["application/json", "text/plain", "application/newlines"]; + lazy_static! { static ref KNOWN_BAD_PAYLOAD_REGEX: Regex = Regex::new(r#"IV":\s*"AAAAAAAAAAAAAAAAAAAAAA=="#).unwrap(); @@ -88,11 +91,46 @@ impl BatchBsoBody { } } -fn get_trimmed_header(headers: &HeaderMap, key: HeaderName, default: &HeaderValue) -> String { - let ct_raw = std::str::from_utf8(headers.get(key).unwrap_or(&default).as_bytes()) +// This tries to do the right thing to get the Accepted header according to +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept, but some corners can absolutely be cut. +// This will pull the first accepted content type listed, or the highest rated non-accepted type. +fn get_weighted_header( + headers: &HeaderMap, + key: HeaderName, + accepted: &[&str], + default: &'static str, +) -> String { + let def_hv = HeaderValue::from_static(default); + let hv_raw = std::str::from_utf8(headers.get(key).unwrap_or(&def_hv).as_bytes()) .unwrap_or_else(|_| "invalid"); - let ct_parts: Vec<&str> = ct_raw.split(';').collect(); - ct_parts[0].trim_end().to_owned() + let hv_parts = hv_raw.split(','); + let mut choice_weight = 0.0; + let mut pick: String = default.to_owned(); + for choice in hv_parts { + let opt_weight: Vec<&str> = choice.split(';').collect(); + let mut opt = opt_weight[0].trim_end().to_lowercase(); + if opt == "*/*" { + opt = default.to_owned() + }; + let weight = if opt_weight.len() > 1 { + let weights: Vec<&str> = opt_weight[1].split('=').collect(); + // if the weight is malformed, ignore this choice. + if weights.len() < 2 { + continue; + } + f32::from_str(weights[1]).unwrap_or(0.0) + } else { + 1.0 + }; + if weight.abs().trunc() > 0.0 && accepted.contains(&&opt.as_ref()) { + return opt; + } + if weight > choice_weight { + pick = opt; + choice_weight = weight; + } + } + pick } #[derive(Default, Deserialize)] @@ -118,23 +156,24 @@ impl FromRequest for BsoBodies { fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { // Only try and parse the body if its a valid content-type let headers = req.headers(); - let default = HeaderValue::from_static(""); - let content_type = get_trimmed_header(headers, CONTENT_TYPE, &default); + let content_type = get_weighted_header( + headers, + CONTENT_TYPE, + &ACCEPTED_CONTENT_TYPES, + "application/json", + ); debug!("content_type: {:?}", &content_type); - match content_type.as_str() { - "application/json" | "text/plain" | "application/newlines" | "" => (), - _ => { - return Box::new(future::err( - ValidationErrorKind::FromDetails( - format!("Invalid Content-Type {:?}", content_type), - RequestErrorLocation::Header, - Some("Content-Type".to_owned()), - ) - .into(), - )); - } + if !ACCEPTED_CONTENT_TYPES.contains(&content_type.as_str()) { + return Box::new(future::err( + ValidationErrorKind::FromDetails( + format!("Invalid Content-Type {:?}", content_type), + RequestErrorLocation::Header, + Some("Content-Type".to_owned()), + ) + .into(), + )); } // Load the entire request into a String @@ -181,7 +220,7 @@ impl FromRequest for BsoBodies { let max_post_bytes = state.limits.max_post_bytes as usize; let fut = fut.and_then(move |body| { - // Get all the raw JSON values + // Get all the raw / values let bsos: Vec = if newlines { let mut bsos = Vec::new(); for item in body.lines() { @@ -298,20 +337,21 @@ impl FromRequest for BsoBody { // Only try and parse the body if its a valid content-type let headers = req.headers(); - let default = HeaderValue::from_static(""); - let content_type = get_trimmed_header(&headers, CONTENT_TYPE, &default); - match content_type.as_str() { - "application/json" | "text/plain" | "" => (), - _ => { - return Box::new(future::err( - ValidationErrorKind::FromDetails( - "Invalid Content-Type".to_owned(), - RequestErrorLocation::Header, - Some("Content-Type".to_owned()), - ) - .into(), - )); - } + let content_type = get_weighted_header( + &headers, + CONTENT_TYPE, + &ACCEPTED_CONTENT_TYPES, + "application/json", + ); + if !ACCEPTED_CONTENT_TYPES.contains(&content_type.as_str()) { + return Box::new(future::err( + ValidationErrorKind::FromDetails( + "Invalid Content-Type".to_owned(), + RequestErrorLocation::Header, + Some("Content-Type".to_owned()), + ) + .into(), + )); } let state = match req.app_data::() { Some(s) => s, @@ -561,8 +601,12 @@ impl FromRequest for CollectionRequest { let db = >::from_request(req, payload)?; let query = BsoQueryParams::from_request(req, payload)?; let collection = CollectionParam::from_request(req, payload)?.collection; - let content_type = - get_trimmed_header(&req.headers(), ACCEPT, &HeaderValue::from_static("")); + let content_type = get_weighted_header( + &req.headers(), + ACCEPT, + &ACCEPTED_CONTENT_TYPES, + "application/json", + ); let reply = match content_type.as_str() { "application/newlines" => ReplyFormat::Newlines, "application/json" | "" => ReplyFormat::Json, @@ -1532,6 +1576,62 @@ mod tests { */ } + #[test] + fn test_weighted_header() { + // test non-priority, full weight selection + let mut header_map = HeaderMap::new(); + header_map.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/json;q=0.9,text/plain"), + ); + let selected = get_weighted_header( + &header_map, + CONTENT_TYPE, + &ACCEPTED_CONTENT_TYPES, + "application/json", + ); + assert_eq!(selected, "text/plain".to_owned()); + + // test default for */* + let mut header_map = HeaderMap::new(); + header_map.insert(CONTENT_TYPE, HeaderValue::from_static("*/*,text/plain")); + let selected = get_weighted_header( + &header_map, + CONTENT_TYPE, + &ACCEPTED_CONTENT_TYPES, + "application/json", + ); + assert_eq!(selected, "application/json".to_owned()); + + // test default for selected weighted. + let mut header_map = HeaderMap::new(); + header_map.insert( + CONTENT_TYPE, + HeaderValue::from_static("foo/bar;q=0.1,application/json;q=0.2,text/plain;q=0.3"), + ); + let selected = get_weighted_header( + &header_map, + CONTENT_TYPE, + &ACCEPTED_CONTENT_TYPES, + "application/json", + ); + assert_eq!(selected, "text/plain".to_owned()); + + // test default for selected weighted. + let mut header_map = HeaderMap::new(); + header_map.insert( + CONTENT_TYPE, + HeaderValue::from_static("foo/bar;0.1,text/plain;q=0.1"), + ); + let selected = get_weighted_header( + &header_map, + CONTENT_TYPE, + &ACCEPTED_CONTENT_TYPES, + "text/plain", + ); + assert_eq!(selected, "text/plain".to_owned()); + } + #[test] fn test_valid_query_args() { let req = TestRequest::with_uri("/?ids=1,2&full=&sort=index&older=2.43") @@ -1675,7 +1775,7 @@ mod tests { let req = TestRequest::with_uri(&uri) .data(state) .header("authorization", header) - .header("accept", "application/json;a=0.9,/;q=0.2") + .header("accept", "application/json;a=0.9,*/*;q=0.2") .method(Method::GET) .param("uid", &USER_ID_STR) .param("collection", "tabs")