runtime/globals/fetch/
mod.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 */
6
7use std::str;
8use std::str::FromStr as _;
9
10use async_recursion::async_recursion;
11use body::FetchBody;
12use client::Client;
13pub use client::{GLOBAL_CLIENT, default_client};
14use const_format::concatcp;
15use futures_util::future::{Either, select};
16pub use header::Headers;
17use header::{FORBIDDEN_RESPONSE_HEADERS, HeadersKind, remove_all_header_entries};
18use http::header::{
19	ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, ACCESS_CONTROL_ALLOW_HEADERS, CACHE_CONTROL, CONTENT_ENCODING,
20	CONTENT_LANGUAGE, CONTENT_LENGTH, CONTENT_LOCATION, CONTENT_TYPE, HOST, IF_MATCH, IF_MODIFIED_SINCE, IF_NONE_MATCH,
21	IF_RANGE, IF_UNMODIFIED_SINCE, LOCATION, PRAGMA, RANGE, REFERER, REFERRER_POLICY, USER_AGENT,
22};
23use http::{HeaderValue, Method, StatusCode};
24use ion::class::{ClassObjectWrapper, Reflector};
25use ion::conversions::ToValue as _;
26use ion::flags::PropertyFlags;
27use ion::function::Opt;
28use ion::{
29	ClassDefinition as _, Context, Error, ErrorKind, Exception, Local, Object, Promise, ResultExc, TracedHeap, js_fn,
30};
31use request::{Referrer, ReferrerPolicy, RequestCache, RequestCredentials, RequestMode, RequestRedirect};
32pub use request::{Request, RequestInfo, RequestInit};
33pub use response::Response;
34use response::{ResponseKind, ResponseTaint, network_error};
35use sys_locale::get_locales;
36use uri_url::url_to_uri;
37use url::Url;
38
39use crate::VERSION;
40use crate::globals::abort::AbortSignal;
41use crate::globals::fetch::body::Body;
42use crate::promise::future_to_promise;
43
44mod body;
45mod client;
46mod header;
47mod request;
48mod response;
49mod scheme;
50
51const DEFAULT_USER_AGENT: &str = concatcp!("Spiderfire/", VERSION);
52
53#[js_fn]
54fn fetch<'cx>(cx: &'cx Context, resource: RequestInfo, init: Opt<RequestInit>) -> Option<Promise<'cx>> {
55	let promise = Promise::new(cx);
56
57	let request = match Request::constructor(cx, resource, init) {
58		Ok(request) => request,
59		Err(error) => {
60			promise.reject(cx, &error.as_value(cx));
61			return Some(promise);
62		}
63	};
64
65	let signal = Object::from(unsafe { Local::from_heap(&request.signal_object) });
66	let signal = AbortSignal::get_private(cx, &signal).unwrap();
67	if let Some(reason) = signal.get_reason() {
68		promise.reject(cx, &cx.root(reason).into());
69		return Some(promise);
70	}
71
72	let headers = Object::from(unsafe { Local::from_heap(&request.headers) });
73	let headers = Headers::get_mut_private(cx, &headers).unwrap();
74	if !headers.headers.contains_key(ACCEPT) {
75		headers.headers.append(ACCEPT, HeaderValue::from_static("*/*"));
76	}
77
78	let mut locales = get_locales().enumerate();
79	let mut locale_string = locales.next().map_or_else(|| String::from("*"), |(_, s)| s);
80	for (index, locale) in locales {
81		locale_string.push(',');
82		locale_string.push_str(&locale);
83		locale_string.push_str(";q=0.");
84		locale_string.push_str(&(1000 - index).to_string());
85	}
86	if !headers.headers.contains_key(ACCEPT_LANGUAGE) {
87		headers.headers.append(ACCEPT_LANGUAGE, HeaderValue::from_str(&locale_string).unwrap());
88	}
89
90	let request = TracedHeap::new(Request::new_object(cx, Box::new(request)));
91	future_to_promise(cx, |cx| async move {
92		let request = Object::from(request.to_local());
93		fetch_internal(&cx, &request, GLOBAL_CLIENT.get().unwrap().clone()).await
94	})
95}
96
97async fn fetch_internal(cx: &Context, request: &Object<'_>, client: Client) -> ResultExc<ClassObjectWrapper<Response>> {
98	let request = Request::get_mut_private(cx, request)?;
99	let signal = Object::from(unsafe { Local::from_heap(&request.signal_object) });
100	let signal = AbortSignal::get_private(cx, &signal)?.signal.clone().poll();
101	let send = Box::pin(main_fetch(cx, request, client, 0));
102
103	let response = match select(send, signal).await {
104		Either::Left((response, _)) => Ok(response),
105		Either::Right((exception, _)) => Err(Exception::Other(exception)),
106	}?;
107	if response.kind == ResponseKind::Error {
108		Err(Exception::Error(Error::new(
109			format!("Network Error: Failed to fetch from {}", &request.url),
110			ErrorKind::Type,
111		)))
112	} else {
113		Ok(ClassObjectWrapper(Box::new(response)))
114	}
115}
116
117static BAD_PORTS: &[u16] = &[
118	1,     // tcpmux
119	7,     // echo
120	9,     // discard
121	11,    // systat
122	13,    // daytime
123	15,    // netstat
124	17,    // qotd
125	19,    // chargen
126	20,    // ftp-data
127	21,    // ftp
128	22,    // ssh
129	23,    // telnet
130	25,    // smtp
131	37,    // time
132	42,    // name
133	43,    // nicname
134	53,    // domain
135	69,    // tftp
136	77,    // —
137	79,    // finger
138	87,    // —
139	95,    // supdup
140	101,   // hostname
141	102,   // iso-tsap
142	103,   // gppitnp
143	104,   // acr-nema
144	109,   // pop2
145	110,   // pop3
146	111,   // sunrpc
147	113,   // auth
148	115,   // sftp
149	117,   // uucp-path
150	119,   // nntp
151	123,   // ntp
152	135,   // epmap
153	137,   // netbios-ns
154	139,   // netbios-ssn
155	143,   // imap
156	161,   // snmp
157	179,   // bgp
158	389,   // ldap
159	427,   // svrloc
160	465,   // submissions
161	512,   // exec
162	513,   // login
163	514,   // shell
164	515,   // printer
165	526,   // tempo
166	530,   // courier
167	531,   // chat
168	532,   // netnews
169	540,   // uucp
170	548,   // afp
171	554,   // rtsp
172	556,   // remotefs
173	563,   // nntps
174	587,   // submission
175	601,   // syslog-conn
176	636,   // ldaps
177	989,   // ftps-data
178	990,   // ftps
179	993,   // imaps
180	995,   // pop3s
181	1719,  // h323gatestat
182	1720,  // h323hostcall
183	1723,  // pptp
184	2049,  // nfs
185	3659,  // apple-sasl
186	4045,  // npp
187	5060,  // sip
188	5061,  // sips
189	6000,  // x11
190	6566,  // sane-port
191	6665,  // ircu
192	6666,  // ircu
193	6667,  // ircu
194	6668,  // ircu
195	6669,  // ircu
196	6697,  // ircs-u
197	10080, // amanda
198];
199
200static SCHEMES: [&str; 4] = ["about", "blob", "data", "file"];
201
202#[async_recursion(?Send)]
203async fn main_fetch(cx: &Context, request: &mut Request, client: Client, redirections: u8) -> Response {
204	let scheme = request.url.scheme();
205
206	// TODO: Upgrade HTTP Schemes if the host is a domain and matches the Known HSTS Domain List
207
208	let mut taint = ResponseTaint::default();
209	let mut opaque_redirect = false;
210	let mut response = {
211		if request.mode == RequestMode::SameOrigin {
212			network_error()
213		} else if SCHEMES.contains(&scheme) {
214			scheme::scheme_fetch(cx, scheme, request, request.url.clone()).await
215		} else if scheme == "https" || scheme == "http" {
216			if let Some(port) = request.url.port()
217				&& BAD_PORTS.contains(&port)
218			{
219				return network_error();
220			}
221			if request.mode == RequestMode::NoCors {
222				if request.redirect != RequestRedirect::Follow {
223					return network_error();
224				}
225			} else {
226				taint = ResponseTaint::Cors;
227			}
228			let (response, opaque) = http_fetch(cx, request, client, taint, redirections).await;
229			opaque_redirect = opaque;
230			response
231		} else {
232			network_error()
233		}
234	};
235
236	let redirected = redirections > 0;
237	if redirected || response.kind == ResponseKind::Error {
238		response.redirected = redirected;
239		return response;
240	}
241
242	response.url.get_or_insert(request.url.clone());
243
244	let headers = Object::from(unsafe { Local::from_heap(&response.headers) });
245	let headers = Headers::get_mut_private(cx, &headers).unwrap();
246
247	if !opaque_redirect
248		&& taint == ResponseTaint::Opaque
249		&& response.status == Some(StatusCode::PARTIAL_CONTENT)
250		&& response.range_requested
251		&& !headers.headers.contains_key(RANGE)
252	{
253		let url = response.url.take().unwrap();
254		response = network_error();
255		response.url = Some(url);
256		return response;
257	}
258
259	if !opaque_redirect
260		&& (matches!(request.method, Method::HEAD | Method::CONNECT)
261		|| response.status.as_ref().map(StatusCode::as_u16) == Some(103) // Early Hints
262		|| matches!(
263				response.status,
264				Some(StatusCode::SWITCHING_PROTOCOLS
265					| StatusCode::NO_CONTENT
266					| StatusCode::RESET_CONTENT
267					| StatusCode::NOT_MODIFIED)
268			)) {
269		response.body = None;
270	}
271
272	if opaque_redirect {
273		response.kind = ResponseKind::OpaqueRedirect;
274		response.url = None;
275		response.status = None;
276		response.status_text = None;
277		response.body = None;
278
279		headers.headers.clear();
280	} else {
281		match taint {
282			ResponseTaint::Basic => {
283				response.kind = ResponseKind::Basic;
284
285				for name in &FORBIDDEN_RESPONSE_HEADERS {
286					remove_all_header_entries(&mut headers.headers, name);
287				}
288			}
289			ResponseTaint::Cors => {
290				response.kind = ResponseKind::Cors;
291
292				let mut allows_all = false;
293				let allowed: Vec<_> = headers
294					.headers
295					.get_all(ACCESS_CONTROL_ALLOW_HEADERS)
296					.into_iter()
297					.map(|v| {
298						if v == "*" {
299							allows_all = true;
300						}
301						v.clone()
302					})
303					.collect();
304				let mut to_remove = Vec::new();
305				if request.credentials != RequestCredentials::Include && allows_all {
306					for name in headers.headers.keys() {
307						if headers.headers.get_all(name).into_iter().size_hint().1.is_none() {
308							to_remove.push(name.clone());
309						}
310					}
311				} else {
312					for name in headers.headers.keys() {
313						let allowed = allowed.iter().any(|allowed| allowed.as_bytes() == name.as_str().as_bytes());
314						if allowed {
315							to_remove.push(name.clone());
316						}
317					}
318				}
319				for name in to_remove {
320					remove_all_header_entries(&mut headers.headers, &name);
321				}
322				for name in &FORBIDDEN_RESPONSE_HEADERS {
323					remove_all_header_entries(&mut headers.headers, name);
324				}
325			}
326			ResponseTaint::Opaque => {
327				response.kind = ResponseKind::Opaque;
328				response.url = None;
329				response.status = None;
330				response.status_text = None;
331				response.body = None;
332
333				headers.headers.clear();
334			}
335		}
336	}
337
338	response
339}
340
341async fn http_fetch(
342	cx: &Context, request: &mut Request, client: Client, taint: ResponseTaint, redirections: u8,
343) -> (Response, bool) {
344	let response = http_network_fetch(cx, request, client.clone(), false).await;
345	match response.status {
346		Some(status) if status.is_redirection() => match request.redirect {
347			RequestRedirect::Follow => (
348				http_redirect_fetch(cx, request, response, client, taint, redirections).await,
349				false,
350			),
351			RequestRedirect::Error => (network_error(), false),
352			RequestRedirect::Manual => (response, true),
353		},
354		_ => (response, false),
355	}
356}
357
358#[async_recursion(?Send)]
359async fn http_network_fetch(cx: &Context, request: &Request, client: Client, is_new: bool) -> Response {
360	let headers = Object::from(unsafe { Local::from_heap(&request.headers) });
361	let mut headers = Headers::get_mut_private(cx, &headers).unwrap().headers.clone();
362
363	let length = request
364		.body
365		.as_ref()
366		.and_then(FetchBody::len)
367		.or_else(|| (request.body.is_none() && matches!(request.method, Method::POST | Method::PUT)).then_some(0));
368
369	if let Some(length) = length {
370		headers.append(CONTENT_LENGTH, HeaderValue::from_str(&length.to_string()).unwrap());
371	}
372
373	if let Referrer::Url(url) = &request.referrer {
374		headers.append(REFERER, HeaderValue::from_str(url.as_str()).unwrap());
375	}
376
377	if !headers.contains_key(USER_AGENT) {
378		headers.append(USER_AGENT, HeaderValue::from_static(DEFAULT_USER_AGENT));
379	}
380
381	let mut cache = request.cache;
382	if cache == RequestCache::Default
383		&& (headers.contains_key(IF_MODIFIED_SINCE)
384			|| headers.contains_key(IF_NONE_MATCH)
385			|| headers.contains_key(IF_UNMODIFIED_SINCE)
386			|| headers.contains_key(IF_MATCH)
387			|| headers.contains_key(IF_RANGE))
388	{
389		cache = RequestCache::NoStore;
390	}
391
392	if cache == RequestCache::NoCache && !headers.contains_key(CACHE_CONTROL) {
393		headers.append(CACHE_CONTROL, HeaderValue::from_static("max-age=0"));
394	}
395
396	if cache == RequestCache::NoStore || cache == RequestCache::Reload {
397		if !headers.contains_key(PRAGMA) {
398			headers.append(PRAGMA, HeaderValue::from_static("no-cache"));
399		}
400		if !headers.contains_key(CACHE_CONTROL) {
401			headers.append(CACHE_CONTROL, HeaderValue::from_static("no-cache"));
402		}
403	}
404
405	if headers.contains_key(RANGE) {
406		headers.append(ACCEPT_ENCODING, HeaderValue::from_static("identity"));
407	}
408
409	if !headers.contains_key(HOST) {
410		let host = request
411			.url
412			.host_str()
413			.map(|host| {
414				if let Some(port) = request.url.port() {
415					format!("{host}:{port}")
416				} else {
417					String::from(host)
418				}
419			})
420			.unwrap();
421		headers.append(HOST, HeaderValue::from_str(&host).unwrap());
422	}
423
424	if request.cache == RequestCache::OnlyIfCached {
425		return network_error();
426	}
427
428	let range_requested = headers.contains_key(RANGE);
429
430	let uri = url_to_uri(&request.url).unwrap();
431	let mut builder = hyper::Request::builder().method(request.method.clone()).uri(uri);
432	*builder.headers_mut().unwrap() = headers;
433	let body = request.body.as_ref().map_or_else(|| Body::Empty, FetchBody::to_http_body);
434	let req = builder.body(body).unwrap();
435
436	let mut response = match client.request(req).await {
437		Ok(response) => {
438			let response = response.map(|incoming| Body::Incoming { incoming });
439			let (headers, response) = Response::from_hyper(response, request.url.clone());
440
441			let headers = Headers {
442				reflector: Reflector::default(),
443				headers,
444				kind: HeadersKind::Immutable,
445			};
446			response.headers.set(Headers::new_object(cx, Box::new(headers)));
447			response
448		}
449		Err(_) => return network_error(),
450	};
451
452	response.range_requested = range_requested;
453
454	if response.status == Some(StatusCode::PROXY_AUTHENTICATION_REQUIRED) && !request.client_window {
455		return network_error();
456	}
457
458	if response.status == Some(StatusCode::MISDIRECTED_REQUEST)
459		&& !is_new
460		&& !request.body.as_ref().is_some_and(FetchBody::is_stream)
461	{
462		return http_network_fetch(cx, request, client, true).await;
463	}
464
465	response
466}
467
468async fn http_redirect_fetch(
469	cx: &Context, request: &mut Request, response: Response, client: Client, taint: ResponseTaint, redirections: u8,
470) -> Response {
471	let headers = Object::from(unsafe { Local::from_heap(&response.headers) });
472	let headers = Headers::get_private(cx, &headers).unwrap();
473	let mut location = headers.headers.get_all(LOCATION).into_iter();
474	let location = match location.size_hint().1 {
475		Some(0) => return response,
476		None => return network_error(),
477		_ => {
478			let location = location.next().unwrap();
479			match Url::options()
480				.base_url(response.url.as_ref())
481				.parse(str::from_utf8(location.as_bytes()).unwrap())
482			{
483				Ok(mut url) => {
484					if url.fragment().is_none() {
485						url.set_fragment(response.url.as_ref().and_then(Url::fragment));
486					}
487					url
488				}
489				Err(_) => return network_error(),
490			}
491		}
492	};
493
494	if !(location.scheme() == "https" || location.scheme() == "http") {
495		return network_error();
496	}
497
498	if redirections >= 20 {
499		return network_error();
500	}
501
502	if taint == ResponseTaint::Cors && (location.username() != "" || location.password().is_some()) {
503		return network_error();
504	}
505
506	if response.status != Some(StatusCode::SEE_OTHER) && request.body.as_ref().is_some_and(FetchBody::is_stream) {
507		return network_error();
508	}
509
510	if ((response.status == Some(StatusCode::MOVED_PERMANENTLY) || response.status == Some(StatusCode::FOUND))
511		&& request.method == Method::POST)
512		|| (response.status == Some(StatusCode::SEE_OTHER) && !matches!(request.method, Method::GET | Method::HEAD))
513	{
514		request.method = Method::GET;
515		request.body = None;
516		let headers = Object::from(unsafe { Local::from_heap(&request.headers) });
517		let headers = Headers::get_mut_private(cx, &headers).unwrap();
518		remove_all_header_entries(&mut headers.headers, &CONTENT_ENCODING);
519		remove_all_header_entries(&mut headers.headers, &CONTENT_LANGUAGE);
520		remove_all_header_entries(&mut headers.headers, &CONTENT_LOCATION);
521		remove_all_header_entries(&mut headers.headers, &CONTENT_TYPE);
522	}
523
524	request.locations.push(location.clone());
525	request.url = location;
526
527	let policy = headers.headers.get_all(REFERRER_POLICY).into_iter().rev();
528	let policy = policy
529		.filter(|v| !v.is_empty())
530		.find_map(|v| ReferrerPolicy::from_str(str::from_utf8(v.as_bytes()).unwrap()).ok());
531	if let Some(policy) = policy {
532		request.referrer_policy = policy;
533	}
534
535	main_fetch(cx, request, client, redirections + 1).await
536}
537
538pub fn define(cx: &Context, global: &Object) -> bool {
539	let _ = GLOBAL_CLIENT.set(default_client());
540	global.define_method(cx, "fetch", fetch, 1, PropertyFlags::CONSTANT_ENUMERATED);
541	Headers::init_class(cx, global).0 && Request::init_class(cx, global).0 && Response::init_class(cx, global).0
542}