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