From bbdf98c606dde0e8cd26712a6f1af29d3fa5d74f Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sun, 13 Sep 2026 03:38:51 +0800 Subject: [PATCH] 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. --- .../window_runtime/navigator.rs | 46 +++- moli-renderer-v8/src/network_host.rs | 18 +- moli-renderer-v8/src/network_host/response.rs | 14 +- .../src/network_host/response/materialize.rs | 187 ++++++++++----- moli-renderer-v8/src/planning.rs | 2 +- .../src/script_vm/subresource_fetch.rs | 22 +- .../src/script_vm/tests/browser_api/misc.rs | 10 +- .../src/script_vm/tests/dom_xhr/null_body.rs | 4 + .../tests/dom_xhr/opaque_response.rs | 170 ++++++++++++++ .../tests/dom_xhr/redirect_filter.rs | 6 +- .../src/script_vm/tests/dom_xhr/xhr.rs | 11 + .../src/script_vm/tests/indexed_db.rs | 1 + moli-renderer-v8/src/script_vm/tests/mod.rs | 1 + .../src/script_vm/tests/webidl_fetch.rs | 2 + .../src/service_worker_runtime/events.rs | 2 + .../src/service_worker_runtime/service.rs | 2 + .../service/event_dispatch.rs | 1 + .../service/fetch_settlement.rs | 216 ++++++++++-------- moli-renderer-v8/src/stylesheet_blocking.rs | 6 +- .../src/stylesheet_runtime/connected.rs | 2 +- moli-renderer-v8/src/types.rs | 22 +- .../src/worker/global_scope/fetch.rs | 111 ++++++--- .../src/worker/global_scope/mod.rs | 7 +- .../src/worker/global_scope/xhr.rs | 4 + .../src/worker/thread/dispatch.rs | 2 + .../src/worker/thread/tests/lifecycle.rs | 4 + moli-storage-service/src/buckets.rs | 172 ++++++++------ 27 files changed, 747 insertions(+), 298 deletions(-) diff --git a/moli-renderer-v8/src/context_bootstrap/window_runtime/navigator.rs b/moli-renderer-v8/src/context_bootstrap/window_runtime/navigator.rs index e34e35c8cd..ddf887ed86 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_runtime/navigator.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_runtime/navigator.rs @@ -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>, 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::>>(&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, ) -> StorageBucketCachedResponse { StorageBucketCachedResponse { + cors_exposed_header_names: head.cors_exposed_header_names, response_type: head.response_type, url: head .final_url @@ -2955,13 +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, &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::>(); + let headers = headers_entries_to_init_array(scope, &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::::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, diff --git a/moli-renderer-v8/src/network_host.rs b/moli-renderer-v8/src/network_host.rs index 5e09dc6d0a..8c8ab43c88 100644 --- a/moli-renderer-v8/src/network_host.rs +++ b/moli-renderer-v8/src/network_host.rs @@ -144,16 +144,11 @@ pub(in crate::network_host) use self::request_scope::{ subresource_request_scope_for_owner, }; 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, + FetchResponseRequest, FetchResponseSecurityViolation, MaterializedResponseBody, + MaterializedResponseHead, build_fetch_response_object_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, @@ -174,6 +169,7 @@ pub(crate) use self::response::{ validate_cors_response_chain_for_origin, response_has_null_body, materialize_response_object_internal_head, + set_filtered_response_internal_head, validate_cors_response_for_origin, validate_cross_origin_embedder_and_document_isolation_policy, validate_cross_origin_resource_policy, @@ -322,3 +318,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, +}; diff --git a/moli-renderer-v8/src/network_host/response.rs b/moli-renderer-v8/src/network_host/response.rs index 489d8890d0..076ad05243 100644 --- a/moli-renderer-v8/src/network_host/response.rs +++ b/moli-renderer-v8/src/network_host/response.rs @@ -46,15 +46,21 @@ pub(crate) use self::cors::{ 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, + materialize_response_object_internal_head, materialized_body_bytes_from_value, + set_filtered_response_internal_head, + network_response_filter, }; #[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, +}; diff --git a/moli-renderer-v8/src/network_host/response/materialize.rs b/moli-renderer-v8/src/network_host/response/materialize.rs index 5dd811116d..7137bbd2af 100644 --- a/moli-renderer-v8/src/network_host/response/materialize.rs +++ b/moli-renderer-v8/src/network_host/response/materialize.rs @@ -17,24 +17,29 @@ pub(crate) struct FetchResponseRequest<'a> { } impl FetchResponseRequest<'_> { - pub(crate) fn filter_response_headers( + pub(crate) fn network_response_filter( self, + document_url: &url::Url, 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_for_origin( - request_origin, - head, - credentials_mode, - ) + ) -> crate::types::AsyncSubresourceFetchResponseFilter { + use crate::types::AsyncSubresourceFetchResponseFilter as Filter; + let filter = response_filter(document_url, head, self); + match compute_fetch_response_type(document_url, head, filter) { + "opaque" => Filter::Opaque, + "opaqueredirect" => Filter::OpaqueRedirect, + "cors" => Filter::Cors( + filter_cors_exposed_response_headers_for_origin( + request_origin, + head, + credentials_mode, + ) + .into_iter() + .map(|(name, _)| name) + .collect(), + ), + _ => Filter::Basic, } } } @@ -52,17 +57,11 @@ enum FetchResponseFilter { OpaqueRedirect, } -impl FetchResponseFilter { - fn is_readable(self) -> bool { - matches!(self, Self::None | Self::Basic | Self::Cors) - } -} - -impl From 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 @@ -111,6 +110,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) } @@ -163,7 +163,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 { @@ -171,27 +175,33 @@ fn filtered_response_url(head: &moli_fetch::ResponseHead, filter: FetchResponseF FetchResponseFilter::Opaque => "", // Manual redirects preserve the URL list; only their status, headers, // and body are filtered. No redirect target was fetched. - FetchResponseFilter::None | FetchResponseFilter::Basic | FetchResponseFilter::Cors | FetchResponseFilter::OpaqueRedirect => head.final_url.as_str(), + FetchResponseFilter::None + | FetchResponseFilter::Basic + | FetchResponseFilter::Cors + | FetchResponseFilter::OpaqueRedirect => head.final_url.as_str(), } } 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 +253,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 +275,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, @@ -265,8 +284,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, + request: FetchResponseRequest<'_>, + head: moli_fetch::ResponseHead, + body: crate::protocol_types::SubresourceResponseBody, + filter_override: Option, +) -> 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 +326,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, @@ -345,13 +394,20 @@ 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) .then_some(filtered_surface_url) .flatten(); - let obj = - build_fetch_response_object_head(scope, &request_origin, &head, filter, filtered_surface_url); + let obj = build_fetch_response_object_head( + scope, + &request_origin, + &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 // not register a body source: subsequent transport chunks and terminal @@ -372,6 +428,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); @@ -390,22 +447,36 @@ 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(), - &head.headers, - ); - } + set_filtered_response_internal_head(scope, obj, head.status, head.status_text(), &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(&header_entries, HeadersGuard::Response); + let headers_obj = + build_headers_object_with_state(scope, &headers, HeadersGuard::Response, true); + install_headers_object_methods(scope, headers_obj); + 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)], @@ -426,6 +497,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>( @@ -441,17 +515,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(header_entries, HeadersGuard::Response); - let headers_obj = - build_headers_object_with_state(scope, &headers, HeadersGuard::Response, true); - install_headers_object_methods(scope, headers_obj); - 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 { @@ -512,6 +575,7 @@ pub(crate) fn build_filtered_cached_response_object<'s>( #[derive(Debug, Clone)] pub(crate) struct MaterializedResponseObject { + pub(crate) cors_exposed_header_names: Option>, pub(crate) final_url: Option, pub(crate) response_type: String, pub(crate) redirected: bool, @@ -523,6 +587,7 @@ pub(crate) struct MaterializedResponseObject { #[derive(Debug, Clone)] pub(crate) struct MaterializedResponseHead { + pub(crate) cors_exposed_header_names: Option>, pub(crate) final_url: Option, pub(crate) response_type: String, pub(crate) redirected: bool, @@ -534,6 +599,7 @@ pub(crate) struct MaterializedResponseHead { impl MaterializedResponseHead { pub(crate) fn with_body(self, body: Vec) -> MaterializedResponseObject { MaterializedResponseObject { + cors_exposed_header_names: self.cors_exposed_header_names, final_url: self.final_url, response_type: self.response_type, redirected: self.redirected, @@ -635,6 +701,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), @@ -652,9 +720,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 = diff --git a/moli-renderer-v8/src/planning.rs b/moli-renderer-v8/src/planning.rs index 2621ee0928..d0c156fc32 100644 --- a/moli-renderer-v8/src/planning.rs +++ b/moli-renderer-v8/src/planning.rs @@ -533,7 +533,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 { diff --git a/moli-renderer-v8/src/script_vm/subresource_fetch.rs b/moli-renderer-v8/src/script_vm/subresource_fetch.rs index c8f31fa5f3..7ee9c9f2a2 100644 --- a/moli-renderer-v8/src/script_vm/subresource_fetch.rs +++ b/moli-renderer-v8/src/script_vm/subresource_fetch.rs @@ -3745,11 +3745,12 @@ impl ScriptVm { mode: pending.request_mode, redirect_mode, }; - head.headers = response_request.filter_response_headers( + let response_filter = response_filter.or_else(|| Some(response_request.network_response_filter( + &pending.info.document_url, &pending.request_origin(), &head, pending.credentials_mode, - ); + ))); if let Some(status_text) = response_status_text { head.status_text = Some(status_text); } @@ -4541,6 +4542,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, @@ -4684,15 +4686,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_for_origin( &pending.info.document_url, @@ -4926,6 +4925,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(), @@ -4977,18 +4977,19 @@ impl ScriptVm { mode: pending.request_mode, redirect_mode: fetch.redirect_mode(), }; - observable_head.headers = response_request.filter_response_headers( + let response_filter = started.response_filter.clone().or_else(|| Some(response_request.network_response_filter( + &pending.info.document_url, &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()); } @@ -5088,6 +5089,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(), diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs index 1fadee70db..469055f147 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs @@ -14638,7 +14638,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"}})); }); "#, ), @@ -14648,7 +14648,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", @@ -14656,7 +14658,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)); } @@ -14705,7 +14707,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; diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/null_body.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/null_body.rs index 246c706731..3394184926 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/null_body.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/null_body.rs @@ -185,6 +185,8 @@ fn xhr_null_body_statuses_discard_buffered_and_streamed_bytes() { let body_source_id = crate::network_host::new_network_body_source_id(); vm.start_streaming_async_subresource_fetch( crate::types::AsyncSubresourceStreamingStarted { + response_filter: None, + skip_fetch_security_validation: false, internal_id: request.internal_id, request_url: request.url.clone(), request_method: method.to_owned(), @@ -321,6 +323,8 @@ fn xhr_null_body_late_completion_preserves_a_replacement_requests_progress() { let body_source_id = crate::network_host::new_network_body_source_id(); vm.start_streaming_async_subresource_fetch( crate::types::AsyncSubresourceStreamingStarted { + response_filter: None, + skip_fetch_security_validation: false, internal_id: request.internal_id, request_url: request.url.clone(), request_method: method.to_owned(), diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/opaque_response.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/opaque_response.rs index a7b25657a9..cbb119f2d7 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/opaque_response.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/opaque_response.rs @@ -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 = &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] { @@ -68,6 +236,8 @@ fn window_filtered_fetch_preserves_internal_head_through_clone_and_cache() { let body_source_id = crate::network_host::new_network_body_source_id(); vm.start_streaming_async_subresource_fetch( crate::types::AsyncSubresourceStreamingStarted { + response_filter: None, + skip_fetch_security_validation: false, internal_id: request.internal_id, request_url: request.url.clone(), request_method: "GET".to_owned(), diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/redirect_filter.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/redirect_filter.rs index 1a226ce5fe..2dffa579fa 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/redirect_filter.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/redirect_filter.rs @@ -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,7 +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(Opaque) => "opaque", Some(OpaqueRedirect) => "opaqueredirect", None if redirect == "manual" && status == 302 => "opaqueredirect", diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs index 43e3d09fcf..3db3fa9a57 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs @@ -1434,6 +1434,8 @@ fn xml_http_request_document_response_requires_an_eligible_mime_and_well_formed_ let body_source_id = crate::network_host::new_network_body_source_id(); vm.start_streaming_async_subresource_fetch( crate::types::AsyncSubresourceStreamingStarted { + response_filter: None, + skip_fetch_security_validation: false, internal_id: request.internal_id, request_url: request.url.clone(), request_method: "GET".to_owned(), @@ -1562,6 +1564,8 @@ fn xhr_streamed_response_documents_keep_distinct_source_modification_times() { let body_source_id = crate::network_host::new_network_body_source_id(); vm.start_streaming_async_subresource_fetch( crate::types::AsyncSubresourceStreamingStarted { + response_filter: None, + skip_fetch_security_validation: false, internal_id: request.internal_id, request_url: request.url.clone(), request_method: "GET".to_owned(), @@ -1699,6 +1703,8 @@ fn xhr_response_decoding_uses_headers_received_overrides_for_buffered_and_stream let body_source_id = crate::network_host::new_network_body_source_id(); vm.start_streaming_async_subresource_fetch( crate::types::AsyncSubresourceStreamingStarted { + response_filter: None, + skip_fetch_security_validation: false, internal_id: request.internal_id, request_url: request.url.clone(), request_method: "GET".to_owned(), @@ -2992,6 +2998,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 { request_origin: moli_url::WebOrigin::from_url(&document_url), info: crate::types::PendingSubresourceFetchInfo { @@ -3280,6 +3287,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 { request_origin: moli_url::WebOrigin::from_url( &(Url::parse("https://streaming-fetch-body-error.test/") @@ -3447,6 +3455,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 { request_origin: moli_url::WebOrigin::from_url(&document_url), info: crate::types::PendingSubresourceFetchInfo { @@ -3551,6 +3560,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 { request_origin: moli_url::WebOrigin::from_url( &(Url::parse("https://streaming-fetch-body-cancel.test/") @@ -4181,6 +4191,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 { request_origin: moli_url::WebOrigin::from_url( &(Url::parse("https://xhr-streaming-materialize-error.test/") diff --git a/moli-renderer-v8/src/script_vm/tests/indexed_db.rs b/moli-renderer-v8/src/script_vm/tests/indexed_db.rs index 64e8985d08..ebfc8209b3 100644 --- a/moli-renderer-v8/src/script_vm/tests/indexed_db.rs +++ b/moli-renderer-v8/src/script_vm/tests/indexed_db.rs @@ -3024,6 +3024,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, diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index 23ee52f79e..e7411e3c9e 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -354,6 +354,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(), diff --git a/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs b/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs index 409842dff2..3506790377 100644 --- a/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs +++ b/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs @@ -9115,6 +9115,8 @@ fn opaque_window_fetch_keeps_blocked_bytes_out_of_internal_clone_consumers() { let id = crate::network_host::new_network_body_source_id(); vm.start_streaming_async_subresource_fetch( crate::types::AsyncSubresourceStreamingStarted { + response_filter: None, + skip_fetch_security_validation: false, internal_id: pending.internal_id, request_url: pending.url.clone(), request_method: "GET".to_owned(), diff --git a/moli-renderer-v8/src/service_worker_runtime/events.rs b/moli-renderer-v8/src/service_worker_runtime/events.rs index 3a8706ddd7..2575b30c9c 100644 --- a/moli-renderer-v8/src/service_worker_runtime/events.rs +++ b/moli-renderer-v8/src/service_worker_runtime/events.rs @@ -628,6 +628,7 @@ pub(crate) struct ServiceWorkerFetchEvent { #[derive(Clone, Debug)] pub(crate) struct ServiceWorkerFetchResponse { + pub(crate) cors_exposed_header_names: Option>, pub(crate) final_url: Option, pub(crate) response_type: String, pub(crate) redirected: bool, @@ -694,6 +695,7 @@ pub(crate) struct ServiceWorkerNavigationPreloadFailure { #[derive(Clone, Debug)] pub(crate) struct MaterializedServiceWorkerFetchResponseHead { + pub(crate) cors_exposed_header_names: Option>, pub(crate) final_url: Option, pub(crate) response_type: String, pub(crate) redirected: bool, diff --git a/moli-renderer-v8/src/service_worker_runtime/service.rs b/moli-renderer-v8/src/service_worker_runtime/service.rs index 0200c51624..64e2293810 100644 --- a/moli-renderer-v8/src/service_worker_runtime/service.rs +++ b/moli-renderer-v8/src/service_worker_runtime/service.rs @@ -8713,6 +8713,7 @@ self.addEventListener("message", event => { 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(), "text/plain".to_owned())], @@ -9014,6 +9015,7 @@ self.addEventListener("message", event => { 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(), "text/plain".to_owned())], diff --git a/moli-renderer-v8/src/service_worker_runtime/service/event_dispatch.rs b/moli-renderer-v8/src/service_worker_runtime/service/event_dispatch.rs index 94a03c6a7a..6392f35448 100644 --- a/moli-renderer-v8/src/service_worker_runtime/service/event_dispatch.rs +++ b/moli-renderer-v8/src/service_worker_runtime/service/event_dispatch.rs @@ -61,6 +61,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(), diff --git a/moli-renderer-v8/src/service_worker_runtime/service/fetch_settlement.rs b/moli-renderer-v8/src/service_worker_runtime/service/fetch_settlement.rs index a548223ac2..1f5a7023fb 100644 --- a/moli-renderer-v8/src/service_worker_runtime/service/fetch_settlement.rs +++ b/moli-renderer-v8/src/service_worker_runtime/service/fetch_settlement.rs @@ -123,10 +123,16 @@ fn service_worker_fetch_can_forward_stream( job: &ServiceWorkerFetchJob, response_head: &MaterializedServiceWorkerFetchResponseHead, ) -> Result { - 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,13 +606,17 @@ 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; } - let response_filter = service_worker_fetch_response_filter(&response, job.redirect_mode); + let response_filter = service_worker_fetch_response_filter(&response, job.request.redirect_mode); let navigation_response = crate::protocol_types::NavigationResponse::from_head_and_body( moli_fetch::ResponseHead { status_text: Some(response.status_text.clone()), @@ -759,57 +774,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, String)], ) -> 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, @@ -841,7 +815,15 @@ fn service_worker_fetch_response_rejection( job: &ServiceWorkerFetchJob, response: &ServiceWorkerFetchResponse, ) -> Option { - 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 { + match response_type { "error" => { return Some("FetchEvent.respondWith rejected an error Response".to_owned()); } @@ -871,7 +853,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(), @@ -884,26 +866,33 @@ fn service_worker_fetch_response_filter( response: &ServiceWorkerFetchResponse, redirect_mode: moli_fetch::RequestRedirectMode, ) -> Option { - 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 { - 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)] @@ -1263,6 +1252,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, @@ -1279,7 +1269,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"); @@ -1361,6 +1354,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, @@ -1399,6 +1393,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, @@ -1446,6 +1441,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, @@ -1523,6 +1519,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, @@ -1547,8 +1544,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); @@ -1584,6 +1584,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, @@ -1629,6 +1630,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, @@ -1673,6 +1675,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, @@ -1716,6 +1719,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, @@ -1764,6 +1768,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, @@ -1887,6 +1892,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, @@ -1948,6 +1954,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, @@ -2055,6 +2062,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, @@ -2129,6 +2137,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, @@ -2176,6 +2185,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, @@ -2243,6 +2253,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, @@ -2290,6 +2301,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, @@ -2336,6 +2348,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, @@ -2398,6 +2411,7 @@ mod tests { run.clone(), ), result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse { + cors_exposed_header_names: None, final_url: None, response_type: "opaque".to_owned(), redirected: false, @@ -2445,6 +2459,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, @@ -2498,6 +2513,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, @@ -2561,6 +2577,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, @@ -2620,6 +2637,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, @@ -2678,6 +2696,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, @@ -2736,6 +2755,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, @@ -2795,6 +2815,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, @@ -2848,6 +2869,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, @@ -2906,6 +2928,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, @@ -2963,6 +2986,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, @@ -3014,6 +3038,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, @@ -3073,6 +3098,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, @@ -3141,6 +3167,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, @@ -3206,6 +3233,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, @@ -3235,7 +3263,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); @@ -3257,27 +3285,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(), "application/json".to_owned())], - 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] @@ -3303,6 +3334,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, @@ -3316,7 +3348,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"); @@ -3347,6 +3382,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, diff --git a/moli-renderer-v8/src/stylesheet_blocking.rs b/moli-renderer-v8/src/stylesheet_blocking.rs index 5744d75796..3516b0d1e0 100644 --- a/moli-renderer-v8/src/stylesheet_blocking.rs +++ b/moli-renderer-v8/src/stylesheet_blocking.rs @@ -255,7 +255,7 @@ async fn fetch_stylesheet_readiness_with_request( } } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] enum StylesheetResponseProvenance { Network, ServiceWorker { @@ -265,7 +265,7 @@ enum StylesheetResponseProvenance { impl StylesheetResponseProvenance { fn is_cors_same_origin( - self, + &self, document_url: &Url, request_url: &Url, response: &crate::protocol_types::NavigationResponse, @@ -307,7 +307,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( diff --git a/moli-renderer-v8/src/stylesheet_runtime/connected.rs b/moli-renderer-v8/src/stylesheet_runtime/connected.rs index 6db19dbfba..9fb7de00d3 100644 --- a/moli-renderer-v8/src/stylesheet_runtime/connected.rs +++ b/moli-renderer-v8/src/stylesheet_runtime/connected.rs @@ -2154,7 +2154,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); diff --git a/moli-renderer-v8/src/types.rs b/moli-renderer-v8/src/types.rs index 713b939cd6..565c212c9e 100644 --- a/moli-renderer-v8/src/types.rs +++ b/moli-renderer-v8/src/types.rs @@ -650,17 +650,17 @@ pub(super) struct AsyncSubresourceFetchCompletion { pub(super) result: std::result::Result, } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub(super) enum AsyncSubresourceFetchResponseFilter { Basic, - Cors, + Cors(Vec), 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(_)) } } @@ -926,6 +926,7 @@ pub(super) struct ServiceWorkerControllerChangeCompletion { pub(super) struct StreamingSubresourceFetchState { pub(super) response_filter: Option, + pub(super) skip_fetch_security_validation: bool, pub(super) pending: PendingSubresourceFetchState, pub(super) request_url: Url, pub(super) request_method: String, @@ -941,12 +942,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.info.document_url, + &self.head.final_url, + &self.head.headers, + self.pending.request_mode, + ) } } diff --git a/moli-renderer-v8/src/worker/global_scope/fetch.rs b/moli-renderer-v8/src/worker/global_scope/fetch.rs index 8c649fa398..7d7c30ddb5 100644 --- a/moli-renderer-v8/src/worker/global_scope/fetch.rs +++ b/moli-renderer-v8/src/worker/global_scope/fetch.rs @@ -322,6 +322,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, @@ -429,6 +431,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()), @@ -463,6 +467,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( @@ -474,6 +480,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), @@ -483,6 +491,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()), @@ -728,6 +738,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), @@ -753,6 +765,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), @@ -793,6 +807,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))), @@ -834,6 +850,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 { @@ -866,6 +884,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), @@ -899,6 +919,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))), @@ -2626,13 +2648,14 @@ 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( + &pending.document_url, &request_origin, &observable_head, pending.credentials_mode, @@ -2645,6 +2668,7 @@ pub(in crate::worker) fn start_worker_streaming_fetch( pending.request_method.clone(), pending.redirect_mode, observable_head, + response_filter, )) } }; @@ -2666,9 +2690,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 { @@ -2678,6 +2703,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()); @@ -2892,6 +2918,8 @@ pub(in crate::worker) fn drain_worker_fetch_completion_result( { 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(), }); @@ -2947,6 +2975,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, }); @@ -2975,36 +3005,40 @@ pub(in crate::worker) fn drain_worker_fetch_completion_result( Ok(response) => { let response_head = response.head(); let request_origin = WebOrigin::from_url(&pending.document_url); - let security_validation = match &response { - WorkerFetchResponse::Materialized(response) => { - validate_fetch_response_security_policy_with_body_classified_for_origin( - &pending.document_url, - &request_origin, - &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 security_validation = if completion.skip_fetch_security_validation { + Ok(()) + } else { + match &response { + WorkerFetchResponse::Materialized(response) => { validate_fetch_response_security_policy_with_body_classified_for_origin( &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_for_origin( + &pending.document_url, + &request_origin, + &response_head, + &body_bytes, + pending.request_mode, + pending.credentials_mode, + pending.policy_context, + ) + }), + } }; let opaque_response_blocked = match security_validation { Ok(()) => false, @@ -3053,40 +3087,43 @@ pub(in crate::worker) fn drain_worker_fetch_completion_result( 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( + &pending.document_url, + &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, ) } }; diff --git a/moli-renderer-v8/src/worker/global_scope/mod.rs b/moli-renderer-v8/src/worker/global_scope/mod.rs index cd70819f3b..23151f4aa9 100644 --- a/moli-renderer-v8/src/worker/global_scope/mod.rs +++ b/moli-renderer-v8/src/worker/global_scope/mod.rs @@ -72,9 +72,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, cors_request_origin_after_redirects, dispatch_xhr_loadstart, enqueue_pending_network_body_chunk, error_pending_network_body_stream_with_reason, @@ -1125,6 +1122,8 @@ pub(super) enum WorkerFetchEvent { } pub(super) struct WorkerFetchCompletion { + response_filter: Option, + skip_fetch_security_validation: bool, fetch_id: u32, network_request_headers: Option>, result: Result, @@ -1241,6 +1240,8 @@ pub(super) struct PendingWorkerCspReport { } pub(super) struct PausedWorkerSubresourceResponse { + pub(super) response_filter: Option, + pub(super) skip_fetch_security_validation: bool, pub(super) head: ResponseHead, pub(super) body: SubresourceResponseBody, } diff --git a/moli-renderer-v8/src/worker/global_scope/xhr.rs b/moli-renderer-v8/src/worker/global_scope/xhr.rs index 3310e1004c..09da1e6ecc 100644 --- a/moli-renderer-v8/src/worker/global_scope/xhr.rs +++ b/moli-renderer-v8/src/worker/global_scope/xhr.rs @@ -1010,6 +1010,8 @@ pub(in crate::worker) fn drain_worker_xhr_completion( { 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(), }); @@ -1066,6 +1068,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, }); diff --git a/moli-renderer-v8/src/worker/thread/dispatch.rs b/moli-renderer-v8/src/worker/thread/dispatch.rs index b49a785f4d..b0c37b5992 100644 --- a/moli-renderer-v8/src/worker/thread/dispatch.rs +++ b/moli-renderer-v8/src/worker/thread/dispatch.rs @@ -3339,6 +3339,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, @@ -3353,6 +3354,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(), diff --git a/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs b/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs index 877153e2c4..33b3c061be 100644 --- a/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs +++ b/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs @@ -761,6 +761,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"), @@ -877,6 +878,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, @@ -1049,6 +1051,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"), @@ -1164,6 +1167,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"), diff --git a/moli-storage-service/src/buckets.rs b/moli-storage-service/src/buckets.rs index d1aa97cb37..b0aeb33411 100644 --- a/moli-storage-service/src/buckets.rs +++ b/moli-storage-service/src/buckets.rs @@ -132,6 +132,7 @@ fn is_false(value: &bool) -> bool { #[derive(Debug, Clone, PartialEq)] pub struct StorageBucketCachedResponse { + pub cors_exposed_header_names: Option>, 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>, usage_bytes: u64, #[serde( default = "default_cache_request_method", @@ -1773,6 +1776,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, @@ -2098,6 +2103,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, @@ -2129,6 +2135,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(), @@ -2827,6 +2834,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, @@ -2904,6 +2912,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, @@ -3133,6 +3142,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, @@ -3178,6 +3188,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, @@ -3234,6 +3245,7 @@ mod tests { ); let replacement_response = StorageBucketCachedResponse { + cors_exposed_header_names: None, response_type: "default".to_owned(), url: String::new(), redirected: false, @@ -3288,6 +3300,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, String)>| StorageBucketCachedResponse { + cors_exposed_header_names: None, response_type: "default".to_owned(), url: String::new(), redirected: false, @@ -3380,6 +3393,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, @@ -3444,6 +3458,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, @@ -3453,6 +3468,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, @@ -3562,6 +3578,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, @@ -3800,6 +3817,7 @@ mod tests { "cache", "/cached.txt", StorageBucketCachedResponse { + cors_exposed_header_names: None, response_type: "default".to_owned(), url: String::new(), redirected: false, @@ -3889,81 +3907,102 @@ 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(), "persisted".to_owned())], - body: b"profile cache body".to_vec(), - }; - { + for cors in [false, true] { + let temp = TempStorePath::new("cache-persist"); + let cache_root = temp.cache_root(); + let 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(), "persisted".to_owned()), + ( + "cross-origin-resource-policy".to_owned(), + "same-origin".to_owned(), + ), + ("vary".to_owned(), "*".to_owned()), + ] + } else { + vec![("x-cache".to_owned(), "persisted".to_owned())] + }, + 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, @@ -3973,6 +4012,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, @@ -4057,6 +4097,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, @@ -4109,6 +4150,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, @@ -4164,6 +4206,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, @@ -4526,6 +4569,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,