1use 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, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, 87, 95, 101, 102, 103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, 139, 143, 161, 179, 389, 427, 465, 512, 513, 514, 515, 526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993, 995, 1719, 1720, 1723, 2049, 3659, 4045, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668, 6669, 6697, 10080, ];
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 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) || 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}