Bring http_network_or_cache_fetch closer to the spec (#33531)

* Document "fetch" method

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Bring http_network_or_cache_fetch closer to the spec

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* fix test-tidy errors

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Move all code into http_loader.rs

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Don't panic if hyper/servo disagree about valid origins

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Add "otherwise" to spec comment

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Convert FIXME's to TODOs when appropriate

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Remove TODO about No-Store cache directive

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Remove indentation from multiline spec comments

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Add fetch assertions

This is from a spec update where assertions
about requests origin not being client
were added.

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Add note about serializing headers

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Add TODO about partitioning http cache

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Convert FIXME to TODO in script/

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Link to relevant issue for TODO comments

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

---------

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
This commit is contained in:
Simon Wülker 2024-10-08 07:36:46 +02:00 committed by GitHub
parent fc0d4d8157
commit 457d8a8a5c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 320 additions and 188 deletions

View file

@ -22,7 +22,7 @@ use headers::{
AccessControlAllowCredentials, AccessControlAllowHeaders, AccessControlAllowMethods, AccessControlAllowCredentials, AccessControlAllowHeaders, AccessControlAllowMethods,
AccessControlAllowOrigin, AccessControlMaxAge, AccessControlRequestHeaders, AccessControlAllowOrigin, AccessControlMaxAge, AccessControlRequestHeaders,
AccessControlRequestMethod, Authorization, CacheControl, ContentLength, HeaderMapExt, AccessControlRequestMethod, Authorization, CacheControl, ContentLength, HeaderMapExt,
IfModifiedSince, LastModified, Origin as HyperOrigin, Pragma, Referer, UserAgent, IfModifiedSince, LastModified, Pragma, Referer, UserAgent,
}; };
use http::header::{ use http::header::{
self, HeaderValue, ACCEPT, AUTHORIZATION, CONTENT_ENCODING, CONTENT_LANGUAGE, CONTENT_LOCATION, self, HeaderValue, ACCEPT, AUTHORIZATION, CONTENT_ENCODING, CONTENT_LANGUAGE, CONTENT_LOCATION,
@ -1082,19 +1082,6 @@ pub async fn http_redirect_fetch(
fetch_response fetch_response
} }
fn try_immutable_origin_to_hyper_origin(url_origin: &ImmutableOrigin) -> Option<HyperOrigin> {
match *url_origin {
ImmutableOrigin::Opaque(_) => Some(HyperOrigin::NULL),
ImmutableOrigin::Tuple(ref scheme, ref host, ref port) => {
let port = match (scheme.as_ref(), port) {
("http", 80) | ("https", 443) => None,
_ => Some(*port),
};
HyperOrigin::try_from_parts(scheme, &host.to_string(), port).ok()
},
}
}
/// [HTTP network or cache fetch](https://fetch.spec.whatwg.org#http-network-or-cache-fetch) /// [HTTP network or cache fetch](https://fetch.spec.whatwg.org#http-network-or-cache-fetch)
#[async_recursion] #[async_recursion]
async fn http_network_or_cache_fetch( async fn http_network_or_cache_fetch(
@ -1104,25 +1091,32 @@ async fn http_network_or_cache_fetch(
done_chan: &mut DoneChannel, done_chan: &mut DoneChannel,
context: &FetchContext, context: &FetchContext,
) -> Response { ) -> Response {
// Step 3: Let httpRequest be null. // Step 1. Let request be fetchParamss request.
// NOTE: We get request as an argument (Fetchparams are not implemented, see #33616)
// Step 3. Let httpRequest be null.
let mut http_request; let mut http_request;
// Step 4: Let response be null. // Step 4. Let response be null.
let mut response: Option<Response> = None; let mut response: Option<Response> = None;
// Step 7: Let the revalidatingFlag be unset. // Step 7. Let the revalidatingFlag be unset.
let mut revalidating_flag = false; let mut revalidating_flag = false;
// TODO(#33616): Step 8. Run these steps, but abort when fetchParams is canceled:
// Step 8.1: If requests window is "no-window" and requests redirect mode is "error", then set // Step 8.1: If requests window is "no-window" and requests redirect mode is "error", then set
// httpFetchParams to fetchParams and httpRequest to request. // httpFetchParams to fetchParams and httpRequest to request.
let request_has_no_window = request.window == RequestWindow::NoWindow; let request_has_no_window = request.window == RequestWindow::NoWindow;
let http_request = if request_has_no_window && request.redirect_mode == RedirectMode::Error { let http_request = if request_has_no_window && request.redirect_mode == RedirectMode::Error {
request request
} else { }
// Step 8.2 Otherwise:
else {
// Step 8.2.1: Set httpRequest to a clone of request. // Step 8.2.1: Set httpRequest to a clone of request.
http_request = request.clone(); http_request = request.clone();
// TODO(#33616): Step 8.2.2-8.2.3
&mut http_request &mut http_request
}; };
@ -1141,34 +1135,47 @@ async fn http_network_or_cache_fetch(
// Step 8.4: If Cross-Origin-Embedder-Policy allows credentials with request returns false, then // Step 8.4: If Cross-Origin-Embedder-Policy allows credentials with request returns false, then
// set includeCredentials to false. // set includeCredentials to false.
// TODO: Requires request's client object // TODO(#33616): Requires request's client object
// Step 8.5: Let contentLength be httpRequests bodys length, if httpRequests body is // Step 8.5 Let contentLength be httpRequests bodys length, if httpRequests body is non-null;
// non-null; otherwise null. // otherwise null.
let content_length_value = match http_request.body { let content_length = http_request
Some(ref http_request_body) => http_request_body.len().map(|size| size as u64), .body
// Step 8.7: If httpRequests body is null and httpRequests method is `POST` or `PUT`, then .as_ref()
// set contentLengthHeaderValue to `0`. .map(|body| body.len().map(|size| size as u64))
None => match http_request.method { .flatten();
Method::POST | Method::PUT => Some(0),
_ => None,
},
};
// Step 8.9: If contentLengthHeaderValue is non-null, then append (`Content-Length`, // Step 8.6 Let contentLengthHeaderValue be null.
// contentLengthHeaderValue) to httpRequests header list. let mut content_length_header_value = None;
if let Some(content_length_value) = content_length_value {
http_request // Step 8.7 If httpRequests body is null and httpRequests method is `POST` or `PUT`,
.headers // then set contentLengthHeaderValue to `0`.
.typed_insert(ContentLength(content_length_value)); if http_request.body.is_none() && matches!(http_request.method, Method::POST | Method::PUT) {
content_length_header_value = Some(0);
} }
// Step 8.10: If contentLength is non-null and httpRequests keepalive is true, then: // Step 8.8 If contentLength is non-null, then set contentLengthHeaderValue to contentLength,
// TODO Keepalive requires request's client object's fetch group // serialized and isomorphic encoded.
// NOTE: The header will later be serialized using HeaderMap::typed_insert
if let Some(content_length) = content_length {
content_length_header_value = Some(content_length);
};
// Step 8.9 If contentLengthHeaderValue is non-null, then append (`Content-Length`, contentLengthHeaderValue)
// to httpRequests header list.
if let Some(content_length_header_value) = content_length_header_value {
http_request
.headers
.typed_insert(ContentLength(content_length_header_value));
}
// Step 8.10 If contentLength is non-null and httpRequests keepalive is true, then:
if content_length.is_some() && http_request.keep_alive {
// TODO(#33616) Keepalive requires request's client object's fetch group
}
// Step 8.11: If httpRequests referrer is a URL, then: // Step 8.11: If httpRequests referrer is a URL, then:
match http_request.referrer { match http_request.referrer {
Referrer::NoReferrer => (),
Referrer::ReferrerUrl(ref http_request_referrer) | Referrer::ReferrerUrl(ref http_request_referrer) |
Referrer::Client(ref http_request_referrer) => { Referrer::Client(ref http_request_referrer) => {
// Step 8.11.1: Let referrerValue be httpRequests referrer, serialized and isomorphic // Step 8.11.1: Let referrerValue be httpRequests referrer, serialized and isomorphic
@ -1180,23 +1187,17 @@ async fn http_network_or_cache_fetch(
// This error should only happen in cases where hyper and rust-url disagree // This error should only happen in cases where hyper and rust-url disagree
// about how to parse a referer. // about how to parse a referer.
// https://github.com/servo/servo/issues/24175 // https://github.com/servo/servo/issues/24175
error!("Failed to parse {} as referer", http_request_referrer); error!("Failed to parse {} as referrer", http_request_referrer);
} }
}, },
_ => {},
}; };
// Step 8.12: Append a request `Origin` header for httpRequest. // Step 8.12 Append a request `Origin` header for httpRequest.
if cors_flag || (http_request.method != Method::GET && http_request.method != Method::HEAD) { append_a_request_origin_header(http_request);
debug_assert_ne!(http_request.origin, Origin::Client);
if let Origin::Origin(ref url_origin) = http_request.origin {
if let Some(hyper_origin) = try_immutable_origin_to_hyper_origin(url_origin) {
http_request.headers.typed_insert(hyper_origin)
}
}
}
// Step 8.13: Append the Fetch metadata headers for httpRequest. // Step 8.13 Append the Fetch metadata headers for httpRequest.
// TODO Implement Sec-Fetch-* headers // TODO(#33616) Implement Sec-Fetch-* headers
// Step 8.14: If httpRequests initiator is "prefetch", then set a structured field value given // Step 8.14: If httpRequests initiator is "prefetch", then set a structured field value given
// (`Sec-Purpose`, the token "prefetch") in httpRequests header list. // (`Sec-Purpose`, the token "prefetch") in httpRequests header list.
@ -1317,8 +1318,7 @@ async fn http_network_or_cache_fetch(
} }
} }
// Step 5.18 // TODO(#33616) Step 8.22 If theres a proxy-authentication entry, use it as appropriate.
// TODO If theres a proxy-authentication entry, use it as appropriate.
// If the cache is not ready to construct a response, wait. // If the cache is not ready to construct a response, wait.
// //
@ -1355,11 +1355,16 @@ async fn http_network_or_cache_fetch(
} }
} }
// Step 5.19 // TODO(#33616): Step 8.23 Set httpCache to the result of determining the
// HTTP cache partition, given httpRequest.
if let Ok(http_cache) = context.state.http_cache.read() { if let Ok(http_cache) = context.state.http_cache.read() {
if let Some(response_from_cache) = // Step 8.25.1 Set storedResponse to the result of selecting a response from the httpCache,
http_cache.construct_response(http_request, done_chan) // possibly needing validation, as per the "Constructing Responses from Caches"
{ // chapter of HTTP Caching, if any.
let stored_response = http_cache.construct_response(http_request, done_chan);
// Step 8.25.2 If storedResponse is non-null, then:
if let Some(response_from_cache) = stored_response {
let response_headers = response_from_cache.response.headers.clone(); let response_headers = response_from_cache.response.headers.clone();
// Substep 1, 2, 3, 4 // Substep 1, 2, 3, 4
let (cached_response, needs_revalidation) = let (cached_response, needs_revalidation) =
@ -1376,6 +1381,7 @@ async fn http_network_or_cache_fetch(
response_from_cache.needs_validation, response_from_cache.needs_validation,
), ),
}; };
if needs_revalidation { if needs_revalidation {
revalidating_flag = true; revalidating_flag = true;
// Substep 5 // Substep 5
@ -1472,12 +1478,11 @@ async fn http_network_or_cache_fetch(
wait_for_cached_response(done_chan, &mut response).await; wait_for_cached_response(done_chan, &mut response).await;
// Step 6 // TODO(#33616): Step 9. If aborted, then return the appropriate network error for fetchParams.
// TODO: https://infra.spec.whatwg.org/#if-aborted
// Step 7 // Step 10. If response is null, then:
if response.is_none() { if response.is_none() {
// Substep 1 // Step 10.1 If httpRequests cache mode is "only-if-cached", then return a network error.
if http_request.cache_mode == CacheMode::OnlyIfCached { if http_request.cache_mode == CacheMode::OnlyIfCached {
// The cache will not be updated, // The cache will not be updated,
// set its state to ready to construct. // set its state to ready to construct.
@ -1486,20 +1491,22 @@ async fn http_network_or_cache_fetch(
"Couldn't find response in cache".into(), "Couldn't find response in cache".into(),
)); ));
} }
}
// More Step 7 // Step 10.2 Let forwardResponse be the result of running HTTP-network fetch given httpFetchParams,
if response.is_none() { // includeCredentials, and isNewConnectionFetch.
// Substep 2
let forward_response = let forward_response =
http_network_fetch(http_request, include_credentials, done_chan, context).await; http_network_fetch(http_request, include_credentials, done_chan, context).await;
// Substep 3
// Step 10.3 If httpRequests method is unsafe and forwardResponses status is in the range 200 to 399,
// inclusive, invalidate appropriate stored responses in httpCache, as per the
// "Invalidating Stored Responses" chapter of HTTP Caching, and set storedResponse to null.
if forward_response.status.in_range(200..=399) && !http_request.method.is_safe() { if forward_response.status.in_range(200..=399) && !http_request.method.is_safe() {
if let Ok(mut http_cache) = context.state.http_cache.write() { if let Ok(mut http_cache) = context.state.http_cache.write() {
http_cache.invalidate(http_request, &forward_response); http_cache.invalidate(http_request, &forward_response);
} }
} }
// Substep 4 // Step 10.4 If the revalidatingFlag is set and forwardResponses status is 304, then:
if revalidating_flag && forward_response.status == StatusCode::NOT_MODIFIED { if revalidating_flag && forward_response.status == StatusCode::NOT_MODIFIED {
if let Ok(mut http_cache) = context.state.http_cache.write() { if let Ok(mut http_cache) = context.state.http_cache.write() {
// Ensure done_chan is None, // Ensure done_chan is None,
@ -1510,88 +1517,29 @@ async fn http_network_or_cache_fetch(
wait_for_cached_response(done_chan, &mut response).await; wait_for_cached_response(done_chan, &mut response).await;
} }
// Substep 5 // Step 10.5 If response is null, then:
if response.is_none() { if response.is_none() {
// Step 10.5.1 Set response to forwardResponse.
let forward_response = response.insert(forward_response);
// Per https://httpwg.org/specs/rfc9111.html#response.cacheability we must not cache responses
// if the No-Store directive is present
if http_request.cache_mode != CacheMode::NoStore { if http_request.cache_mode != CacheMode::NoStore {
// Subsubstep 2, doing it first to avoid a clone of forward_response. // Step 10.5.2 Store httpRequest and forwardResponse in httpCache, as per the
// "Storing Responses in Caches" chapter of HTTP Caching.
if let Ok(mut http_cache) = context.state.http_cache.write() { if let Ok(mut http_cache) = context.state.http_cache.write() {
http_cache.store(http_request, &forward_response); http_cache.store(http_request, forward_response);
} }
} }
// Subsubstep 1
response = Some(forward_response);
} }
} }
let mut response = response.unwrap();
// The cache has been updated, set its state to ready to construct. // The cache has been updated, set its state to ready to construct.
update_http_cache_state(context, http_request); update_http_cache_state(context, http_request);
// Step 8 let mut response = response.unwrap();
// TODO: if necessary set response's range-requested flag
// Step 9
// https://fetch.spec.whatwg.org/#cross-origin-resource-policy-check
#[derive(PartialEq)]
enum CrossOriginResourcePolicy {
Allowed,
Blocked,
}
fn cross_origin_resource_policy_check(
request: &Request,
response: &Response,
) -> CrossOriginResourcePolicy {
// Step 1
if request.mode != RequestMode::NoCors {
return CrossOriginResourcePolicy::Allowed;
}
// Step 2
let current_url_origin = request.current_url().origin();
let same_origin = if let Origin::Origin(ref origin) = request.origin {
*origin == request.current_url().origin()
} else {
false
};
if same_origin {
return CrossOriginResourcePolicy::Allowed;
}
// Step 3
let policy = response
.headers
.get(HeaderName::from_static("cross-origin-resource-policy"))
.map(|h| h.to_str().unwrap_or(""))
.unwrap_or("");
// Step 4
if policy == "same-origin" {
return CrossOriginResourcePolicy::Blocked;
}
// Step 5
if let Origin::Origin(ref request_origin) = request.origin {
let schemeless_same_origin =
is_schemelessy_same_site(request_origin, &current_url_origin);
if schemeless_same_origin &&
(request_origin.scheme() == Some("https") ||
response.https_state == HttpsState::None)
{
return CrossOriginResourcePolicy::Allowed;
}
};
// Step 6
if policy == "same-site" {
return CrossOriginResourcePolicy::Blocked;
}
CrossOriginResourcePolicy::Allowed
}
// FIXME: The spec doesn't tell us to do this *here*, but if we don't do it then
// tests fail. Where should we do it instead? See also #33615
if http_request.response_tainting != ResponseTainting::CorsTainting && if http_request.response_tainting != ResponseTainting::CorsTainting &&
cross_origin_resource_policy_check(http_request, &response) == cross_origin_resource_policy_check(http_request, &response) ==
CrossOriginResourcePolicy::Blocked CrossOriginResourcePolicy::Blocked
@ -1601,26 +1549,31 @@ async fn http_network_or_cache_fetch(
)); ));
} }
// Step 10 // TODO(#33616): Step 11. Set responses URL list to a clone of httpRequests URL list.
// FIXME: Figure out what to do with request window objects // TODO(#33616): Step 12. If httpRequests header list contains `Range`,
// then set responses range-requested flag.
// TODO(#33616): Step 13 Set responses request-includes-credentials to includeCredentials.
// Step 14. If responses status is 401, httpRequests response tainting is not "cors",
// includeCredentials is true, and requests window is an environment settings object, then:
// TODO(#33616): Figure out what to do with request window objects
if let (Some(StatusCode::UNAUTHORIZED), false, true) = if let (Some(StatusCode::UNAUTHORIZED), false, true) =
(response.status.try_code(), cors_flag, include_credentials) (response.status.try_code(), cors_flag, include_credentials)
{ {
// Substep 1 // TODO: Step 14.1 Spec says requires testing on multiple WWW-Authenticate headers
// TODO: Spec says requires testing on multiple WWW-Authenticate headers
// Substep 2 // Step 14.2 If requests body is non-null, then:
if http_request.body.is_some() { if http_request.body.is_some() {
// TODO Implement body source // TODO Implement body source
} }
// Substep 3 // Step 14.3 If requests use-URL-credentials flag is unset or isAuthenticationFetch is true, then:
if !http_request.use_url_credentials || authentication_fetch_flag { if !http_request.use_url_credentials || authentication_fetch_flag {
// FIXME: Prompt the user for username and password from the window // TODO(#33616, #27439): Prompt the user for username and password from the window
// Wrong, but will have to do until we are able to prompt the user // Wrong, but will have to do until we are able to prompt the user
// otherwise this creates an infinite loop // otherwise this creates an infinite loop
// We basically pretend that the user declined to enter credentials // We basically pretend that the user declined to enter credentials (#33616)
return response; return response;
} }
@ -1628,7 +1581,7 @@ async fn http_network_or_cache_fetch(
// since we're about to start a new `http_network_or_cache_fetch`. // since we're about to start a new `http_network_or_cache_fetch`.
*done_chan = None; *done_chan = None;
// Substep 4 // Step 14.4 Set response to the result of running HTTP-network-or-cache fetch given fetchParams and true.
response = http_network_or_cache_fetch( response = http_network_or_cache_fetch(
http_request, http_request,
true, /* authentication flag */ true, /* authentication flag */
@ -1639,40 +1592,109 @@ async fn http_network_or_cache_fetch(
.await; .await;
} }
// Step 11 // Step 15. If responses status is 407, then:
if response.status == StatusCode::PROXY_AUTHENTICATION_REQUIRED { if response.status == StatusCode::PROXY_AUTHENTICATION_REQUIRED {
// Step 1 // Step 15.1 If requests window is "no-window", then return a network error.
if request_has_no_window { if request_has_no_window {
return Response::network_error(NetworkError::Internal( return Response::network_error(NetworkError::Internal(
"Can't find Window object".into(), "Can't find Window object".into(),
)); ));
} }
// Step 2 // (Step 15.2 does not exist, requires testing on Proxy-Authenticate headers)
// TODO: Spec says requires testing on Proxy-Authenticate headers
// Step 3 // TODO(#33616): Step 15.3 If fetchParams is canceled, then return
// FIXME: Prompt the user for proxy authentication credentials // the appropriate network error for fetchParams.
// TODO(#33616): Step 15.4 Prompt the end user as appropriate in requests window and store the
// result as a proxy-authentication entry.
// Step 15.5 Set response to the result of running HTTP-network-or-cache fetch given fetchParams.
// Wrong, but will have to do until we are able to prompt the user // Wrong, but will have to do until we are able to prompt the user
// otherwise this creates an infinite loop // otherwise this creates an infinite loop
// We basically pretend that the user declined to enter credentials // We basically pretend that the user declined to enter credentials (#33616)
return response; return response;
// Step 4
// return http_network_or_cache_fetch(request, authentication_fetch_flag,
// cors_flag, done_chan, context);
} }
// Step 12 // TODO(#33616): Step 16. If all of the following are true:
// * responses status is 421
// * isNewConnectionFetch is false
// * requests body is null, or requests body is non-null and requests bodys source is non-null
// then: [..]
// Step 17. If isAuthenticationFetch is true, then create an authentication entry for request and the given realm.
if authentication_fetch_flag { if authentication_fetch_flag {
// TODO Create the authentication entry for request and the given realm // TODO(#33616)
} }
// Step 13 // Step 18. Return response.
response response
} }
/// <https://fetch.spec.whatwg.org/#cross-origin-resource-policy-check>
///
/// This is obtained from [cross_origin_resource_policy_check]
#[derive(PartialEq)]
enum CrossOriginResourcePolicy {
Allowed,
Blocked,
}
// TODO(#33615): Judging from the name, this appears to be https://fetch.spec.whatwg.org/#cross-origin-resource-policy-check,
// but the steps aren't even close to the spec. Perhaps this needs to be rewritten?
fn cross_origin_resource_policy_check(
request: &Request,
response: &Response,
) -> CrossOriginResourcePolicy {
// Step 1
if request.mode != RequestMode::NoCors {
return CrossOriginResourcePolicy::Allowed;
}
// Step 2
let current_url_origin = request.current_url().origin();
let same_origin = if let Origin::Origin(ref origin) = request.origin {
*origin == request.current_url().origin()
} else {
false
};
if same_origin {
return CrossOriginResourcePolicy::Allowed;
}
// Step 3
let policy = response
.headers
.get(HeaderName::from_static("cross-origin-resource-policy"))
.map(|h| h.to_str().unwrap_or(""))
.unwrap_or("");
// Step 4
if policy == "same-origin" {
return CrossOriginResourcePolicy::Blocked;
}
// Step 5
if let Origin::Origin(ref request_origin) = request.origin {
let schemeless_same_origin = is_schemelessy_same_site(request_origin, &current_url_origin);
if schemeless_same_origin &&
(request_origin.scheme() == Some("https") ||
response.https_state == HttpsState::None)
{
return CrossOriginResourcePolicy::Allowed;
}
};
// Step 6
if policy == "same-site" {
return CrossOriginResourcePolicy::Blocked;
}
CrossOriginResourcePolicy::Allowed
}
// Convenience struct that implements Done, for setting responseEnd on function return // Convenience struct that implements Done, for setting responseEnd on function return
struct ResponseEndTimer(Option<Arc<Mutex<ResourceFetchTiming>>>); struct ResponseEndTimer(Option<Arc<Mutex<ResourceFetchTiming>>>);
@ -2223,3 +2245,115 @@ pub fn is_redirect_status(status: StatusCode) -> bool {
StatusCode::PERMANENT_REDIRECT StatusCode::PERMANENT_REDIRECT
) )
} }
/// <https://fetch.spec.whatwg.org/#concept-request-tainted-origin>
fn request_has_redirect_tainted_origin(request: &Request) -> bool {
// Step 1. Assert: requests origin is not "client".
let Origin::Origin(request_origin) = &request.origin else {
panic!("origin cannot be \"client\" at this point in time");
};
// Step 2. Let lastURL be null.
let mut last_url = None;
// Step 3. For each url of requests URL list:
for url in &request.url_list {
// Step 3.1 If lastURL is null, then set lastURL to url and continue.
let Some(last_url) = &mut last_url else {
last_url = Some(url);
continue;
};
// Step 3.2 If urls origin is not same origin with lastURLs origin and
// requests origin is not same origin with lastURLs origin, then return true.
if url.origin() != last_url.origin() && *request_origin != last_url.origin() {
return true;
}
// Step 3.3 Set lastURL to url.
*last_url = url;
}
// Step 4. Return false.
false
}
/// <https://fetch.spec.whatwg.org/#serializing-a-request-origin>
fn serialize_request_origin(request: &Request) -> headers::Origin {
// Step 1. Assert: requests origin is not "client".
let Origin::Origin(origin) = &request.origin else {
panic!("origin cannot be \"client\" at this point in time");
};
// Step 2. If request has a redirect-tainted origin, then return "null".
if request_has_redirect_tainted_origin(request) {
return headers::Origin::NULL;
}
// Step 3. Return requests origin, serialized.
match origin {
ImmutableOrigin::Opaque(_) => headers::Origin::NULL,
ImmutableOrigin::Tuple(scheme, host, port) => {
// TODO: Ensure that hyper/servo don't disagree about valid origin headers
headers::Origin::try_from_parts(scheme, &host.to_string(), *port)
.unwrap_or(headers::Origin::NULL)
},
}
}
/// <https://fetch.spec.whatwg.org/#append-a-request-origin-header>
pub fn append_a_request_origin_header(request: &mut Request) {
// Step 1. Assert: requests origin is not "client".
let Origin::Origin(request_origin) = &request.origin else {
panic!("origin cannot be \"client\" at this point in time");
};
// Step 2. Let serializedOrigin be the result of byte-serializing a request origin with request.
let mut serialized_origin = serialize_request_origin(request);
// Step 3. If requests response tainting is "cors" or requests mode is "websocket",
// then append (`Origin`, serializedOrigin) to requests header list.
if request.response_tainting == ResponseTainting::CorsTainting ||
matches!(request.mode, RequestMode::WebSocket { .. })
{
request.headers.typed_insert(serialized_origin);
}
// Step 4. Otherwise, if requests method is neither `GET` nor `HEAD`, then:
else if !matches!(request.method, Method::GET | Method::HEAD) {
// Step 4.1 If requests mode is not "cors", then switch on requests referrer policy:
if request.mode != RequestMode::CorsMode {
match request.referrer_policy {
Some(ReferrerPolicy::NoReferrer) => {
// Set serializedOrigin to `null`.
serialized_origin = headers::Origin::NULL;
},
Some(
ReferrerPolicy::NoReferrerWhenDowngrade |
ReferrerPolicy::StrictOrigin |
ReferrerPolicy::StrictOriginWhenCrossOrigin,
) => {
// If requests origin is a tuple origin, its scheme is "https", and
// requests current URLs scheme is not "https", then set serializedOrigin to `null`.
if let ImmutableOrigin::Tuple(scheme, _, _) = &request_origin {
if scheme == "https" && request.current_url().scheme() != "https" {
serialized_origin = headers::Origin::NULL;
}
}
},
Some(ReferrerPolicy::SameOrigin) => {
// If requests origin is not same origin with requests current URLs origin,
// then set serializedOrigin to `null`.
if *request_origin != request.current_url().origin() {
serialized_origin = headers::Origin::NULL;
}
},
_ => {
// Otherwise, do nothing.
},
};
}
// Step 4.2. Append (`Origin`, serializedOrigin) to requests header list.
request.headers.typed_insert(serialized_origin);
}
}

View file

@ -138,7 +138,7 @@ fn request_init_from_request(request: NetTraitsRequest) -> RequestBuilder {
} }
} }
// https://fetch.spec.whatwg.org/#fetch-method /// <https://fetch.spec.whatwg.org/#fetch-method>
#[allow(crown::unrooted_must_root, non_snake_case)] #[allow(crown::unrooted_must_root, non_snake_case)]
pub fn Fetch( pub fn Fetch(
global: &GlobalScope, global: &GlobalScope,
@ -148,33 +148,47 @@ pub fn Fetch(
) -> Rc<Promise> { ) -> Rc<Promise> {
let core_resource_thread = global.core_resource_thread(); let core_resource_thread = global.core_resource_thread();
// Step 1 // Step 1. Let p be a new promise.
let promise = Promise::new_in_current_realm(comp); let promise = Promise::new_in_current_realm(comp);
let response = Response::new(global);
// Step 2 // Step 7. Let responseObject be null.
// NOTE: We do initialize the object earlier earlier so we can use it to track errors
let response = Response::new(global);
response.Headers().set_guard(Guard::Immutable);
// Step 2. Let requestObject be the result of invoking the initial value of Request as constructor
// with input and init as arguments. If this throws an exception, reject p with it and return p.
let request = match Request::Constructor(global, None, CanGc::note(), input, init) { let request = match Request::Constructor(global, None, CanGc::note(), input, init) {
Err(e) => { Err(e) => {
response.error_stream(e.clone()); response.error_stream(e.clone());
promise.reject_error(e); promise.reject_error(e);
return promise; return promise;
}, },
Ok(r) => r.get_request(), Ok(r) => {
// Step 3. Let request be requestObjects request.
r.get_request()
},
}; };
let timing_type = request.timing_type(); let timing_type = request.timing_type();
let mut request_init = request_init_from_request(request); let mut request_init = request_init_from_request(request);
request_init.csp_list.clone_from(&global.get_csp_list()); request_init.csp_list.clone_from(&global.get_csp_list());
// Step 3 // TODO: Step 4. If requestObjects signal is aborted, then: [..]
// Step 5. Let globalObject be requests clients global object.
// NOTE: We already get the global object as an argument
// Step 6. If globalObject is a ServiceWorkerGlobalScope object, then set requests
// service-workers mode to "none".
if global.downcast::<ServiceWorkerGlobalScope>().is_some() { if global.downcast::<ServiceWorkerGlobalScope>().is_some() {
request_init.service_workers_mode = ServiceWorkersMode::None; request_init.service_workers_mode = ServiceWorkersMode::None;
} }
// Step 4 // TODO: Steps 8-11, abortcontroller stuff
response.Headers().set_guard(Guard::Immutable);
// Step 5 // Step 12. Set controller to the result of calling fetch given request and
// processResponse given response being these steps: [..]
let (action_sender, action_receiver) = ipc::channel().unwrap(); let (action_sender, action_receiver) = ipc::channel().unwrap();
let fetch_context = Arc::new(Mutex::new(FetchContext { let fetch_context = Arc::new(Mutex::new(FetchContext {
fetch_promise: Some(TrustedPromise::new(promise.clone())), fetch_promise: Some(TrustedPromise::new(promise.clone())),
@ -200,6 +214,7 @@ pub fn Fetch(
)) ))
.unwrap(); .unwrap();
// Step 13. Return p.
promise promise
} }
@ -263,6 +278,7 @@ impl FetchResponseListener for FetchContext {
}, },
}, },
} }
// Step 4.3 // Step 4.3
promise.resolve_native(&self.response_object.root()); promise.resolve_native(&self.response_object.root());
self.fetch_promise = Some(TrustedPromise::new(promise)); self.fetch_promise = Some(TrustedPromise::new(promise));

View file

@ -1,13 +1,4 @@
[assorted.window.html] [assorted.window.html]
[Origin header and 308 redirect]
expected: FAIL
[Origin header and POST cross-origin fetch no-cors mode with Referrer-Policy no-referrer]
expected: FAIL
[Origin header and POST same-origin fetch no-cors mode with Referrer-Policy no-referrer]
expected: FAIL
[Origin header and POST cross-origin navigation with Referrer-Policy origin-when-cross-origin] [Origin header and POST cross-origin navigation with Referrer-Policy origin-when-cross-origin]
expected: FAIL expected: FAIL
@ -17,17 +8,8 @@
[Origin header and POST navigation] [Origin header and POST navigation]
expected: FAIL expected: FAIL
[Origin header and POST cross-origin navigation with Referrer-Policy no-referrer]
expected: FAIL
[Origin header and POST cross-origin navigation with Referrer-Policy same-origin] [Origin header and POST cross-origin navigation with Referrer-Policy same-origin]
expected: FAIL expected: FAIL
[Origin header and POST cross-origin fetch no-cors mode with Referrer-Policy same-origin]
expected: FAIL
[Origin header and POST same-origin navigation with Referrer-Policy no-referrer]
expected: FAIL
[Origin header and POST cross-origin navigation with Referrer-Policy no-referrer-when-downgrade] [Origin header and POST cross-origin navigation with Referrer-Policy no-referrer-when-downgrade]
expected: FAIL expected: FAIL