mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
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:
parent
fc0d4d8157
commit
457d8a8a5c
3 changed files with 320 additions and 188 deletions
|
@ -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 fetchParams’s 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 request’s window is "no-window" and request’s redirect mode is "error", then set
|
// Step 8.1: If request’s window is "no-window" and request’s 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 httpRequest’s body’s length, if httpRequest’s body is
|
// Step 8.5 Let contentLength be httpRequest’s body’s length, if httpRequest’s 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 httpRequest’s body is null and httpRequest’s 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 httpRequest’s header list.
|
let mut content_length_header_value = None;
|
||||||
if let Some(content_length_value) = content_length_value {
|
|
||||||
http_request
|
// Step 8.7 If httpRequest’s body is null and httpRequest’s 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 httpRequest’s 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 httpRequest’s 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 httpRequest’s 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 httpRequest’s referrer is a URL, then:
|
// Step 8.11: If httpRequest’s 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 httpRequest’s referrer, serialized and isomorphic
|
// Step 8.11.1: Let referrerValue be httpRequest’s 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 httpRequest’s initiator is "prefetch", then set a structured field value given
|
// Step 8.14: If httpRequest’s initiator is "prefetch", then set a structured field value given
|
||||||
// (`Sec-Purpose`, the token "prefetch") in httpRequest’s header list.
|
// (`Sec-Purpose`, the token "prefetch") in httpRequest’s header list.
|
||||||
|
@ -1317,8 +1318,7 @@ async fn http_network_or_cache_fetch(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 5.18
|
// TODO(#33616) Step 8.22 If there’s a proxy-authentication entry, use it as appropriate.
|
||||||
// TODO If there’s 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 httpRequest’s 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 httpRequest’s method is unsafe and forwardResponse’s 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 forwardResponse’s 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, ¤t_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 response’s URL list to a clone of httpRequest’s URL list.
|
||||||
// FIXME: Figure out what to do with request window objects
|
// TODO(#33616): Step 12. If httpRequest’s header list contains `Range`,
|
||||||
|
// then set response’s range-requested flag.
|
||||||
|
// TODO(#33616): Step 13 Set response’s request-includes-credentials to includeCredentials.
|
||||||
|
|
||||||
|
// Step 14. If response’s status is 401, httpRequest’s response tainting is not "cors",
|
||||||
|
// includeCredentials is true, and request’s 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 request’s 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 request’s 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 response’s status is 407, then:
|
||||||
if response.status == StatusCode::PROXY_AUTHENTICATION_REQUIRED {
|
if response.status == StatusCode::PROXY_AUTHENTICATION_REQUIRED {
|
||||||
// Step 1
|
// Step 15.1 If request’s 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 request’s 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:
|
||||||
|
// * response’s status is 421
|
||||||
|
// * isNewConnectionFetch is false
|
||||||
|
// * request’s body is null, or request’s body is non-null and request’s body’s 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, ¤t_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: request’s 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 request’s 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 url’s origin is not same origin with lastURL’s origin and
|
||||||
|
// request’s origin is not same origin with lastURL’s 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: request’s 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 request’s 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: request’s 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 request’s response tainting is "cors" or request’s mode is "websocket",
|
||||||
|
// then append (`Origin`, serializedOrigin) to request’s header list.
|
||||||
|
if request.response_tainting == ResponseTainting::CorsTainting ||
|
||||||
|
matches!(request.mode, RequestMode::WebSocket { .. })
|
||||||
|
{
|
||||||
|
request.headers.typed_insert(serialized_origin);
|
||||||
|
}
|
||||||
|
// Step 4. Otherwise, if request’s method is neither `GET` nor `HEAD`, then:
|
||||||
|
else if !matches!(request.method, Method::GET | Method::HEAD) {
|
||||||
|
// Step 4.1 If request’s mode is not "cors", then switch on request’s 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 request’s origin is a tuple origin, its scheme is "https", and
|
||||||
|
// request’s current URL’s 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 request’s origin is not same origin with request’s current URL’s 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 request’s header list.
|
||||||
|
request.headers.typed_insert(serialized_origin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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 requestObject’s 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 requestObject’s signal is aborted, then: [..]
|
||||||
|
|
||||||
|
// Step 5. Let globalObject be request’s client’s global object.
|
||||||
|
// NOTE: We already get the global object as an argument
|
||||||
|
|
||||||
|
// Step 6. If globalObject is a ServiceWorkerGlobalScope object, then set request’s
|
||||||
|
// 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));
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue