diff options
Diffstat (limited to 'components/script/dom/headers.rs')
-rw-r--r-- | components/script/dom/headers.rs | 427 |
1 files changed, 246 insertions, 181 deletions
diff --git a/components/script/dom/headers.rs b/components/script/dom/headers.rs index ac5b332b51b..03fecd79c5a 100644 --- a/components/script/dom/headers.rs +++ b/components/script/dom/headers.rs @@ -1,32 +1,32 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use dom::bindings::cell::DOMRefCell; -use dom::bindings::codegen::Bindings::HeadersBinding::{HeadersInit, HeadersMethods, HeadersWrap}; -use dom::bindings::error::{Error, ErrorResult, Fallible}; -use dom::bindings::iterable::Iterable; -use dom::bindings::js::Root; -use dom::bindings::reflector::{Reflector, reflect_dom_object}; -use dom::bindings::str::{ByteString, is_token}; -use dom::globalscope::GlobalScope; + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::dom::bindings::cell::DomRefCell; +use crate::dom::bindings::codegen::Bindings::HeadersBinding::{HeadersInit, HeadersMethods}; +use crate::dom::bindings::error::{Error, ErrorResult, Fallible}; +use crate::dom::bindings::iterable::Iterable; +use crate::dom::bindings::reflector::{reflect_dom_object, Reflector}; +use crate::dom::bindings::root::DomRoot; +use crate::dom::bindings::str::{is_token, ByteString}; +use crate::dom::globalscope::GlobalScope; +use data_url::mime::Mime as DataUrlMime; use dom_struct::dom_struct; -use hyper::header::Headers as HyperHeaders; -use mime::{Mime, TopLevel, SubLevel}; +use http::header::{HeaderMap as HyperHeaders, HeaderName, HeaderValue}; +use net_traits::request::is_cors_safelisted_request_header; use std::cell::Cell; -use std::result::Result; -use std::str; +use std::str::{self, FromStr}; #[dom_struct] pub struct Headers { reflector_: Reflector, guard: Cell<Guard>, - #[ignore_heap_size_of = "Defined in hyper"] - header_list: DOMRefCell<HyperHeaders> + #[ignore_malloc_size_of = "Defined in hyper"] + header_list: DomRefCell<HyperHeaders>, } // https://fetch.spec.whatwg.org/#concept-headers-guard -#[derive(Copy, Clone, JSTraceable, HeapSizeOf, PartialEq)] +#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)] pub enum Guard { Immutable, Request, @@ -40,19 +40,22 @@ impl Headers { Headers { reflector_: Reflector::new(), guard: Cell::new(Guard::None), - header_list: DOMRefCell::new(HyperHeaders::new()), + header_list: DomRefCell::new(HyperHeaders::new()), } } - pub fn new(global: &GlobalScope) -> Root<Headers> { - reflect_dom_object(box Headers::new_inherited(), global, HeadersWrap) + pub fn new(global: &GlobalScope) -> DomRoot<Headers> { + reflect_dom_object(Box::new(Headers::new_inherited()), global) } // https://fetch.spec.whatwg.org/#dom-headers - pub fn Constructor(global: &GlobalScope, init: Option<HeadersInit>) - -> Fallible<Root<Headers>> { + #[allow(non_snake_case)] + pub fn Constructor( + global: &GlobalScope, + init: Option<HeadersInit>, + ) -> Fallible<DomRoot<Headers>> { let dom_headers_new = Headers::new(global); - try!(dom_headers_new.fill(init)); + dom_headers_new.fill(init)?; Ok(dom_headers_new) } } @@ -63,7 +66,7 @@ impl HeadersMethods for Headers { // Step 1 let value = normalize_value(value); // Step 2 - let (mut valid_name, valid_value) = try!(validate_name_and_value(name, value)); + let (mut valid_name, valid_value) = validate_name_and_value(name, value)?; valid_name = valid_name.to_lowercase(); // Step 3 if self.guard.get() == Guard::Immutable { @@ -74,7 +77,9 @@ impl HeadersMethods for Headers { return Ok(()); } // Step 5 - if self.guard.get() == Guard::RequestNoCors && !is_cors_safelisted_request_header(&valid_name, &valid_value) { + if self.guard.get() == Guard::RequestNoCors && + !is_cors_safelisted_request_header(&valid_name, &valid_value) + { return Ok(()); } // Step 6 @@ -82,20 +87,40 @@ impl HeadersMethods for Headers { return Ok(()); } // Step 7 + // FIXME: this is NOT what WHATWG says to do when appending + // another copy of an existing header. HyperHeaders + // might not expose the information we need to do it right. let mut combined_value: Vec<u8> = vec![]; - if let Some(v) = self.header_list.borrow().get_raw(&valid_name) { - combined_value = v[0].clone(); - combined_value.push(b','); + if let Some(v) = self + .header_list + .borrow() + .get(HeaderName::from_str(&valid_name).unwrap()) + { + combined_value = v.as_bytes().to_vec(); + combined_value.extend(b", "); } combined_value.extend(valid_value.iter().cloned()); - self.header_list.borrow_mut().set_raw(valid_name, vec![combined_value]); + match HeaderValue::from_bytes(&combined_value) { + Ok(value) => { + self.header_list + .borrow_mut() + .insert(HeaderName::from_str(&valid_name).unwrap(), value); + }, + Err(_) => { + // can't add the header, but we don't need to panic the browser over it + warn!( + "Servo thinks \"{:?}\" is a valid HTTP header value but HeaderValue doesn't.", + combined_value + ); + }, + }; Ok(()) } // https://fetch.spec.whatwg.org/#dom-headers-delete fn Delete(&self, name: ByteString) -> ErrorResult { // Step 1 - let valid_name = try!(validate_name(name)); + let valid_name = validate_name(name)?; // Step 2 if self.guard.get() == Guard::Immutable { return Err(Error::Type("Guard is immutable".to_string())); @@ -106,33 +131,36 @@ impl HeadersMethods for Headers { } // Step 4 if self.guard.get() == Guard::RequestNoCors && - !is_cors_safelisted_request_header(&valid_name, &b"invalid".to_vec()) { - return Ok(()); - } + !is_cors_safelisted_request_header(&valid_name, &b"invalid".to_vec()) + { + return Ok(()); + } // Step 5 if self.guard.get() == Guard::Response && is_forbidden_response_header(&valid_name) { return Ok(()); } // Step 6 - self.header_list.borrow_mut().remove_raw(&valid_name); + self.header_list.borrow_mut().remove(&valid_name); Ok(()) } // https://fetch.spec.whatwg.org/#dom-headers-get fn Get(&self, name: ByteString) -> Fallible<Option<ByteString>> { // Step 1 - let valid_name = &try!(validate_name(name)); - Ok(self.header_list.borrow().get_raw(&valid_name).map(|v| { - ByteString::new(v[0].clone()) - })) + let valid_name = validate_name(name)?; + Ok(self + .header_list + .borrow() + .get(HeaderName::from_str(&valid_name).unwrap()) + .map(|v| ByteString::new(v.as_bytes().to_vec()))) } // https://fetch.spec.whatwg.org/#dom-headers-has fn Has(&self, name: ByteString) -> Fallible<bool> { // Step 1 - let valid_name = try!(validate_name(name)); + let valid_name = validate_name(name)?; // Step 2 - Ok(self.header_list.borrow_mut().get_raw(&valid_name).is_some()) + Ok(self.header_list.borrow_mut().get(&valid_name).is_some()) } // https://fetch.spec.whatwg.org/#dom-headers-set @@ -140,7 +168,7 @@ impl HeadersMethods for Headers { // Step 1 let value = normalize_value(value); // Step 2 - let (mut valid_name, valid_value) = try!(validate_name_and_value(name, value)); + let (mut valid_name, valid_value) = validate_name_and_value(name, value)?; valid_name = valid_name.to_lowercase(); // Step 3 if self.guard.get() == Guard::Immutable { @@ -151,7 +179,9 @@ impl HeadersMethods for Headers { return Ok(()); } // Step 5 - if self.guard.get() == Guard::RequestNoCors && !is_cors_safelisted_request_header(&valid_name, &valid_value) { + if self.guard.get() == Guard::RequestNoCors && + !is_cors_safelisted_request_header(&valid_name, &valid_value) + { return Ok(()); } // Step 6 @@ -160,32 +190,34 @@ impl HeadersMethods for Headers { } // Step 7 // https://fetch.spec.whatwg.org/#concept-header-list-set - self.header_list.borrow_mut().set_raw(valid_name, vec![valid_value]); + self.header_list.borrow_mut().insert( + HeaderName::from_str(&valid_name).unwrap(), + HeaderValue::from_bytes(&valid_value).unwrap(), + ); Ok(()) } } impl Headers { + pub fn copy_from_headers(&self, headers: DomRoot<Headers>) -> ErrorResult { + for (name, value) in headers.header_list.borrow().iter() { + self.Append( + ByteString::new(Vec::from(name.as_str())), + ByteString::new(Vec::from(value.as_bytes())), + )?; + } + Ok(()) + } + // https://fetch.spec.whatwg.org/#concept-headers-fill pub fn fill(&self, filler: Option<HeadersInit>) -> ErrorResult { match filler { - // Step 1 - Some(HeadersInit::Headers(h)) => { - for header in h.header_list.borrow().iter() { - try!(self.Append( - ByteString::new(Vec::from(header.name())), - ByteString::new(Vec::from(header.value_string().into_bytes())) - )); - } - Ok(()) - }, - // Step 2 Some(HeadersInit::ByteStringSequenceSequence(v)) => { for mut seq in v { if seq.len() == 2 { let val = seq.pop().unwrap(); let name = seq.pop().unwrap(); - try!(self.Append(name, val)); + self.Append(name, val)?; } else { return Err(Error::Type( format!("Each header object must be a sequence of length 2 - found one with length {}", @@ -194,11 +226,9 @@ impl Headers { } Ok(()) }, - Some(HeadersInit::ByteStringMozMap(m)) => { + Some(HeadersInit::ByteStringByteStringRecord(m)) => { for (key, value) in m.iter() { - let key_vec = key.as_ref().to_string().into(); - let headers_key = ByteString::new(key_vec); - try!(self.Append(headers_key, value.clone())); + self.Append(key.clone(), value.clone())?; } Ok(()) }, @@ -206,13 +236,13 @@ impl Headers { } } - pub fn for_request(global: &GlobalScope) -> Root<Headers> { + pub fn for_request(global: &GlobalScope) -> DomRoot<Headers> { let headers_for_request = Headers::new(global); headers_for_request.guard.set(Guard::Request); headers_for_request } - pub fn for_response(global: &GlobalScope) -> Root<Headers> { + pub fn for_response(global: &GlobalScope) -> DomRoot<Headers> { let headers_for_response = Headers::new(global); headers_for_response.guard.set(Guard::Response); headers_for_response @@ -234,18 +264,22 @@ impl Headers { *self.header_list.borrow_mut() = hyper_headers; } + pub fn get_headers_list(&self) -> HyperHeaders { + self.header_list.borrow_mut().clone() + } + // https://fetch.spec.whatwg.org/#concept-header-extract-mime-type pub fn extract_mime_type(&self) -> Vec<u8> { - self.header_list.borrow().get_raw("content-type").map_or(vec![], |v| v[0].clone()) + extract_mime_type(&*self.header_list.borrow()).unwrap_or(vec![]) } - pub fn sort_header_list(&self) -> Vec<(String, String)> { + pub fn sort_header_list(&self) -> Vec<(String, Vec<u8>)> { let borrowed_header_list = self.header_list.borrow(); let headers_iter = borrowed_header_list.iter(); let mut header_vec = vec![]; - for header in headers_iter { - let name = header.name().to_string(); - let value = header.value_string(); + for (name, value) in headers_iter { + let name = name.as_str().to_owned(); + let value = value.as_bytes().to_vec(); let name_value = (name, value); header_vec.push(name_value); } @@ -265,7 +299,7 @@ impl Iterable for Headers { fn get_value_at_index(&self, n: u32) -> ByteString { let sorted_header_vec = self.sort_header_list(); let value = sorted_header_vec[n as usize].1.clone(); - ByteString::new(value.into_bytes().to_vec()) + ByteString::new(value) } fn get_key_at_index(&self, n: u32) -> ByteString { @@ -275,94 +309,60 @@ impl Iterable for Headers { } } -fn is_cors_safelisted_request_content_type(value: &[u8]) -> bool { - let value_string = if let Ok(s) = str::from_utf8(value) { - s - } else { - return false; - }; - let value_mime_result: Result<Mime, _> = value_string.parse(); - match value_mime_result { - Err(_) => false, - Ok(value_mime) => { - match value_mime { - Mime(TopLevel::Application, SubLevel::WwwFormUrlEncoded, _) | - Mime(TopLevel::Multipart, SubLevel::FormData, _) | - Mime(TopLevel::Text, SubLevel::Plain, _) => true, - _ => false, - } - } - } -} - -// TODO: "DPR", "Downlink", "Save-Data", "Viewport-Width", "Width": -// ... once parsed, the value should not be failure. -// https://fetch.spec.whatwg.org/#cors-safelisted-request-header -fn is_cors_safelisted_request_header(name: &str, value: &[u8]) -> bool { - match name { - "accept" | - "accept-language" | - "content-language" => true, - "content-type" => is_cors_safelisted_request_content_type(value), - _ => false, - } -} - // https://fetch.spec.whatwg.org/#forbidden-response-header-name fn is_forbidden_response_header(name: &str) -> bool { match name { - "set-cookie" | - "set-cookie2" => true, + "set-cookie" | "set-cookie2" => true, _ => false, } } // https://fetch.spec.whatwg.org/#forbidden-header-name pub fn is_forbidden_header_name(name: &str) -> bool { - let disallowed_headers = - ["accept-charset", "accept-encoding", - "access-control-request-headers", - "access-control-request-method", - "connection", "content-length", - "cookie", "cookie2", "date", "dnt", - "expect", "host", "keep-alive", "origin", - "referer", "te", "trailer", "transfer-encoding", - "upgrade", "via"]; + let disallowed_headers = [ + "accept-charset", + "accept-encoding", + "access-control-request-headers", + "access-control-request-method", + "connection", + "content-length", + "cookie", + "cookie2", + "date", + "dnt", + "expect", + "host", + "keep-alive", + "origin", + "referer", + "te", + "trailer", + "transfer-encoding", + "upgrade", + "via", + ]; let disallowed_header_prefixes = ["sec-", "proxy-"]; disallowed_headers.iter().any(|header| *header == name) || - disallowed_header_prefixes.iter().any(|prefix| name.starts_with(prefix)) + disallowed_header_prefixes + .iter() + .any(|prefix| name.starts_with(prefix)) } // There is some unresolved confusion over the definition of a name and a value. -// The fetch spec [1] defines a name as "a case-insensitive byte -// sequence that matches the field-name token production. The token -// productions are viewable in [2]." A field-name is defined as a -// token, which is defined in [3]. -// ISSUE 1: -// It defines a value as "a byte sequence that matches the field-content token production." -// To note, there is a difference between field-content and -// field-value (which is made up of field-content and obs-fold). The -// current definition does not allow for obs-fold (which are white -// space and newlines) in values. So perhaps a value should be defined -// as "a byte sequence that matches the field-value token production." -// However, this would then allow values made up entirely of white space and newlines. -// RELATED ISSUE 2: -// According to a previously filed Errata ID: 4189 in [4], "the -// specified field-value rule does not allow single field-vchar -// surrounded by whitespace anywhere". They provided a fix for the -// field-content production, but ISSUE 1 has still not been resolved. -// The production definitions likely need to be re-written. -// [1] https://fetch.spec.whatwg.org/#concept-header-value -// [2] https://tools.ietf.org/html/rfc7230#section-3.2 -// [3] https://tools.ietf.org/html/rfc7230#section-3.2.6 -// [4] https://www.rfc-editor.org/errata_search.php?rfc=7230 -fn validate_name_and_value(name: ByteString, value: ByteString) - -> Fallible<(String, Vec<u8>)> { - let valid_name = try!(validate_name(name)); - if !is_field_content(&value) { - return Err(Error::Type("Value is not valid".to_string())); +// +// As of December 2019, WHATWG has no formal grammar production for value; +// https://fetch.spec.whatg.org/#concept-header-value just says not to have +// newlines, nulls, or leading/trailing whitespace. It even allows +// octets that aren't a valid UTF-8 encoding, and WPT tests reflect this. +// The HeaderValue class does not fully reflect this, so headers +// containing bytes with values 1..31 or 127 can't be created, failing +// WPT tests but probably not affecting anything important on the real Internet. +fn validate_name_and_value(name: ByteString, value: ByteString) -> Fallible<(String, Vec<u8>)> { + let valid_name = validate_name(name)?; + if !is_legal_header_value(&value) { + return Err(Error::Type("Header value is not valid".to_string())); } Ok((valid_name, value.into())) } @@ -380,19 +380,22 @@ fn validate_name(name: ByteString) -> Fallible<String> { // Removes trailing and leading HTTP whitespace bytes. // https://fetch.spec.whatwg.org/#concept-header-value-normalize pub fn normalize_value(value: ByteString) -> ByteString { - match (index_of_first_non_whitespace(&value), index_of_last_non_whitespace(&value)) { + match ( + index_of_first_non_whitespace(&value), + index_of_last_non_whitespace(&value), + ) { (Some(begin), Some(end)) => ByteString::new(value[begin..end + 1].to_owned()), _ => ByteString::new(vec![]), } } -fn is_HTTP_whitespace(byte: u8) -> bool { +fn is_http_whitespace(byte: u8) -> bool { byte == b'\t' || byte == b'\n' || byte == b'\r' || byte == b' ' } fn index_of_first_non_whitespace(value: &ByteString) -> Option<usize> { for (index, &byte) in value.iter().enumerate() { - if !is_HTTP_whitespace(byte) { + if !is_http_whitespace(byte) { return Some(index); } } @@ -401,7 +404,7 @@ fn index_of_first_non_whitespace(value: &ByteString) -> Option<usize> { fn index_of_last_non_whitespace(value: &ByteString) -> Option<usize> { for (index, &byte) in value.iter().enumerate().rev() { - if !is_HTTP_whitespace(byte) { + if !is_http_whitespace(byte) { return Some(index); } } @@ -413,53 +416,46 @@ fn is_field_name(name: &ByteString) -> bool { is_token(&*name) } -// https://tools.ietf.org/html/rfc7230#section-3.2 -// http://www.rfc-editor.org/errata_search.php?rfc=7230 -// Errata ID: 4189 -// field-content = field-vchar [ 1*( SP / HTAB / field-vchar ) -// field-vchar ] -fn is_field_content(value: &ByteString) -> bool { +// https://fetch.spec.whatg.org/#concept-header-value +fn is_legal_header_value(value: &ByteString) -> bool { let value_len = value.len(); - if value_len == 0 { - return false; - } - if !is_field_vchar(value[0]) { - return false; + return true; } - - if value_len > 2 { - for &ch in &value[1..value_len - 1] { - if !is_field_vchar(ch) && !is_space(ch) && !is_htab(ch) { - return false; - } + match value[0] { + b' ' | b'\t' => return false, + _ => {}, + }; + match value[value_len - 1] { + b' ' | b'\t' => return false, + _ => {}, + }; + for &ch in &value[..] { + match ch { + b'\0' | b'\n' | b'\r' => return false, + _ => {}, } } - - if !is_field_vchar(value[value_len - 1]) { - return false; - } - - return true; -} - -fn is_space(x: u8) -> bool { - x == b' ' -} - -fn is_htab(x: u8) -> bool { - x == b'\t' -} - -// https://tools.ietf.org/html/rfc7230#section-3.2 -fn is_field_vchar(x: u8) -> bool { - is_vchar(x) || is_obs_text(x) + true + // If accepting non-UTF8 header values causes breakage, + // removing the above "true" and uncommenting the below code + // would ameliorate it while still accepting most reasonable headers: + //match str::from_utf8(value) { + // Ok(_) => true, + // Err(_) => { + // warn!( + // "Rejecting spec-legal but non-UTF8 header value: {:?}", + // value + // ); + // false + // }, + // } } // https://tools.ietf.org/html/rfc5234#appendix-B.1 pub fn is_vchar(x: u8) -> bool { match x { - 0x21...0x7E => true, + 0x21..=0x7E => true, _ => false, } } @@ -467,7 +463,76 @@ pub fn is_vchar(x: u8) -> bool { // http://tools.ietf.org/html/rfc7230#section-3.2.6 pub fn is_obs_text(x: u8) -> bool { match x { - 0x80...0xFF => true, + 0x80..=0xFF => true, _ => false, } } + +// https://fetch.spec.whatwg.org/#concept-header-extract-mime-type +// This function uses data_url::Mime to parse the MIME Type because +// mime::Mime does not provide a parser following the Fetch spec +// see https://github.com/hyperium/mime/issues/106 +pub fn extract_mime_type(headers: &HyperHeaders) -> Option<Vec<u8>> { + let mut charset: Option<String> = None; + let mut essence: String = "".to_string(); + let mut mime_type: Option<DataUrlMime> = None; + + // Step 4 + let headers_values = headers.get_all(http::header::CONTENT_TYPE).iter(); + + // Step 5 + if headers_values.size_hint() == (0, Some(0)) { + return None; + } + + // Step 6 + for header_value in headers_values { + // Step 6.1 + match DataUrlMime::from_str(header_value.to_str().unwrap_or("")) { + // Step 6.2 + Err(_) => continue, + Ok(temp_mime) => { + let temp_essence = format!("{}/{}", temp_mime.type_, temp_mime.subtype); + + // Step 6.2 + if temp_essence == "*/*" { + continue; + } + + let temp_charset = &temp_mime.get_parameter("charset"); + + // Step 6.3 + mime_type = Some(DataUrlMime { + type_: temp_mime.type_.to_string(), + subtype: temp_mime.subtype.to_string(), + parameters: temp_mime.parameters.clone(), + }); + + // Step 6.4 + if temp_essence != essence { + charset = temp_charset.map(|c| c.to_string()); + essence = temp_essence.to_owned(); + } else { + // Step 6.5 + if temp_charset.is_none() && charset.is_some() { + let DataUrlMime { + type_: t, + subtype: st, + parameters: p, + } = mime_type.unwrap(); + let mut params = p; + params.push(("charset".to_string(), charset.clone().unwrap())); + mime_type = Some(DataUrlMime { + type_: t.to_string(), + subtype: st.to_string(), + parameters: params, + }) + } + } + }, + } + } + + // Step 7, 8 + return mime_type.map(|m| format!("{}", m).into_bytes()); +} |