fix(fetch): preserve service worker response types and CORS headers

Carry the original response filter and validation state through Window
streams and Worker completions, including paused responses. Synthesized
responses remain basic, and already fetched bodies are not filtered by ORB
again. Keep client CORP/COEP checks against actual network response URLs.

Retain internal headers separately from the CORS public header view through
clone, deferred Cache.put, persistent Cache storage and respondWith. Match
Vary using the public header view and keep filtered cached headers immutable.
This commit is contained in:
ldm0
2026-09-13 17:17:16 +08:00
parent 89c3475a2d
commit bbdf98c606
27 changed files with 747 additions and 298 deletions
@@ -64,6 +64,8 @@ const STORAGE_BUCKET_CACHE_PUT_RESPONSE_STATUS_SLOT: &str =
"__moliStorageBucketCachePutResponseStatus";
const STORAGE_BUCKET_CACHE_PUT_RESPONSE_STATUS_TEXT_SLOT: &str =
"__moliStorageBucketCachePutResponseStatusText";
const STORAGE_BUCKET_CACHE_PUT_RESPONSE_CORS_EXPOSED_HEADERS_SLOT: &str =
"__lmStorageBucketCachePutResponseCorsExposedHeaders";
const STORAGE_BUCKET_CACHE_PUT_RESPONSE_HEADERS_SLOT: &str =
"__moliStorageBucketCachePutResponseHeaders";
const NAVIGATOR_UA_DATA_USER_AGENT_SLOT: &str = "__moliNavigatorUADataUserAgent";
@@ -372,6 +374,8 @@ struct StorageBucketCachePutPendingDataDeclaration<'scope> {
#[webapi(slot = STORAGE_BUCKET_CACHE_PUT_RESPONSE_HEADERS_SLOT)]
response_headers_json: String,
#[webapi(slot = STORAGE_BUCKET_CACHE_PUT_RESPONSE_CORS_EXPOSED_HEADERS_SLOT)]
response_cors_exposed_header_names_json: String,
}
enum StorageBucketCachedResponseMaterialization<'scope> {
@@ -1801,6 +1805,10 @@ fn storage_bucket_cache_put_pending_body<'s>(
response_status_text: head.status_text,
response_headers_json: serde_json::to_string(&head.headers)
.unwrap_or_else(|_| "[]".to_owned()),
response_cors_exposed_header_names_json: serde_json::to_string(
&head.cors_exposed_header_names,
)
.unwrap_or_else(|_| "null".to_owned()),
}
.bind(scope)
.expect("Cache.put pending body data should bind");
@@ -1858,6 +1866,7 @@ fn storage_bucket_cache_put_body_fulfilled_callback<'s>(
}
};
let response = StorageBucketCachedResponse {
cors_exposed_header_names: pending.response_cors_exposed_header_names,
response_type: pending.response_type,
url: pending.response_url,
redirected: pending.response_redirected,
@@ -1897,6 +1906,7 @@ fn storage_bucket_cache_put_body_rejected_callback<'s>(
}
struct StorageBucketCachePutPendingData<'scope> {
response_cors_exposed_header_names: Option<Vec<String>>,
resolver: v8::Local<'scope, v8::PromiseResolver>,
handle: StorageBucketCacheHandle,
request: CacheRequestInfo,
@@ -1958,6 +1968,13 @@ fn storage_bucket_cache_put_pending_data<'s>(
data,
STORAGE_BUCKET_CACHE_PUT_RESPONSE_STATUS_TEXT_SLOT,
)?;
let response_cors_exposed_header_names =
serde_json::from_str::<Option<Vec<String>>>(&data_private_string(
scope,
data,
STORAGE_BUCKET_CACHE_PUT_RESPONSE_CORS_EXPOSED_HEADERS_SLOT,
)?)
.ok()?;
let response_headers_json =
data_private_string(scope, data, STORAGE_BUCKET_CACHE_PUT_RESPONSE_HEADERS_SLOT)?;
let response_headers =
@@ -1967,6 +1984,7 @@ fn storage_bucket_cache_put_pending_data<'s>(
indexed_db_storage_key: bucket_storage_key,
};
Some(StorageBucketCachePutPendingData {
response_cors_exposed_header_names,
resolver,
handle: StorageBucketCacheHandle {
bucket,
@@ -2816,6 +2834,7 @@ fn storage_bucket_cached_response_from_head_body(
body: Vec<u8>,
) -> StorageBucketCachedResponse {
StorageBucketCachedResponse {
cors_exposed_header_names: head.cors_exposed_header_names,
response_type: head.response_type,
url: head
.final_url
@@ -2955,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::<Vec<_>>();
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::<v8::Function>::try_from(value).ok())?;
let response_obj = constructor.new_instance(scope, &[body, init.into_value()])?;
if matches!(response.response_type.as_str(), "basic" | "cors") {
crate::network_host::set_filtered_response_internal_head(
scope,
response_obj,
response.status,
&response.status_text,
&response.headers,
);
}
crate::network_host::set_response_slot_string(
scope,
response_obj,
+10 -8
View File
@@ -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,
};
+10 -4
View File
@@ -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,
};
@@ -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<crate::types::AsyncSubresourceFetchResponseFilter> for FetchResponseFilter {
fn from(value: crate::types::AsyncSubresourceFetchResponseFilter) -> Self {
impl From<&crate::types::AsyncSubresourceFetchResponseFilter> for FetchResponseFilter {
fn from(value: &crate::types::AsyncSubresourceFetchResponseFilter) -> Self {
match value {
crate::types::AsyncSubresourceFetchResponseFilter::Basic => Self::Basic,
crate::types::AsyncSubresourceFetchResponseFilter::Cors => Self::Cors,
crate::types::AsyncSubresourceFetchResponseFilter::Cors(_) => Self::Cors,
crate::types::AsyncSubresourceFetchResponseFilter::Opaque => Self::Opaque,
crate::types::AsyncSubresourceFetchResponseFilter::OpaqueRedirect => {
Self::OpaqueRedirect
@@ -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<WebOrigin>,
@@ -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<WebOrigin>,
request: FetchResponseRequest<'_>,
head: moli_fetch::ResponseHead,
body: crate::protocol_types::SubresourceResponseBody,
filter_override: Option<crate::types::AsyncSubresourceFetchResponseFilter>,
) -> v8::Local<'s, v8::Object> {
let request_origin = request_origin.into();
let filter = filter_override
.as_ref()
.map(FetchResponseFilter::from)
.unwrap_or_else(|| response_filter(&request_origin, &head, request));
let obj = build_fetch_response_object_head(
scope,
&request_origin,
&head,
filter,
None,
filter_override.as_ref(),
);
let body_stream = if response_has_null_body(request.method, head.status) {
None
} else if filtered_response_exposes_body(filter) {
@@ -278,6 +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<WebOrigin>,
@@ -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<Vec<String>>,
pub(crate) final_url: Option<url::Url>,
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<Vec<String>>,
pub(crate) final_url: Option<url::Url>,
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<u8>) -> MaterializedResponseObject {
MaterializedResponseObject {
cors_exposed_header_names: self.cors_exposed_header_names,
final_url: self.final_url,
response_type: self.response_type,
redirected: self.redirected,
@@ -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 =
+1 -1
View File
@@ -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 {
@@ -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(),
@@ -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;
@@ -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(),
@@ -1,6 +1,174 @@
use super::*;
use crate::util::v8str;
#[test]
fn window_service_worker_response_filter_survives_streaming_clone_and_cache() {
use crate::types::AsyncSubresourceFetchResponseFilter as Filter;
for streaming in [false, true] {
for mode in ["cors", "no-cors"] {
for cors in [false, true] {
let mut vm = new_storage_test_vm("https://response-client.test/");
vm.set_fetch_subresource_interception(
true,
Some(crate::types::SubresourceResourceType::Fetch),
);
let response_type = if cors { "cors" } else { "basic" };
vm.eval(&format!(r#"
globalThis.result = 'pending';
fetch('https://response-remote.test/body', {{mode: '{mode}'}}).then(async response => {{
globalThis.original = response;
globalThis.cloned = response.clone();
const bucket = await navigator.storageBuckets.open('response-filter');
const cache = await bucket.caches.open('responses');
await cache.put('/key', cloned.clone());
globalThis.cached = await cache.match('/key');
await navigator.storageBuckets.delete('response-filter');
for (const entry of [original, cloned, cached]) {{
if (entry.type !== '{response_type}' || entry.status !== 200 || entry.body === null ||
entry.headers.get('x-visible') !== 'visible' || entry.headers.has('set-cookie') ||
entry.headers.has('x-hidden') === {cors}) throw new Error('public response');
let error;
try {{ entry.headers.set('x-author', 'changed'); }} catch (value) {{ error = value; }}
if (!(error instanceof TypeError)) throw new Error('immutable headers');
if (await entry.clone().text() !== 'hello') throw new Error('body');
}}
result = 'ok';
}}).catch(error => result = String(error.stack || error));
"#)).unwrap();
let requests = vm.take_pending_subresource_fetch_infos();
assert_eq!(requests.len(), 1);
let request = &requests[0];
let headers = vec![
("Content-Type".to_owned(), "text/plain".to_owned()),
("Content-Length".to_owned(), "5".to_owned()),
("X-Visible".to_owned(), "visible".to_owned()),
("X-Hidden".to_owned(), "secret".to_owned()),
(
"Cross-Origin-Resource-Policy".to_owned(),
"cross-origin".to_owned(),
),
("Set-Cookie".to_owned(), "hidden=secret".to_owned()),
(
"Vary".to_owned(),
if cors { "*" } else { "Accept" }.to_owned(),
),
];
let head = moli_fetch::ResponseHead {
final_url: request.url.clone(),
status: 200,
status_text: Some("OK".to_owned()),
headers: headers.clone(),
request_cookie_report: None,
cookie_set_reports: Vec::new(),
redirected: false,
redirect_chain: Vec::new(),
from_cache: false,
negotiated_http_version: None,
};
let response_filter = Some(if cors {
Filter::Cors(vec!["content-type".to_owned(), "x-visible".to_owned()])
} else {
Filter::Basic
});
if streaming {
let body_source_id = crate::network_host::new_network_body_source_id();
vm.start_streaming_async_subresource_fetch(
crate::types::AsyncSubresourceStreamingStarted {
response_filter,
skip_fetch_security_validation: true,
internal_id: request.internal_id,
request_url: request.url.clone(),
request_method: "GET".to_owned(),
request_headers: Vec::new(),
request_body: None,
body_source_id,
head,
network_request_headers: None,
},
)
.unwrap();
vm.append_streaming_async_subresource_fetch_chunk(
body_source_id,
b"hello".to_vec(),
);
vm.finish_streaming_async_subresource_fetch(
request.internal_id,
body_source_id,
Ok(()),
)
.unwrap();
} else {
vm.complete_async_subresource_fetch(
crate::types::AsyncSubresourceFetchCompletion {
response_filter,
skip_fetch_security_validation: true,
internal_id: request.internal_id,
request_url: request.url.clone(),
request_method: "GET".to_owned(),
request_headers: Vec::new(),
request_body: None,
response_status_text: None,
network_error_text: None,
result: Ok(
crate::protocol_types::NavigationResponse::from_head_and_body(
head,
"hello".to_owned(),
b"hello".to_vec(),
),
),
},
)
.unwrap();
}
vm.exec("0", None).unwrap();
assert_eq!(
vm.eval("result").unwrap(),
"ok",
"{response_type}/{mode}/streaming={streaming}"
);
let context_ptr: *const v8::Global<v8::Context> = &vm.page_default_context;
vm.renderer_document_isolate
.with_entered_renderer_document_isolate(move |isolate| {
let scope = std::pin::pin!(v8::HandleScope::new(isolate));
let scope = &mut scope.init();
let context = unsafe { v8::Local::new(scope, &*context_ptr) };
let scope = &mut v8::ContextScope::new(scope, context);
for name in ["original", "cloned", "cached"] {
let value = context
.global(scope)
.get(scope, v8str(scope, name).into())
.unwrap();
let (head, _) =
crate::network_host::materialize_response_object_internal_head(
scope, value, "test",
)
.unwrap();
assert_eq!(head.response_type, response_type);
for (name, value) in &headers {
assert!(
head.headers
.iter()
.any(|(key, entry)| key.eq_ignore_ascii_case(name)
&& entry == value),
"{name}: {:?}",
head.headers
);
}
if cors {
assert_eq!(
head.cors_exposed_header_names,
Some(vec!["content-type".to_owned(), "x-visible".to_owned()])
);
}
}
Ok(())
})
.unwrap();
}
}
}
}
#[test]
fn window_filtered_fetch_preserves_internal_head_through_clone_and_cache() {
for streaming in [false, true] {
@@ -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(),
@@ -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",
@@ -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/")
@@ -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,
@@ -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(),
@@ -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(),
@@ -628,6 +628,7 @@ pub(crate) struct ServiceWorkerFetchEvent {
#[derive(Clone, Debug)]
pub(crate) struct ServiceWorkerFetchResponse {
pub(crate) cors_exposed_header_names: Option<Vec<String>>,
pub(crate) final_url: Option<Url>,
pub(crate) response_type: String,
pub(crate) redirected: bool,
@@ -694,6 +695,7 @@ pub(crate) struct ServiceWorkerNavigationPreloadFailure {
#[derive(Clone, Debug)]
pub(crate) struct MaterializedServiceWorkerFetchResponseHead {
pub(crate) cors_exposed_header_names: Option<Vec<String>>,
pub(crate) final_url: Option<Url>,
pub(crate) response_type: String,
pub(crate) redirected: bool,
@@ -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())],
@@ -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(),
@@ -123,10 +123,16 @@ fn service_worker_fetch_can_forward_stream(
job: &ServiceWorkerFetchJob,
response_head: &MaterializedServiceWorkerFetchResponseHead,
) -> Result<bool, String> {
let final_url = response_head.final_url.as_ref().unwrap_or(&job.request.url);
if let Some(message) = service_worker_fetch_response_head_rejection(
job,
&response_head.response_type,
response_head.redirected,
) {
return Err(message);
}
validate_service_worker_fetch_response_head_security_policy(
job,
final_url,
response_head.final_url.as_ref(),
&response_head.headers,
)?;
Ok(job.direct_completion_tx.is_none()
@@ -140,8 +146,10 @@ fn service_worker_fetch_can_forward_stream(
| crate::types::SubresourceResourceType::Video
)
&& !is_redirect_status(response_head.status)
&& matches!(response_head.response_type.as_str(), "default" | "basic")
&& !service_worker_fetch_response_requires_body_security_policy(job))
&& matches!(
response_head.response_type.as_str(),
"default" | "basic" | "cors"
))
}
fn apply_service_worker_synthetic_redirect(
@@ -383,10 +391,13 @@ impl ServiceWorkerRuntimeService {
job.completion_tx.clone(),
AsyncSubresourceFetchEvent::StreamingStarted(Box::new(
AsyncSubresourceStreamingStarted {
skip_fetch_security_validation: true,
response_filter: service_worker_response_type_filter(
response_filter: service_worker_fetch_response_head_filter(
&started.response_head.response_type,
started.response_head.cors_exposed_header_names.as_deref(),
started.response_head.status,
job.request.redirect_mode,
),
skip_fetch_security_validation: true,
internal_id: job.internal_id,
request_url: job.request.url.clone(),
request_method: job.request.method.clone(),
@@ -595,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<String> {
match response.response_type.as_str() {
service_worker_fetch_response_head_rejection(job, &response.response_type, response.redirected)
}
fn service_worker_fetch_response_head_rejection(
job: &ServiceWorkerFetchJob,
response_type: &str,
redirected: bool,
) -> Option<String> {
match response_type {
"error" => {
return Some("FetchEvent.respondWith rejected an error Response".to_owned());
}
@@ -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<AsyncSubresourceFetchResponseFilter> {
if redirect_mode == moli_fetch::RequestRedirectMode::Manual
&& is_redirect_status(response.status)
&& !matches!(response.response_type.as_str(), "opaque" | "opaqueredirect")
{
Some(AsyncSubresourceFetchResponseFilter::OpaqueRedirect)
} else {
service_worker_response_type_filter(&response.response_type)
}
service_worker_fetch_response_head_filter(
&response.response_type,
response.cors_exposed_header_names.as_deref(),
response.status,
redirect_mode,
)
}
fn service_worker_response_type_filter(
fn service_worker_fetch_response_head_filter(
response_type: &str,
cors_exposed_header_names: Option<&[String]>,
status: u16,
redirect_mode: moli_fetch::RequestRedirectMode,
) -> Option<AsyncSubresourceFetchResponseFilter> {
match response_type {
"basic" => Some(AsyncSubresourceFetchResponseFilter::Basic),
"cors" => Some(AsyncSubresourceFetchResponseFilter::Cors),
"opaque" => Some(AsyncSubresourceFetchResponseFilter::Opaque),
"opaqueredirect" => Some(AsyncSubresourceFetchResponseFilter::OpaqueRedirect),
_ => None,
}
Some(match response_type {
"opaque" => AsyncSubresourceFetchResponseFilter::Opaque,
"opaqueredirect" => AsyncSubresourceFetchResponseFilter::OpaqueRedirect,
_ if redirect_mode == moli_fetch::RequestRedirectMode::Manual
&& is_redirect_status(status) =>
{
AsyncSubresourceFetchResponseFilter::OpaqueRedirect
}
"cors" => AsyncSubresourceFetchResponseFilter::Cors(
cors_exposed_header_names.unwrap_or_default().to_vec(),
),
_ => AsyncSubresourceFetchResponseFilter::Basic,
})
}
#[cfg(test)]
@@ -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,
+3 -3
View File
@@ -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(
@@ -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);
+12 -10
View File
@@ -650,17 +650,17 @@ pub(super) struct AsyncSubresourceFetchCompletion {
pub(super) result: std::result::Result<NavigationResponse, String>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) enum AsyncSubresourceFetchResponseFilter {
Basic,
Cors,
Cors(Vec<String>),
Opaque,
OpaqueRedirect,
}
impl AsyncSubresourceFetchResponseFilter {
pub(super) fn is_readable(self) -> bool {
matches!(self, Self::Basic | Self::Cors)
pub(super) fn is_readable(&self) -> bool {
matches!(self, Self::Basic | Self::Cors(_))
}
}
@@ -926,6 +926,7 @@ pub(super) struct ServiceWorkerControllerChangeCompletion {
pub(super) struct StreamingSubresourceFetchState {
pub(super) response_filter: Option<AsyncSubresourceFetchResponseFilter>,
pub(super) skip_fetch_security_validation: bool,
pub(super) pending: PendingSubresourceFetchState,
pub(super) request_url: Url,
pub(super) request_method: String,
@@ -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,
)
}
}
@@ -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,
)
}
};
@@ -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<crate::types::AsyncSubresourceFetchResponseFilter>,
skip_fetch_security_validation: bool,
fetch_id: u32,
network_request_headers: Option<Vec<(String, String)>>,
result: Result<WorkerFetchResponse, String>,
@@ -1241,6 +1240,8 @@ pub(super) struct PendingWorkerCspReport {
}
pub(super) struct PausedWorkerSubresourceResponse {
pub(super) response_filter: Option<crate::types::AsyncSubresourceFetchResponseFilter>,
pub(super) skip_fetch_security_validation: bool,
pub(super) head: ResponseHead,
pub(super) body: SubresourceResponseBody,
}
@@ -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,
});
@@ -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(),
@@ -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"),
+108 -64
View File
@@ -132,6 +132,7 @@ fn is_false(value: &bool) -> bool {
#[derive(Debug, Clone, PartialEq)]
pub struct StorageBucketCachedResponse {
pub cors_exposed_header_names: Option<Vec<String>>,
pub response_type: String,
pub url: String,
pub redirected: bool,
@@ -388,6 +389,8 @@ struct StorageBucketCacheJson {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct StorageBucketCacheJsonEntry {
#[serde(default, skip_serializing_if = "Option::is_none")]
cors_exposed_header_names: Option<Vec<String>>,
usage_bytes: u64,
#[serde(
default = "default_cache_request_method",
@@ -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,