|
| 1 | + |
| 2 | + |
| 3 | + |
| 4 | + |
| 5 | +use dom::bindings::cell::DOMRefCell; |
| 6 | +use dom::bindings::codegen::Bindings::HeadersBinding; |
| 7 | +use dom::bindings::codegen::Bindings::HeadersBinding::HeadersMethods; |
| 8 | +use dom::bindings::error::Error; |
| 9 | +use dom::bindings::global::GlobalRef; |
| 10 | +use dom::bindings::js::Root; |
| 11 | +use dom::bindings::reflector::{Reflector, reflect_dom_object}; |
| 12 | +use dom::bindings::str::{ByteString, is_token}; |
| 13 | +use hyper; |
| 14 | +use std::result::Result; |
| 15 | + |
| 16 | +#[dom_struct] |
| 17 | +pub struct Headers { |
| 18 | + reflector_: Reflector, |
| 19 | + guard: Guard, |
| 20 | + #[ignore_heap_size_of = "Defined in hyper"] |
| 21 | + header_list: DOMRefCell<hyper::header::Headers> |
| 22 | +} |
| 23 | + |
| 24 | + |
| 25 | +#[derive(JSTraceable, HeapSizeOf, PartialEq)] |
| 26 | +pub enum Guard { |
| 27 | + Immutable, |
| 28 | + Request, |
| 29 | + RequestNoCors, |
| 30 | + Response, |
| 31 | + None, |
| 32 | +} |
| 33 | + |
| 34 | +impl Headers { |
| 35 | + pub fn new_inherited() -> Headers { |
| 36 | + Headers { |
| 37 | + reflector_: Reflector::new(), |
| 38 | + guard: Guard::None, |
| 39 | + header_list: DOMRefCell::new(hyper::header::Headers::new()), |
| 40 | + } |
| 41 | + } |
| 42 | + |
| 43 | + pub fn new(global: GlobalRef) -> Root<Headers> { |
| 44 | + reflect_dom_object(box Headers::new_inherited(), global, HeadersBinding::Wrap) |
| 45 | + } |
| 46 | +} |
| 47 | + |
| 48 | +impl HeadersMethods for Headers { |
| 49 | + // https://fetch.spec.whatwg.org/#concept-headers-append |
| 50 | + fn Append(&self, name: ByteString, value: ByteString) -> Result<(), Error> { |
| 51 | + // Step 1 |
| 52 | + let value = normalize_value(value); |
| 53 | + |
| 54 | + // Step 2 |
| 55 | + let (valid_name, valid_value) = try!(validate_name_and_value(name, value)); |
| 56 | + // Step 3 |
| 57 | + if self.guard == Guard::Immutable { |
| 58 | + return Err(Error::Type("Guard is immutable".to_string())); |
| 59 | + } |
| 60 | + |
| 61 | + // Step 4 |
| 62 | + if self.guard == Guard::Request && is_forbidden_header_name(&valid_name) { |
| 63 | + return Ok(()); |
| 64 | + } |
| 65 | + |
| 66 | + // Step 5 |
| 67 | + if self.guard == Guard::RequestNoCors && !is_cors_safelisted_request_header(&valid_name) { |
| 68 | + return Ok(()); |
| 69 | + } |
| 70 | + |
| 71 | + // Step 6 |
| 72 | + if self.guard == Guard::Response && is_forbidden_response_header(&valid_name) { |
| 73 | + return Ok(()); |
| 74 | + } |
| 75 | + |
| 76 | + // Step 7 |
| 77 | + self.header_list.borrow_mut().set_raw(valid_name, vec![valid_value]); |
| 78 | + return Ok(()); |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +// TODO |
| 83 | +// "Content-Type" once parsed, the value should be |
| 84 | +// `application/x-www-form-urlencoded`, `multipart/form-data`, |
| 85 | +// or `text/plain`. |
| 86 | +// "DPR", "Downlink", "Save-Data", "Viewport-Width", "Width": |
| 87 | +// once parsed, the value should not be failure. |
| 88 | +// https://fetch.spec.whatwg.org/#cors-safelisted-request-header |
| 89 | +fn is_cors_safelisted_request_header(name: &str) -> bool { |
| 90 | + match name { |
| 91 | + "accept" | |
| 92 | + "accept-language" | |
| 93 | + "content-language" => true, |
| 94 | + _ => false, |
| 95 | + } |
| 96 | +} |
| 97 | + |
| 98 | +// https://fetch.spec.whatwg.org/#forbidden-response-header-name |
| 99 | +fn is_forbidden_response_header(name: &str) -> bool { |
| 100 | + match name { |
| 101 | + "set-cookie" | |
| 102 | + "set-cookie2" => true, |
| 103 | + _ => false, |
| 104 | + } |
| 105 | +} |
| 106 | + |
| 107 | +// https://fetch.spec.whatwg.org/#forbidden-header-name |
| 108 | +pub fn is_forbidden_header_name(name: &str) -> bool { |
| 109 | + let disallowed_headers = |
| 110 | + ["accept-charset", "accept-encoding", |
| 111 | + "access-control-request-headers", |
| 112 | + "access-control-request-method", |
| 113 | + "connection", "content-length", |
| 114 | + "cookie", "cookie2", "date", "dnt", |
| 115 | + "expect", "host", "keep-alive", "origin", |
| 116 | + "referer", "te", "trailer", "transfer-encoding", |
| 117 | + "upgrade", "via"]; |
| 118 | + |
| 119 | + let disallowed_header_prefixes = ["sec-", "proxy-"]; |
| 120 | + |
| 121 | + disallowed_headers.iter().any(|header| *header == name) || |
| 122 | + disallowed_header_prefixes.iter().any(|prefix| name.starts_with(prefix)) |
| 123 | +} |
| 124 | + |
| 125 | +// There is some unresolved confusion over the definition of a name and a value. |
| 126 | +// The fetch spec [1] defines a name as "a case-insensitive byte |
| 127 | +// sequence that matches the field-name token production. The token |
| 128 | +// productions are viewable in [2]." A field-name is defined as a |
| 129 | +// token, which is defined in [3]. |
| 130 | +// ISSUE 1: |
| 131 | +// It defines a value as "a byte sequence that matches the field-content token production." |
| 132 | +// To note, there is a difference between field-content and |
| 133 | +// field-value (which is made up of fied-content and obs-fold). The |
| 134 | +// current definition does not allow for obs-fold (which are white |
| 135 | +// space and newlines) in values. So perhaps a value should be defined |
| 136 | +// as "a byte sequence that matches the field-value token production." |
| 137 | +// However, this would then allow values made up entirely of white space and newlines. |
| 138 | +// RELATED ISSUE 2: |
| 139 | +// According to a previously filed Errata ID: 4189 in [4], "the |
| 140 | +// specified field-value rule does not allow single field-vchar |
| 141 | +// surrounded by whitespace anywhere". They provided a fix for the |
| 142 | +// field-content production, but ISSUE 1 has still not been resolved. |
| 143 | +// The production definitions likely need to be re-written. |
| 144 | +// [1] https://fetch.spec.whatwg.org/#concept-header-value |
| 145 | +// [2] https://tools.ietf.org/html/rfc7230#section-3.2 |
| 146 | +// [3] https://tools.ietf.org/html/rfc7230#section-3.2.6 |
| 147 | +// [4] https://www.rfc-editor.org/errata_search.php?rfc=7230 |
| 148 | +fn validate_name_and_value(name: ByteString, value: ByteString) |
| 149 | + -> Result<(String, Vec<u8>), Error> { |
| 150 | + if !is_field_name(&name) { |
| 151 | + return Err(Error::Type("Name is not valid".to_string())); |
| 152 | + } |
| 153 | + if !is_field_content(&value) { |
| 154 | + return Err(Error::Type("Value is not valid".to_string())); |
| 155 | + } |
| 156 | + match String::from_utf8(name.into()) { |
| 157 | + Ok(ns) => Ok((ns, value.into())), |
| 158 | + _ => Err(Error::Type("Non-UTF8 header name found".to_string())), |
| 159 | + } |
| 160 | +} |
| 161 | + |
| 162 | +// Removes trailing and leading HTTP whitespace bytes. |
| 163 | +// https://fetch.spec.whatwg.org/#concept-header-value-normalize |
| 164 | +pub fn normalize_value(value: ByteString) -> ByteString { |
| 165 | + match (index_of_first_non_whitespace(&value), index_of_last_non_whitespace(&value)) { |
| 166 | + (Some(begin), Some(end)) => ByteString::new(value[begin..end + 1].to_owned()), |
| 167 | + _ => ByteString::new(vec![]), |
| 168 | + } |
| 169 | +} |
| 170 | + |
| 171 | +fn is_HTTP_whitespace(byte: u8) -> bool { |
| 172 | + byte == b'\t' || byte == b'\n' || byte == b'\r' || byte == b' ' |
| 173 | +} |
| 174 | + |
| 175 | +fn index_of_first_non_whitespace(value: &ByteString) -> Option<usize> { |
| 176 | + for (index, &byte) in value.iter().enumerate() { |
| 177 | + if !is_HTTP_whitespace(byte) { |
| 178 | + return Some(index); |
| 179 | + } |
| 180 | + } |
| 181 | + None |
| 182 | +} |
| 183 | + |
| 184 | +fn index_of_last_non_whitespace(value: &ByteString) -> Option<usize> { |
| 185 | + for (index, &byte) in value.iter().enumerate().rev() { |
| 186 | + if !is_HTTP_whitespace(byte) { |
| 187 | + return Some(index); |
| 188 | + } |
| 189 | + } |
| 190 | + None |
| 191 | +} |
| 192 | + |
| 193 | +// http://tools.ietf.org/html/rfc7230#section-3.2 |
| 194 | +fn is_field_name(name: &ByteString) -> bool { |
| 195 | + is_token(&*name) |
| 196 | +} |
| 197 | + |
| 198 | +// https://tools.ietf.org/html/rfc7230#section-3.2 |
| 199 | +// http://www.rfc-editor.org/errata_search.php?rfc=7230 |
| 200 | +// Errata ID: 4189 |
| 201 | +// field-content = field-vchar [ 1*( SP / HTAB / field-vchar ) |
| 202 | +// field-vchar ] |
| 203 | +fn is_field_content(value: &ByteString) -> bool { |
| 204 | + if value.len() == 0 { |
| 205 | + return false; |
| 206 | + } |
| 207 | + if !is_field_vchar(value[0]) { |
| 208 | + return false; |
| 209 | + } |
| 210 | + |
| 211 | + for &ch in &value[1..value.len() - 1] { |
| 212 | + if !is_field_vchar(ch) || !is_space(ch) || !is_htab(ch) { |
| 213 | + return false; |
| 214 | + } |
| 215 | + } |
| 216 | + |
| 217 | + if !is_field_vchar(value[value.len() - 1]) { |
| 218 | + return false; |
| 219 | + } |
| 220 | + |
| 221 | + return true; |
| 222 | +} |
| 223 | + |
| 224 | +fn is_space(x: u8) -> bool { |
| 225 | + x == b' ' |
| 226 | +} |
| 227 | + |
| 228 | +fn is_htab(x: u8) -> bool { |
| 229 | + x == b'\t' |
| 230 | +} |
| 231 | + |
| 232 | + |
| 233 | +fn is_field_vchar(x: u8) -> bool { |
| 234 | + is_vchar(x) || is_obs_text(x) |
| 235 | +} |
| 236 | + |
| 237 | + |
| 238 | +fn is_vchar(x: u8) -> bool { |
| 239 | + match x { |
| 240 | + 0x21...0x7E => true, |
| 241 | + _ => false, |
| 242 | + } |
| 243 | +} |
| 244 | + |
| 245 | + |
| 246 | +fn is_obs_text(x: u8) -> bool { |
| 247 | + match x { |
| 248 | + 0x80...0xFF => true, |
| 249 | + _ => false, |
| 250 | + } |
| 251 | +} |
0 commit comments