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