mirror of
https://github.com/lexmount/moli.git
synced 2026-10-08 16:01:05 +00:00
fix(fetch): preserve service worker response types and CORS headers
Carry the original response filter and validation state through Window streams and Worker completions, including paused responses. Synthesized responses remain basic, and already fetched bodies are not filtered by ORB again. Keep client CORP/COEP checks against actual network response URLs. Retain internal headers separately from the CORS public header view through clone, deferred Cache.put, persistent Cache storage and respondWith. Match Vary using the public header view and keep filtered cached headers immutable.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user