aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--components/script/dom/bindings/str.rs2
-rw-r--r--components/script/dom/headers.rs248
-rw-r--r--components/script/dom/mod.rs1
-rw-r--r--components/script/dom/webidls/Headers.webidl22
-rw-r--r--python/tidy/servo_tidy/tidy.py1
-rw-r--r--tests/unit/script/headers.rs74
-rw-r--r--tests/unit/script/lib.rs1
7 files changed, 348 insertions, 1 deletions
diff --git a/components/script/dom/bindings/str.rs b/components/script/dom/bindings/str.rs
index 310285a8bcd..4867c2449db 100644
--- a/components/script/dom/bindings/str.rs
+++ b/components/script/dom/bindings/str.rs
@@ -15,7 +15,7 @@ use std::str::{Bytes, FromStr};
use string_cache::Atom;
/// Encapsulates the IDL `ByteString` type.
-#[derive(JSTraceable, Clone, Eq, PartialEq, HeapSizeOf)]
+#[derive(JSTraceable, Clone, Eq, PartialEq, HeapSizeOf, Debug)]
pub struct ByteString(Vec<u8>);
impl ByteString {
diff --git a/components/script/dom/headers.rs b/components/script/dom/headers.rs
new file mode 100644
index 00000000000..0180acc5f1f
--- /dev/null
+++ b/components/script/dom/headers.rs
@@ -0,0 +1,248 @@
+/* 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;
+use dom::bindings::error::Error;
+use dom::bindings::global::GlobalRef;
+use dom::bindings::js::Root;
+use dom::bindings::reflector::{Reflector, reflect_dom_object};
+use dom::bindings::str::{ByteString, is_token};
+use hyper;
+use std::result::Result;
+
+#[dom_struct]
+pub struct Headers {
+ reflector_: Reflector,
+ guard: Guard,
+ #[ignore_heap_size_of = "Defined in hyper"]
+ header_list: DOMRefCell<hyper::header::Headers>
+}
+
+// https://fetch.spec.whatwg.org/#concept-headers-guard
+#[derive(JSTraceable, HeapSizeOf, PartialEq)]
+pub enum Guard {
+ Immutable,
+ Request,
+ RequestNoCors,
+ Response,
+ None,
+}
+
+impl Headers {
+ pub fn new_inherited() -> Headers {
+ Headers {
+ reflector_: Reflector::new(),
+ guard: Guard::None,
+ header_list: DOMRefCell::new(hyper::header::Headers::new()),
+ }
+ }
+
+ pub fn new(global: GlobalRef) -> Root<Headers> {
+ reflect_dom_object(box Headers::new_inherited(), global, HeadersBinding::Wrap)
+ }
+
+ // https://fetch.spec.whatwg.org/#concept-headers-append
+ pub fn Append(&self, name: ByteString, value: ByteString) -> Result<(), Error> {
+ // Step 1
+ let value = normalize_value(value);
+
+ // Step 2
+ let (valid_name, valid_value) = try!(validate_name_and_value(name, value));
+ // Step 3
+ if self.guard == Guard::Immutable {
+ return Err(Error::Type("Guard is immutable".to_string()));
+ }
+
+ // Step 4
+ if self.guard == Guard::Request && is_forbidden_header_name(&valid_name) {
+ return Ok(());
+ }
+
+ // Step 5
+ if self.guard == Guard::RequestNoCors && !is_cors_safelisted_request_header(&valid_name) {
+ return Ok(());
+ }
+
+ // Step 6
+ if self.guard == Guard::Response && is_forbidden_response_header(&valid_name) {
+ return Ok(());
+ }
+
+ // Step 7
+ self.header_list.borrow_mut().set_raw(valid_name, vec![valid_value]);
+ return Ok(());
+ }
+}
+
+// TODO
+// "Content-Type" once parsed, the value should be
+// `application/x-www-form-urlencoded`, `multipart/form-data`,
+// or `text/plain`.
+// "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) -> bool {
+ match name {
+ "accept" |
+ "accept-language" |
+ "content-language" => true,
+ _ => 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,
+ _ => false,
+ }
+}
+
+// https://fetch.spec.whatwg.org/#forbidden-header-name
+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_header_prefixes = ["sec-", "proxy-"];
+
+ disallowed_headers.iter().any(|header| *header == name) ||
+ 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 fied-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)
+ -> Result<(String, Vec<u8>), Error> {
+ if !is_field_name(&name) {
+ return Err(Error::Type("Name is not valid".to_string()));
+ }
+ if !is_field_content(&value) {
+ return Err(Error::Type("Value is not valid".to_string()));
+ }
+ match String::from_utf8(name.into()) {
+ Ok(ns) => Ok((ns, value.into())),
+ _ => Err(Error::Type("Non-UTF8 header name found".to_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)) {
+ (Some(begin), Some(end)) => ByteString::new(value[begin..end + 1].to_owned()),
+ _ => ByteString::new(vec![]),
+ }
+}
+
+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) {
+ return Some(index);
+ }
+ }
+ None
+}
+
+fn index_of_last_non_whitespace(value: &ByteString) -> Option<usize> {
+ for (index, &byte) in value.iter().enumerate().rev() {
+ if !is_HTTP_whitespace(byte) {
+ return Some(index);
+ }
+ }
+ None
+}
+
+// http://tools.ietf.org/html/rfc7230#section-3.2
+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 {
+ if value.len() == 0 {
+ return false;
+ }
+ if !is_field_vchar(value[0]) {
+ return false;
+ }
+
+ for &ch in &value[1..value.len() - 1] {
+ if !is_field_vchar(ch) || !is_space(ch) || !is_htab(ch) {
+ 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)
+}
+
+// https://tools.ietf.org/html/rfc5234#appendix-B.1
+fn is_vchar(x: u8) -> bool {
+ match x {
+ 0x21...0x7E => true,
+ _ => false,
+ }
+}
+
+// http://tools.ietf.org/html/rfc7230#section-3.2.6
+fn is_obs_text(x: u8) -> bool {
+ match x {
+ 0x80...0xFF => true,
+ _ => false,
+ }
+}
diff --git a/components/script/dom/mod.rs b/components/script/dom/mod.rs
index d76721ed040..172026c4d8e 100644
--- a/components/script/dom/mod.rs
+++ b/components/script/dom/mod.rs
@@ -269,6 +269,7 @@ pub mod focusevent;
pub mod forcetouchevent;
pub mod formdata;
pub mod hashchangeevent;
+pub mod headers;
pub mod htmlanchorelement;
pub mod htmlappletelement;
pub mod htmlareaelement;
diff --git a/components/script/dom/webidls/Headers.webidl b/components/script/dom/webidls/Headers.webidl
new file mode 100644
index 00000000000..0b9c0ce0156
--- /dev/null
+++ b/components/script/dom/webidls/Headers.webidl
@@ -0,0 +1,22 @@
+/* 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/. */
+
+// https://fetch.spec.whatwg.org/#headers-class
+
+/* typedef (Headers or sequence<sequence<ByteString>>) HeadersInit; */
+
+/* [Constructor(optional HeadersInit init),
+ * Exposed=(Window,Worker)] */
+
+interface Headers {
+ [Throws]
+ void append(ByteString name, ByteString value);
+};
+
+/* void delete(ByteString name);
+ * ByteString? get(ByteString name);
+ * boolean has(ByteString name);
+ * void set(ByteString name, ByteString value);
+ * iterable<ByteString, ByteString>;
+ * }; */
diff --git a/python/tidy/servo_tidy/tidy.py b/python/tidy/servo_tidy/tidy.py
index fc52fbedd82..02e3263d64d 100644
--- a/python/tidy/servo_tidy/tidy.py
+++ b/python/tidy/servo_tidy/tidy.py
@@ -88,6 +88,7 @@ WEBIDL_STANDARDS = [
"//drafts.csswg.org/cssom",
"//drafts.fxtf.org",
"//encoding.spec.whatwg.org",
+ "//fetch.spec.whatwg.org",
"//html.spec.whatwg.org",
"//url.spec.whatwg.org",
"//xhr.spec.whatwg.org",
diff --git a/tests/unit/script/headers.rs b/tests/unit/script/headers.rs
new file mode 100644
index 00000000000..5334056ec70
--- /dev/null
+++ b/tests/unit/script/headers.rs
@@ -0,0 +1,74 @@
+/* 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 script::dom::bindings::str::ByteString;
+use script::dom::headers;
+
+#[test]
+fn test_normalize_empty_bytestring() {
+ // empty ByteString test
+ let empty_bytestring = ByteString::new(vec![]);
+ let actual = headers::normalize_value(empty_bytestring);
+ let expected = ByteString::new(vec![]);
+ assert_eq!(actual, expected);
+}
+
+#[test]
+fn test_normalize_all_whitespace_bytestring() {
+ // All whitespace test. A horizontal tab, a line feed, a carriage return , and a space
+ let all_whitespace_bytestring = ByteString::new(vec![b'\t', b'\n', b'\r', b' ']);
+ let actual = headers::normalize_value(all_whitespace_bytestring);
+ let expected = ByteString::new(vec![]);
+ assert_eq!(actual, expected);
+}
+
+#[test]
+fn test_normalize_non_empty_no_whitespace_bytestring() {
+ // Non-empty, no whitespace ByteString test
+ let no_whitespace_bytestring = ByteString::new(vec![b'S', b'!']);
+ let actual = headers::normalize_value(no_whitespace_bytestring);
+ let expected = ByteString::new(vec![b'S', b'!']);
+ assert_eq!(actual, expected);
+}
+
+#[test]
+fn test_normalize_non_empty_leading_whitespace_bytestring() {
+ // Non-empty, leading whitespace, no trailing whitespace ByteString test
+ let leading_whitespace_bytestring = ByteString::new(vec![b'\t', b'\n', b' ', b'\r', b'S', b'!']);
+ let actual = headers::normalize_value(leading_whitespace_bytestring);
+ let expected = ByteString::new(vec![b'S', b'!']);
+ assert_eq!(actual, expected);
+}
+
+#[test]
+fn test_normalize_non_empty_no_leading_whitespace_trailing_whitespace_bytestring() {
+ // Non-empty, no leading whitespace, but with trailing whitespace ByteString test
+ let trailing_whitespace_bytestring = ByteString::new(vec![b'S', b'!', b'\t', b'\n', b' ', b'\r']);
+ let actual = headers::normalize_value(trailing_whitespace_bytestring);
+ let expected = ByteString::new(vec![b'S', b'!']);
+ assert_eq!(actual, expected);
+}
+
+#[test]
+fn test_normalize_non_empty_leading_and_trailing_whitespace_bytestring() {
+ // Non-empty, leading whitespace, and trailing whitespace ByteString test
+ let whitespace_sandwich_bytestring =
+ ByteString::new(vec![b'\t', b'\n', b' ', b'\r', b'S', b'!', b'\t', b'\n', b' ', b'\r']);
+ let actual = headers::normalize_value(whitespace_sandwich_bytestring);
+ let expected = ByteString::new(vec![b'S', b'!']);
+ assert_eq!(actual, expected);
+}
+
+#[test]
+fn test_normalize_non_empty_leading_trailing_and_internal_whitespace_bytestring() {
+ // Non-empty, leading whitespace, trailing whitespace,
+ // and internal whitespace ByteString test
+ let whitespace_bigmac_bytestring =
+ ByteString::new(vec![b'\t', b'\n', b' ', b'\r', b'S',
+ b'\t', b'\n', b' ', b'\r', b'!',
+ b'\t', b'\n', b' ', b'\r']);
+ let actual = headers::normalize_value(whitespace_bigmac_bytestring);
+ let expected = ByteString::new(vec![b'S', b'\t', b'\n', b' ', b'\r', b'!']);
+ assert_eq!(actual, expected);
+}
diff --git a/tests/unit/script/lib.rs b/tests/unit/script/lib.rs
index 9267038f9bd..e05f96f1f0f 100644
--- a/tests/unit/script/lib.rs
+++ b/tests/unit/script/lib.rs
@@ -12,3 +12,4 @@ extern crate url;
#[cfg(test)] mod origin;
#[cfg(all(test, target_pointer_width = "64"))] mod size_of;
#[cfg(test)] mod textinput;
+#[cfg(test)] mod headers;