Skip to main content

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