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