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