mirror of
https://github.com/lexmount/moli.git
synced 2026-11-04 00:06:51 +00:00
fix(fetch): enforce blob URL storage-key access
Authorize captured entries using pinned Window/XHR owners and Worker storage keys. Preserve Request entries when another environment is denied, and use Basic responses for authorized reads, including opaque origins and integrity checks. Keep Document creation URLs stable across history, transport replacement, and document.open() so self-fetch exemptions remain tied to the original environment. Validation: cargo fmt --all; strict workspace/all-targets/all-features Clippy; 21465 nextest tests passed, 16 skipped, 32 threads and zero retries.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
use crate::context_bootstrap::media_source::{MediaSourceObject, media_source_object};
|
||||
use crate::{native_bridge, web_api_interfaces};
|
||||
use moli_file_api::{
|
||||
BlobId, BlobLineEndings, BlobStore, ObjectUrlData, ObjectUrlTarget, blob_slice_relative_index,
|
||||
BlobId, BlobLineEndings, BlobStore, ObjectUrlEntry, ObjectUrlTarget, blob_slice_relative_index,
|
||||
clamp_blob_long_long, normalize_blob_line_endings_with_native_ending, normalize_blob_mime_type,
|
||||
};
|
||||
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
|
||||
@@ -43,12 +43,35 @@ struct BlobPrototypeDeclaration {
|
||||
r#type: (),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct ObjectUrlAccessKey {
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct ObjectUrlAccessKey {
|
||||
partition: RendererStoragePartitionIdentity,
|
||||
storage_key: moli_storage_key::MoliStorageKey,
|
||||
}
|
||||
|
||||
impl ObjectUrlAccessKey {
|
||||
pub(crate) fn new(
|
||||
partition: RendererStoragePartitionIdentity,
|
||||
storage_key: moli_storage_key::MoliStorageKey,
|
||||
) -> Self {
|
||||
Self {
|
||||
partition,
|
||||
storage_key,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn for_worker(scope: &mut v8::PinScope<'_, '_>) -> Option<Self> {
|
||||
Some(Self::new(
|
||||
crate::worker::worker_storage_partition_identity(scope)?,
|
||||
crate::worker::worker_storage_key(scope)?,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn same_partition(&self, other: &Self) -> bool {
|
||||
self.partition == other.partition
|
||||
}
|
||||
}
|
||||
|
||||
type RendererBlobStore = BlobStore<
|
||||
ResourceOwnerId,
|
||||
RendererStoragePartitionIdentity,
|
||||
@@ -431,10 +454,10 @@ pub(super) fn create_object_url_for_object<'s>(
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) type RendererObjectUrlData = ObjectUrlData<Arc<MediaSourceObject>>;
|
||||
pub(crate) type RendererObjectUrlEntry = ObjectUrlEntry<ObjectUrlAccessKey, Arc<MediaSourceObject>>;
|
||||
|
||||
pub(crate) fn object_url_data(url: &str) -> Option<RendererObjectUrlData> {
|
||||
blob_store().object_url_data(url)
|
||||
pub(crate) fn object_url_entry(url: &str) -> Option<RendererObjectUrlEntry> {
|
||||
blob_store().object_url_entry(url)
|
||||
}
|
||||
|
||||
pub(crate) fn object_url_policy(url: &str) -> Option<ObjectUrlPolicySnapshot> {
|
||||
|
||||
@@ -80,6 +80,7 @@ impl Drop for DocumentResourceLoaderAuthority {
|
||||
#[derive(Clone)]
|
||||
pub struct DocumentResourceLoader {
|
||||
request_client: ResourceRequestClient,
|
||||
creation_url: Arc<url::Url>,
|
||||
authority: Arc<DocumentResourceLoaderAuthority>,
|
||||
csp_reports: Arc<crate::content_security_policy::ContentSecurityPolicyReports>,
|
||||
}
|
||||
@@ -156,6 +157,7 @@ impl DocumentResourceLoader {
|
||||
.with_document_preloads(preloads.clone());
|
||||
Self {
|
||||
request_client,
|
||||
creation_url: Arc::new(context.document_url().clone()),
|
||||
csp_reports: Default::default(),
|
||||
authority: Arc::new(DocumentResourceLoaderAuthority {
|
||||
id: NEXT_DOCUMENT_RESOURCE_LOADER_ID
|
||||
@@ -214,6 +216,7 @@ impl DocumentResourceLoader {
|
||||
// document.open() rotates Moli's load owner, but keeps the Document's
|
||||
// CSP. In-flight requests and new requests must share its report history.
|
||||
replacement.csp_reports = previous.csp_reports.clone();
|
||||
replacement.creation_url = previous.creation_url.clone();
|
||||
// document.open() replaces the load owner, not the Document. Transfer
|
||||
// retirement authority along with its one-shot preload map.
|
||||
previous
|
||||
@@ -268,6 +271,7 @@ impl DocumentResourceLoader {
|
||||
Self {
|
||||
request_client,
|
||||
authority: Arc::clone(&self.authority),
|
||||
creation_url: self.creation_url.clone(),
|
||||
csp_reports: self.csp_reports.clone(),
|
||||
}
|
||||
}
|
||||
@@ -316,6 +320,12 @@ impl DocumentResourceLoader {
|
||||
self.authority.lifecycle.lock().context.clone()
|
||||
}
|
||||
|
||||
/// The settings object's creation URL is unaffected by history changes,
|
||||
/// base URL changes, transport replacement, or document.open().
|
||||
pub(crate) fn creation_url(&self) -> &url::Url {
|
||||
&self.creation_url
|
||||
}
|
||||
|
||||
/// Updates the URL inputs for later loads without changing this Document's
|
||||
/// origin, authority, or requests that already captured their settings.
|
||||
pub(crate) fn update_document_urls(&self, document_url: url::Url, base_url: url::Url) {
|
||||
|
||||
@@ -41,6 +41,30 @@ fn resource_task_runner() -> crate::network::RendererResourceTaskRunner {
|
||||
.expect("resource authority test must own a Tokio runtime")
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn document_creation_url_survives_history_transport_and_document_open() {
|
||||
let transport = ResourceRequestClient::new(&FetchConfig::default()).unwrap();
|
||||
let first = document_loader(transport.clone(), 1, "blob:https://example.test/creation");
|
||||
let creation = first.creation_url().clone();
|
||||
let changed = Url::parse("blob:https://example.test/history").unwrap();
|
||||
first.update_document_urls(changed.clone(), changed.clone());
|
||||
assert_eq!(first.fetch_context().document_url(), &changed);
|
||||
assert_eq!(first.creation_url(), &creation);
|
||||
let replacement = first.with_replacement_transport(transport.handle());
|
||||
assert_eq!(replacement.creation_url(), &creation);
|
||||
let opened = DocumentResourceLoader::for_document_open(
|
||||
context(2, changed.as_str()),
|
||||
super::document::DocumentResourceAuthoritySource::Inherited(replacement),
|
||||
&first,
|
||||
);
|
||||
assert_eq!(opened.creation_url(), &creation);
|
||||
let navigated = first.fork_for_document(context(3, "https://example.test/navigated"));
|
||||
assert_eq!(
|
||||
navigated.creation_url().as_str(),
|
||||
"https://example.test/navigated"
|
||||
);
|
||||
}
|
||||
|
||||
fn document_loader(
|
||||
request_client: ResourceRequestClient,
|
||||
document_id: u64,
|
||||
|
||||
@@ -30,7 +30,9 @@ use moli_webapi_declare::WebApiObject;
|
||||
use crate::network::ResourceRequestClient;
|
||||
|
||||
pub(in crate::network_host) use self::blob_url_entry::BLOB_URL_ENTRY_SLOT;
|
||||
pub(crate) use self::blob_url_entry::{CapturedBlobUrl, blob_url_entry, set_blob_url_entry};
|
||||
pub(crate) use self::blob_url_entry::{
|
||||
BlobUrlFetchEnvironment, CapturedBlobUrl, blob_url_entry, set_blob_url_entry,
|
||||
};
|
||||
|
||||
pub(crate) use self::async_fetch::{
|
||||
collect_image_response_into_parkable,
|
||||
@@ -160,7 +162,7 @@ pub(crate) use self::request_scope::{
|
||||
pub(crate) use self::response::{
|
||||
FetchResponseRequest, FetchResponseSecurityViolation, MaterializedResponseBody,
|
||||
MaterializedResponseHead, build_error_response_object,
|
||||
build_fetch_response_object_for_request_mode,
|
||||
build_fetch_response_object_for_request_mode_with_filter,
|
||||
build_fetch_response_object_from_body_source_for_request_mode_with_filter,
|
||||
build_fetch_response_object_from_stream_for_request_mode_with_filter,
|
||||
build_fetch_response_object_from_subresource_body_for_request_mode_with_filter,
|
||||
|
||||
@@ -3,18 +3,60 @@
|
||||
//! XHR. Private carriers retain the Blob bytes or native MediaSource identity
|
||||
//! until their last JS owner is collected; isolate teardown also releases them.
|
||||
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
|
||||
|
||||
use crate::util::{get_private_value, set_private_value};
|
||||
use moli_file_api::ObjectUrlData;
|
||||
|
||||
/// Fetch authority comes from the pinned execution context, never from the
|
||||
/// serialized blob URL or the realm in which an argument getter runs.
|
||||
pub(crate) struct BlobUrlFetchEnvironment {
|
||||
access_key: crate::blob::ObjectUrlAccessKey,
|
||||
top_level_creation_url: Option<url::Url>,
|
||||
}
|
||||
|
||||
impl BlobUrlFetchEnvironment {
|
||||
pub(crate) fn for_window(
|
||||
host: &mut crate::native_bridge::JsContextHost,
|
||||
binding: &crate::native_bridge::WindowExecutionContextBinding,
|
||||
) -> Option<Self> {
|
||||
let identity = binding.resolve_identity(host)?;
|
||||
let storage_context =
|
||||
host.storage_context_for_window_execution_context_identity(identity)?;
|
||||
let dispatch_scope = identity.dispatch_scope();
|
||||
let top_level_creation_url = match dispatch_scope {
|
||||
crate::native_bridge::OwnerDispatchScope::Child(_) => None,
|
||||
crate::native_bridge::OwnerDispatchScope::Top
|
||||
| crate::native_bridge::OwnerDispatchScope::LightweightPopup(_) => {
|
||||
let loader = host.document_resource_loader_for_dispatch_scope(dispatch_scope)?;
|
||||
Some(loader.creation_url().clone())
|
||||
}
|
||||
};
|
||||
Some(Self {
|
||||
access_key: crate::blob::ObjectUrlAccessKey::new(
|
||||
host.browser_context_runtime().storage_partition_identity(),
|
||||
storage_context.storage_key().clone(),
|
||||
),
|
||||
top_level_creation_url,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn for_worker(scope: &mut v8::PinScope<'_, '_>) -> Option<Self> {
|
||||
Some(Self {
|
||||
access_key: crate::blob::ObjectUrlAccessKey::for_worker(scope)?,
|
||||
top_level_creation_url: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::network_host) const BLOB_URL_ENTRY_SLOT: &str = "__lmBlobUrlEntry";
|
||||
const ID_SLOT: &str = "__lmBlobUrlEntryId";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct CapturedBlobUrl {
|
||||
url: url::Url,
|
||||
data: Option<crate::blob::RendererObjectUrlData>,
|
||||
entry: Option<Arc<crate::blob::RendererObjectUrlEntry>>,
|
||||
fetch_authorized: bool,
|
||||
}
|
||||
|
||||
impl CapturedBlobUrl {
|
||||
@@ -22,10 +64,38 @@ impl CapturedBlobUrl {
|
||||
if url.scheme() != "blob" {
|
||||
return None;
|
||||
}
|
||||
let mut url = url.clone();
|
||||
url.set_fragment(None);
|
||||
let data = crate::blob::object_url_data(url.as_str());
|
||||
Some(Self { url, data })
|
||||
let url = url.clone();
|
||||
let entry = crate::blob::object_url_entry(url.as_str()).map(Arc::new);
|
||||
Some(Self {
|
||||
url,
|
||||
entry,
|
||||
fetch_authorized: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Scope a copy for this fetch without changing the parsed Request/XHR
|
||||
/// entry. A denied lookup stays captured and must not fall back to a fresh
|
||||
/// unrestricted lookup in the store.
|
||||
pub(crate) fn for_environment(mut self, environment: Option<&BlobUrlFetchEnvironment>) -> Self {
|
||||
self.fetch_authorized = self.entry.as_ref().is_some_and(|entry| {
|
||||
environment.is_some_and(|environment| {
|
||||
entry.access_key.as_ref().is_some_and(|key| {
|
||||
key == &environment.access_key
|
||||
// Fetch explicitly exempts a top-level document fetching
|
||||
// its own creation URL from storage-key partition checks.
|
||||
|| (key.same_partition(&environment.access_key)
|
||||
&& environment.top_level_creation_url.as_ref() == Some(&self.url))
|
||||
})
|
||||
})
|
||||
});
|
||||
if !self.fetch_authorized {
|
||||
self.entry = None;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn is_authorized_fetch(&self, url: &url::Url) -> bool {
|
||||
self.fetch_authorized && self.matches(url)
|
||||
}
|
||||
|
||||
pub(super) fn response(
|
||||
@@ -36,7 +106,7 @@ impl CapturedBlobUrl {
|
||||
let ObjectUrlData::Blob {
|
||||
bytes: body,
|
||||
mime_type,
|
||||
} = self.data.as_ref()?
|
||||
} = &self.entry.as_ref()?.data
|
||||
else {
|
||||
// Fetch's blob scheme only reads Blob objects, never MediaSource.
|
||||
return Some(Err(super::browser_response::LocalUrlError::MediaSource));
|
||||
@@ -50,7 +120,10 @@ impl CapturedBlobUrl {
|
||||
}
|
||||
|
||||
pub(super) fn matches(&self, url: &url::Url) -> bool {
|
||||
self.url.as_str()
|
||||
self.url
|
||||
.as_str()
|
||||
.split_once('#')
|
||||
.map_or(self.url.as_str(), |(url, _)| url)
|
||||
== url
|
||||
.as_str()
|
||||
.split_once('#')
|
||||
@@ -140,7 +213,8 @@ mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
fn bytes(entry: &CapturedBlobUrl) -> &Arc<[u8]> {
|
||||
let Some(ObjectUrlData::Blob { bytes, .. }) = &entry.data else {
|
||||
let Some(ObjectUrlData::Blob { bytes, .. }) = entry.entry.as_ref().map(|entry| &entry.data)
|
||||
else {
|
||||
panic!("expected captured Blob bytes");
|
||||
};
|
||||
bytes
|
||||
@@ -149,13 +223,165 @@ mod tests {
|
||||
fn entry() -> CapturedBlobUrl {
|
||||
CapturedBlobUrl {
|
||||
url: url::Url::parse("blob:https://example.test/captured").unwrap(),
|
||||
data: Some(ObjectUrlData::Blob {
|
||||
bytes: Arc::from([0, 128, 255]),
|
||||
mime_type: "application/example".to_owned(),
|
||||
}),
|
||||
entry: Some(Arc::new(moli_file_api::ObjectUrlEntry {
|
||||
data: ObjectUrlData::Blob {
|
||||
bytes: Arc::from([0, 128, 255]),
|
||||
mime_type: "application/example".to_owned(),
|
||||
},
|
||||
access_key: None,
|
||||
})),
|
||||
fetch_authorized: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fetch_access_uses_full_creator_identity_without_mutating_the_parsed_entry() {
|
||||
use crate::runtime::RendererBrowserContextRuntime;
|
||||
use moli_storage_key::{MoliStorageKey, OpaqueOriginNonce};
|
||||
let browser = RendererBrowserContextRuntime::new();
|
||||
let partition = browser.handle().storage_partition_identity();
|
||||
let url = url::Url::parse("data:text/plain,opaque").unwrap();
|
||||
let storage_key =
|
||||
MoliStorageKey::first_party_from_url(&url, Some(OpaqueOriginNonce::new(101)));
|
||||
let creator_key =
|
||||
crate::blob::ObjectUrlAccessKey::new(partition.clone(), storage_key.clone());
|
||||
let mut captured = entry();
|
||||
captured.url = url::Url::parse("blob:null/captured").unwrap();
|
||||
Arc::get_mut(captured.entry.as_mut().unwrap())
|
||||
.unwrap()
|
||||
.access_key = Some(creator_key.clone());
|
||||
let own = BlobUrlFetchEnvironment {
|
||||
access_key: creator_key,
|
||||
top_level_creation_url: None,
|
||||
};
|
||||
let other_browser = RendererBrowserContextRuntime::new();
|
||||
let foreign_keys = [
|
||||
crate::blob::ObjectUrlAccessKey::new(
|
||||
other_browser.handle().storage_partition_identity(),
|
||||
storage_key.clone(),
|
||||
),
|
||||
crate::blob::ObjectUrlAccessKey::new(
|
||||
partition.clone(),
|
||||
MoliStorageKey::first_party_from_url(&url, Some(OpaqueOriginNonce::new(102))),
|
||||
),
|
||||
crate::blob::ObjectUrlAccessKey::new(
|
||||
partition.clone(),
|
||||
MoliStorageKey::from_url_and_top_level_site(
|
||||
&url,
|
||||
"https://another-site.test".to_owned(),
|
||||
storage_key.opaque_nonce(),
|
||||
),
|
||||
),
|
||||
crate::blob::ObjectUrlAccessKey::new(partition, storage_key.with_cross_site_ancestor()),
|
||||
];
|
||||
for access_key in foreign_keys {
|
||||
let environment = BlobUrlFetchEnvironment {
|
||||
access_key,
|
||||
top_level_creation_url: None,
|
||||
};
|
||||
let denied = captured.clone().for_environment(Some(&environment));
|
||||
assert!(!denied.is_authorized_fetch(&captured.url));
|
||||
assert!(
|
||||
super::super::local_url_response_with_blob_entry(
|
||||
&captured.url,
|
||||
"GET",
|
||||
&[],
|
||||
Some(&denied)
|
||||
)
|
||||
.unwrap()
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
captured
|
||||
.clone()
|
||||
.for_environment(None)
|
||||
.response(&captured.url, &[])
|
||||
.is_none()
|
||||
);
|
||||
let authorized = captured.clone().for_environment(Some(&own));
|
||||
assert!(authorized.is_authorized_fetch(&captured.url));
|
||||
assert_eq!(
|
||||
authorized
|
||||
.response(&captured.url, &[])
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.body_bytes(),
|
||||
[0, 128, 255]
|
||||
);
|
||||
assert!(captured.entry.is_some());
|
||||
assert!(!captured.fetch_authorized);
|
||||
let mut fragment = captured.url.clone();
|
||||
fragment.set_fragment(Some("fragment"));
|
||||
assert!(authorized.is_authorized_fetch(&fragment));
|
||||
assert!(!authorized.is_authorized_fetch(&url));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_level_self_fetch_exception_does_not_cross_browser_partitions_or_cover_other_urls() {
|
||||
use crate::runtime::RendererBrowserContextRuntime;
|
||||
use moli_storage_key::MoliStorageKey;
|
||||
let browser = RendererBrowserContextRuntime::new();
|
||||
let partition = browser.handle().storage_partition_identity();
|
||||
let url = url::Url::parse("https://example.test/").unwrap();
|
||||
let creator = MoliStorageKey::first_party_from_url(&url, None);
|
||||
let embedded = MoliStorageKey::from_url_and_top_level_site(
|
||||
&url,
|
||||
"https://foreign.test".to_owned(),
|
||||
None,
|
||||
);
|
||||
let mut captured = entry();
|
||||
Arc::get_mut(captured.entry.as_mut().unwrap())
|
||||
.unwrap()
|
||||
.access_key = Some(crate::blob::ObjectUrlAccessKey::new(
|
||||
partition.clone(),
|
||||
embedded,
|
||||
));
|
||||
let mut environment = BlobUrlFetchEnvironment {
|
||||
access_key: crate::blob::ObjectUrlAccessKey::new(partition, creator.clone()),
|
||||
top_level_creation_url: Some(captured.url.clone()),
|
||||
};
|
||||
assert!(
|
||||
captured
|
||||
.clone()
|
||||
.for_environment(Some(&environment))
|
||||
.is_authorized_fetch(&captured.url)
|
||||
);
|
||||
let mut different_fragment = captured.clone();
|
||||
different_fragment.url.set_fragment(Some("other"));
|
||||
assert!(
|
||||
!different_fragment
|
||||
.clone()
|
||||
.for_environment(Some(&environment))
|
||||
.is_authorized_fetch(&different_fragment.url)
|
||||
);
|
||||
environment.top_level_creation_url = Some(url);
|
||||
assert!(
|
||||
!captured
|
||||
.clone()
|
||||
.for_environment(Some(&environment))
|
||||
.is_authorized_fetch(&captured.url)
|
||||
);
|
||||
environment.top_level_creation_url = Some(captured.url.clone());
|
||||
let other_browser = RendererBrowserContextRuntime::new();
|
||||
environment.access_key = crate::blob::ObjectUrlAccessKey::new(
|
||||
other_browser.handle().storage_partition_identity(),
|
||||
creator,
|
||||
);
|
||||
assert!(
|
||||
!captured
|
||||
.clone()
|
||||
.for_environment(Some(&environment))
|
||||
.is_authorized_fetch(&captured.url)
|
||||
);
|
||||
let unkeyed = entry();
|
||||
assert!(
|
||||
!unkeyed
|
||||
.for_environment(Some(&environment))
|
||||
.is_authorized_fetch(&captured.url)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_entries_follow_the_last_owner_across_realms_gc_and_isolate_drop() {
|
||||
moli_v8_test_util::ensure_v8();
|
||||
@@ -232,7 +458,11 @@ mod tests {
|
||||
.unwrap()
|
||||
.is_err()
|
||||
);
|
||||
let missing = CapturedBlobUrl { url, data: None };
|
||||
let missing = CapturedBlobUrl {
|
||||
url,
|
||||
entry: None,
|
||||
fetch_authorized: false,
|
||||
};
|
||||
assert!(missing.response(&missing.url, &[]).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +176,14 @@ pub(crate) fn window_fetch_callback<'s>(
|
||||
);
|
||||
return;
|
||||
};
|
||||
let Some(blob_environment) =
|
||||
BlobUrlFetchEnvironment::for_window(unsafe { &mut *host_ptr }, &binding)
|
||||
else {
|
||||
rv.set(
|
||||
make_rejected_promise(scope, "fetch: document execution context is unavailable").into(),
|
||||
);
|
||||
return;
|
||||
};
|
||||
let fetch_context = crate::native_bridge::WindowFetchContext::from_realm(binding);
|
||||
let relevant_context = {
|
||||
let context = fetch_context.script_realm().context(scope);
|
||||
@@ -185,7 +193,13 @@ pub(crate) fn window_fetch_callback<'s>(
|
||||
let request_body_owner = parsed.request_body_owner.take();
|
||||
let prepared = {
|
||||
let scope = &mut v8::ContextScope::new(scope, relevant_context);
|
||||
prepare_window_fetch_request(scope, parsed, fetch_context, unsafe { &*host_ptr })
|
||||
prepare_window_fetch_request(
|
||||
scope,
|
||||
parsed,
|
||||
fetch_context,
|
||||
unsafe { &*host_ptr },
|
||||
&blob_environment,
|
||||
)
|
||||
};
|
||||
let prepared = match prepared {
|
||||
Ok(prepared) => prepared,
|
||||
@@ -262,8 +276,13 @@ fn window_fetch_callback_in_relevant_realm<'s>(
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(message) = moli_fetch::FetchUrlList::new(&prepared.resolved_url, &[])
|
||||
.validate_request_mode(prepared.request_mode, &prepared.request_origin)
|
||||
let authorized_blob = prepared
|
||||
.blob_url_entry
|
||||
.as_ref()
|
||||
.is_some_and(|entry| entry.is_authorized_fetch(&prepared.resolved_url));
|
||||
if !authorized_blob
|
||||
&& let Err(message) = moli_fetch::FetchUrlList::new(&prepared.resolved_url, &[])
|
||||
.validate_request_mode(prepared.request_mode, &prepared.request_origin)
|
||||
{
|
||||
rv.set(make_rejected_promise(scope, &message).into());
|
||||
return;
|
||||
@@ -295,7 +314,7 @@ fn window_fetch_callback_in_relevant_realm<'s>(
|
||||
// Renderer-owned URLs, including local errors, bypass network interception.
|
||||
match resolve_local_fetch(host, &prepared) {
|
||||
Ok(Some(response)) => {
|
||||
let response_obj = build_fetch_response_object_for_request_mode(
|
||||
let response_obj = build_fetch_response_object_for_request_mode_with_filter(
|
||||
scope,
|
||||
&prepared.request_origin,
|
||||
FetchResponseRequest {
|
||||
@@ -304,6 +323,7 @@ fn window_fetch_callback_in_relevant_realm<'s>(
|
||||
redirect_mode: prepared.redirect_mode,
|
||||
},
|
||||
response,
|
||||
authorized_blob.then_some(crate::types::AsyncSubresourceFetchResponseFilter::Basic),
|
||||
);
|
||||
resolver.resolve(scope, response_obj.into());
|
||||
rv.set(promise.into());
|
||||
|
||||
@@ -175,16 +175,24 @@ pub(super) fn resolve_local_fetch(
|
||||
.map_err(LocalFetchError::Url)
|
||||
.and_then(|response| {
|
||||
if !prepared.integrity.is_empty() {
|
||||
let filter = FetchResponseRequest {
|
||||
method: &prepared.method,
|
||||
mode: prepared.request_mode,
|
||||
redirect_mode: prepared.redirect_mode,
|
||||
}
|
||||
.network_response_filter(
|
||||
&prepared.request_origin,
|
||||
&response.head(),
|
||||
prepared.credentials_mode,
|
||||
);
|
||||
let filter = if prepared
|
||||
.blob_url_entry
|
||||
.as_ref()
|
||||
.is_some_and(|entry| entry.is_authorized_fetch(&prepared.resolved_url))
|
||||
{
|
||||
crate::types::AsyncSubresourceFetchResponseFilter::Basic
|
||||
} else {
|
||||
FetchResponseRequest {
|
||||
method: &prepared.method,
|
||||
mode: prepared.request_mode,
|
||||
redirect_mode: prepared.redirect_mode,
|
||||
}
|
||||
.network_response_filter(
|
||||
&prepared.request_origin,
|
||||
&response.head(),
|
||||
prepared.credentials_mode,
|
||||
)
|
||||
};
|
||||
validate_fetch_response_integrity(
|
||||
&prepared.integrity,
|
||||
&prepared.method,
|
||||
|
||||
@@ -100,6 +100,7 @@ pub(super) fn prepare_window_fetch_request<'s>(
|
||||
parsed: ParsedWindowFetchInput,
|
||||
fetch_context: crate::native_bridge::WindowFetchContext,
|
||||
host: &JsContextHost,
|
||||
blob_environment: &BlobUrlFetchEnvironment,
|
||||
) -> Result<PreparedWindowFetchRequest, FetchPrepareError> {
|
||||
// Match Request's Headers representation before preflight and context
|
||||
// overrides: repeated Range fields form one unsafe, comma-joined value.
|
||||
@@ -164,7 +165,8 @@ pub(super) fn prepare_window_fetch_request<'s>(
|
||||
|
||||
let blob_url_entry = parsed
|
||||
.blob_url_entry
|
||||
.or_else(|| CapturedBlobUrl::capture(&resolved_url));
|
||||
.or_else(|| CapturedBlobUrl::capture(&resolved_url))
|
||||
.map(|entry| entry.for_environment(Some(blob_environment)));
|
||||
Ok(PreparedWindowFetchRequest {
|
||||
frame_id,
|
||||
fetch_context,
|
||||
|
||||
@@ -29,7 +29,7 @@ pub(crate) use self::cors::{
|
||||
};
|
||||
pub(crate) use self::materialize::{
|
||||
FetchResponseRequest, MaterializedResponseBody, MaterializedResponseHead,
|
||||
build_fetch_response_object_for_request_mode,
|
||||
build_fetch_response_object_for_request_mode_with_filter,
|
||||
build_fetch_response_object_from_body_source_for_request_mode_with_filter,
|
||||
build_fetch_response_object_from_stream_for_request_mode_with_filter,
|
||||
build_fetch_response_object_from_subresource_body_for_request_mode_with_filter,
|
||||
|
||||
@@ -211,38 +211,22 @@ fn filtered_response_status_text(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_fetch_response_object_for_request_mode<'s>(
|
||||
pub(crate) fn build_fetch_response_object_for_request_mode_with_filter<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
request_origin: impl Into<WebOrigin>,
|
||||
request: FetchResponseRequest<'_>,
|
||||
response: Response,
|
||||
filter_override: Option<crate::types::AsyncSubresourceFetchResponseFilter>,
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
let request_origin = request_origin.into();
|
||||
let (head, body) = response.into_body();
|
||||
build_fetch_response_object_from_body_source_for_request_mode(
|
||||
scope,
|
||||
&request_origin,
|
||||
request,
|
||||
head,
|
||||
body,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn build_fetch_response_object_from_body_source_for_request_mode<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
request_origin: impl Into<WebOrigin>,
|
||||
request: FetchResponseRequest<'_>,
|
||||
head: moli_fetch::ResponseHead,
|
||||
body: moli_fetch::ResponseBody,
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
let request_origin = request_origin.into();
|
||||
build_fetch_response_object_from_body_source_for_request_mode_with_filter(
|
||||
scope,
|
||||
&request_origin,
|
||||
request,
|
||||
head,
|
||||
body,
|
||||
None,
|
||||
filter_override,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ pub(super) fn xhr_dom_debugger_request_url<'s>(
|
||||
|
||||
pub(super) fn prepare_xhr_send_request<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
host: &JsContextHost,
|
||||
host: &mut JsContextHost,
|
||||
xhr: v8::Local<'s, v8::Object>,
|
||||
method: String,
|
||||
prepared_body: PreparedXhrSendBody,
|
||||
@@ -67,6 +67,8 @@ pub(super) fn prepare_xhr_send_request<'s>(
|
||||
|
||||
let execution_context = xhr_execution_context_binding(scope, host, xhr)
|
||||
.ok_or(XhrSendPrepareError::ExecutionContext)?;
|
||||
let blob_environment = BlobUrlFetchEnvironment::for_window(host, &execution_context)
|
||||
.ok_or(XhrSendPrepareError::ExecutionContext)?;
|
||||
let owner = execution_context.dispatch_scope();
|
||||
let resource_loader = host
|
||||
.document_resource_loader_for_dispatch_scope(owner)
|
||||
@@ -105,7 +107,8 @@ pub(super) fn prepare_xhr_send_request<'s>(
|
||||
document_referrer_policy,
|
||||
policy_context,
|
||||
resolved_url,
|
||||
blob_url_entry: blob_url_entry(scope, xhr),
|
||||
blob_url_entry: blob_url_entry(scope, xhr)
|
||||
.map(|entry| entry.for_environment(Some(&blob_environment))),
|
||||
method,
|
||||
request_headers,
|
||||
cors_preflight_request_headers,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
use super::*;
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn blob_url_fetch_and_xhr_use_creator_storage_keys_in_window_and_worker() {
|
||||
let loader = static_http_loader([]);
|
||||
let mut vm = new_page_task_executor_test_vm_with_loader("https://blob-storage.test/", &loader);
|
||||
let fixture = include_str!("../../../tests/fixtures/blob-storage-key.js");
|
||||
vm.eval(&format!(
|
||||
"globalThis.blobStorageResult = null; {fixture}.then(value => {{blobStorageResult = value;}}, error => {{blobStorageResult = String(error.stack || error);}});"
|
||||
)).unwrap();
|
||||
advance_page_task_executor_until_eval_equals(
|
||||
&mut vm,
|
||||
&loader,
|
||||
"String(blobStorageResult !== null)",
|
||||
"true",
|
||||
"Blob storage-key probes should finish without network access",
|
||||
)
|
||||
.await;
|
||||
let result: serde_json::Value =
|
||||
serde_json::from_str(&vm.eval("JSON.stringify(__uiEventResults)").unwrap()).unwrap();
|
||||
let checks = result["checks"].as_array().unwrap();
|
||||
let failures: Vec<_> = checks
|
||||
.iter()
|
||||
.filter(|check| check["passed"] != true)
|
||||
.collect();
|
||||
assert_eq!(checks.len(), 46);
|
||||
assert_eq!(result["complete"], true);
|
||||
assert_eq!(
|
||||
vm.eval("String(blobStorageResult)").unwrap(),
|
||||
"true",
|
||||
"{failures:?}"
|
||||
);
|
||||
assert!(failures.is_empty(), "{failures:?}");
|
||||
}
|
||||
@@ -2217,6 +2217,7 @@ mod audio_source_interfaces;
|
||||
mod bitmap_rendering_context_interface_interfaces;
|
||||
mod blob_range;
|
||||
mod blob_response_headers;
|
||||
mod blob_storage_key;
|
||||
mod body_completion;
|
||||
mod body_mime;
|
||||
mod body_mime_consolidation;
|
||||
|
||||
+29
-26
@@ -975,32 +975,34 @@ fn materialize_response_object_preserves_redirected_slot() {
|
||||
let scope = &mut scope.init();
|
||||
let context = unsafe { v8::Local::new(scope, &*context_ptr) };
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
let response = crate::network_host::build_fetch_response_object_for_request_mode(
|
||||
scope,
|
||||
&document_url,
|
||||
crate::network_host::FetchResponseRequest {
|
||||
redirect_mode: moli_fetch::RequestRedirectMode::Follow,
|
||||
method: "GET",
|
||||
mode: moli_fetch::RequestMode::Cors,
|
||||
},
|
||||
moli_fetch::Response::from_head_and_text_body(
|
||||
moli_fetch::ResponseHead {
|
||||
status_text: None,
|
||||
final_url: final_url.clone(),
|
||||
status: 200,
|
||||
headers: vec![("content-type".to_owned(), b"text/plain".to_vec())],
|
||||
request_cookie_report: None,
|
||||
cookie_set_reports: Vec::new(),
|
||||
redirected: true,
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
let response =
|
||||
crate::network_host::build_fetch_response_object_for_request_mode_with_filter(
|
||||
scope,
|
||||
&document_url,
|
||||
crate::network_host::FetchResponseRequest {
|
||||
redirect_mode: moli_fetch::RequestRedirectMode::Follow,
|
||||
method: "GET",
|
||||
mode: moli_fetch::RequestMode::Cors,
|
||||
},
|
||||
"redirected-body".to_owned(),
|
||||
),
|
||||
);
|
||||
moli_fetch::Response::from_head_and_text_body(
|
||||
moli_fetch::ResponseHead {
|
||||
status_text: None,
|
||||
final_url: final_url.clone(),
|
||||
status: 200,
|
||||
headers: vec![("content-type".to_owned(), b"text/plain".to_vec())],
|
||||
request_cookie_report: None,
|
||||
cookie_set_reports: Vec::new(),
|
||||
redirected: true,
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
"redirected-body".to_owned(),
|
||||
),
|
||||
None,
|
||||
);
|
||||
let materialized =
|
||||
crate::network_host::materialize_response_object(scope, response.into(), "test")
|
||||
.expect("materialized response should be accepted");
|
||||
@@ -1382,9 +1384,10 @@ redirect_mode: moli_fetch::RequestRedirectMode::Follow,
|
||||
};
|
||||
let id = crate::network_host::new_network_body_source_id();
|
||||
let response = match source {
|
||||
"response" => crate::network_host::build_fetch_response_object_for_request_mode(
|
||||
"response" => crate::network_host::build_fetch_response_object_for_request_mode_with_filter(
|
||||
scope, &document_url, request,
|
||||
moli_fetch::Response::from_head_and_text_body(head, "discard me".to_owned()),
|
||||
None,
|
||||
),
|
||||
"bytes" => crate::network_host::build_fetch_response_object_from_body_source_for_request_mode_with_filter(
|
||||
scope, &document_url, request, head,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
use crate::network_host::{
|
||||
CapturedBlobUrl, FetchArgumentError, convert_fetch_arguments,
|
||||
BlobUrlFetchEnvironment, CapturedBlobUrl, FetchArgumentError, convert_fetch_arguments,
|
||||
local_url_response_with_blob_entry, normalized_headers_entries,
|
||||
};
|
||||
use crate::service_worker_runtime::{
|
||||
@@ -222,6 +222,9 @@ pub(in crate::worker) fn spawn_worker_fetch_network(
|
||||
) {
|
||||
// Resolve local URLs before scheduling: fetch(url) already parsed and
|
||||
// captured its entry when JavaScript regains control and may revoke it.
|
||||
let authorized_blob = blob_url_entry
|
||||
.as_ref()
|
||||
.is_some_and(|entry| entry.is_authorized_fetch(&resolved_url));
|
||||
let local_response = local_url_response_with_blob_entry(
|
||||
&resolved_url,
|
||||
&method,
|
||||
@@ -230,8 +233,8 @@ pub(in crate::worker) fn spawn_worker_fetch_network(
|
||||
);
|
||||
tokio::task::spawn_local(async move {
|
||||
let loader = load.request_client();
|
||||
let (result, network_request_headers) = if let Err(message) =
|
||||
moli_fetch::FetchUrlList::new(&resolved_url, &[])
|
||||
let (result, network_request_headers) = if !authorized_blob
|
||||
&& let Err(message) = moli_fetch::FetchUrlList::new(&resolved_url, &[])
|
||||
.validate_request_mode(request_mode, &moli_url::WebOrigin::from_url(&document_url))
|
||||
{
|
||||
(Err(message), None)
|
||||
@@ -399,8 +402,9 @@ 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,
|
||||
response_filter: authorized_blob
|
||||
.then_some(crate::types::AsyncSubresourceFetchResponseFilter::Basic),
|
||||
skip_fetch_security_validation: authorized_blob,
|
||||
fetch_id,
|
||||
network_request_headers,
|
||||
result,
|
||||
@@ -2279,6 +2283,9 @@ pub(super) fn start_worker_resource_fetch<'s>(
|
||||
metadata: request_metadata,
|
||||
signal,
|
||||
} = input;
|
||||
let blob_environment = BlobUrlFetchEnvironment::for_worker(scope);
|
||||
let blob_url_entry =
|
||||
blob_url_entry.map(|entry| entry.for_environment(blob_environment.as_ref()));
|
||||
// Use the same normalized header list as Request before policy checks and
|
||||
// context overrides can otherwise discard a repeated Range field.
|
||||
let request_headers = normalized_headers_entries(&request_headers);
|
||||
@@ -2378,8 +2385,11 @@ pub(super) fn start_worker_resource_fetch<'s>(
|
||||
return Some(make_rejected_promise(scope, &message));
|
||||
}
|
||||
|
||||
if let Err(message) = moli_fetch::FetchUrlList::new(&resolved_url, &[])
|
||||
.validate_request_mode(request_mode, &moli_url::WebOrigin::from_url(&document_url))
|
||||
if !blob_url_entry
|
||||
.as_ref()
|
||||
.is_some_and(|entry| entry.is_authorized_fetch(&resolved_url))
|
||||
&& let Err(message) = moli_fetch::FetchUrlList::new(&resolved_url, &[])
|
||||
.validate_request_mode(request_mode, &moli_url::WebOrigin::from_url(&document_url))
|
||||
{
|
||||
return Some(make_rejected_promise(scope, &message));
|
||||
}
|
||||
|
||||
@@ -1213,6 +1213,7 @@ pub(in crate::worker) fn prepare_worker_xhr_send_request<'s>(
|
||||
let resolved_url = resolve_context_url(&document_url, &url_str, None)
|
||||
.map_err(WorkerXhrSendPrepareError::Url)?;
|
||||
let request_headers = xhr_author_request_headers(scope, xhr, &prepared_body);
|
||||
let blob_environment = crate::network_host::BlobUrlFetchEnvironment::for_worker(scope);
|
||||
let credentials_mode =
|
||||
if xhr_state_bool_property(scope, xhr, XHR_WITH_CREDENTIALS_SLOT).unwrap_or(false) {
|
||||
RequestCredentialsMode::Include
|
||||
@@ -1224,7 +1225,8 @@ pub(in crate::worker) fn prepare_worker_xhr_send_request<'s>(
|
||||
use_cors_preflight: capture_xhr_upload_listener_flag(scope, xhr),
|
||||
document_url,
|
||||
resolved_url,
|
||||
blob_url_entry: blob_url_entry(scope, xhr),
|
||||
blob_url_entry: blob_url_entry(scope, xhr)
|
||||
.map(|entry| entry.for_environment(blob_environment.as_ref())),
|
||||
method,
|
||||
request_headers: moli_fetch::RequestHeaders::from_byte_strings(&request_headers)
|
||||
.expect("validated worker XHR headers are ByteStrings"),
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
(async () => {
|
||||
const checks = [];
|
||||
const check = (name, passed, observed) => checks.push({name, passed, observed});
|
||||
const modes = ['cors', 'no-cors', 'same-origin'];
|
||||
async function read(url, mode) {
|
||||
try {
|
||||
const r = await fetch(url, {mode});
|
||||
return {type: r.type, status: r.status, text: await r.text()};
|
||||
} catch (e) { return {error: e.name}; }
|
||||
}
|
||||
function xhr(url) {
|
||||
return new Promise(resolve => {
|
||||
const x = new XMLHttpRequest(); x.open('GET', url);
|
||||
x.onload = () => resolve({status: x.status, text: x.responseText});
|
||||
x.onerror = () => resolve({error: 'NetworkError', status: x.status}); x.send();
|
||||
});
|
||||
}
|
||||
const peerSource = `const read=${read.toString()}; const xhr=${xhr.toString()};
|
||||
const own=URL.createObjectURL(new Blob(['peer'],{type:'text/plain'}));
|
||||
const tell=x=>${'typeof parent !== "undefined" ? parent.postMessage(x,"*") : postMessage(x)'};
|
||||
tell({kind:'ready',url:own});
|
||||
onmessage=async e=>{if(e.data.kind!=='audit')return;
|
||||
const rows=[];for(const [name,url] of [['own',own],['parent',e.data.parent],['foreign',e.data.foreign]]){
|
||||
for(const mode of ['cors','no-cors','same-origin'])rows.push({name,mode,...await read(url,mode)});
|
||||
rows.push({name,mode:'xhr',...await xhr(url)});
|
||||
}tell({kind:'done',rows});};`;
|
||||
const wait = (source, kind) => new Promise(resolve => {
|
||||
const listener = e => {if(e.source===source && e.data?.kind===kind){removeEventListener('message',listener);resolve(e.data);}};
|
||||
addEventListener('message',listener);
|
||||
});
|
||||
const opaque = document.createElement('iframe'); opaque.sandbox='allow-scripts';
|
||||
opaque.srcdoc='<script>'+peerSource+'<\/script>';
|
||||
document.body.append(opaque);
|
||||
const opaqueReady=await wait(opaque.contentWindow,'ready');
|
||||
const parentUrl=URL.createObjectURL(new Blob(['parent'],{type:'text/plain'}));
|
||||
const opaqueDone=wait(opaque.contentWindow,'done');
|
||||
opaque.contentWindow.postMessage({kind:'audit',parent:parentUrl,foreign:parentUrl},'*');
|
||||
for(const row of (await opaqueDone).rows){
|
||||
const own=row.name==='own';
|
||||
check('opaque '+row.name+' '+row.mode, own
|
||||
? row.status===200 && row.text==='peer' && (row.mode==='xhr'||row.type==='basic')
|
||||
: !!row.error && (row.mode==='xhr'?row.status===0:row.error==='TypeError'), row);
|
||||
}
|
||||
const same = document.createElement('iframe'); same.srcdoc='<script>'+peerSource+'<\/script>';
|
||||
document.body.append(same);
|
||||
const sameReady=await wait(same.contentWindow,'ready');
|
||||
const sameDone=wait(same.contentWindow,'done');
|
||||
same.contentWindow.postMessage({kind:'audit',parent:parentUrl,foreign:opaqueReady.url},'*');
|
||||
for(const row of (await sameDone).rows){
|
||||
const allowed=row.name!=='foreign';
|
||||
check('same origin '+row.name+' '+row.mode, allowed
|
||||
? row.status===200 && row.text===(row.name==='own'?'peer':'parent') && (row.mode==='xhr'||row.type==='basic')
|
||||
: !!row.error && (row.mode==='xhr'?row.status===0:row.error==='TypeError'), row);
|
||||
}
|
||||
const script=URL.createObjectURL(new Blob([peerSource],{type:'text/javascript'}));
|
||||
const worker=new Worker(script);
|
||||
const workerReady=await new Promise(resolve=>worker.onmessage=e=>resolve(e.data));
|
||||
const workerDone=new Promise(resolve=>worker.onmessage=e=>resolve(e.data));
|
||||
worker.postMessage({kind:'audit',parent:parentUrl,foreign:opaqueReady.url});
|
||||
for(const row of (await workerDone).rows){
|
||||
const allowed=row.name!=='foreign';
|
||||
check('worker '+row.name+' '+row.mode, allowed
|
||||
? row.status===200 && row.text===(row.name==='own'?'peer':'parent') && (row.mode==='xhr'||row.type==='basic')
|
||||
: !!row.error && (row.mode==='xhr'?row.status===0:row.error==='TypeError'), row);
|
||||
}
|
||||
for(const [name,url] of [['same origin',sameReady.url],['worker',workerReady.url]]){
|
||||
for(const mode of modes){const r=await read(url,mode);check('parent reads '+name+' '+mode,r.status===200&&r.text==='peer'&&r.type==='basic',r);}
|
||||
const r=await xhr(url);check('parent reads '+name+' xhr',r.status===200&&r.text==='peer',r);
|
||||
}
|
||||
// The fetch receiver determines the environment when a method is borrowed.
|
||||
const foreignRequest=new same.contentWindow.Request(opaqueReady.url);
|
||||
try {await same.contentWindow.fetch.call(window,foreignRequest);check('borrowed fetch rejects foreign Request',false);}
|
||||
catch(e){check('borrowed fetch rejects foreign Request',e.name==='TypeError',e.name);}
|
||||
const r=await same.contentWindow.fetch.call(window,new Request(parentUrl,{mode:'no-cors'}));
|
||||
check('borrowed fetch reads own Request',r.type==='basic'&&await r.text()==='parent',r.type);
|
||||
worker.terminate();opaque.remove();same.remove();URL.revokeObjectURL(script);URL.revokeObjectURL(parentUrl);
|
||||
globalThis.__uiEventResults={checks,total:checks.length,passed:checks.filter(r=>r.passed).length,complete:true};
|
||||
return checks.every(r=>r.passed);
|
||||
})()
|
||||
Reference in New Issue
Block a user