Skip to main content

net_traits/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::iter::Peekable;
6use std::str::{Chars, FromStr};
7
8use data_url::mime::Mime as DataUrlMime;
9use headers::HeaderMap;
10
11/// <https://fetch.spec.whatwg.org/#http-tab-or-space>
12const HTTP_TAB_OR_SPACE: &[char] = &['\u{0009}', '\u{0020}'];
13
14/// <https://fetch.spec.whatwg.org/#concept-header-list-get>
15pub fn get_value_from_header_list(name: &str, headers: &HeaderMap) -> Option<Vec<u8>> {
16    let values = headers.get_all(name).iter().map(|val| val.as_bytes());
17
18    // Step 1: If list does not contain name, then return null.
19    if values.size_hint() == (0, Some(0)) {
20        return None;
21    }
22
23    // Step 2: Return the values of all headers in list whose name is a byte-case-insensitive match
24    // for name, separated from each other by 0x2C 0x20, in order.
25    Some(values.collect::<Vec<&[u8]>>().join(&[0x2C, 0x20][..]))
26}
27
28/// <https://fetch.spec.whatwg.org/#forbidden-method>
29pub fn is_forbidden_method(method: &[u8]) -> bool {
30    method.eq_ignore_ascii_case(b"connect") ||
31        method.eq_ignore_ascii_case(b"trace") ||
32        method.eq_ignore_ascii_case(b"track")
33}
34
35/// <https://fetch.spec.whatwg.org/#concept-header-list-get-decode-split>
36pub fn get_decode_and_split_header_name(name: &str, headers: &HeaderMap) -> Option<Vec<String>> {
37    // Step 1: Let value be the result of getting name from list.
38    // Step 2: If value is null, then return null.
39    // Step 3: Return the result of getting, decoding, and splitting value.
40    get_value_from_header_list(name, headers).map(get_decode_and_split_header_value)
41}
42
43/// <https://fetch.spec.whatwg.org/#header-value-get-decode-and-split>
44pub fn get_decode_and_split_header_value(value: Vec<u8>) -> Vec<String> {
45    fn char_is_not_quote_or_comma(c: char) -> bool {
46        c != '\u{0022}' && c != '\u{002C}'
47    }
48
49    // Step 1. Let input be the result of isomorphic decoding value.
50    let input = value.into_iter().map(char::from).collect::<String>();
51
52    // Step 2. Let position be a position variable for input,
53    // initially pointing at the start of input.
54    let mut position = input.chars().peekable();
55
56    // Step 3. Let values be a list of strings, initially « ».
57    let mut values: Vec<String> = vec![];
58
59    // Step 4. Let temporaryValue be the empty string.
60    let mut temporary_value = String::new();
61
62    // Step 5. While true:
63    loop {
64        // Step 5.1. Append the result of collecting a sequence of code points that
65        // are not U+0022 (") or U+002C (,) from input, given position, to temporaryValue.
66        temporary_value += &*collect_sequence(&mut position, char_is_not_quote_or_comma);
67
68        // Step 5.2. If position is not past the end of input and the code point
69        // at position within input is U+0022 ("):
70        if let Some(&ch) = position.peek() &&
71            ch == '\u{0022}'
72        {
73            // Step 5.2.1. Append the result of collecting an HTTP quoted string from input,
74            // given position, to temporaryValue.
75            temporary_value += &*collect_http_quoted_string(&mut position, false);
76
77            // Step 5.2.2. If position is not past the end of input, then continue.
78            if position.peek().is_some() {
79                continue;
80            }
81        }
82
83        // Step 5.3. Remove all HTTP tab or space from the start and end of temporaryValue.
84        temporary_value = temporary_value.trim_matches(HTTP_TAB_OR_SPACE).to_string();
85
86        // Step 5.4. Append temporaryValue to values.
87        values.push(temporary_value);
88
89        // Step 5.5. Set temporaryValue to the empty string.
90        temporary_value = String::new();
91
92        // Step 5.8. Advance position by 1.
93        let Some(ch) = position.next() else {
94            // Step 5.6. If position is past the end of input, then return values.
95            return values;
96        };
97        // Step 5.7. Assert: the code point at position within input is U+002C (,).
98        assert_eq!(ch, '\u{002C}');
99    }
100}
101
102/// <https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points>
103fn collect_sequence<F>(position: &mut Peekable<Chars>, condition: F) -> String
104where
105    F: Fn(char) -> bool,
106{
107    // Step 1: Let result be the empty string.
108    let mut result = String::new();
109
110    // Step 2: While position doesn’t point past the end of input and the code point at position
111    // within input meets the condition condition:
112    while let Some(&ch) = position.peek() {
113        if !condition(ch) {
114            break;
115        }
116
117        // Step 2.1: Append that code point to the end of result.
118        result.push(ch);
119
120        // Step 2.2: Advance position by 1.
121        position.next();
122    }
123
124    // Step 3: Return result.
125    result
126}
127
128/// <https://fetch.spec.whatwg.org/#collect-an-http-quoted-string>
129fn collect_http_quoted_string(position: &mut Peekable<Chars>, extract_value: bool) -> String {
130    fn char_is_not_quote_or_backslash(c: char) -> bool {
131        c != '\u{0022}' && c != '\u{005C}'
132    }
133
134    // Step 2: let value be the empty string
135    //
136    // We will store the 'extracted value' or the raw value
137    let mut value = String::new();
138
139    // Step 4. Advance position by 1.
140    let should_be_quote = position.next();
141    if let Some(ch) = should_be_quote {
142        // Step 3. Assert: the code point at position within input is U+0022 (").
143        assert_eq!(ch, '\u{0022}');
144
145        if !extract_value {
146            value.push(ch)
147        }
148    }
149
150    // Step 5: While true:
151    loop {
152        // Step 5.1: Append the result of collecting a sequence of code points that are not U+0022
153        // (") or U+005C (\) from input, given position, to value.
154        value += &*collect_sequence(position, char_is_not_quote_or_backslash);
155
156        // Step 5.3: Let quoteOrBackslash be the code point at position within input.
157        // Step 5.4: Advance position by 1.
158        let Some(quote_or_backslash) = position.next() else {
159            // Step 5.2: If position is past the end of input, then break.
160            break;
161        };
162
163        if !extract_value {
164            value.push(quote_or_backslash);
165        }
166
167        // Step 5.5. If quoteOrBackslash is U+005C (\), then:
168        if quote_or_backslash == '\u{005C}' {
169            // Step 5.5.3. Advance position by 1.
170            if let Some(ch) = position.next() {
171                // Step 5.5.2. Append the code point at position within input to value.
172                value.push(ch);
173            } else {
174                // Step 5.5.1. If position is past the end of input, then append U+005C (\) to value and break.
175                if extract_value {
176                    value.push('\u{005C}');
177                }
178
179                break;
180            }
181        // Step 5.6. Otherwise:
182        } else {
183            // Step 5.6.1. Assert: quoteOrBackslash is U+0022 (").
184            assert_eq!(quote_or_backslash, '\u{0022}');
185
186            // Step 5.6.2. Break.
187            break;
188        }
189    }
190
191    // Step 6. If extract-value is true, then return value.
192    // Step 7. Return the code points from positionStart to position, inclusive, within input.
193    value
194}
195
196/// <https://fetch.spec.whatwg.org/#concept-header-extract-mime-type>
197/// This function uses data_url::Mime to parse the MIME Type because
198/// mime::Mime does not provide a parser following the Fetch spec
199/// see <https://github.com/hyperium/mime/issues/106>
200pub fn extract_mime_type_as_dataurl_mime(headers: &HeaderMap) -> Option<DataUrlMime> {
201    // > 1: Let charset be null.
202    let mut charset = None;
203    // > 2: Let essence be null.
204    let mut essence = String::new();
205    // > 3: Let mimeType be null.
206    let mut mime_type = None;
207
208    // > 4: Let values be the result of getting, decoding, and splitting `Content-Type`
209    // from headers.
210    // > 5: If values is null, then return failure.
211    let headers_values = get_decode_and_split_header_name("content-type", headers)?;
212
213    // > 6: For each value of values:
214    for header_value in headers_values.iter() {
215        // > 6.1: Let temporaryMimeType be the result of parsing value.
216        match DataUrlMime::from_str(header_value) {
217            // > 6.2: If temporaryMimeType is failure or its essence is "*/*", then continue.
218            Err(_) => continue,
219            Ok(temp_mime) => {
220                let temp_essence = format!("{}/{}", temp_mime.type_, temp_mime.subtype);
221
222                // > 6.2: If temporaryMimeType is failure or its essence is "*/*", then
223                // continue.
224                if temp_essence == "*/*" {
225                    continue;
226                }
227
228                // > 6.3: Set mimeType to temporaryMimeType.
229                mime_type = Some(DataUrlMime {
230                    type_: temp_mime.type_.to_string(),
231                    subtype: temp_mime.subtype.to_string(),
232                    parameters: temp_mime.parameters.clone(),
233                });
234
235                // > 6.4: If mimeType’s essence is not essence, then:
236                let temp_charset = &temp_mime.get_parameter("charset");
237                if temp_essence != essence {
238                    // > 6.4.1: Set charset to null.
239                    // > 6.4.2: If mimeType’s parameters["charset"] exists, then set
240                    //   charset to mimeType’s parameters["charset"].
241                    charset = temp_charset.map(|c| c.to_string());
242                    // > 6.4.3: Set essence to mimeType’s essence.
243                    essence = temp_essence.to_owned();
244                } else {
245                    // > 6.5: Otherwise, if mimeType’s parameters["charset"] does not exist,
246                    //   and charset is non-null, set mimeType’s parameters["charset"] to charset.
247                    if temp_charset.is_none() && charset.is_some() {
248                        let DataUrlMime {
249                            type_: t,
250                            subtype: st,
251                            parameters: p,
252                        } = mime_type.unwrap();
253                        let mut params = p;
254                        params.push(("charset".to_string(), charset.clone().unwrap()));
255                        mime_type = Some(DataUrlMime {
256                            type_: t.to_string(),
257                            subtype: st.to_string(),
258                            parameters: params,
259                        })
260                    }
261                }
262            },
263        }
264    }
265
266    // > 7: If mimeType is null, then return failure.
267    // > 8: Return mimeType.
268    mime_type
269}
270
271pub fn extract_mime_type(headers: &HeaderMap) -> Option<Vec<u8>> {
272    extract_mime_type_as_dataurl_mime(headers).map(|m| format!("{}", m).into_bytes())
273}
274
275pub fn extract_mime_type_as_mime(headers: &HeaderMap) -> Option<mime::Mime> {
276    extract_mime_type_as_dataurl_mime(headers).and_then(|mime: DataUrlMime| {
277        // Try to transform a data-url::mime::Mime into a mime::Mime
278        let mut mime_as_str = format!("{}/{}", mime.type_, mime.subtype);
279        for p in mime.parameters {
280            mime_as_str.push_str(format!("; {}={}", p.0, p.1).as_str());
281        }
282        mime_as_str.parse().ok()
283    })
284}
285
286/// <https://fetch.spec.whatwg.org/#determine-nosniff>
287pub fn determine_nosniff(headers: &HeaderMap) -> bool {
288    let values = get_decode_and_split_header_name("x-content-type-options", headers);
289
290    values.is_some_and(|values| !values.is_empty() && values[0].eq_ignore_ascii_case("nosniff"))
291}