mirror of
https://github.com/lexmount/moli.git
synced 2026-09-28 08:01:37 +00:00
fix(fetch): preserve service worker response types and CORS headers
Carry the original response filter and validation state through Window streams and Worker completions, including paused responses. Synthesized responses remain basic, and already fetched bodies are not filtered by ORB again. Keep client CORP/COEP checks against actual network response URLs. Retain internal headers separately from the CORS public header view through clone, deferred Cache.put, persistent Cache storage and respondWith. Match Vary using the public header view and keep filtered cached headers immutable.
This commit is contained in:
@@ -119,6 +119,35 @@ fn fixture_response(request: &IncomingRequest) -> (u16, String, String) {
|
||||
.to_owned(),
|
||||
);
|
||||
}
|
||||
if url.path() == "/sandboxed-fetch-filter" {
|
||||
return (
|
||||
200,
|
||||
"Content-Type: text/html\r\nContent-Security-Policy: sandbox allow-scripts\r\n"
|
||||
.to_owned(),
|
||||
r#"<!doctype html><script>
|
||||
(async () => {
|
||||
const results = [];
|
||||
for (const mode of ['cors', 'no-cors']) {
|
||||
const response = await fetch('/fetch-filter-response', {mode});
|
||||
results.push({mode, type: response.type, status: response.status,
|
||||
visible: response.headers.get('x-visible'),
|
||||
private: response.headers.get('x-private'), body: await response.text()});
|
||||
}
|
||||
parent.postMessage(JSON.stringify(results), '*');
|
||||
})().catch(error => parent.postMessage(JSON.stringify({error: String(error)}), '*'));
|
||||
</script>"#
|
||||
.to_owned(),
|
||||
);
|
||||
}
|
||||
if url.path() == "/fetch-filter-response" {
|
||||
return (
|
||||
200,
|
||||
format!(
|
||||
"{cors}Content-Type: text/plain\r\nCache-Control: no-store\r\nCross-Origin-Resource-Policy: cross-origin\r\nAccess-Control-Expose-Headers: X-Visible\r\nX-Visible: visible\r\nX-Private: secret\r\n"
|
||||
),
|
||||
"ok".to_owned(),
|
||||
);
|
||||
}
|
||||
if url.path() == "/sandboxed-preload" {
|
||||
return (
|
||||
200,
|
||||
@@ -819,3 +848,47 @@ async fn csp_sandboxed_child_script_and_modulepreload_preserve_opaque_origin() -
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn csp_sandboxed_fetch_keeps_cors_and_opaque_response_filters() -> Result<()> {
|
||||
let server = SecurityServers::spawn().await?;
|
||||
let browser = Browser::new(BrowserConfig::default())?;
|
||||
let mut page = browser.fetch(&format!("{}/page", server.origin)).await?;
|
||||
let observed = results(
|
||||
page.evaluate_runtime_expression_with_await_async(
|
||||
r#"new Promise(resolve => {
|
||||
const frame = document.createElement('iframe');
|
||||
const handler = event => {
|
||||
if (event.source !== frame.contentWindow) return;
|
||||
removeEventListener('message', handler);
|
||||
frame.remove();
|
||||
resolve(event.data);
|
||||
};
|
||||
addEventListener('message', handler);
|
||||
frame.src = '/sandboxed-fetch-filter';
|
||||
document.body.append(frame);
|
||||
})"#,
|
||||
true,
|
||||
)
|
||||
.await?,
|
||||
)?;
|
||||
assert_eq!(
|
||||
observed,
|
||||
json!([
|
||||
{"mode":"cors", "type":"cors", "status":200, "visible":"visible", "private":null, "body":"ok"},
|
||||
{"mode":"no-cors", "type":"opaque", "status":0, "visible":null, "private":null, "body":""}
|
||||
])
|
||||
);
|
||||
let requests = server.requests.lock();
|
||||
let fetches = requests
|
||||
.iter()
|
||||
.filter(|request| request.path == "/fetch-filter-response")
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(fetches.len(), 2, "{requests:?}");
|
||||
assert_eq!(fetches[0].origin.as_deref(), Some("null"), "{fetches:?}");
|
||||
assert!(
|
||||
fetches.iter().all(|request| request.cookie.is_none()),
|
||||
"{fetches:?}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1069,20 +1069,18 @@ async fn service_worker_redirect_preserves_worker_response_filter() -> Result<()
|
||||
}})()"#, cross = servers.cross_origin), true,
|
||||
).await?;
|
||||
let results: serde_json::Value = serde_json::from_str(result["value"].as_str().unwrap())?;
|
||||
// A synthesized Response has an empty URL list and remains basic after
|
||||
// the intercepted redirect, including its ordinary response headers.
|
||||
for kind in ["basic", "basic-buffered", "cors", "default"] {
|
||||
assert_eq!(results[kind]["body"], SCRIPT, "{kind}: {results}");
|
||||
assert_eq!(
|
||||
results[kind]["type"],
|
||||
if kind.starts_with("basic") {
|
||||
"basic"
|
||||
} else {
|
||||
"cors"
|
||||
},
|
||||
if kind != "cors" { "basic" } else { "cors" },
|
||||
"{kind}: {results}"
|
||||
);
|
||||
assert_eq!(
|
||||
results[kind]["privateHeader"],
|
||||
if kind.starts_with("basic") {
|
||||
if kind != "cors" {
|
||||
serde_json::json!("yes")
|
||||
} else {
|
||||
serde_json::Value::Null
|
||||
|
||||
@@ -64,6 +64,8 @@ const STORAGE_BUCKET_CACHE_PUT_RESPONSE_STATUS_SLOT: &str =
|
||||
"__moliStorageBucketCachePutResponseStatus";
|
||||
const STORAGE_BUCKET_CACHE_PUT_RESPONSE_STATUS_TEXT_SLOT: &str =
|
||||
"__moliStorageBucketCachePutResponseStatusText";
|
||||
const STORAGE_BUCKET_CACHE_PUT_RESPONSE_CORS_EXPOSED_HEADERS_SLOT: &str =
|
||||
"__lmStorageBucketCachePutResponseCorsExposedHeaders";
|
||||
const STORAGE_BUCKET_CACHE_PUT_RESPONSE_HEADERS_SLOT: &str =
|
||||
"__moliStorageBucketCachePutResponseHeaders";
|
||||
const NAVIGATOR_UA_DATA_USER_AGENT_SLOT: &str = "__moliNavigatorUADataUserAgent";
|
||||
@@ -372,6 +374,8 @@ struct StorageBucketCachePutPendingDataDeclaration<'scope> {
|
||||
|
||||
#[webapi(slot = STORAGE_BUCKET_CACHE_PUT_RESPONSE_HEADERS_SLOT)]
|
||||
response_headers_json: String,
|
||||
#[webapi(slot = STORAGE_BUCKET_CACHE_PUT_RESPONSE_CORS_EXPOSED_HEADERS_SLOT)]
|
||||
response_cors_exposed_header_names_json: String,
|
||||
}
|
||||
|
||||
enum StorageBucketCachedResponseMaterialization<'scope> {
|
||||
@@ -1801,6 +1805,10 @@ fn storage_bucket_cache_put_pending_body<'s>(
|
||||
response_status_text: head.status_text,
|
||||
response_headers_json: serde_json::to_string(&head.headers)
|
||||
.unwrap_or_else(|_| "[]".to_owned()),
|
||||
response_cors_exposed_header_names_json: serde_json::to_string(
|
||||
&head.cors_exposed_header_names,
|
||||
)
|
||||
.unwrap_or_else(|_| "null".to_owned()),
|
||||
}
|
||||
.bind(scope)
|
||||
.expect("Cache.put pending body data should bind");
|
||||
@@ -1858,6 +1866,7 @@ fn storage_bucket_cache_put_body_fulfilled_callback<'s>(
|
||||
}
|
||||
};
|
||||
let response = StorageBucketCachedResponse {
|
||||
cors_exposed_header_names: pending.response_cors_exposed_header_names,
|
||||
response_type: pending.response_type,
|
||||
url: pending.response_url,
|
||||
redirected: pending.response_redirected,
|
||||
@@ -1897,6 +1906,7 @@ fn storage_bucket_cache_put_body_rejected_callback<'s>(
|
||||
}
|
||||
|
||||
struct StorageBucketCachePutPendingData<'scope> {
|
||||
response_cors_exposed_header_names: Option<Vec<String>>,
|
||||
resolver: v8::Local<'scope, v8::PromiseResolver>,
|
||||
handle: StorageBucketCacheHandle,
|
||||
request: CacheRequestInfo,
|
||||
@@ -1958,6 +1968,13 @@ fn storage_bucket_cache_put_pending_data<'s>(
|
||||
data,
|
||||
STORAGE_BUCKET_CACHE_PUT_RESPONSE_STATUS_TEXT_SLOT,
|
||||
)?;
|
||||
let response_cors_exposed_header_names =
|
||||
serde_json::from_str::<Option<Vec<String>>>(&data_private_string(
|
||||
scope,
|
||||
data,
|
||||
STORAGE_BUCKET_CACHE_PUT_RESPONSE_CORS_EXPOSED_HEADERS_SLOT,
|
||||
)?)
|
||||
.ok()?;
|
||||
let response_headers_json =
|
||||
data_private_string(scope, data, STORAGE_BUCKET_CACHE_PUT_RESPONSE_HEADERS_SLOT)?;
|
||||
let response_headers =
|
||||
@@ -1967,6 +1984,7 @@ fn storage_bucket_cache_put_pending_data<'s>(
|
||||
indexed_db_storage_key: bucket_storage_key,
|
||||
};
|
||||
Some(StorageBucketCachePutPendingData {
|
||||
response_cors_exposed_header_names,
|
||||
resolver,
|
||||
handle: StorageBucketCacheHandle {
|
||||
bucket,
|
||||
@@ -2816,6 +2834,7 @@ fn storage_bucket_cached_response_from_head_body(
|
||||
body: Vec<u8>,
|
||||
) -> StorageBucketCachedResponse {
|
||||
StorageBucketCachedResponse {
|
||||
cors_exposed_header_names: head.cors_exposed_header_names,
|
||||
response_type: head.response_type,
|
||||
url: head
|
||||
.final_url
|
||||
@@ -2955,16 +2974,38 @@ fn build_storage_bucket_cached_response_object<'s>(
|
||||
);
|
||||
let status_text = v8_string(scope, &response.status_text)?;
|
||||
init.set_string_property(scope, "statusText", status_text.into());
|
||||
let headers = headers_entries_to_init_array(
|
||||
scope,
|
||||
&moli_fetch::headers_to_byte_strings(&response.headers),
|
||||
);
|
||||
let public_headers = response
|
||||
.headers
|
||||
.iter()
|
||||
.filter(|(name, _)| {
|
||||
response.response_type != "cors"
|
||||
|| response
|
||||
.cors_exposed_header_names
|
||||
.as_ref()
|
||||
.is_none_or(|names| {
|
||||
names
|
||||
.iter()
|
||||
.any(|exposed| exposed.eq_ignore_ascii_case(name))
|
||||
})
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let headers = headers_entries_to_init_array(scope, &moli_fetch::headers_to_byte_strings(&public_headers));
|
||||
init.set_string_property(scope, "headers", headers.into());
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let constructor = global
|
||||
.get(scope, v8str(scope, "Response").into())
|
||||
.and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())?;
|
||||
let response_obj = constructor.new_instance(scope, &[body, init.into_value()])?;
|
||||
if matches!(response.response_type.as_str(), "basic" | "cors") {
|
||||
crate::network_host::set_filtered_response_internal_head(
|
||||
scope,
|
||||
response_obj,
|
||||
response.status,
|
||||
&response.status_text,
|
||||
&response.headers,
|
||||
);
|
||||
}
|
||||
crate::network_host::set_response_slot_string(
|
||||
scope,
|
||||
response_obj,
|
||||
|
||||
@@ -147,11 +147,9 @@ pub(in crate::network_host) use self::request_scope::{
|
||||
pub(crate) use self::response::{
|
||||
FetchResponseRequest, FetchResponseSecurityViolation, MaterializedResponseBody,
|
||||
MaterializedResponseHead, build_fetch_response_object_for_request_mode,
|
||||
build_fetch_response_object_from_body_source_for_request_mode,
|
||||
build_fetch_response_object_from_body_source_for_request_mode_with_filter,
|
||||
build_fetch_response_object_from_stream_for_request_mode,
|
||||
build_fetch_response_object_from_stream_for_request_mode_with_filter,
|
||||
build_fetch_response_object_from_subresource_body_for_request_mode,
|
||||
build_fetch_response_object_from_subresource_body_for_request_mode_with_filter,
|
||||
build_filtered_cached_response_object,
|
||||
build_navigation_preload_response_object_from_stream_for_request_mode,
|
||||
cors_preflight_request_headers, fetch_response_needs_orb_body_validation,
|
||||
@@ -159,12 +157,11 @@ pub(crate) use self::response::{
|
||||
materialize_response_object_body, materialize_response_object_body_with_chunk_callback,
|
||||
materialize_response_object_internal_head, materialized_body_bytes_from_value,
|
||||
network_response_filter, response_constructor_callback, response_has_null_body,
|
||||
validate_cors_preflight_response, validate_cors_response_chain,
|
||||
validate_cross_origin_embedder_and_document_isolation_policy,
|
||||
set_filtered_response_internal_head, validate_cors_preflight_response,
|
||||
validate_cors_response_chain, validate_cross_origin_embedder_and_document_isolation_policy,
|
||||
validate_cross_origin_resource_policy, validate_fetch_response_headers,
|
||||
validate_fetch_response_security_policy, validate_fetch_response_security_policy_with_body,
|
||||
validate_fetch_response_security_policy_with_body_classified,
|
||||
validate_opaque_response_blocking_with_body, validated_opaque_response_body,
|
||||
validate_fetch_response_security_policy_with_body_classified, validated_opaque_response_body,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use self::response::{materialize_response_object, materialize_response_object_head};
|
||||
@@ -303,3 +300,9 @@ use super::{
|
||||
v8str,
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use self::response::{
|
||||
build_fetch_response_object_from_stream_for_request_mode,
|
||||
build_fetch_response_object_from_subresource_body_for_request_mode,
|
||||
};
|
||||
|
||||
@@ -24,22 +24,25 @@ pub(crate) use self::cors::{
|
||||
validate_cross_origin_embedder_and_document_isolation_policy,
|
||||
validate_cross_origin_resource_policy, validate_fetch_response_headers,
|
||||
validate_fetch_response_security_policy, validate_fetch_response_security_policy_with_body,
|
||||
validate_fetch_response_security_policy_with_body_classified,
|
||||
validate_opaque_response_blocking_with_body, validated_opaque_response_body,
|
||||
validate_fetch_response_security_policy_with_body_classified, validated_opaque_response_body,
|
||||
};
|
||||
pub(crate) use self::materialize::{
|
||||
FetchResponseRequest, MaterializedResponseBody, MaterializedResponseHead,
|
||||
build_fetch_response_object_for_request_mode,
|
||||
build_fetch_response_object_from_body_source_for_request_mode,
|
||||
build_fetch_response_object_from_body_source_for_request_mode_with_filter,
|
||||
build_fetch_response_object_from_stream_for_request_mode,
|
||||
build_fetch_response_object_from_stream_for_request_mode_with_filter,
|
||||
build_fetch_response_object_from_subresource_body_for_request_mode,
|
||||
build_fetch_response_object_from_subresource_body_for_request_mode_with_filter,
|
||||
build_filtered_cached_response_object,
|
||||
build_navigation_preload_response_object_from_stream_for_request_mode,
|
||||
materialize_response_object_body, materialize_response_object_body_with_chunk_callback,
|
||||
materialize_response_object_internal_head, materialized_body_bytes_from_value,
|
||||
network_response_filter,
|
||||
network_response_filter, set_filtered_response_internal_head,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use self::materialize::{materialize_response_object, materialize_response_object_head};
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use self::materialize::{
|
||||
build_fetch_response_object_from_stream_for_request_mode,
|
||||
build_fetch_response_object_from_subresource_body_for_request_mode,
|
||||
};
|
||||
|
||||
@@ -795,54 +795,73 @@ mod tests {
|
||||
let home = url("https://page.test/a");
|
||||
let away = url("https://script.test/b");
|
||||
let elsewhere = url("https://other.test/c");
|
||||
let origin = WebOrigin::from_url(&home);
|
||||
for (redirects, serialized_origin) in [
|
||||
(vec![(&home, &away)], origin.ascii_serialization()),
|
||||
(vec![(&away, &elsewhere)], "null"),
|
||||
(vec![(&home, &away), (&away, &home)], "null"),
|
||||
] {
|
||||
let final_url = redirects.last().expect("redirect target").1.clone();
|
||||
let mut head = header_response(
|
||||
final_url,
|
||||
vec![(
|
||||
"Access-Control-Allow-Origin".to_owned(),
|
||||
serialized_origin.to_owned(),
|
||||
)],
|
||||
);
|
||||
head.redirected = true;
|
||||
head.redirect_chain = redirects
|
||||
.into_iter()
|
||||
.map(|(from, to)| moli_fetch::RedirectInfo {
|
||||
source: RedirectSource::Network,
|
||||
from_url: from.clone(),
|
||||
to_url: to.clone(),
|
||||
status: 302,
|
||||
headers: vec![(
|
||||
"Access-Control-Allow-Origin".to_owned(),
|
||||
origin.ascii_serialization().to_owned(),
|
||||
)],
|
||||
network_extra_info_available: false,
|
||||
request_extra_info: None,
|
||||
response_extra_info: None,
|
||||
redirect_has_extra_info: false,
|
||||
request_cookie_report: None,
|
||||
cookie_set_reports: Vec::new(),
|
||||
from_cache: false,
|
||||
negotiated_http_version: None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
head.url_list().serialized_origin(&origin),
|
||||
serialized_origin
|
||||
);
|
||||
validate_cors_response_chain(&origin, &head, RequestCredentialsMode::SameOrigin)
|
||||
.expect("each response must be authorized against its own request hop origin");
|
||||
head.headers.clear();
|
||||
assert!(
|
||||
validate_cors_response_chain(&origin, &head, RequestCredentialsMode::SameOrigin)
|
||||
.is_err(),
|
||||
"returning to the initial origin must not bypass CORS"
|
||||
);
|
||||
for origin in [WebOrigin::from_url(&home), WebOrigin::Opaque] {
|
||||
for source in [
|
||||
RedirectSource::Network,
|
||||
RedirectSource::ServiceWorker,
|
||||
RedirectSource::Internal,
|
||||
] {
|
||||
for (urls, tuple_origin) in [
|
||||
(vec![&home, &away], "https://page.test"),
|
||||
(vec![&away, &elsewhere], "null"),
|
||||
(vec![&home, &away, &home], "null"),
|
||||
] {
|
||||
let expected_origin = if origin.is_opaque() {
|
||||
"null"
|
||||
} else {
|
||||
tuple_origin
|
||||
};
|
||||
let mut head = header_response(
|
||||
(*urls.last().unwrap()).clone(),
|
||||
vec![("Access-Control-Allow-Origin".into(), expected_origin.into())],
|
||||
);
|
||||
head.redirected = true;
|
||||
head.redirect_chain = urls
|
||||
.windows(2)
|
||||
.map(|pair| moli_fetch::RedirectInfo {
|
||||
source,
|
||||
from_url: pair[0].clone(),
|
||||
to_url: pair[1].clone(),
|
||||
status: 302,
|
||||
// The response to each hop is checked before its redirect
|
||||
// changes the serialized origin for the following request.
|
||||
headers: if source == RedirectSource::Network {
|
||||
vec![(
|
||||
"Access-Control-Allow-Origin".into(),
|
||||
origin.ascii_serialization().into(),
|
||||
)]
|
||||
} else {
|
||||
Vec::new()
|
||||
},
|
||||
network_extra_info_available: false,
|
||||
request_extra_info: None,
|
||||
response_extra_info: None,
|
||||
redirect_has_extra_info: false,
|
||||
request_cookie_report: None,
|
||||
cookie_set_reports: Vec::new(),
|
||||
from_cache: false,
|
||||
negotiated_http_version: None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(head.url_list().serialized_origin(&origin), expected_origin);
|
||||
assert!(head.url_list().has_cross_origin_url(&origin));
|
||||
validate_cors_response_chain(
|
||||
&origin,
|
||||
&head,
|
||||
RequestCredentialsMode::SameOrigin,
|
||||
)
|
||||
.expect("authorize network hops using their individual request origins");
|
||||
head.headers.clear();
|
||||
validate_cors_response_chain(
|
||||
&origin,
|
||||
&head,
|
||||
RequestCredentialsMode::SameOrigin,
|
||||
)
|
||||
.expect_err(
|
||||
"returning to the client origin cannot bypass final CORS validation",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,20 +18,24 @@ pub(crate) struct FetchResponseRequest<'a> {
|
||||
}
|
||||
|
||||
impl FetchResponseRequest<'_> {
|
||||
pub(crate) fn filter_response_headers(
|
||||
pub(crate) fn network_response_filter(
|
||||
self,
|
||||
request_origin: &moli_url::WebOrigin,
|
||||
head: &moli_fetch::ResponseHead,
|
||||
credentials_mode: moli_fetch::RequestCredentialsMode,
|
||||
) -> Vec<(String, String)> {
|
||||
// Opaque responses need their internal headers for Cache and respondWith.
|
||||
// Their public header list is made empty when the Response is built.
|
||||
if self.mode == RequestMode::NoCors
|
||||
|| self.redirect_mode == RequestRedirectMode::Manual && is_redirect_status(head.status)
|
||||
{
|
||||
head.headers.clone()
|
||||
} else {
|
||||
filter_cors_exposed_response_headers(request_origin, head, credentials_mode)
|
||||
) -> crate::types::AsyncSubresourceFetchResponseFilter {
|
||||
use crate::types::AsyncSubresourceFetchResponseFilter as Filter;
|
||||
let filter = response_filter(request_origin, head, self);
|
||||
match compute_fetch_response_type(request_origin, head, filter) {
|
||||
"opaque" => Filter::Opaque,
|
||||
"opaqueredirect" => Filter::OpaqueRedirect,
|
||||
"cors" => Filter::Cors(
|
||||
filter_cors_exposed_response_headers(request_origin, head, credentials_mode)
|
||||
.into_iter()
|
||||
.map(|(name, _)| name)
|
||||
.collect(),
|
||||
),
|
||||
_ => Filter::Basic,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,17 +53,11 @@ enum FetchResponseFilter {
|
||||
OpaqueRedirect,
|
||||
}
|
||||
|
||||
impl FetchResponseFilter {
|
||||
fn is_readable(self) -> bool {
|
||||
matches!(self, Self::None | Self::Basic | Self::Cors)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::types::AsyncSubresourceFetchResponseFilter> for FetchResponseFilter {
|
||||
fn from(value: crate::types::AsyncSubresourceFetchResponseFilter) -> Self {
|
||||
impl From<&crate::types::AsyncSubresourceFetchResponseFilter> for FetchResponseFilter {
|
||||
fn from(value: &crate::types::AsyncSubresourceFetchResponseFilter) -> Self {
|
||||
match value {
|
||||
crate::types::AsyncSubresourceFetchResponseFilter::Basic => Self::Basic,
|
||||
crate::types::AsyncSubresourceFetchResponseFilter::Cors => Self::Cors,
|
||||
crate::types::AsyncSubresourceFetchResponseFilter::Cors(_) => Self::Cors,
|
||||
crate::types::AsyncSubresourceFetchResponseFilter::Opaque => Self::Opaque,
|
||||
crate::types::AsyncSubresourceFetchResponseFilter::OpaqueRedirect => {
|
||||
Self::OpaqueRedirect
|
||||
@@ -108,6 +106,7 @@ fn response_filter(
|
||||
) -> FetchResponseFilter {
|
||||
let request_origin = request_origin.into();
|
||||
network_response_filter(&request_origin, head, request.mode, request.redirect_mode)
|
||||
.as_ref()
|
||||
.map_or(FetchResponseFilter::None, Into::into)
|
||||
}
|
||||
|
||||
@@ -160,7 +159,11 @@ fn compute_fetch_response_type(
|
||||
}
|
||||
|
||||
fn filtered_response_status(head: &moli_fetch::ResponseHead, filter: FetchResponseFilter) -> u16 {
|
||||
if filter.is_readable() { head.status } else { 0 }
|
||||
if filtered_response_exposes_body(filter) {
|
||||
head.status
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fn filtered_response_url(head: &moli_fetch::ResponseHead, filter: FetchResponseFilter) -> &str {
|
||||
@@ -176,22 +179,25 @@ fn filtered_response_url(head: &moli_fetch::ResponseHead, filter: FetchResponseF
|
||||
}
|
||||
|
||||
fn filtered_response_exposes_body(filter: FetchResponseFilter) -> bool {
|
||||
filter.is_readable()
|
||||
!matches!(
|
||||
filter,
|
||||
FetchResponseFilter::Opaque | FetchResponseFilter::OpaqueRedirect
|
||||
)
|
||||
}
|
||||
|
||||
fn filtered_response_exposes_redirected(filter: FetchResponseFilter) -> bool {
|
||||
filter.is_readable()
|
||||
filtered_response_exposes_body(filter)
|
||||
}
|
||||
|
||||
fn filtered_response_exposes_headers(filter: FetchResponseFilter) -> bool {
|
||||
filter.is_readable()
|
||||
filtered_response_exposes_body(filter)
|
||||
}
|
||||
|
||||
fn filtered_response_status_text(
|
||||
head: &moli_fetch::ResponseHead,
|
||||
filter: FetchResponseFilter,
|
||||
) -> &str {
|
||||
if filter.is_readable() {
|
||||
if filtered_response_exposes_body(filter) {
|
||||
head.status_text()
|
||||
} else {
|
||||
""
|
||||
@@ -243,9 +249,17 @@ pub(crate) fn build_fetch_response_object_from_body_source_for_request_mode_with
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
let request_origin = request_origin.into();
|
||||
let filter = filter_override
|
||||
.as_ref()
|
||||
.map(FetchResponseFilter::from)
|
||||
.unwrap_or_else(|| response_filter(&request_origin, &head, request));
|
||||
let obj = build_fetch_response_object_head(scope, &request_origin, &head, filter, None);
|
||||
let obj = build_fetch_response_object_head(
|
||||
scope,
|
||||
&request_origin,
|
||||
&head,
|
||||
filter,
|
||||
None,
|
||||
filter_override.as_ref(),
|
||||
);
|
||||
let body_stream = if response_has_null_body(request.method, head.status) {
|
||||
None
|
||||
} else if filtered_response_exposes_body(filter) {
|
||||
@@ -257,6 +271,7 @@ pub(crate) fn build_fetch_response_object_from_body_source_for_request_mode_with
|
||||
finish_fetch_response_object_with_body_stream(scope, obj, &head, body_stream)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn build_fetch_response_object_from_subresource_body_for_request_mode<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
request_origin: impl Into<WebOrigin>,
|
||||
@@ -265,8 +280,37 @@ pub(crate) fn build_fetch_response_object_from_subresource_body_for_request_mode
|
||||
body: crate::protocol_types::SubresourceResponseBody,
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
let request_origin = request_origin.into();
|
||||
let filter = response_filter(&request_origin, &head, request);
|
||||
let obj = build_fetch_response_object_head(scope, &request_origin, &head, filter, None);
|
||||
build_fetch_response_object_from_subresource_body_for_request_mode_with_filter(
|
||||
scope,
|
||||
&request_origin,
|
||||
request,
|
||||
head,
|
||||
body,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn build_fetch_response_object_from_subresource_body_for_request_mode_with_filter<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
request_origin: impl Into<WebOrigin>,
|
||||
request: FetchResponseRequest<'_>,
|
||||
head: moli_fetch::ResponseHead,
|
||||
body: crate::protocol_types::SubresourceResponseBody,
|
||||
filter_override: Option<crate::types::AsyncSubresourceFetchResponseFilter>,
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
let request_origin = request_origin.into();
|
||||
let filter = filter_override
|
||||
.as_ref()
|
||||
.map(FetchResponseFilter::from)
|
||||
.unwrap_or_else(|| response_filter(&request_origin, &head, request));
|
||||
let obj = build_fetch_response_object_head(
|
||||
scope,
|
||||
&request_origin,
|
||||
&head,
|
||||
filter,
|
||||
None,
|
||||
filter_override.as_ref(),
|
||||
);
|
||||
let body_stream = if response_has_null_body(request.method, head.status) {
|
||||
None
|
||||
} else if filtered_response_exposes_body(filter) {
|
||||
@@ -278,6 +322,7 @@ pub(crate) fn build_fetch_response_object_from_subresource_body_for_request_mode
|
||||
finish_fetch_response_object_with_body_stream(scope, obj, &head, body_stream)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn build_fetch_response_object_from_stream_for_request_mode<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
request_origin: impl Into<WebOrigin>,
|
||||
@@ -345,6 +390,7 @@ fn build_fetch_response_object_from_stream_for_request_mode_with_surface_url<'s>
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
let request_origin = request_origin.into();
|
||||
let filter = filter_override
|
||||
.as_ref()
|
||||
.map(FetchResponseFilter::from)
|
||||
.unwrap_or_else(|| response_filter(&request_origin, &head, request));
|
||||
let filtered_surface_url = (filter == FetchResponseFilter::OpaqueRedirect)
|
||||
@@ -356,6 +402,7 @@ fn build_fetch_response_object_from_stream_for_request_mode_with_surface_url<'s>
|
||||
&head,
|
||||
filter,
|
||||
filtered_surface_url,
|
||||
filter_override.as_ref(),
|
||||
);
|
||||
if response_has_null_body(request.method, head.status) {
|
||||
// Fetch nulls the internal body, including for filtered responses. Do
|
||||
@@ -377,6 +424,7 @@ fn build_fetch_response_object_head<'s>(
|
||||
head: &moli_fetch::ResponseHead,
|
||||
filter: FetchResponseFilter,
|
||||
filtered_surface_url: Option<&str>,
|
||||
filter_override: Option<&crate::types::AsyncSubresourceFetchResponseFilter>,
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
let request_origin = request_origin.into();
|
||||
let status = filtered_response_status(head, filter);
|
||||
@@ -395,22 +443,35 @@ fn build_fetch_response_object_head<'s>(
|
||||
FetchResponseInternalUrlDeclaration::new(head.final_url.to_string())
|
||||
.initialize(scope, obj)
|
||||
.expect("Fetch Response internal URL declaration should initialize");
|
||||
if filter != FetchResponseFilter::None {
|
||||
set_filtered_response_internal_head(
|
||||
scope,
|
||||
obj,
|
||||
head.status,
|
||||
head.status_text(),
|
||||
&moli_fetch::headers_to_byte_strings(&head.headers),
|
||||
);
|
||||
}
|
||||
set_filtered_response_internal_head(scope, obj, head.status, head.status_text(), &moli_fetch::headers_to_byte_strings(&head.headers));
|
||||
let header_entries = if filtered_response_exposes_headers(filter) {
|
||||
match filter_override {
|
||||
Some(crate::types::AsyncSubresourceFetchResponseFilter::Cors(names)) => head
|
||||
.headers
|
||||
.iter()
|
||||
.filter(|(name, _)| {
|
||||
names
|
||||
.iter()
|
||||
.any(|exposed| exposed.eq_ignore_ascii_case(name))
|
||||
})
|
||||
.cloned()
|
||||
.collect(),
|
||||
_ => head.headers.clone(),
|
||||
}
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let headers = filter_headers_for_guard(&moli_fetch::headers_to_byte_strings(&header_entries), HeadersGuard::Response);
|
||||
let headers_obj =
|
||||
build_headers_object_with_state(scope, &headers, HeadersGuard::Response, true);
|
||||
set_response_slot_value(scope, obj, RESPONSE_HEADERS_SLOT, headers_obj.into());
|
||||
mark_response_object(scope, obj);
|
||||
obj
|
||||
}
|
||||
|
||||
fn set_filtered_response_internal_head(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
obj: v8::Local<'_, v8::Object>,
|
||||
pub(crate) fn set_filtered_response_internal_head<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
obj: v8::Local<'s, v8::Object>,
|
||||
status: u16,
|
||||
status_text: &str,
|
||||
headers: &[(String, String)],
|
||||
@@ -430,6 +491,9 @@ fn set_filtered_response_internal_head(
|
||||
RESPONSE_INTERNAL_HEADERS_SLOT,
|
||||
internal_headers.into(),
|
||||
);
|
||||
if let Some(headers) = response_slot_object(scope, obj, RESPONSE_HEADERS_SLOT) {
|
||||
super::super::headers::mark_headers_immutable(scope, headers);
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_fetch_response_object_with_body_stream<'s>(
|
||||
@@ -445,19 +509,6 @@ fn finish_fetch_response_object_with_body_stream<'s>(
|
||||
"opaqueredirect" => FetchResponseFilter::OpaqueRedirect,
|
||||
_ => FetchResponseFilter::None,
|
||||
};
|
||||
let header_entries = if filtered_response_exposes_headers(filter) {
|
||||
head.headers.as_slice()
|
||||
} else {
|
||||
&[][..]
|
||||
};
|
||||
let headers = filter_headers_for_guard(
|
||||
&moli_fetch::headers_to_byte_strings(header_entries),
|
||||
HeadersGuard::Response,
|
||||
);
|
||||
let headers_obj =
|
||||
build_headers_object_with_state(scope, &headers, HeadersGuard::Response, true);
|
||||
set_response_slot_value(scope, obj, RESPONSE_HEADERS_SLOT, headers_obj.into());
|
||||
|
||||
let body_value = if !filtered_response_exposes_body(filter) {
|
||||
v8::null(scope).into()
|
||||
} else if let Some(stream) = body_stream {
|
||||
@@ -517,6 +568,7 @@ pub(crate) fn build_filtered_cached_response_object<'s>(
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct MaterializedResponseObject {
|
||||
pub(crate) cors_exposed_header_names: Option<Vec<String>>,
|
||||
pub(crate) final_url: Option<url::Url>,
|
||||
pub(crate) response_type: String,
|
||||
pub(crate) redirected: bool,
|
||||
@@ -528,6 +580,7 @@ pub(crate) struct MaterializedResponseObject {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct MaterializedResponseHead {
|
||||
pub(crate) cors_exposed_header_names: Option<Vec<String>>,
|
||||
pub(crate) final_url: Option<url::Url>,
|
||||
pub(crate) response_type: String,
|
||||
pub(crate) redirected: bool,
|
||||
@@ -539,6 +592,7 @@ pub(crate) struct MaterializedResponseHead {
|
||||
impl MaterializedResponseHead {
|
||||
pub(crate) fn with_body(self, body: Vec<u8>) -> MaterializedResponseObject {
|
||||
MaterializedResponseObject {
|
||||
cors_exposed_header_names: self.cors_exposed_header_names,
|
||||
final_url: self.final_url,
|
||||
response_type: self.response_type,
|
||||
redirected: self.redirected,
|
||||
@@ -643,6 +697,8 @@ pub(crate) fn materialize_response_object_head<'s>(
|
||||
|
||||
Ok((
|
||||
MaterializedResponseHead {
|
||||
cors_exposed_header_names: (response_type == "cors")
|
||||
.then(|| headers.iter().map(|(name, _)| name.clone()).collect()),
|
||||
final_url,
|
||||
response_type,
|
||||
redirected: response_slot_bool(scope, response, RESPONSE_REDIRECTED_SLOT),
|
||||
@@ -660,9 +716,8 @@ pub(crate) fn materialize_response_object_internal_head<'s>(
|
||||
context: &str,
|
||||
) -> Result<(MaterializedResponseHead, v8::Local<'s, v8::Object>), String> {
|
||||
let (mut head, response) = materialize_response_object_head(scope, value, context)?;
|
||||
if matches!(head.response_type.as_str(), "opaque" | "opaqueredirect")
|
||||
&& let Some(internal_status) =
|
||||
response_slot_number(scope, response, RESPONSE_INTERNAL_STATUS_SLOT)
|
||||
if let Some(internal_status) =
|
||||
response_slot_number(scope, response, RESPONSE_INTERNAL_STATUS_SLOT)
|
||||
{
|
||||
head.status = internal_status as u16;
|
||||
head.status_text =
|
||||
|
||||
@@ -427,7 +427,7 @@ pub(crate) async fn load_service_worker_aware_external_script_source_outcome(
|
||||
ClassicScriptResponseProvenance::Network
|
||||
} else {
|
||||
ClassicScriptResponseProvenance::ServiceWorker {
|
||||
filter: response.response_filter,
|
||||
filter: response.response_filter.clone(),
|
||||
}
|
||||
};
|
||||
external_script_source_load_outcome_from_response_inner(
|
||||
@@ -554,7 +554,7 @@ pub(crate) fn spawn_service_worker_aware_external_script_source_load(
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
enum ClassicScriptResponseProvenance {
|
||||
Network,
|
||||
ServiceWorker {
|
||||
|
||||
@@ -3850,7 +3850,7 @@ impl ScriptVm {
|
||||
record_started,
|
||||
);
|
||||
let mut observable_response = response;
|
||||
if pending.info.resource_type == SubresourceResourceType::Xhr && !response_filter.is_some_and(|filter| filter.is_readable()) {
|
||||
if pending.info.resource_type == SubresourceResourceType::Xhr && !response_filter.as_ref().is_some_and(|filter| filter.is_readable()) {
|
||||
observable_response.headers =
|
||||
crate::network_host::filter_cors_exposed_response_headers(
|
||||
&pending.request_origin,
|
||||
@@ -3873,13 +3873,11 @@ impl ScriptVm {
|
||||
mode: pending.request_mode,
|
||||
redirect_mode,
|
||||
};
|
||||
if !response_filter.is_some_and(|filter| filter.is_readable()) {
|
||||
head.headers = response_request.filter_response_headers(
|
||||
&pending.request_origin,
|
||||
&head,
|
||||
pending.credentials_mode,
|
||||
);
|
||||
}
|
||||
let response_filter = response_filter.or_else(|| Some(response_request.network_response_filter(
|
||||
&pending.request_origin,
|
||||
&head,
|
||||
pending.credentials_mode,
|
||||
)));
|
||||
if let Some(status_text) = response_status_text {
|
||||
head.status_text = Some(status_text);
|
||||
}
|
||||
@@ -4698,6 +4696,7 @@ impl ScriptVm {
|
||||
.borrow_mut()
|
||||
.record_streaming_subresource_fetch(StreamingSubresourceFetchState {
|
||||
response_filter: started.response_filter,
|
||||
skip_fetch_security_validation: started.skip_fetch_security_validation,
|
||||
pending,
|
||||
request_url: started.request_url,
|
||||
request_method: started.request_method,
|
||||
@@ -4841,15 +4840,12 @@ impl ScriptVm {
|
||||
None
|
||||
}
|
||||
.or_else(|| {
|
||||
if started.skip_fetch_security_validation {
|
||||
return None;
|
||||
}
|
||||
matches!(
|
||||
(!started.skip_fetch_security_validation && matches!(
|
||||
pending.info.resource_type,
|
||||
SubresourceResourceType::EventSource
|
||||
| SubresourceResourceType::Fetch
|
||||
| SubresourceResourceType::Xhr
|
||||
)
|
||||
))
|
||||
.then(|| {
|
||||
crate::network_host::validate_fetch_response_headers(
|
||||
&pending.request_origin,
|
||||
@@ -5038,7 +5034,7 @@ impl ScriptVm {
|
||||
}
|
||||
|
||||
let mut observable_head = started.head.clone();
|
||||
if pending.info.resource_type == SubresourceResourceType::Xhr && !started.response_filter.is_some_and(|filter| filter.is_readable()) {
|
||||
if pending.info.resource_type == SubresourceResourceType::Xhr && !started.response_filter.as_ref().is_some_and(|filter| filter.is_readable()) {
|
||||
observable_head.headers = crate::network_host::filter_cors_exposed_response_headers(
|
||||
&pending.request_origin,
|
||||
&observable_head,
|
||||
@@ -5083,6 +5079,7 @@ impl ScriptVm {
|
||||
.borrow_mut()
|
||||
.record_streaming_subresource_fetch(StreamingSubresourceFetchState {
|
||||
response_filter: started.response_filter,
|
||||
skip_fetch_security_validation: started.skip_fetch_security_validation,
|
||||
pending,
|
||||
request_url: started.request_url.clone(),
|
||||
request_method: started.request_method.clone(),
|
||||
@@ -5134,20 +5131,18 @@ impl ScriptVm {
|
||||
mode: pending.request_mode,
|
||||
redirect_mode: fetch.redirect_mode(),
|
||||
};
|
||||
if !started.response_filter.is_some_and(|filter| filter.is_readable()) {
|
||||
observable_head.headers = response_request.filter_response_headers(
|
||||
&pending.request_origin,
|
||||
&observable_head,
|
||||
pending.credentials_mode,
|
||||
);
|
||||
}
|
||||
let response_filter = started.response_filter.clone().or_else(|| Some(response_request.network_response_filter(
|
||||
&pending.request_origin,
|
||||
&observable_head,
|
||||
pending.credentials_mode,
|
||||
)));
|
||||
let response_obj = crate::network_host::build_fetch_response_object_from_stream_for_request_mode_with_filter(
|
||||
scope,
|
||||
&pending.request_origin,
|
||||
response_request,
|
||||
observable_head,
|
||||
started.body_source_id,
|
||||
started.response_filter,
|
||||
response_filter,
|
||||
);
|
||||
resolver.resolve(scope, response_obj.into());
|
||||
}
|
||||
@@ -5248,6 +5243,7 @@ impl ScriptVm {
|
||||
self._context_host.borrow_mut().record_streaming_subresource_fetch(
|
||||
StreamingSubresourceFetchState {
|
||||
response_filter: started.response_filter,
|
||||
skip_fetch_security_validation: started.skip_fetch_security_validation,
|
||||
pending,
|
||||
request_url: started.request_url.clone(),
|
||||
request_method: started.request_method.clone(),
|
||||
|
||||
+6
-4
@@ -1003,7 +1003,7 @@ async fn navigator_service_worker_fetch_event_request_preserves_worker_fetch_pol
|
||||
"referrerPolicy=" + event.request.referrerPolicy,
|
||||
"integrity=" + event.request.integrity,
|
||||
"keepalive=" + event.request.keepalive
|
||||
].join("|")));
|
||||
].join("|"), {headers: {"x-response": "from-worker"}}));
|
||||
});
|
||||
"#,
|
||||
),
|
||||
@@ -1013,7 +1013,9 @@ async fn navigator_service_worker_fetch_event_request_preserves_worker_fetch_pol
|
||||
r#"
|
||||
self.onmessage = async () => {
|
||||
try {
|
||||
const response = await fetch("api/worker-metadata.txt", {
|
||||
const target = new URL("api/worker-metadata.txt", location.href);
|
||||
target.hostname = target.hostname === "127.0.0.1" ? "localhost" : "127.0.0.1";
|
||||
const response = await fetch(target, {
|
||||
cache: "reload",
|
||||
referrer: "./worker-referrer.html",
|
||||
referrerPolicy: "origin",
|
||||
@@ -1021,7 +1023,7 @@ async fn navigator_service_worker_fetch_event_request_preserves_worker_fetch_pol
|
||||
keepalive: true,
|
||||
priority: "high"
|
||||
});
|
||||
postMessage(response.status + "|" + await response.text());
|
||||
postMessage([response.status, response.type, response.headers.get("x-response"), await response.text()].join("|"));
|
||||
} catch (error) {
|
||||
postMessage("error:" + String(error && error.message));
|
||||
}
|
||||
@@ -1070,7 +1072,7 @@ async fn navigator_service_worker_fetch_event_request_preserves_worker_fetch_pol
|
||||
&loader,
|
||||
"String(globalThis.__serviceWorkerWorkerFetchRequestPolicyMetadataProbe)",
|
||||
&format!(
|
||||
"200|cache=reload|referrer={base_url}/app/worker-referrer.html|referrerPolicy=origin|integrity=sha256-test|keepalive=true"
|
||||
"200|basic|from-worker|cache=reload|referrer={base_url}/app/worker-referrer.html|referrerPolicy=origin|integrity=sha256-test|keepalive=true"
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1,6 +1,174 @@
|
||||
use super::*;
|
||||
use crate::util::v8str;
|
||||
|
||||
#[test]
|
||||
fn window_service_worker_response_filter_survives_streaming_clone_and_cache() {
|
||||
use crate::types::AsyncSubresourceFetchResponseFilter as Filter;
|
||||
for streaming in [false, true] {
|
||||
for mode in ["cors", "no-cors"] {
|
||||
for cors in [false, true] {
|
||||
let mut vm = new_storage_test_vm("https://response-client.test/");
|
||||
vm.set_fetch_subresource_interception(
|
||||
true,
|
||||
Some(crate::types::SubresourceResourceType::Fetch),
|
||||
);
|
||||
let response_type = if cors { "cors" } else { "basic" };
|
||||
vm.eval(&format!(r#"
|
||||
globalThis.result = 'pending';
|
||||
fetch('https://response-remote.test/body', {{mode: '{mode}'}}).then(async response => {{
|
||||
globalThis.original = response;
|
||||
globalThis.cloned = response.clone();
|
||||
const bucket = await navigator.storageBuckets.open('response-filter');
|
||||
const cache = await bucket.caches.open('responses');
|
||||
await cache.put('/key', cloned.clone());
|
||||
globalThis.cached = await cache.match('/key');
|
||||
await navigator.storageBuckets.delete('response-filter');
|
||||
for (const entry of [original, cloned, cached]) {{
|
||||
if (entry.type !== '{response_type}' || entry.status !== 200 || entry.body === null ||
|
||||
entry.headers.get('x-visible') !== 'visible' || entry.headers.has('set-cookie') ||
|
||||
entry.headers.has('x-hidden') === {cors}) throw new Error('public response');
|
||||
let error;
|
||||
try {{ entry.headers.set('x-author', 'changed'); }} catch (value) {{ error = value; }}
|
||||
if (!(error instanceof TypeError)) throw new Error('immutable headers');
|
||||
if (await entry.clone().text() !== 'hello') throw new Error('body');
|
||||
}}
|
||||
result = 'ok';
|
||||
}}).catch(error => result = String(error.stack || error));
|
||||
"#)).unwrap();
|
||||
let requests = vm.take_pending_subresource_fetch_infos();
|
||||
assert_eq!(requests.len(), 1);
|
||||
let request = &requests[0];
|
||||
let headers = vec![
|
||||
("Content-Type".to_owned(), "text/plain".to_owned()),
|
||||
("Content-Length".to_owned(), "5".to_owned()),
|
||||
("X-Visible".to_owned(), "visible".to_owned()),
|
||||
("X-Hidden".to_owned(), "secret".to_owned()),
|
||||
(
|
||||
"Cross-Origin-Resource-Policy".to_owned(),
|
||||
"cross-origin".to_owned(),
|
||||
),
|
||||
("Set-Cookie".to_owned(), "hidden=secret".to_owned()),
|
||||
(
|
||||
"Vary".to_owned(),
|
||||
if cors { "*" } else { "Accept" }.to_owned(),
|
||||
),
|
||||
];
|
||||
let head = moli_fetch::ResponseHead {
|
||||
final_url: request.url.clone(),
|
||||
status: 200,
|
||||
status_text: Some("OK".to_owned()),
|
||||
headers: headers.clone(),
|
||||
request_cookie_report: None,
|
||||
cookie_set_reports: Vec::new(),
|
||||
redirected: false,
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
negotiated_http_version: None,
|
||||
};
|
||||
let response_filter = Some(if cors {
|
||||
Filter::Cors(vec!["content-type".to_owned(), "x-visible".to_owned()])
|
||||
} else {
|
||||
Filter::Basic
|
||||
});
|
||||
if streaming {
|
||||
let body_source_id = crate::network_host::new_network_body_source_id();
|
||||
vm.start_streaming_async_subresource_fetch(
|
||||
crate::types::AsyncSubresourceStreamingStarted {
|
||||
response_filter,
|
||||
skip_fetch_security_validation: true,
|
||||
internal_id: request.internal_id,
|
||||
request_url: request.url.clone(),
|
||||
request_method: "GET".to_owned(),
|
||||
request_headers: Vec::new(),
|
||||
request_body: None,
|
||||
body_source_id,
|
||||
head,
|
||||
network_request_headers: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
vm.append_streaming_async_subresource_fetch_chunk(
|
||||
body_source_id,
|
||||
b"hello".to_vec(),
|
||||
);
|
||||
vm.finish_streaming_async_subresource_fetch(
|
||||
request.internal_id,
|
||||
body_source_id,
|
||||
Ok(()),
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
vm.complete_async_subresource_fetch(
|
||||
crate::types::AsyncSubresourceFetchCompletion {
|
||||
response_filter,
|
||||
skip_fetch_security_validation: true,
|
||||
internal_id: request.internal_id,
|
||||
request_url: request.url.clone(),
|
||||
request_method: "GET".to_owned(),
|
||||
request_headers: Vec::new(),
|
||||
request_body: None,
|
||||
response_status_text: None,
|
||||
network_error_text: None,
|
||||
result: Ok(
|
||||
crate::protocol_types::NavigationResponse::from_head_and_body(
|
||||
head,
|
||||
"hello".to_owned(),
|
||||
b"hello".to_vec(),
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
vm.exec("0", None).unwrap();
|
||||
assert_eq!(
|
||||
vm.eval("result").unwrap(),
|
||||
"ok",
|
||||
"{response_type}/{mode}/streaming={streaming}"
|
||||
);
|
||||
let context_ptr: *const v8::Global<v8::Context> = &vm.page_default_context;
|
||||
vm.renderer_document_isolate
|
||||
.with_entered_renderer_document_isolate(move |isolate| {
|
||||
let scope = std::pin::pin!(v8::HandleScope::new(isolate));
|
||||
let scope = &mut scope.init();
|
||||
let context = unsafe { v8::Local::new(scope, &*context_ptr) };
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
for name in ["original", "cloned", "cached"] {
|
||||
let value = context
|
||||
.global(scope)
|
||||
.get(scope, v8str(scope, name).into())
|
||||
.unwrap();
|
||||
let (head, _) =
|
||||
crate::network_host::materialize_response_object_internal_head(
|
||||
scope, value, "test",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(head.response_type, response_type);
|
||||
for (name, value) in &headers {
|
||||
assert!(
|
||||
head.headers
|
||||
.iter()
|
||||
.any(|(key, entry)| key.eq_ignore_ascii_case(name)
|
||||
&& entry == value),
|
||||
"{name}: {:?}",
|
||||
head.headers
|
||||
);
|
||||
}
|
||||
if cors {
|
||||
assert_eq!(
|
||||
head.cors_exposed_header_names,
|
||||
Some(vec!["content-type".to_owned(), "x-visible".to_owned()])
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_filtered_fetch_preserves_internal_head_through_clone_and_cache() {
|
||||
for streaming in [false, true] {
|
||||
|
||||
@@ -43,7 +43,7 @@ fn redirect_filter_completion_keeps_status_text_and_explicit_filters() {
|
||||
request_body: None,
|
||||
response_status_text: Some("Override Text".to_owned()),
|
||||
skip_fetch_security_validation: true,
|
||||
response_filter: filter,
|
||||
response_filter: filter.clone(),
|
||||
network_error_text: None,
|
||||
result: Ok(
|
||||
crate::protocol_types::NavigationResponse::from_head_and_body(
|
||||
@@ -66,9 +66,9 @@ fn redirect_filter_completion_keeps_status_text_and_explicit_filters() {
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let expected_type = match filter {
|
||||
let expected_type = match &filter {
|
||||
Some(crate::types::AsyncSubresourceFetchResponseFilter::Basic) => "basic",
|
||||
Some(crate::types::AsyncSubresourceFetchResponseFilter::Cors) => "cors",
|
||||
Some(crate::types::AsyncSubresourceFetchResponseFilter::Cors(_)) => "cors",
|
||||
Some(Opaque) => "opaque",
|
||||
Some(OpaqueRedirect) => "opaqueredirect",
|
||||
None if redirect == "manual" && status == 302 => "opaqueredirect",
|
||||
|
||||
@@ -3069,6 +3069,7 @@ async fn streaming_subresource_finish_preserves_response_head_cache_state() {
|
||||
.borrow_mut()
|
||||
.record_streaming_subresource_fetch(super::StreamingSubresourceFetchState {
|
||||
response_filter: None,
|
||||
skip_fetch_security_validation: false,
|
||||
pending: super::PendingSubresourceFetchState {
|
||||
redirect_headers: None,
|
||||
request_origin: moli_url::WebOrigin::from_url(&document_url),
|
||||
@@ -3362,6 +3363,7 @@ async fn streaming_fetch_body_error_records_response_started_then_body_failed()
|
||||
.borrow_mut()
|
||||
.record_streaming_subresource_fetch(super::StreamingSubresourceFetchState {
|
||||
response_filter: None,
|
||||
skip_fetch_security_validation: false,
|
||||
pending: super::PendingSubresourceFetchState {
|
||||
redirect_headers: None,
|
||||
request_origin: moli_url::WebOrigin::from_url(
|
||||
@@ -3530,6 +3532,7 @@ fn install_streaming_fetch_response_fixture(
|
||||
.borrow_mut()
|
||||
.record_streaming_subresource_fetch(super::StreamingSubresourceFetchState {
|
||||
response_filter: None,
|
||||
skip_fetch_security_validation: false,
|
||||
pending: super::PendingSubresourceFetchState {
|
||||
redirect_headers: None,
|
||||
request_origin: moli_url::WebOrigin::from_url(&document_url),
|
||||
@@ -3635,6 +3638,7 @@ async fn streaming_fetch_body_cancel_aborts_streaming_subresource() {
|
||||
.borrow_mut()
|
||||
.record_streaming_subresource_fetch(super::StreamingSubresourceFetchState {
|
||||
response_filter: None,
|
||||
skip_fetch_security_validation: false,
|
||||
pending: super::PendingSubresourceFetchState {
|
||||
redirect_headers: None,
|
||||
request_origin: moli_url::WebOrigin::from_url(
|
||||
@@ -4268,6 +4272,7 @@ async fn streaming_xhr_materialization_failure_errors_body_source_before_close()
|
||||
.borrow_mut()
|
||||
.record_streaming_subresource_fetch(super::StreamingSubresourceFetchState {
|
||||
response_filter: None,
|
||||
skip_fetch_security_validation: false,
|
||||
pending: super::PendingSubresourceFetchState {
|
||||
redirect_headers: None,
|
||||
request_origin: moli_url::WebOrigin::from_url(
|
||||
|
||||
@@ -3049,6 +3049,7 @@ fn default_bucket_quota_is_shared_by_cache_indexed_db_and_opfs() {
|
||||
"fixture",
|
||||
"/reserved",
|
||||
moli_storage_service::StorageBucketCachedResponse {
|
||||
cors_exposed_header_names: None,
|
||||
response_type: "default".to_owned(),
|
||||
url: format!("{page_url}reserved"),
|
||||
redirected: false,
|
||||
|
||||
@@ -353,6 +353,7 @@ fn register_pending_window_fetch_for_test(
|
||||
host.record_streaming_subresource_fetch(
|
||||
crate::types::StreamingSubresourceFetchState {
|
||||
response_filter: None,
|
||||
skip_fetch_security_validation: false,
|
||||
pending,
|
||||
request_url: url.clone(),
|
||||
request_method: "GET".to_owned(),
|
||||
|
||||
@@ -629,6 +629,7 @@ pub(crate) struct ServiceWorkerFetchEvent {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ServiceWorkerFetchResponse {
|
||||
pub(crate) cors_exposed_header_names: Option<Vec<String>>,
|
||||
pub(crate) final_url: Option<Url>,
|
||||
pub(crate) response_type: String,
|
||||
pub(crate) redirected: bool,
|
||||
@@ -695,6 +696,7 @@ pub(crate) struct ServiceWorkerNavigationPreloadFailure {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct MaterializedServiceWorkerFetchResponseHead {
|
||||
pub(crate) cors_exposed_header_names: Option<Vec<String>>,
|
||||
pub(crate) final_url: Option<Url>,
|
||||
pub(crate) response_type: String,
|
||||
pub(crate) redirected: bool,
|
||||
|
||||
@@ -63,6 +63,7 @@ fn navigation_preload_response_head(
|
||||
head: moli_fetch::ResponseHead,
|
||||
) -> MaterializedServiceWorkerFetchResponseHead {
|
||||
MaterializedServiceWorkerFetchResponseHead {
|
||||
cors_exposed_header_names: None,
|
||||
status_text: head.status_text().to_owned(),
|
||||
final_url: Some(head.final_url),
|
||||
response_type: "default".to_owned(),
|
||||
|
||||
@@ -123,10 +123,16 @@ fn service_worker_fetch_can_forward_stream(
|
||||
job: &ServiceWorkerFetchJob,
|
||||
response_head: &MaterializedServiceWorkerFetchResponseHead,
|
||||
) -> Result<bool, String> {
|
||||
let final_url = response_head.final_url.as_ref().unwrap_or(&job.request.url);
|
||||
if let Some(message) = service_worker_fetch_response_head_rejection(
|
||||
job,
|
||||
&response_head.response_type,
|
||||
response_head.redirected,
|
||||
) {
|
||||
return Err(message);
|
||||
}
|
||||
validate_service_worker_fetch_response_head_security_policy(
|
||||
job,
|
||||
final_url,
|
||||
response_head.final_url.as_ref(),
|
||||
&response_head.headers,
|
||||
)?;
|
||||
Ok(job.direct_completion_tx.is_none()
|
||||
@@ -140,8 +146,10 @@ fn service_worker_fetch_can_forward_stream(
|
||||
| crate::types::SubresourceResourceType::Video
|
||||
)
|
||||
&& !is_redirect_status(response_head.status)
|
||||
&& matches!(response_head.response_type.as_str(), "default" | "basic")
|
||||
&& !service_worker_fetch_response_requires_body_security_policy(job))
|
||||
&& matches!(
|
||||
response_head.response_type.as_str(),
|
||||
"default" | "basic" | "cors"
|
||||
))
|
||||
}
|
||||
|
||||
fn apply_service_worker_synthetic_redirect(
|
||||
@@ -383,10 +391,13 @@ impl ServiceWorkerRuntimeService {
|
||||
job.completion_tx.clone(),
|
||||
AsyncSubresourceFetchEvent::StreamingStarted(Box::new(
|
||||
AsyncSubresourceStreamingStarted {
|
||||
skip_fetch_security_validation: true,
|
||||
response_filter: service_worker_response_type_filter(
|
||||
response_filter: service_worker_fetch_response_head_filter(
|
||||
&started.response_head.response_type,
|
||||
started.response_head.cors_exposed_header_names.as_deref(),
|
||||
started.response_head.status,
|
||||
job.request.redirect_mode,
|
||||
),
|
||||
skip_fetch_security_validation: true,
|
||||
internal_id: job.internal_id,
|
||||
request_url: job.request.url.clone(),
|
||||
request_method: job.request.method.clone(),
|
||||
@@ -595,9 +606,13 @@ impl ServiceWorkerRuntimeService {
|
||||
.final_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| job.request.url.clone());
|
||||
if let Err(message) =
|
||||
validate_service_worker_fetch_response_security_policy(&job, &response, &final_url)
|
||||
{
|
||||
// The inner network fetch already applies ORB. Synthesized bodies belong
|
||||
// to the service worker; only the receiving client's CORP/COEP is checked here.
|
||||
if let Err(message) = validate_service_worker_fetch_response_head_security_policy(
|
||||
&job,
|
||||
response.final_url.as_ref(),
|
||||
&response.headers,
|
||||
) {
|
||||
self.complete_fetch_with_failure(job, message);
|
||||
return;
|
||||
}
|
||||
@@ -760,57 +775,16 @@ fn service_worker_fetch_is_navigation_request(job: &ServiceWorkerFetchJob) -> bo
|
||||
)
|
||||
}
|
||||
|
||||
fn validate_service_worker_fetch_response_security_policy(
|
||||
job: &ServiceWorkerFetchJob,
|
||||
response: &ServiceWorkerFetchResponse,
|
||||
final_url: &Url,
|
||||
) -> Result<(), String> {
|
||||
validate_service_worker_fetch_response_body_security_policy(job, response, final_url)?;
|
||||
validate_service_worker_fetch_response_head_security_policy(job, final_url, &response.headers)
|
||||
}
|
||||
|
||||
fn service_worker_fetch_response_requires_body_security_policy(
|
||||
job: &ServiceWorkerFetchJob,
|
||||
) -> bool {
|
||||
job.request.request_mode == moli_fetch::RequestMode::NoCors
|
||||
&& matches!(
|
||||
job.network_context.resource_type,
|
||||
crate::types::SubresourceResourceType::Fetch
|
||||
| crate::types::SubresourceResourceType::Xhr
|
||||
)
|
||||
}
|
||||
|
||||
fn validate_service_worker_fetch_response_body_security_policy(
|
||||
job: &ServiceWorkerFetchJob,
|
||||
response: &ServiceWorkerFetchResponse,
|
||||
final_url: &Url,
|
||||
) -> Result<(), String> {
|
||||
if job.request.request_mode != moli_fetch::RequestMode::NoCors
|
||||
|| !matches!(
|
||||
job.network_context.resource_type,
|
||||
crate::types::SubresourceResourceType::Fetch
|
||||
| crate::types::SubresourceResourceType::Xhr
|
||||
)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
validate_service_worker_fetch_response_head_security_policy(job, final_url, &response.headers)?;
|
||||
crate::network_host::validate_opaque_response_blocking_with_body(
|
||||
job.request
|
||||
.browser_origin()
|
||||
.map_err(|error| error.to_string())?,
|
||||
final_url,
|
||||
&response.headers,
|
||||
&response.body,
|
||||
)
|
||||
}
|
||||
|
||||
fn validate_service_worker_fetch_response_head_security_policy(
|
||||
job: &ServiceWorkerFetchJob,
|
||||
final_url: &Url,
|
||||
final_url: Option<&Url>,
|
||||
headers: &[(String, Vec<u8>)],
|
||||
) -> Result<(), String> {
|
||||
// Synthesized responses have an empty URL list and are same-origin to the
|
||||
// service worker's client. A network response retains its actual origin.
|
||||
let Some(final_url) = final_url else {
|
||||
return Ok(());
|
||||
};
|
||||
if job.request.request_mode != moli_fetch::RequestMode::NoCors
|
||||
|| matches!(
|
||||
job.network_context.resource_type,
|
||||
@@ -842,7 +816,15 @@ fn service_worker_fetch_response_rejection(
|
||||
job: &ServiceWorkerFetchJob,
|
||||
response: &ServiceWorkerFetchResponse,
|
||||
) -> Option<String> {
|
||||
match response.response_type.as_str() {
|
||||
service_worker_fetch_response_head_rejection(job, &response.response_type, response.redirected)
|
||||
}
|
||||
|
||||
fn service_worker_fetch_response_head_rejection(
|
||||
job: &ServiceWorkerFetchJob,
|
||||
response_type: &str,
|
||||
redirected: bool,
|
||||
) -> Option<String> {
|
||||
match response_type {
|
||||
"error" => {
|
||||
return Some("FetchEvent.respondWith rejected an error Response".to_owned());
|
||||
}
|
||||
@@ -872,7 +854,7 @@ fn service_worker_fetch_response_rejection(
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if response.redirected && job.request.redirect_mode != moli_fetch::RequestRedirectMode::Follow {
|
||||
if redirected && job.request.redirect_mode != moli_fetch::RequestRedirectMode::Follow {
|
||||
return Some(
|
||||
"FetchEvent.respondWith rejected a redirected Response for a request whose redirect mode is not follow"
|
||||
.to_owned(),
|
||||
@@ -885,26 +867,33 @@ fn service_worker_fetch_response_filter(
|
||||
response: &ServiceWorkerFetchResponse,
|
||||
redirect_mode: moli_fetch::RequestRedirectMode,
|
||||
) -> Option<AsyncSubresourceFetchResponseFilter> {
|
||||
if redirect_mode == moli_fetch::RequestRedirectMode::Manual
|
||||
&& is_redirect_status(response.status)
|
||||
&& !matches!(response.response_type.as_str(), "opaque" | "opaqueredirect")
|
||||
{
|
||||
Some(AsyncSubresourceFetchResponseFilter::OpaqueRedirect)
|
||||
} else {
|
||||
service_worker_response_type_filter(&response.response_type)
|
||||
}
|
||||
service_worker_fetch_response_head_filter(
|
||||
&response.response_type,
|
||||
response.cors_exposed_header_names.as_deref(),
|
||||
response.status,
|
||||
redirect_mode,
|
||||
)
|
||||
}
|
||||
|
||||
fn service_worker_response_type_filter(
|
||||
fn service_worker_fetch_response_head_filter(
|
||||
response_type: &str,
|
||||
cors_exposed_header_names: Option<&[String]>,
|
||||
status: u16,
|
||||
redirect_mode: moli_fetch::RequestRedirectMode,
|
||||
) -> Option<AsyncSubresourceFetchResponseFilter> {
|
||||
match response_type {
|
||||
"basic" => Some(AsyncSubresourceFetchResponseFilter::Basic),
|
||||
"cors" => Some(AsyncSubresourceFetchResponseFilter::Cors),
|
||||
"opaque" => Some(AsyncSubresourceFetchResponseFilter::Opaque),
|
||||
"opaqueredirect" => Some(AsyncSubresourceFetchResponseFilter::OpaqueRedirect),
|
||||
_ => None,
|
||||
}
|
||||
Some(match response_type {
|
||||
"opaque" => AsyncSubresourceFetchResponseFilter::Opaque,
|
||||
"opaqueredirect" => AsyncSubresourceFetchResponseFilter::OpaqueRedirect,
|
||||
_ if redirect_mode == moli_fetch::RequestRedirectMode::Manual
|
||||
&& is_redirect_status(status) =>
|
||||
{
|
||||
AsyncSubresourceFetchResponseFilter::OpaqueRedirect
|
||||
}
|
||||
"cors" => AsyncSubresourceFetchResponseFilter::Cors(
|
||||
cors_exposed_header_names.unwrap_or_default().to_vec(),
|
||||
),
|
||||
_ => AsyncSubresourceFetchResponseFilter::Basic,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1264,6 +1253,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(final_url.clone()),
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -1280,7 +1270,10 @@ mod tests {
|
||||
assert_eq!(completion.request_method, "GET");
|
||||
assert_eq!(completion.response_status_text.as_deref(), Some("Accepted"));
|
||||
assert!(completion.skip_fetch_security_validation);
|
||||
assert_eq!(completion.response_filter, None);
|
||||
assert_eq!(
|
||||
completion.response_filter,
|
||||
Some(AsyncSubresourceFetchResponseFilter::Basic)
|
||||
);
|
||||
let response = completion
|
||||
.result
|
||||
.expect("service worker response should resolve");
|
||||
@@ -1364,6 +1357,7 @@ mod tests {
|
||||
),
|
||||
body_source_id,
|
||||
response_head: MaterializedServiceWorkerFetchResponseHead {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(final_url.clone()),
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -1402,6 +1396,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(final_url),
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -1449,6 +1444,7 @@ mod tests {
|
||||
),
|
||||
body_source_id,
|
||||
response_head: MaterializedServiceWorkerFetchResponseHead {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(final_url),
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -1526,6 +1522,7 @@ mod tests {
|
||||
version_id, run,
|
||||
),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: None,
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -1550,8 +1547,11 @@ mod tests {
|
||||
} else {
|
||||
assert_eq!(
|
||||
completion.response_filter,
|
||||
(mode == Manual)
|
||||
.then_some(AsyncSubresourceFetchResponseFilter::OpaqueRedirect)
|
||||
Some(if mode == Manual {
|
||||
AsyncSubresourceFetchResponseFilter::OpaqueRedirect
|
||||
} else {
|
||||
AsyncSubresourceFetchResponseFilter::Basic
|
||||
})
|
||||
);
|
||||
let response = completion.result.unwrap();
|
||||
assert_eq!(response.status, status);
|
||||
@@ -1587,6 +1587,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: None,
|
||||
response_type: "opaque".to_owned(),
|
||||
redirected: false,
|
||||
@@ -1632,6 +1633,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: None,
|
||||
response_type: "opaqueredirect".to_owned(),
|
||||
redirected: false,
|
||||
@@ -1676,6 +1678,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(final_url.clone()),
|
||||
response_type: "default".to_owned(),
|
||||
redirected: true,
|
||||
@@ -1719,6 +1722,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(url("https://example.test/app/manual-final.txt")),
|
||||
response_type: "default".to_owned(),
|
||||
redirected: true,
|
||||
@@ -1767,6 +1771,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: None,
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -1890,6 +1895,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: None,
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -1951,6 +1957,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: None,
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -2058,6 +2065,7 @@ mod tests {
|
||||
.without_inferred_referrer()
|
||||
.with_fetch_priority_hint(Some(moli_fetch::FetchPriorityHint::High));
|
||||
let response = ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(request_url.clone()),
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -2132,6 +2140,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: None,
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -2179,6 +2188,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(response_url.clone()),
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -2246,6 +2256,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: None,
|
||||
response_type: "error".to_owned(),
|
||||
redirected: false,
|
||||
@@ -2293,6 +2304,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(url("https://cross-origin.test/data.txt")),
|
||||
response_type: "cors".to_owned(),
|
||||
redirected: false,
|
||||
@@ -2339,6 +2351,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: None,
|
||||
response_type: "opaque".to_owned(),
|
||||
redirected: false,
|
||||
@@ -2401,6 +2414,7 @@ mod tests {
|
||||
run.clone(),
|
||||
),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: None,
|
||||
response_type: "opaque".to_owned(),
|
||||
redirected: false,
|
||||
@@ -2448,6 +2462,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(url("https://cdn.example.test/app/image.png")),
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -2501,6 +2516,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(url("https://cdn.example.test/app/image.png")),
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -2564,6 +2580,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(url("https://cdn.example.test/app/pixel.png")),
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -2623,6 +2640,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(url("https://cdn.example.test/app/pixel.png")),
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -2681,6 +2699,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(url("https://cdn.example.test/app/pixel.png")),
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -2739,6 +2758,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(url("https://cdn.example.test/app/pixel.png")),
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -2798,6 +2818,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(url("https://cdn.example.test/app/pixel.png")),
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -2851,6 +2872,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(url("https://cdn.example.test/app/pixel.png")),
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -2909,6 +2931,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(url("https://cdn.example.test/app/pixel.png")),
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -2966,6 +2989,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(url("https://cdn.example.test/app/pixel.png")),
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -3017,6 +3041,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(url("https://cdn.example.test/app/pixel.png")),
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -3076,6 +3101,7 @@ mod tests {
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
body_source_id,
|
||||
response_head: MaterializedServiceWorkerFetchResponseHead {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(url("https://cdn.example.test/app/pixel.png")),
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -3144,6 +3170,7 @@ mod tests {
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
body_source_id,
|
||||
response_head: MaterializedServiceWorkerFetchResponseHead {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(url("https://cdn.example.test/app/pixel.png")),
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -3209,6 +3236,7 @@ mod tests {
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
body_source_id,
|
||||
response_head: MaterializedServiceWorkerFetchResponseHead {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(url("https://cdn.example.test/app/pixel.png")),
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -3238,7 +3266,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_completion_rejects_no_cors_service_worker_response_blocked_by_orb() {
|
||||
fn response_completion_keeps_synthesized_json_response_visible_to_no_cors_fetch() {
|
||||
let service = new_service_worker_runtime_service();
|
||||
let event_id = ServiceWorkerEventId(51);
|
||||
let version_id = ServiceWorkerVersionId(1);
|
||||
@@ -3260,27 +3288,30 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
final_url: Some(url("https://cdn.example.test/app/data.json")),
|
||||
cors_exposed_header_names: None,
|
||||
final_url: None,
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
status: 200,
|
||||
status_text: "OK".to_owned(),
|
||||
headers: vec![("content-type".to_owned(), b"application/json".to_vec())],
|
||||
body: br#"{"secret":true}"#.to_vec(),
|
||||
body: br#"{"visible":true}"#.to_vec(),
|
||||
}),
|
||||
});
|
||||
|
||||
let completion = pop_async_subresource_completion(&mut completion_queue);
|
||||
assert_eq!(completion.internal_id, 331);
|
||||
assert_eq!(completion.response_status_text, None);
|
||||
assert_eq!(completion.response_filter, None);
|
||||
assert!(!completion.skip_fetch_security_validation);
|
||||
assert!(
|
||||
completion
|
||||
.result
|
||||
.expect_err("ORB should reject")
|
||||
.contains("OpaqueResponseBlocking")
|
||||
assert_eq!(completion.response_status_text.as_deref(), Some("OK"));
|
||||
assert_eq!(
|
||||
completion.response_filter,
|
||||
Some(AsyncSubresourceFetchResponseFilter::Basic)
|
||||
);
|
||||
assert!(completion.skip_fetch_security_validation);
|
||||
let response = completion
|
||||
.result
|
||||
.expect("A synthesized response is readable by the service worker's client");
|
||||
assert_eq!(response.status, 200);
|
||||
assert_eq!(response.body_text(), r#"{"visible":true}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3306,6 +3337,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(url("https://cdn.example.test/app/data.json")),
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -3319,7 +3351,10 @@ mod tests {
|
||||
let completion = pop_async_subresource_completion(&mut completion_queue);
|
||||
assert_eq!(completion.internal_id, 332);
|
||||
assert!(completion.skip_fetch_security_validation);
|
||||
assert_eq!(completion.response_filter, None);
|
||||
assert_eq!(
|
||||
completion.response_filter,
|
||||
Some(AsyncSubresourceFetchResponseFilter::Basic)
|
||||
);
|
||||
let response = completion
|
||||
.result
|
||||
.expect("Service Worker response should not need ACAO");
|
||||
@@ -3350,6 +3385,7 @@ mod tests {
|
||||
event_id,
|
||||
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(version_id, run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: None,
|
||||
response_type: "opaqueredirect".to_owned(),
|
||||
redirected: false,
|
||||
|
||||
@@ -1072,8 +1072,10 @@ fn navigation_preload_completion_keeps_response_started_preload_alive() {
|
||||
);
|
||||
|
||||
assert!(
|
||||
service
|
||||
.mark_navigation_preload_response_started(event_id, &test_run_owner(version_id, &run),)
|
||||
service.mark_navigation_preload_response_started(
|
||||
event_id,
|
||||
&test_run_owner(version_id, &run),
|
||||
)
|
||||
);
|
||||
assert!(
|
||||
!navigation_preload_cancel_handle.is_cancelled(),
|
||||
@@ -1084,6 +1086,7 @@ fn navigation_preload_completion_keeps_response_started_preload_alive() {
|
||||
event_id,
|
||||
owner: test_run_owner(version_id, &run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
status: 200,
|
||||
status_text: "OK".to_owned(),
|
||||
headers: vec![("content-type".to_owned(), b"text/plain".to_vec())],
|
||||
@@ -1381,6 +1384,7 @@ fn abort_controlled_fetch_drops_direct_worker_completion_and_ignores_late_comple
|
||||
event_id,
|
||||
owner: test_run_owner(version_id, &run),
|
||||
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: None,
|
||||
status: 200,
|
||||
status_text: "OK".to_owned(),
|
||||
headers: vec![("content-type".to_owned(), b"text/plain".to_vec())],
|
||||
|
||||
@@ -272,7 +272,7 @@ async fn fetch_stylesheet_readiness_with_request(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
enum StylesheetResponseProvenance {
|
||||
Network,
|
||||
ServiceWorker {
|
||||
@@ -282,7 +282,7 @@ enum StylesheetResponseProvenance {
|
||||
|
||||
impl StylesheetResponseProvenance {
|
||||
fn is_cors_same_origin(
|
||||
self,
|
||||
&self,
|
||||
request_origin: &moli_url::WebOrigin,
|
||||
request_url: &Url,
|
||||
head: &moli_fetch::ResponseHead,
|
||||
@@ -321,7 +321,7 @@ fn stylesheet_terminal_from_response(
|
||||
let (request_mode, credentials_mode) = options.request_mode_and_credentials();
|
||||
let head = response.head();
|
||||
let cors_usability =
|
||||
(request_mode == moli_fetch::RequestMode::Cors).then(|| match response_provenance {
|
||||
(request_mode == moli_fetch::RequestMode::Cors).then(|| match &response_provenance {
|
||||
StylesheetResponseProvenance::ServiceWorker {
|
||||
filter:
|
||||
Some(
|
||||
|
||||
@@ -2174,7 +2174,7 @@ async fn fetch_connected_link_readiness_with_service_worker(
|
||||
Ok(Some(response)) => {
|
||||
let response_filter = response.response_filter;
|
||||
let origin_clean =
|
||||
connected_link_origin_clean_from_service_worker_filter(response_filter);
|
||||
connected_link_origin_clean_from_service_worker_filter(response_filter.clone());
|
||||
let response = *response.response;
|
||||
let load_event_successful =
|
||||
connected_link_load_event_successful(&response, response_filter);
|
||||
|
||||
@@ -737,17 +737,17 @@ impl From<std::result::Result<NavigationResponse, String>> for AsyncSubresourceF
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(super) enum AsyncSubresourceFetchResponseFilter {
|
||||
Basic,
|
||||
Cors,
|
||||
Cors(Vec<String>),
|
||||
Opaque,
|
||||
OpaqueRedirect,
|
||||
}
|
||||
|
||||
impl AsyncSubresourceFetchResponseFilter {
|
||||
pub(super) fn is_readable(self) -> bool {
|
||||
matches!(self, Self::Basic | Self::Cors)
|
||||
pub(super) fn is_readable(&self) -> bool {
|
||||
matches!(self, Self::Basic | Self::Cors(_))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1013,6 +1013,7 @@ pub(super) struct ServiceWorkerControllerChangeCompletion {
|
||||
|
||||
pub(super) struct StreamingSubresourceFetchState {
|
||||
pub(super) response_filter: Option<AsyncSubresourceFetchResponseFilter>,
|
||||
pub(super) skip_fetch_security_validation: bool,
|
||||
pub(super) pending: PendingSubresourceFetchState,
|
||||
pub(super) request_url: Url,
|
||||
pub(super) request_method: String,
|
||||
@@ -1028,12 +1029,13 @@ pub(super) struct StreamingSubresourceFetchState {
|
||||
|
||||
impl StreamingSubresourceFetchState {
|
||||
pub(super) fn needs_orb_body_validation(&self) -> bool {
|
||||
crate::network_host::fetch_response_needs_orb_body_validation(
|
||||
&self.pending.info.document_url,
|
||||
&self.head.final_url,
|
||||
&self.head.headers,
|
||||
self.pending.request_mode,
|
||||
)
|
||||
!self.skip_fetch_security_validation
|
||||
&& crate::network_host::fetch_response_needs_orb_body_validation(
|
||||
&self.pending.request_origin,
|
||||
&self.head.final_url,
|
||||
&self.head.headers,
|
||||
self.pending.request_mode,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -330,6 +330,8 @@ pub(in crate::worker) fn spawn_worker_fetch_network(
|
||||
};
|
||||
let _ = completion_tx.send(WorkerFetchEvent::Completion(Box::new(
|
||||
WorkerFetchCompletion {
|
||||
response_filter: None,
|
||||
skip_fetch_security_validation: false,
|
||||
fetch_id,
|
||||
network_request_headers,
|
||||
result,
|
||||
@@ -437,6 +439,8 @@ fn spawn_worker_fetch_service_worker(
|
||||
if !runtime.dispatch_controlled_fetch(dispatch) {
|
||||
let _ = completion_tx.send(WorkerFetchEvent::Completion(Box::new(
|
||||
WorkerFetchCompletion {
|
||||
response_filter: None,
|
||||
skip_fetch_security_validation: false,
|
||||
fetch_id,
|
||||
network_request_headers: None,
|
||||
result: Err("service worker fetch dispatch failed".to_owned()),
|
||||
@@ -473,6 +477,8 @@ fn spawn_worker_fetch_service_worker(
|
||||
Ok(ServiceWorkerDirectFetchResult::Response(response)) => {
|
||||
let _ = completion_tx.send(WorkerFetchEvent::Completion(Box::new(
|
||||
WorkerFetchCompletion {
|
||||
response_filter: response.response_filter,
|
||||
skip_fetch_security_validation: !response.from_network_fallback,
|
||||
fetch_id,
|
||||
network_request_headers: None,
|
||||
result: Ok(WorkerFetchResponse::Materialized(Box::new(
|
||||
@@ -484,6 +490,8 @@ fn spawn_worker_fetch_service_worker(
|
||||
Ok(ServiceWorkerDirectFetchResult::Failure(message)) => {
|
||||
let _ = completion_tx.send(WorkerFetchEvent::Completion(Box::new(
|
||||
WorkerFetchCompletion {
|
||||
response_filter: None,
|
||||
skip_fetch_security_validation: false,
|
||||
fetch_id,
|
||||
network_request_headers: None,
|
||||
result: Err(message),
|
||||
@@ -493,6 +501,8 @@ fn spawn_worker_fetch_service_worker(
|
||||
Err(_) => {
|
||||
let _ = completion_tx.send(WorkerFetchEvent::Completion(Box::new(
|
||||
WorkerFetchCompletion {
|
||||
response_filter: None,
|
||||
skip_fetch_security_validation: false,
|
||||
fetch_id,
|
||||
network_request_headers: None,
|
||||
result: Err("service worker fetch completion channel closed".to_owned()),
|
||||
@@ -774,6 +784,8 @@ pub(in crate::worker) fn fail_pending_worker_fetch(
|
||||
};
|
||||
let _ = completion_tx.send(WorkerFetchEvent::Completion(Box::new(
|
||||
WorkerFetchCompletion {
|
||||
response_filter: None,
|
||||
skip_fetch_security_validation: false,
|
||||
fetch_id,
|
||||
network_request_headers: None,
|
||||
result: Err(error_text),
|
||||
@@ -799,6 +811,8 @@ pub(in crate::worker) fn fail_pending_worker_fetch_auth(
|
||||
};
|
||||
let _ = completion_tx.send(WorkerFetchEvent::Completion(Box::new(
|
||||
WorkerFetchCompletion {
|
||||
response_filter: None,
|
||||
skip_fetch_security_validation: false,
|
||||
fetch_id,
|
||||
network_request_headers: None,
|
||||
result: Err(error_text),
|
||||
@@ -840,6 +854,8 @@ pub(in crate::worker) fn fulfill_pending_worker_fetch(
|
||||
};
|
||||
let _ = completion.0.send(WorkerFetchEvent::Completion(Box::new(
|
||||
WorkerFetchCompletion {
|
||||
response_filter: None,
|
||||
skip_fetch_security_validation: false,
|
||||
fetch_id: completion.2,
|
||||
network_request_headers: None,
|
||||
result: Ok(WorkerFetchResponse::Materialized(Box::new(completion.1))),
|
||||
@@ -881,6 +897,8 @@ pub(in crate::worker) fn continue_pending_worker_fetch_response(
|
||||
};
|
||||
let _ = completion.0.send(WorkerFetchEvent::Completion(Box::new(
|
||||
WorkerFetchCompletion {
|
||||
response_filter: completion.1.response_filter,
|
||||
skip_fetch_security_validation: completion.1.skip_fetch_security_validation,
|
||||
fetch_id: completion.2,
|
||||
network_request_headers: None,
|
||||
result: Ok(WorkerFetchResponse::Streamed {
|
||||
@@ -913,6 +931,8 @@ pub(in crate::worker) fn fail_pending_worker_fetch_response(
|
||||
};
|
||||
let _ = completion_tx.send(WorkerFetchEvent::Completion(Box::new(
|
||||
WorkerFetchCompletion {
|
||||
response_filter: None,
|
||||
skip_fetch_security_validation: false,
|
||||
fetch_id,
|
||||
network_request_headers: None,
|
||||
result: Err(error_text),
|
||||
@@ -946,6 +966,8 @@ pub(in crate::worker) fn fulfill_pending_worker_fetch_response(
|
||||
};
|
||||
let _ = completion.0.send(WorkerFetchEvent::Completion(Box::new(
|
||||
WorkerFetchCompletion {
|
||||
response_filter: None,
|
||||
skip_fetch_security_validation: false,
|
||||
fetch_id: completion.2,
|
||||
network_request_headers: None,
|
||||
result: Ok(WorkerFetchResponse::Materialized(Box::new(completion.1))),
|
||||
@@ -2716,13 +2738,13 @@ pub(in crate::worker) fn start_worker_streaming_fetch(
|
||||
&started.head.headers,
|
||||
pending.request_mode,
|
||||
);
|
||||
let mut observable_head = started.head.clone();
|
||||
observable_head.headers = crate::network_host::FetchResponseRequest {
|
||||
let observable_head = started.head.clone();
|
||||
let response_filter = crate::network_host::FetchResponseRequest {
|
||||
method: &pending.request_method,
|
||||
mode: pending.request_mode,
|
||||
redirect_mode: pending.redirect_mode,
|
||||
}
|
||||
.filter_response_headers(
|
||||
.network_response_filter(
|
||||
&request_origin,
|
||||
&observable_head,
|
||||
pending.credentials_mode,
|
||||
@@ -2735,6 +2757,7 @@ pub(in crate::worker) fn start_worker_streaming_fetch(
|
||||
pending.request_method.clone(),
|
||||
pending.redirect_mode,
|
||||
observable_head,
|
||||
response_filter,
|
||||
))
|
||||
}
|
||||
};
|
||||
@@ -2756,9 +2779,10 @@ pub(in crate::worker) fn start_worker_streaming_fetch(
|
||||
request_method,
|
||||
redirect_mode,
|
||||
observable_head,
|
||||
response_filter,
|
||||
)) = response_input
|
||||
{
|
||||
let response_obj = build_fetch_response_object_from_stream_for_request_mode(
|
||||
let response_obj = crate::network_host::build_fetch_response_object_from_stream_for_request_mode_with_filter(
|
||||
scope,
|
||||
&document_url,
|
||||
crate::network_host::FetchResponseRequest {
|
||||
@@ -2768,6 +2792,7 @@ pub(in crate::worker) fn start_worker_streaming_fetch(
|
||||
},
|
||||
observable_head,
|
||||
started.body_source_id,
|
||||
Some(response_filter),
|
||||
);
|
||||
let resolver = v8::Local::new(scope, &resolver);
|
||||
let _ = resolver.resolve(scope, response_obj.into());
|
||||
@@ -2983,6 +3008,8 @@ pub(in crate::worker) fn drain_worker_fetch_completion_result(
|
||||
record.follow_redirects(&response_head);
|
||||
let response_body = response.subresource_response_body();
|
||||
pending.paused_response = Some(PausedWorkerSubresourceResponse {
|
||||
response_filter: completion.response_filter.clone(),
|
||||
skip_fetch_security_validation: completion.skip_fetch_security_validation,
|
||||
head: response_head.clone(),
|
||||
body: response_body.clone(),
|
||||
});
|
||||
@@ -3038,6 +3065,8 @@ pub(in crate::worker) fn drain_worker_fetch_completion_result(
|
||||
from_cache: response_head.from_cache,
|
||||
};
|
||||
pending.paused_response = Some(PausedWorkerSubresourceResponse {
|
||||
response_filter: completion.response_filter.clone(),
|
||||
skip_fetch_security_validation: completion.skip_fetch_security_validation,
|
||||
head: response_head,
|
||||
body: response_body,
|
||||
});
|
||||
@@ -3065,34 +3094,39 @@ pub(in crate::worker) fn drain_worker_fetch_completion_result(
|
||||
match completion.result {
|
||||
Ok(response) => {
|
||||
let response_head = response.head();
|
||||
let security_validation = match &response {
|
||||
WorkerFetchResponse::Materialized(response) => {
|
||||
validate_fetch_response_security_policy_with_body_classified(
|
||||
&pending.document_url,
|
||||
&response_head,
|
||||
response.body_bytes(),
|
||||
pending.request_mode,
|
||||
pending.credentials_mode,
|
||||
pending.policy_context,
|
||||
)
|
||||
}
|
||||
WorkerFetchResponse::Streamed { body, .. } => body
|
||||
.try_bytes()
|
||||
.map_err(|error| {
|
||||
FetchResponseSecurityViolation::Rejected(format!(
|
||||
"fetch: failed to read response body: {error}"
|
||||
))
|
||||
})
|
||||
.and_then(|body_bytes| {
|
||||
let request_origin = moli_url::WebOrigin::from_url(&pending.document_url);
|
||||
let security_validation = if completion.skip_fetch_security_validation {
|
||||
Ok(())
|
||||
} else {
|
||||
match &response {
|
||||
WorkerFetchResponse::Materialized(response) => {
|
||||
validate_fetch_response_security_policy_with_body_classified(
|
||||
&pending.document_url,
|
||||
&request_origin,
|
||||
&response_head,
|
||||
&body_bytes,
|
||||
response.body_bytes(),
|
||||
pending.request_mode,
|
||||
pending.credentials_mode,
|
||||
pending.policy_context,
|
||||
)
|
||||
}),
|
||||
}
|
||||
WorkerFetchResponse::Streamed { body, .. } => body
|
||||
.try_bytes()
|
||||
.map_err(|error| {
|
||||
FetchResponseSecurityViolation::Rejected(format!(
|
||||
"fetch: failed to read response body: {error}"
|
||||
))
|
||||
})
|
||||
.and_then(|body_bytes| {
|
||||
validate_fetch_response_security_policy_with_body_classified(
|
||||
&request_origin,
|
||||
&response_head,
|
||||
&body_bytes,
|
||||
pending.request_mode,
|
||||
pending.credentials_mode,
|
||||
pending.policy_context,
|
||||
)
|
||||
}),
|
||||
}
|
||||
};
|
||||
let opaque_response_blocked = match security_validation {
|
||||
Ok(()) => false,
|
||||
@@ -3136,46 +3170,47 @@ pub(in crate::worker) fn drain_worker_fetch_completion_result(
|
||||
},
|
||||
));
|
||||
}
|
||||
let request_origin = moli_url::WebOrigin::from_url(&pending.document_url);
|
||||
let response_request = crate::network_host::FetchResponseRequest {
|
||||
method: &pending.request_method,
|
||||
mode: pending.request_mode,
|
||||
redirect_mode: pending.redirect_mode,
|
||||
};
|
||||
let filtered_headers = response_request.filter_response_headers(
|
||||
&request_origin,
|
||||
&response_head,
|
||||
pending.credentials_mode,
|
||||
);
|
||||
let response_filter = completion.response_filter.or_else(|| {
|
||||
Some(response_request.network_response_filter(
|
||||
&request_origin,
|
||||
&response_head,
|
||||
pending.credentials_mode,
|
||||
))
|
||||
});
|
||||
let response_obj = match response.into_fetch_parts() {
|
||||
WorkerFetchResponseParts::Materialized { mut head, body } => {
|
||||
head.headers = filtered_headers;
|
||||
WorkerFetchResponseParts::Materialized { head, body } => {
|
||||
let body = if opaque_response_blocked {
|
||||
ResponseBody::materialized_bytes(Vec::new())
|
||||
} else {
|
||||
*body
|
||||
};
|
||||
build_fetch_response_object_from_body_source_for_request_mode(
|
||||
crate::network_host::build_fetch_response_object_from_body_source_for_request_mode_with_filter(
|
||||
scope,
|
||||
&pending.document_url,
|
||||
response_request,
|
||||
head,
|
||||
body,
|
||||
response_filter,
|
||||
)
|
||||
}
|
||||
WorkerFetchResponseParts::Subresource { mut head, body } => {
|
||||
head.headers = filtered_headers;
|
||||
WorkerFetchResponseParts::Subresource { head, body } => {
|
||||
let body = if opaque_response_blocked {
|
||||
SubresourceResponseBody::from_bytes(Vec::new())
|
||||
} else {
|
||||
body
|
||||
};
|
||||
build_fetch_response_object_from_subresource_body_for_request_mode(
|
||||
crate::network_host::build_fetch_response_object_from_subresource_body_for_request_mode_with_filter(
|
||||
scope,
|
||||
&pending.document_url,
|
||||
response_request,
|
||||
head,
|
||||
body,
|
||||
response_filter,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -71,9 +71,6 @@ use crate::network_host::{
|
||||
XHR_SEND_FLAG_SLOT, XHR_TIMEOUT_SLOT, XHR_TIMEOUT_START_MS_SLOT, XHR_TIMEOUT_TIMER_SLOT,
|
||||
XHR_URL_SLOT, XHR_WITH_CREDENTIALS_SLOT, append_default_body_content_type, apply_xhr_failure,
|
||||
apply_xhr_response, apply_xhr_response_body_source, apply_xhr_timeout, apply_xhr_upload_event,
|
||||
build_fetch_response_object_from_body_source_for_request_mode,
|
||||
build_fetch_response_object_from_stream_for_request_mode,
|
||||
build_fetch_response_object_from_subresource_body_for_request_mode,
|
||||
capture_xhr_upload_listener_flag, close_pending_network_body_stream, dispatch_xhr_loadstart,
|
||||
enqueue_pending_network_body_chunk, error_pending_network_body_stream_with_reason,
|
||||
extract_subresource_auth_challenge,
|
||||
@@ -1095,6 +1092,8 @@ pub(super) enum WorkerFetchEvent {
|
||||
}
|
||||
|
||||
pub(super) struct WorkerFetchCompletion {
|
||||
response_filter: Option<crate::types::AsyncSubresourceFetchResponseFilter>,
|
||||
skip_fetch_security_validation: bool,
|
||||
fetch_id: u32,
|
||||
network_request_headers: Option<Vec<(String, String)>>,
|
||||
result: Result<WorkerFetchResponse, String>,
|
||||
@@ -1234,6 +1233,8 @@ pub(super) struct PendingWorkerCspReport {
|
||||
}
|
||||
|
||||
pub(super) struct PausedWorkerSubresourceResponse {
|
||||
pub(super) response_filter: Option<crate::types::AsyncSubresourceFetchResponseFilter>,
|
||||
pub(super) skip_fetch_security_validation: bool,
|
||||
pub(super) head: ResponseHead,
|
||||
pub(super) body: SubresourceResponseBody,
|
||||
}
|
||||
|
||||
@@ -1045,6 +1045,8 @@ pub(in crate::worker) fn drain_worker_xhr_completion(
|
||||
record.follow_redirects(&response_head);
|
||||
let response_body = response.subresource_response_body();
|
||||
pending.paused_response = Some(PausedWorkerSubresourceResponse {
|
||||
response_filter: None,
|
||||
skip_fetch_security_validation: false,
|
||||
head: response_head.clone(),
|
||||
body: response_body.clone(),
|
||||
});
|
||||
@@ -1101,6 +1103,8 @@ pub(in crate::worker) fn drain_worker_xhr_completion(
|
||||
from_cache: response_head.from_cache,
|
||||
};
|
||||
pending.paused_response = Some(PausedWorkerSubresourceResponse {
|
||||
response_filter: None,
|
||||
skip_fetch_security_validation: false,
|
||||
head: response_head,
|
||||
body: response_body,
|
||||
});
|
||||
|
||||
@@ -3364,6 +3364,7 @@ fn service_worker_fetch_response_from_materialized(
|
||||
) -> ServiceWorkerFetchResponse {
|
||||
let response = head.with_body(body);
|
||||
ServiceWorkerFetchResponse {
|
||||
cors_exposed_header_names: response.cors_exposed_header_names,
|
||||
final_url: response.final_url,
|
||||
response_type: response.response_type,
|
||||
redirected: response.redirected,
|
||||
@@ -3378,6 +3379,7 @@ fn service_worker_fetch_response_head_from_materialized(
|
||||
head: &MaterializedResponseHead,
|
||||
) -> MaterializedServiceWorkerFetchResponseHead {
|
||||
MaterializedServiceWorkerFetchResponseHead {
|
||||
cors_exposed_header_names: head.cors_exposed_header_names.clone(),
|
||||
status_text: head.status_text.clone(),
|
||||
final_url: head.final_url.clone(),
|
||||
response_type: head.response_type.clone(),
|
||||
|
||||
@@ -763,6 +763,7 @@ async fn service_worker_fetch_event_preload_response_resolves_network_response()
|
||||
request_mode: moli_fetch::RequestMode::Navigate,
|
||||
body_source_id,
|
||||
response_head: MaterializedServiceWorkerFetchResponseHead {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(
|
||||
url::Url::parse("https://example.test/app/navigation.html")
|
||||
.expect("navigation preload response URL"),
|
||||
@@ -879,6 +880,7 @@ async fn service_worker_fetch_event_preload_response_opaqueredirect_exposes_requ
|
||||
request_mode: moli_fetch::RequestMode::Navigate,
|
||||
body_source_id,
|
||||
response_head: MaterializedServiceWorkerFetchResponseHead {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(request_url),
|
||||
response_type: "default".to_owned(),
|
||||
redirected: false,
|
||||
@@ -1051,6 +1053,7 @@ async fn service_worker_fetch_event_preload_response_body_errors_after_response(
|
||||
request_mode: moli_fetch::RequestMode::Navigate,
|
||||
body_source_id,
|
||||
response_head: MaterializedServiceWorkerFetchResponseHead {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(
|
||||
url::Url::parse("https://example.test/app/navigation.html")
|
||||
.expect("navigation preload response URL"),
|
||||
@@ -1166,6 +1169,7 @@ async fn service_worker_fetch_event_preload_response_body_completes_after_fetch_
|
||||
request_mode: moli_fetch::RequestMode::Navigate,
|
||||
body_source_id,
|
||||
response_head: MaterializedServiceWorkerFetchResponseHead {
|
||||
cors_exposed_header_names: None,
|
||||
final_url: Some(
|
||||
url::Url::parse("https://example.test/app/navigation.html")
|
||||
.expect("navigation preload response URL"),
|
||||
|
||||
@@ -132,6 +132,7 @@ fn is_false(value: &bool) -> bool {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct StorageBucketCachedResponse {
|
||||
pub cors_exposed_header_names: Option<Vec<String>>,
|
||||
pub response_type: String,
|
||||
pub url: String,
|
||||
pub redirected: bool,
|
||||
@@ -388,6 +389,8 @@ struct StorageBucketCacheJson {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct StorageBucketCacheJsonEntry {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
cors_exposed_header_names: Option<Vec<String>>,
|
||||
usage_bytes: u64,
|
||||
#[serde(
|
||||
default = "default_cache_request_method",
|
||||
@@ -1774,6 +1777,8 @@ fn cache_entry_matches_query(
|
||||
query.ignore_vary
|
||||
// Opaque public headers are empty even though Cache retains the internal head.
|
||||
|| matches!(entry.response.response_type.as_str(), "opaque" | "opaqueredirect")
|
||||
|| entry.response.response_type == "cors" && entry.response.cors_exposed_header_names.as_ref()
|
||||
.is_some_and(|names| !names.iter().any(|name| name.eq_ignore_ascii_case("vary")))
|
||||
|| cached_response_vary_matches_request(
|
||||
&entry.response.headers,
|
||||
&entry.request.headers,
|
||||
@@ -2125,6 +2130,7 @@ fn load_storage_bucket_cache_file(
|
||||
headers: entry.request_headers,
|
||||
},
|
||||
response: StorageBucketCachedResponse {
|
||||
cors_exposed_header_names: entry.cors_exposed_header_names,
|
||||
response_type: entry.response_type,
|
||||
url: entry.url,
|
||||
redirected: entry.redirected,
|
||||
@@ -2156,6 +2162,7 @@ fn save_storage_bucket_cache_file(
|
||||
(
|
||||
request_key.clone(),
|
||||
StorageBucketCacheJsonEntry {
|
||||
cors_exposed_header_names: entry.response.cors_exposed_header_names.clone(),
|
||||
usage_bytes: entry.usage_bytes,
|
||||
request_method: entry.request.method.clone(),
|
||||
request_headers: entry.request.headers.clone(),
|
||||
@@ -2854,6 +2861,7 @@ mod tests {
|
||||
"cache",
|
||||
"request",
|
||||
StorageBucketCachedResponse {
|
||||
cors_exposed_header_names: None,
|
||||
response_type: "default".to_owned(),
|
||||
url: "https://quota.test/resource".to_owned(),
|
||||
redirected: false,
|
||||
@@ -2931,6 +2939,7 @@ mod tests {
|
||||
"cache",
|
||||
"request",
|
||||
StorageBucketCachedResponse {
|
||||
cors_exposed_header_names: None,
|
||||
response_type: "default".to_owned(),
|
||||
url: "https://default-quota.test/resource".to_owned(),
|
||||
redirected: false,
|
||||
@@ -3160,6 +3169,7 @@ mod tests {
|
||||
let identity = store.open_bucket("https://a.test", "bucket")?;
|
||||
assert!(store.open_cache_for_identity(&identity, "cache")?);
|
||||
let response = StorageBucketCachedResponse {
|
||||
cors_exposed_header_names: None,
|
||||
response_type: "default".to_owned(),
|
||||
url: String::new(),
|
||||
redirected: false,
|
||||
@@ -3205,6 +3215,7 @@ mod tests {
|
||||
let identity = store.open_bucket("https://a.test", "bucket")?;
|
||||
assert!(store.open_cache_for_identity(&identity, "cache")?);
|
||||
let first_response = StorageBucketCachedResponse {
|
||||
cors_exposed_header_names: None,
|
||||
response_type: "default".to_owned(),
|
||||
url: String::new(),
|
||||
redirected: false,
|
||||
@@ -3261,6 +3272,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let replacement_response = StorageBucketCachedResponse {
|
||||
cors_exposed_header_names: None,
|
||||
response_type: "default".to_owned(),
|
||||
url: String::new(),
|
||||
redirected: false,
|
||||
@@ -3315,6 +3327,7 @@ mod tests {
|
||||
let identity = store.open_bucket("https://a.test", "bucket")?;
|
||||
assert!(store.open_cache_for_identity(&identity, "cache")?);
|
||||
let response = |body: &str, headers: Vec<(String, Vec<u8>)>| StorageBucketCachedResponse {
|
||||
cors_exposed_header_names: None,
|
||||
response_type: "default".to_owned(),
|
||||
url: String::new(),
|
||||
redirected: false,
|
||||
@@ -3407,6 +3420,7 @@ mod tests {
|
||||
.open_cache_handle_for_identity(&identity, "cache")?
|
||||
.expect("bucket should remain current");
|
||||
let response = StorageBucketCachedResponse {
|
||||
cors_exposed_header_names: None,
|
||||
response_type: "default".to_owned(),
|
||||
url: String::new(),
|
||||
redirected: false,
|
||||
@@ -3471,6 +3485,7 @@ mod tests {
|
||||
)?;
|
||||
assert!(store.open_cache_for_identity(&identity, "cache")?);
|
||||
let original = StorageBucketCachedResponse {
|
||||
cors_exposed_header_names: None,
|
||||
response_type: "default".to_owned(),
|
||||
url: String::new(),
|
||||
redirected: false,
|
||||
@@ -3480,6 +3495,7 @@ mod tests {
|
||||
body: b"small".to_vec(),
|
||||
};
|
||||
let oversized = StorageBucketCachedResponse {
|
||||
cors_exposed_header_names: None,
|
||||
response_type: "default".to_owned(),
|
||||
url: String::new(),
|
||||
redirected: false,
|
||||
@@ -3589,6 +3605,7 @@ mod tests {
|
||||
"global-cache",
|
||||
"/entry",
|
||||
StorageBucketCachedResponse {
|
||||
cors_exposed_header_names: None,
|
||||
response_type: "default".to_owned(),
|
||||
url: "https://a.test/entry".to_owned(),
|
||||
redirected: false,
|
||||
@@ -3827,6 +3844,7 @@ mod tests {
|
||||
"cache",
|
||||
"/cached.txt",
|
||||
StorageBucketCachedResponse {
|
||||
cors_exposed_header_names: None,
|
||||
response_type: "default".to_owned(),
|
||||
url: String::new(),
|
||||
redirected: false,
|
||||
@@ -3916,86 +3934,107 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn json_storage_bucket_store_persists_cache_entries() -> Result<()> {
|
||||
let temp = TempStorePath::new("cache-persist");
|
||||
let cache_root = temp.cache_root();
|
||||
let response = StorageBucketCachedResponse {
|
||||
response_type: "default".to_owned(),
|
||||
url: String::new(),
|
||||
redirected: false,
|
||||
status: 202,
|
||||
status_text: "Accepted".to_owned(),
|
||||
headers: vec![
|
||||
("x-cache".to_owned(), b"persisted".to_vec()),
|
||||
for cors in [false, true] {
|
||||
let temp = TempStorePath::new("cache-persist");
|
||||
let cache_root = temp.cache_root();
|
||||
let mut response = StorageBucketCachedResponse {
|
||||
cors_exposed_header_names: cors.then(|| vec!["x-cache".to_owned()]),
|
||||
response_type: if cors { "cors" } else { "default" }.to_owned(),
|
||||
url: if cors {
|
||||
"https://remote.test/cached.txt"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
.to_owned(),
|
||||
redirected: false,
|
||||
status: 202,
|
||||
status_text: "Accepted".to_owned(),
|
||||
headers: if cors {
|
||||
vec![
|
||||
("x-cache".to_owned(), b"persisted".to_vec()),
|
||||
(
|
||||
"cross-origin-resource-policy".to_owned(),
|
||||
b"same-origin".to_vec(),
|
||||
),
|
||||
("vary".to_owned(), b"*".to_vec()),
|
||||
]
|
||||
} else {
|
||||
vec![("x-cache".to_owned(), b"persisted".to_vec())]
|
||||
},
|
||||
body: b"profile cache body".to_vec(),
|
||||
};
|
||||
response.headers.extend([
|
||||
("x-bytes".to_owned(), b"\xff\x80\xa0".to_vec()),
|
||||
("x-bytes".to_owned(), b"\xc3\xbf".to_vec()),
|
||||
("x-empty".to_owned(), Vec::new()),
|
||||
],
|
||||
body: b"profile cache body".to_vec(),
|
||||
};
|
||||
{
|
||||
]);
|
||||
{
|
||||
let store =
|
||||
new_shared_json_storage_bucket_store_with_cache_root(&temp.path, &cache_root)?;
|
||||
let mut store = store.lock();
|
||||
let identity = store.open_bucket("https://a.test", "bucket")?;
|
||||
assert!(store.open_cache_for_identity(&identity, "cache")?);
|
||||
assert_eq!(
|
||||
store.put_cache_entry_for_identity(
|
||||
&identity,
|
||||
"cache",
|
||||
"/cached.txt",
|
||||
response.clone(),
|
||||
64,
|
||||
0,
|
||||
)?,
|
||||
StorageBucketCachePutOutcome::Stored
|
||||
);
|
||||
}
|
||||
|
||||
assert!(
|
||||
cache_root.exists(),
|
||||
"profile-backed CacheStorage root should be written"
|
||||
);
|
||||
let (next, previous) = storage_bucket_cache_replacement_paths(&cache_root)?;
|
||||
assert!(
|
||||
!next.exists(),
|
||||
"profile-backed CacheStorage replacement root should not be left after save"
|
||||
);
|
||||
assert!(
|
||||
!previous.exists(),
|
||||
"profile-backed CacheStorage previous root should not be left after save"
|
||||
);
|
||||
|
||||
{
|
||||
let store =
|
||||
new_shared_json_storage_bucket_store_with_cache_root(&temp.path, &cache_root)?;
|
||||
let mut store = store.lock();
|
||||
let identity = store.open_bucket("https://a.test", "bucket")?;
|
||||
assert_eq!(
|
||||
store.cache_names_for_identity(&identity),
|
||||
Some(vec!["cache".to_owned()])
|
||||
);
|
||||
let matched = store
|
||||
.match_cache_entry_for_identity(&identity, "cache", "/cached.txt")
|
||||
.flatten()
|
||||
.expect("cache entry should persist across reopen");
|
||||
assert_eq!(matched, response);
|
||||
assert_eq!(store.cache_usage_for_identity(&identity), Some(64));
|
||||
assert_eq!(
|
||||
store.delete_cache_for_identity(&identity, "cache")?,
|
||||
Some(true)
|
||||
);
|
||||
}
|
||||
|
||||
let store =
|
||||
new_shared_json_storage_bucket_store_with_cache_root(&temp.path, &cache_root)?;
|
||||
let mut store = store.lock();
|
||||
let identity = store.open_bucket("https://a.test", "bucket")?;
|
||||
assert!(store.open_cache_for_identity(&identity, "cache")?);
|
||||
assert_eq!(
|
||||
store.put_cache_entry_for_identity(
|
||||
&identity,
|
||||
"cache",
|
||||
"/cached.txt",
|
||||
response.clone(),
|
||||
64,
|
||||
0,
|
||||
)?,
|
||||
StorageBucketCachePutOutcome::Stored
|
||||
);
|
||||
assert_eq!(store.cache_names_for_identity(&identity), Some(Vec::new()));
|
||||
}
|
||||
|
||||
assert!(
|
||||
cache_root.exists(),
|
||||
"profile-backed CacheStorage root should be written"
|
||||
);
|
||||
let (next, previous) = storage_bucket_cache_replacement_paths(&cache_root)?;
|
||||
assert!(
|
||||
!next.exists(),
|
||||
"profile-backed CacheStorage replacement root should not be left after save"
|
||||
);
|
||||
assert!(
|
||||
!previous.exists(),
|
||||
"profile-backed CacheStorage previous root should not be left after save"
|
||||
);
|
||||
|
||||
{
|
||||
let store =
|
||||
new_shared_json_storage_bucket_store_with_cache_root(&temp.path, &cache_root)?;
|
||||
let mut store = store.lock();
|
||||
let identity = store.open_bucket("https://a.test", "bucket")?;
|
||||
assert_eq!(
|
||||
store.cache_names_for_identity(&identity),
|
||||
Some(vec!["cache".to_owned()])
|
||||
);
|
||||
let matched = store
|
||||
.match_cache_entry_for_identity(&identity, "cache", "/cached.txt")
|
||||
.flatten()
|
||||
.expect("cache entry should persist across reopen");
|
||||
assert_eq!(matched, response);
|
||||
assert_eq!(store.cache_usage_for_identity(&identity), Some(64));
|
||||
assert_eq!(
|
||||
store.delete_cache_for_identity(&identity, "cache")?,
|
||||
Some(true)
|
||||
);
|
||||
}
|
||||
|
||||
let store = new_shared_json_storage_bucket_store_with_cache_root(&temp.path, &cache_root)?;
|
||||
let mut store = store.lock();
|
||||
let identity = store.open_bucket("https://a.test", "bucket")?;
|
||||
assert_eq!(store.cache_names_for_identity(&identity), Some(Vec::new()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_storage_bucket_cache_crash_points_recover_one_committed_root() -> Result<()> {
|
||||
let old_response = StorageBucketCachedResponse {
|
||||
cors_exposed_header_names: None,
|
||||
response_type: "default".to_owned(),
|
||||
url: String::new(),
|
||||
redirected: false,
|
||||
@@ -4005,6 +4044,7 @@ mod tests {
|
||||
body: b"old committed cache body".to_vec(),
|
||||
};
|
||||
let new_response = StorageBucketCachedResponse {
|
||||
cors_exposed_header_names: None,
|
||||
response_type: "default".to_owned(),
|
||||
url: String::new(),
|
||||
redirected: false,
|
||||
@@ -4089,6 +4129,7 @@ mod tests {
|
||||
let temp = TempStorePath::new("cache-promote-next");
|
||||
let cache_root = temp.cache_root();
|
||||
let response = StorageBucketCachedResponse {
|
||||
cors_exposed_header_names: None,
|
||||
response_type: "default".to_owned(),
|
||||
url: String::new(),
|
||||
redirected: false,
|
||||
@@ -4141,6 +4182,7 @@ mod tests {
|
||||
let temp = TempStorePath::new("cache-restore-previous");
|
||||
let cache_root = temp.cache_root();
|
||||
let response = StorageBucketCachedResponse {
|
||||
cors_exposed_header_names: None,
|
||||
response_type: "default".to_owned(),
|
||||
url: String::new(),
|
||||
redirected: false,
|
||||
@@ -4196,6 +4238,7 @@ mod tests {
|
||||
let indexed_db_manager = new_indexed_db_manager(None).map_err(anyhow::Error::msg)?;
|
||||
let storage_service = StorageService::on_disk(temp.opfs_root())?;
|
||||
let response = StorageBucketCachedResponse {
|
||||
cors_exposed_header_names: None,
|
||||
response_type: "default".to_owned(),
|
||||
url: String::new(),
|
||||
redirected: false,
|
||||
@@ -4558,6 +4601,7 @@ mod tests {
|
||||
let bucket_name = "bucket";
|
||||
let bucket_id = StorageBucketId::new(17).unwrap();
|
||||
let response = StorageBucketCachedResponse {
|
||||
cors_exposed_header_names: None,
|
||||
response_type: "default".to_owned(),
|
||||
url: "https://a.test/cached".to_owned(),
|
||||
redirected: false,
|
||||
|
||||
Reference in New Issue
Block a user