script/dom/
headers.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6use std::str::{self, FromStr};
7
8use dom_struct::dom_struct;
9use http::header::{HeaderMap as HyperHeaders, HeaderName, HeaderValue};
10use js::rust::HandleObject;
11use net_traits::fetch::headers::{
12    extract_mime_type, get_decode_and_split_header_value, get_value_from_header_list,
13    is_forbidden_method,
14};
15use net_traits::request::is_cors_safelisted_request_header;
16use net_traits::trim_http_whitespace;
17
18use crate::dom::bindings::cell::DomRefCell;
19use crate::dom::bindings::codegen::Bindings::HeadersBinding::{HeadersInit, HeadersMethods};
20use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
21use crate::dom::bindings::iterable::Iterable;
22use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto};
23use crate::dom::bindings::root::DomRoot;
24use crate::dom::bindings::str::{ByteString, is_token};
25use crate::dom::globalscope::GlobalScope;
26use crate::script_runtime::CanGc;
27
28#[dom_struct]
29pub(crate) struct Headers {
30    reflector_: Reflector,
31    guard: Cell<Guard>,
32    #[ignore_malloc_size_of = "Defined in hyper"]
33    #[no_trace]
34    header_list: DomRefCell<HyperHeaders>,
35}
36
37/// <https://fetch.spec.whatwg.org/#concept-headers-guard>
38#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)]
39pub(crate) enum Guard {
40    Immutable,
41    Request,
42    RequestNoCors,
43    Response,
44    None,
45}
46
47impl Headers {
48    pub(crate) fn new_inherited() -> Headers {
49        Headers {
50            reflector_: Reflector::new(),
51            guard: Cell::new(Guard::None),
52            header_list: DomRefCell::new(HyperHeaders::new()),
53        }
54    }
55
56    pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot<Headers> {
57        Self::new_with_proto(global, None, can_gc)
58    }
59
60    fn new_with_proto(
61        global: &GlobalScope,
62        proto: Option<HandleObject>,
63        can_gc: CanGc,
64    ) -> DomRoot<Headers> {
65        reflect_dom_object_with_proto(Box::new(Headers::new_inherited()), global, proto, can_gc)
66    }
67}
68
69impl HeadersMethods<crate::DomTypeHolder> for Headers {
70    /// <https://fetch.spec.whatwg.org/#dom-headers>
71    fn Constructor(
72        global: &GlobalScope,
73        proto: Option<HandleObject>,
74        can_gc: CanGc,
75        init: Option<HeadersInit>,
76    ) -> Fallible<DomRoot<Headers>> {
77        let dom_headers_new = Headers::new_with_proto(global, proto, can_gc);
78        dom_headers_new.fill(init)?;
79        Ok(dom_headers_new)
80    }
81
82    /// <https://fetch.spec.whatwg.org/#concept-headers-append>
83    fn Append(&self, name: ByteString, value: ByteString) -> ErrorResult {
84        // 1. Normalize value.
85        let value = trim_http_whitespace(&value);
86
87        // 2. If validating (name, value) for headers returns false, then return.
88        let Some((mut valid_name, valid_value)) =
89            self.validate_name_and_value(name, ByteString::new(value.into()))?
90        else {
91            return Ok(());
92        };
93
94        valid_name = valid_name.to_lowercase();
95
96        // 3. If headers’s guard is "request-no-cors":
97        if self.guard.get() == Guard::RequestNoCors {
98            // 3.1. Let temporaryValue be the result of getting name from headers’s header list.
99            let tmp_value = if let Some(mut value) =
100                get_value_from_header_list(&valid_name, &self.header_list.borrow())
101            {
102                // 3.3. Otherwise, set temporaryValue to temporaryValue, followed by 0x2C 0x20, followed by value.
103                value.extend(b", ");
104                value.extend(valid_value.to_vec());
105                value
106            } else {
107                // 3.2. If temporaryValue is null, then set temporaryValue to value.
108                valid_value.to_vec()
109            };
110            // 3.4. If (name, temporaryValue) is not a no-CORS-safelisted request-header, then return.
111            if !is_cors_safelisted_request_header(&valid_name, &tmp_value) {
112                return Ok(());
113            }
114        }
115
116        // 4. Append (name, value) to headers’s header list.
117        match HeaderValue::from_bytes(&valid_value) {
118            Ok(value) => {
119                self.header_list
120                    .borrow_mut()
121                    .append(HeaderName::from_str(&valid_name).unwrap(), value);
122            },
123            Err(_) => {
124                // can't add the header, but we don't need to panic the browser over it
125                warn!(
126                    "Servo thinks \"{:?}\" is a valid HTTP header value but HeaderValue doesn't.",
127                    valid_value
128                );
129            },
130        };
131
132        // 5. If headers’s guard is "request-no-cors", then remove privileged no-CORS request-headers from headers.
133        if self.guard.get() == Guard::RequestNoCors {
134            self.remove_privileged_no_cors_request_headers();
135        }
136
137        Ok(())
138    }
139
140    /// <https://fetch.spec.whatwg.org/#dom-headers-delete>
141    fn Delete(&self, name: ByteString) -> ErrorResult {
142        // Step 1 If validating (name, ``) for this returns false, then return.
143        let name_and_value = self.validate_name_and_value(name, ByteString::new(vec![]))?;
144        let Some((mut valid_name, _valid_value)) = name_and_value else {
145            return Ok(());
146        };
147
148        valid_name = valid_name.to_lowercase();
149
150        // Step 2 If this’s guard is "request-no-cors", name is not a no-CORS-safelisted request-header name,
151        // and name is not a privileged no-CORS request-header name, then return.
152        if self.guard.get() == Guard::RequestNoCors &&
153            !is_cors_safelisted_request_header(&valid_name, &b"invalid".to_vec())
154        {
155            return Ok(());
156        }
157
158        // 3. If this’s header list does not contain name, then return.
159        // 4. Delete name from this’s header list.
160        self.header_list.borrow_mut().remove(valid_name);
161
162        // 5. If this’s guard is "request-no-cors", then remove privileged no-CORS request-headers from this.
163        if self.guard.get() == Guard::RequestNoCors {
164            self.remove_privileged_no_cors_request_headers();
165        }
166
167        Ok(())
168    }
169
170    /// <https://fetch.spec.whatwg.org/#dom-headers-get>
171    fn Get(&self, name: ByteString) -> Fallible<Option<ByteString>> {
172        // 1. If name is not a header name, then throw a TypeError.
173        let valid_name = validate_name(name)?;
174
175        // 2. Return the result of getting name from this’s header list.
176        Ok(
177            get_value_from_header_list(&valid_name, &self.header_list.borrow())
178                .map(ByteString::new),
179        )
180    }
181
182    /// <https://fetch.spec.whatwg.org/#dom-headers-getsetcookie>
183    fn GetSetCookie(&self) -> Vec<ByteString> {
184        // 1. If this’s header list does not contain `Set-Cookie`, then return « ».
185        // 2. Return the values of all headers in this’s header list whose name is a
186        // byte-case-insensitive match for `Set-Cookie`, in order.
187        self.header_list
188            .borrow()
189            .get_all("set-cookie")
190            .iter()
191            .map(|v| ByteString::new(v.as_bytes().to_vec()))
192            .collect()
193    }
194
195    /// <https://fetch.spec.whatwg.org/#dom-headers-has>
196    fn Has(&self, name: ByteString) -> Fallible<bool> {
197        // 1. If name is not a header name, then throw a TypeError.
198        let valid_name = validate_name(name)?;
199        // 2. Return true if this’s header list contains name; otherwise false.
200        Ok(self.header_list.borrow_mut().get(&valid_name).is_some())
201    }
202
203    /// <https://fetch.spec.whatwg.org/#dom-headers-set>
204    fn Set(&self, name: ByteString, value: ByteString) -> Fallible<()> {
205        // 1. Normalize value
206        let value = trim_http_whitespace(&value);
207
208        // 2. If validating (name, value) for this returns false, then return.
209        let Some((mut valid_name, valid_value)) =
210            self.validate_name_and_value(name, ByteString::new(value.into()))?
211        else {
212            return Ok(());
213        };
214        valid_name = valid_name.to_lowercase();
215
216        // 3. If this’s guard is "request-no-cors" and (name, value) is not a
217        // no-CORS-safelisted request-header, then return.
218        if self.guard.get() == Guard::RequestNoCors &&
219            !is_cors_safelisted_request_header(&valid_name, &valid_value.to_vec())
220        {
221            return Ok(());
222        }
223
224        // 4. Set (name, value) in this’s header list.
225        // https://fetch.spec.whatwg.org/#concept-header-list-set
226        match HeaderValue::from_bytes(&valid_value) {
227            Ok(value) => {
228                self.header_list
229                    .borrow_mut()
230                    .insert(HeaderName::from_str(&valid_name).unwrap(), value);
231            },
232            Err(_) => {
233                // can't add the header, but we don't need to panic the browser over it
234                warn!(
235                    "Servo thinks \"{:?}\" is a valid HTTP header value but HeaderValue doesn't.",
236                    valid_value
237                );
238            },
239        };
240
241        // 5. If this’s guard is "request-no-cors", then remove privileged no-CORS request-headers from this.
242        if self.guard.get() == Guard::RequestNoCors {
243            self.remove_privileged_no_cors_request_headers();
244        }
245
246        Ok(())
247    }
248}
249
250impl Headers {
251    pub(crate) fn copy_from_headers(&self, headers: DomRoot<Headers>) -> ErrorResult {
252        for (name, value) in headers.header_list.borrow().iter() {
253            self.Append(
254                ByteString::new(Vec::from(name.as_str())),
255                ByteString::new(Vec::from(value.as_bytes())),
256            )?;
257        }
258        Ok(())
259    }
260
261    /// <https://fetch.spec.whatwg.org/#concept-headers-fill>
262    pub(crate) fn fill(&self, filler: Option<HeadersInit>) -> ErrorResult {
263        match filler {
264            Some(HeadersInit::ByteStringSequenceSequence(v)) => {
265                for mut seq in v {
266                    if seq.len() == 2 {
267                        let val = seq.pop().unwrap();
268                        let name = seq.pop().unwrap();
269                        self.Append(name, val)?;
270                    } else {
271                        return Err(Error::Type(format!(
272                            "Each header object must be a sequence of length 2 - found one with length {}",
273                            seq.len()
274                        )));
275                    }
276                }
277                Ok(())
278            },
279            Some(HeadersInit::ByteStringByteStringRecord(m)) => {
280                for (key, value) in m.iter() {
281                    self.Append(key.clone(), value.clone())?;
282                }
283                Ok(())
284            },
285            None => Ok(()),
286        }
287    }
288
289    pub(crate) fn for_request(global: &GlobalScope, can_gc: CanGc) -> DomRoot<Headers> {
290        let headers_for_request = Headers::new(global, can_gc);
291        headers_for_request.guard.set(Guard::Request);
292        headers_for_request
293    }
294
295    pub(crate) fn for_response(global: &GlobalScope, can_gc: CanGc) -> DomRoot<Headers> {
296        let headers_for_response = Headers::new(global, can_gc);
297        headers_for_response.guard.set(Guard::Response);
298        headers_for_response
299    }
300
301    pub(crate) fn set_guard(&self, new_guard: Guard) {
302        self.guard.set(new_guard)
303    }
304
305    pub(crate) fn get_guard(&self) -> Guard {
306        self.guard.get()
307    }
308
309    pub(crate) fn set_headers(&self, hyper_headers: HyperHeaders) {
310        *self.header_list.borrow_mut() = hyper_headers;
311    }
312
313    pub(crate) fn get_headers_list(&self) -> HyperHeaders {
314        self.header_list.borrow_mut().clone()
315    }
316
317    /// <https://fetch.spec.whatwg.org/#concept-header-extract-mime-type>
318    pub(crate) fn extract_mime_type(&self) -> Vec<u8> {
319        extract_mime_type(&self.header_list.borrow()).unwrap_or_default()
320    }
321
322    /// <https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine>
323    pub(crate) fn sort_and_combine(&self) -> Vec<(String, Vec<u8>)> {
324        let borrowed_header_list = self.header_list.borrow();
325        let mut header_vec = vec![];
326
327        for name in borrowed_header_list.keys() {
328            let name = name.as_str();
329            if name == "set-cookie" {
330                for value in borrowed_header_list.get_all(name).iter() {
331                    header_vec.push((name.to_owned(), value.as_bytes().to_vec()));
332                }
333            } else if let Some(value) = get_value_from_header_list(name, &borrowed_header_list) {
334                header_vec.push((name.to_owned(), value));
335            }
336        }
337
338        header_vec.sort_by(|a, b| a.0.cmp(&b.0));
339        header_vec
340    }
341
342    /// <https://fetch.spec.whatwg.org/#ref-for-privileged-no-cors-request-header-name>
343    pub(crate) fn remove_privileged_no_cors_request_headers(&self) {
344        // <https://fetch.spec.whatwg.org/#privileged-no-cors-request-header-name>
345        self.header_list.borrow_mut().remove("range");
346    }
347
348    /// <https://fetch.spec.whatwg.org/#headers-validate>
349    pub(crate) fn validate_name_and_value(
350        &self,
351        name: ByteString,
352        value: ByteString,
353    ) -> Fallible<Option<(String, ByteString)>> {
354        // 1. If name is not a header name or value is not a header value, then throw a TypeError.
355        let valid_name = validate_name(name)?;
356        if !is_legal_header_value(&value) {
357            return Err(Error::Type("Header value is not valid".to_string()));
358        }
359        // 2. If headers’s guard is "immutable", then throw a TypeError.
360        if self.guard.get() == Guard::Immutable {
361            return Err(Error::Type("Guard is immutable".to_string()));
362        }
363        // 3. If headers’s guard is "request" and (name, value) is a forbidden request-header, then return false.
364        if self.guard.get() == Guard::Request && is_forbidden_request_header(&valid_name, &value) {
365            return Ok(None);
366        }
367        // 4. If headers’s guard is "response" and name is a forbidden response-header name, then return false.
368        if self.guard.get() == Guard::Response && is_forbidden_response_header(&valid_name) {
369            return Ok(None);
370        }
371
372        Ok(Some((valid_name, value)))
373    }
374}
375
376impl Iterable for Headers {
377    type Key = ByteString;
378    type Value = ByteString;
379
380    fn get_iterable_length(&self) -> u32 {
381        let sorted_header_vec = self.sort_and_combine();
382        sorted_header_vec.len() as u32
383    }
384
385    fn get_value_at_index(&self, n: u32) -> ByteString {
386        let sorted_header_vec = self.sort_and_combine();
387        let value = sorted_header_vec[n as usize].1.clone();
388        ByteString::new(value)
389    }
390
391    fn get_key_at_index(&self, n: u32) -> ByteString {
392        let sorted_header_vec = self.sort_and_combine();
393        let key = sorted_header_vec[n as usize].0.clone();
394        ByteString::new(key.into_bytes().to_vec())
395    }
396}
397
398/// This function will internally convert `name` to lowercase for matching, so explicitly converting
399/// before calling is not necessary
400///
401/// <https://fetch.spec.whatwg.org/#forbidden-request-header>
402pub(crate) fn is_forbidden_request_header(name: &str, value: &[u8]) -> bool {
403    let forbidden_header_names = [
404        "accept-charset",
405        "accept-encoding",
406        "access-control-request-headers",
407        "access-control-request-method",
408        "connection",
409        "content-length",
410        "cookie",
411        "cookie2",
412        "date",
413        "dnt",
414        "expect",
415        "host",
416        "keep-alive",
417        "origin",
418        "referer",
419        "set-cookie",
420        "te",
421        "trailer",
422        "transfer-encoding",
423        "upgrade",
424        "via",
425        // This list is defined in the fetch spec, however the draft spec for private-network-access
426        // proposes this additional forbidden name, which is currently included in WPT tests. See:
427        // https://wicg.github.io/private-network-access/#forbidden-header-names
428        "access-control-request-private-network",
429    ];
430
431    // Step 1: If name is a byte-case-insensitive match for one of (forbidden_header_names), return
432    // true
433    let lowercase_name = name.to_lowercase();
434
435    if forbidden_header_names.contains(&lowercase_name.as_str()) {
436        return true;
437    }
438
439    let forbidden_header_prefixes = ["sec-", "proxy-"];
440
441    // Step 2: If name when byte-lowercased starts with `proxy-` or `sec-`, then return true.
442    if forbidden_header_prefixes
443        .iter()
444        .any(|prefix| lowercase_name.starts_with(prefix))
445    {
446        return true;
447    }
448
449    let potentially_forbidden_header_names = [
450        "x-http-method",
451        "x-http-method-override",
452        "x-method-override",
453    ];
454
455    // Step 3: If name is a byte-case-insensitive match for one of (potentially_forbidden_header_names)
456    if potentially_forbidden_header_names
457        .iter()
458        .any(|header| *header == lowercase_name)
459    {
460        // Step 3.1: Let parsedValues be the result of getting, decoding, and splitting value.
461        let parsed_values = get_decode_and_split_header_value(value.to_vec());
462
463        // Step 3.2: For each method of parsedValues: if the isomorphic encoding of method is a
464        // forbidden method, then return true.
465        return parsed_values
466            .iter()
467            .any(|s| is_forbidden_method(s.as_bytes()));
468    }
469
470    // Step 4: Return false.
471    false
472}
473
474/// <https://fetch.spec.whatwg.org/#forbidden-response-header-name>
475fn is_forbidden_response_header(name: &str) -> bool {
476    // A forbidden response-header name is a header name that is a byte-case-insensitive match for one of
477    let name = name.to_ascii_lowercase();
478    matches!(name.as_str(), "set-cookie" | "set-cookie2")
479}
480
481fn validate_name(name: ByteString) -> Fallible<String> {
482    if !is_field_name(&name) {
483        return Err(Error::Type("Name is not valid".to_string()));
484    }
485    match String::from_utf8(name.into()) {
486        Ok(ns) => Ok(ns),
487        _ => Err(Error::Type("Non-UTF8 header name found".to_string())),
488    }
489}
490
491/// <http://tools.ietf.org/html/rfc7230#section-3.2>
492fn is_field_name(name: &ByteString) -> bool {
493    is_token(name)
494}
495
496// As of December 2019, WHATWG has no formal grammar production for value;
497// https://fetch.spec.whatg.org/#concept-header-value just says not to have
498// newlines, nulls, or leading/trailing whitespace. It even allows
499// octets that aren't a valid UTF-8 encoding, and WPT tests reflect this.
500// The HeaderValue class does not fully reflect this, so headers
501// containing bytes with values 1..31 or 127 can't be created, failing
502// WPT tests but probably not affecting anything important on the real Internet.
503/// <https://fetch.spec.whatg.org/#concept-header-value>
504fn is_legal_header_value(value: &[u8]) -> bool {
505    let value_len = value.len();
506    if value_len == 0 {
507        return true;
508    }
509    match value[0] {
510        b' ' | b'\t' => return false,
511        _ => {},
512    };
513    match value[value_len - 1] {
514        b' ' | b'\t' => return false,
515        _ => {},
516    };
517    for &ch in value {
518        match ch {
519            b'\0' | b'\n' | b'\r' => return false,
520            _ => {},
521        }
522    }
523    true
524    // If accepting non-UTF8 header values causes breakage,
525    // removing the above "true" and uncommenting the below code
526    // would ameliorate it while still accepting most reasonable headers:
527    // match str::from_utf8(value) {
528    //    Ok(_) => true,
529    //    Err(_) => {
530    //        warn!(
531    //            "Rejecting spec-legal but non-UTF8 header value: {:?}",
532    //            value
533    //        );
534    //        false
535    //    },
536    // }
537}
538
539/// <https://tools.ietf.org/html/rfc5234#appendix-B.1>
540pub(crate) fn is_vchar(x: u8) -> bool {
541    matches!(x, 0x21..=0x7E)
542}
543
544/// <http://tools.ietf.org/html/rfc7230#section-3.2.6>
545pub(crate) fn is_obs_text(x: u8) -> bool {
546    matches!(x, 0x80..=0xFF)
547}