diff --git a/components/fonts/font_context.rs b/components/fonts/font_context.rs index bb5aa4f1c3f..37a597a2244 100644 --- a/components/fonts/font_context.rs +++ b/components/fonts/font_context.rs @@ -16,6 +16,7 @@ use fonts_traits::{ }; use log::{debug, trace}; use malloc_size_of_derive::MallocSizeOf; +use net_traits::blob_url_store::UrlWithBlobClaim; use net_traits::policy_container::PolicyContainer; use net_traits::request::{ CredentialsMode, Destination, InsecureRequestsPolicy, Referrer, RequestBuilder, RequestClient, @@ -986,7 +987,7 @@ impl RemoteWebFontDownloader { let request = RequestBuilder::new( state.webview_id, - url.clone().into(), + UrlWithBlobClaim::from_url_without_having_claimed_blob(url.clone().into()), Referrer::ReferrerUrl(document_context.document_url.clone()), ) .destination(Destination::Font) diff --git a/components/net/fetch/methods.rs b/components/net/fetch/methods.rs index 521106ab32b..186a7445c65 100644 --- a/components/net/fetch/methods.rs +++ b/components/net/fetch/methods.rs @@ -540,12 +540,12 @@ pub async fn main_fetch( .await; let mut response = match response { - Some(res) => res, + Some(response) => response, None => { // Step 12. If response is null, then set response to the result // of running the steps corresponding to the first matching statement: let same_origin = if let Origin::Origin(ref origin) = request.origin { - *origin == current_url.origin() + *origin == request.current_url_with_blob_claim().origin() } else { false }; @@ -726,7 +726,11 @@ pub async fn main_fetch( // Step 16. If internalResponse’s URL list is empty, then set it to a clone of request’s URL list. if internal_response.url_list.is_empty() { - internal_response.url_list.clone_from(&request.url_list) + internal_response.url_list = request + .url_list + .iter() + .map(|locked_url| locked_url.url()) + .collect(); } // Step 17. Set internalResponse’s redirect taint to request’s redirect-taint. @@ -1056,18 +1060,22 @@ async fn scheme_fetch( // Step 2: Let request be fetchParams’s request. let request = &mut fetch_params.request; - let url = request.current_url(); + let url_and_blob_lock = request.current_url_with_blob_claim(); - let scheme = url.scheme(); + let scheme = url_and_blob_lock.scheme(); match scheme { - "about" if url.path() == "blank" => create_blank_reply(url, request.timing_type()), - "about" if url.path() == "memory" => create_about_memory(url, request.timing_type()), + "about" if url_and_blob_lock.path() == "blank" => { + create_blank_reply(url_and_blob_lock.url(), request.timing_type()) + }, + "about" if url_and_blob_lock.path() == "memory" => { + create_about_memory(url_and_blob_lock.url(), request.timing_type()) + }, - "chrome" if url.path() == "allowcert" => { + "chrome" if url_and_blob_lock.path() == "allowcert" => { if let Err(error) = handle_allowcert_request(request, context) { warn!("Could not handle allowcert request: {error}"); } - create_blank_reply(url, request.timing_type()) + create_blank_reply(url_and_blob_lock.url(), request.timing_type()) }, "http" | "https" => { diff --git a/components/net/filemanager_thread.rs b/components/net/filemanager_thread.rs index 44df8cf4f45..aa150565a66 100644 --- a/components/net/filemanager_thread.rs +++ b/components/net/filemanager_thread.rs @@ -18,10 +18,10 @@ use http::header::{self, HeaderValue}; use ipc_channel::ipc::IpcSender; use log::warn; use mime::{self, Mime}; -use net_traits::blob_url_store::{BlobBuf, BlobURLStoreError}; +use net_traits::blob_url_store::{BlobBuf, BlobTokenCommunicator, BlobURLStoreError}; use net_traits::filemanager_thread::{ FileManagerResult, FileManagerThreadError, FileManagerThreadMsg, FileTokenCheck, - ReadFileProgress, RelativePos, + GetTokenForFileReply, ReadFileProgress, RelativePos, }; use net_traits::http_percent_encode; use net_traits::response::{Response, ResponseBody}; @@ -84,13 +84,18 @@ enum FileImpl { pub struct FileManager { embedder_proxy: GenericEmbedderProxy, store: Arc, + blob_token_communicator: Arc>, } impl FileManager { - pub fn new(embedder_proxy: GenericEmbedderProxy) -> FileManager { + pub fn new( + embedder_proxy: GenericEmbedderProxy, + blob_token_communicator: Arc>, + ) -> FileManager { FileManager { embedder_proxy, store: Arc::new(FileManagerStore::new()), + blob_token_communicator, } } @@ -108,8 +113,8 @@ impl FileManager { }); } - pub(crate) fn get_token_for_file(&self, file_id: &Uuid) -> FileTokenCheck { - self.store.get_token_for_file(file_id) + pub(crate) fn get_token_for_file(&self, file_id: &Uuid, allow_revoked: bool) -> FileTokenCheck { + self.store.get_token_for_file(file_id, allow_revoked) } pub(crate) fn invalidate_token(&self, token: &FileTokenCheck, file_id: &Uuid) { @@ -182,6 +187,22 @@ impl FileManager { FileManagerThreadMsg::ActivateBlobURL(id, sender, origin) => { let _ = sender.send(self.store.set_blob_url_validity(true, &id, &origin)); }, + FileManagerThreadMsg::GetTokenForFile(id, _origin, sender) => { + let token = match self.get_token_for_file(&id, false) { + FileTokenCheck::Required(token) => Some(token), + _ => None, + }; + + let communicator = self.blob_token_communicator.lock(); + let _ = sender.send(GetTokenForFileReply { + token, + revoke_sender: communicator.revoke_sender.clone(), + refresh_sender: communicator.refresh_token_sender.clone(), + }); + }, + FileManagerThreadMsg::RevokeTokenForFile(token, id) => { + self.invalidate_token(&FileTokenCheck::Required(token), &id); + }, } } @@ -459,7 +480,7 @@ impl FileManagerStore { } } - pub(crate) fn get_token_for_file(&self, file_id: &Uuid) -> FileTokenCheck { + pub(crate) fn get_token_for_file(&self, file_id: &Uuid, allow_revoked: bool) -> FileTokenCheck { let mut entries = self.entries.write(); let parent_id = match entries.get(file_id) { Some(entry) => { @@ -471,12 +492,11 @@ impl FileManagerStore { }, None => return FileTokenCheck::ShouldFail, }; - let file_id = match parent_id.as_ref() { - Some(id) => id, - None => file_id, - }; + let file_id = parent_id.as_ref().unwrap_or(file_id); + if let Some(entry) = entries.get_mut(file_id) { - if !entry.is_valid_url.load(Ordering::Acquire) { + if !allow_revoked && !entry.is_valid_url.load(Ordering::Acquire) { + log::warn!("Refusing to grant token for revoked blob url: {file_id:?}"); return FileTokenCheck::ShouldFail; } let token = Uuid::new_v4(); diff --git a/components/net/http_loader.rs b/components/net/http_loader.rs index 6cbec98338f..5509d8778c3 100644 --- a/components/net/http_loader.rs +++ b/components/net/http_loader.rs @@ -40,6 +40,7 @@ use ipc_channel::ipc::{self, IpcSender}; use ipc_channel::router::ROUTER; use log::{debug, error, info, log_enabled, warn}; use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; +use net_traits::blob_url_store::UrlWithBlobClaim; use net_traits::fetch::headers::get_value_from_header_list; use net_traits::http_status::HttpStatus; use net_traits::policy_container::RequestPolicyContainer; @@ -1368,7 +1369,11 @@ pub async fn http_redirect_fetch( // Steps 15-17 relate to timing, which is not implemented 1:1 with the spec. // Step 18: Append locationURL to request’s URL list. - request.url_list.push(location_url); + request + .url_list + .push(UrlWithBlobClaim::from_url_without_having_claimed_blob( + location_url, + )); // Step 19: Invoke set request’s referrer policy on redirect on request and internalResponse. set_requests_referrer_policy_on_redirect(request, response.actual_response()); @@ -1768,7 +1773,11 @@ async fn http_network_or_cache_fetch( let mut response = response.unwrap(); // Step 11. Set response’s URL list to a clone of httpRequest’s URL list. - response.url_list = http_request.url_list.clone(); + response.url_list = http_request + .url_list + .iter() + .map(|claimed_url| claimed_url.url()) + .collect(); // Step 12. If httpRequest’s header list contains `Range`, then set response’s range-requested flag. if http_request.headers.contains_key(RANGE) { @@ -2450,7 +2459,7 @@ async fn cors_preflight_fetch( // referrer policy, mode is "cors", and response tainting is "cors". let mut preflight = RequestBuilder::new( request.target_webview_id, - request.current_url(), + request.current_url_with_blob_claim(), request.referrer.clone(), ) .method(Method::OPTIONS) diff --git a/components/net/protocols/blob.rs b/components/net/protocols/blob.rs index 5f82ee8bb5c..a1caf9eccd2 100644 --- a/components/net/protocols/blob.rs +++ b/components/net/protocols/blob.rs @@ -28,8 +28,8 @@ impl ProtocolHandler for BlobProtocolHander { done_chan: &mut DoneChannel, context: &FetchContext, ) -> Pin + Send>> { - let url = request.current_url(); - debug!("Loading blob {}", url.as_str()); + let url_and_blob_claim = request.current_url_with_blob_claim(); + debug!("Loading blob {}", url_and_blob_claim.as_str()); // Step 2. if request.method != Method::GET { @@ -39,16 +39,22 @@ impl ProtocolHandler for BlobProtocolHander { let range_header = request.headers.typed_get::(); let is_range_request = range_header.is_some(); - let (id, origin) = match parse_blob_url(&url) { - Ok((id, origin)) => (id, origin), - Err(error) => { + let (file_id, origin) = if let Some(token) = url_and_blob_claim.token() { + (token.file_id, token.origin.clone()) + } else { + // FIXME: This should never happen, we should have acquired a token beforehand + let Ok((id, _)) = parse_blob_url(&url_and_blob_claim.url()) else { return Box::pin(ready(Response::network_error( - NetworkError::ResourceLoadError(format!("Invalid blob URL ({error})")), + NetworkError::ResourceLoadError("Invalid blob URL".into()), ))); - }, + }; + (id, url_and_blob_claim.url().origin()) }; - let mut response = Response::new(url, ResourceFetchTiming::new(request.timing_type())); + let mut response = Response::new( + url_and_blob_claim.url(), + ResourceFetchTiming::new(request.timing_type()), + ); response.status = HttpStatus::default(); if is_range_request { @@ -63,7 +69,7 @@ impl ProtocolHandler for BlobProtocolHander { if let Err(err) = context.filemanager.fetch_file( &mut done_sender, context.cancellation_listener.clone(), - id, + file_id, &context.file_token, origin, &mut response, diff --git a/components/net/protocols/mod.rs b/components/net/protocols/mod.rs index 63f13cbc277..fbc2d68fa54 100644 --- a/components/net/protocols/mod.rs +++ b/components/net/protocols/mod.rs @@ -11,6 +11,7 @@ use std::pin::Pin; use headers::Range; use http::StatusCode; use log::error; +use net_traits::blob_url_store::UrlWithBlobClaim; use net_traits::filemanager_thread::RelativePos; use net_traits::request::Request; use net_traits::response::Response; @@ -219,7 +220,9 @@ impl ProtocolHandler for WebPageContentProtocolHandler { // Ensure we did a proper substitution with a HTTP result assert!(matches!(result_url.scheme(), "http" | "https")); // Step 9. Navigate an appropriate navigable to resultURL. - request.url_list.push(result_url); + request + .url_list + .push(UrlWithBlobClaim::new(result_url, None)); let request2 = request.clone(); let context2 = context.clone(); Box::pin(async move { fetch(request2, &mut DiscardFetch, &context2).await }) diff --git a/components/net/resource_thread.rs b/components/net/resource_thread.rs index 358fd7fa36d..94d03f0e03d 100644 --- a/components/net/resource_thread.rs +++ b/components/net/resource_thread.rs @@ -19,7 +19,7 @@ use embedder_traits::GenericEmbedderProxy; use hyper_serde::Serde; use ipc_channel::ipc::IpcSender; use log::{debug, trace, warn}; -use net_traits::blob_url_store::parse_blob_url; +use net_traits::blob_url_store::{BlobTokenCommunicator, parse_blob_url}; use net_traits::filemanager_thread::FileTokenCheck; use net_traits::pub_domains::public_suffix_list_size_of; use net_traits::request::{Destination, PreloadEntry, PreloadId, RequestBuilder, RequestId}; @@ -140,7 +140,13 @@ pub fn new_core_resource_thread( let (public_setup_chan, public_setup_port) = generic_channel::channel().unwrap(); let (private_setup_chan, private_setup_port) = generic_channel::channel().unwrap(); let (report_chan, report_port) = generic_channel::channel().unwrap(); + let (revoke_sender, revoke_receiver) = generic_channel::channel().unwrap(); + let (refresh_sender, refresh_receiver) = generic_channel::channel().unwrap(); + let blob_token_communicator = Arc::new(Mutex::new(BlobTokenCommunicator { + revoke_sender, + refresh_token_sender: refresh_sender, + })); thread::Builder::new() .name("ResourceManager".to_owned()) .spawn(move || { @@ -150,6 +156,7 @@ pub fn new_core_resource_thread( embedder_proxy.clone(), ca_certificates.clone(), ignore_certificate_errors, + blob_token_communicator, ); let mut channel_manager = ResourceChannelManager { @@ -167,6 +174,8 @@ pub fn new_core_resource_thread( public_setup_port, private_setup_port, report_port, + revoke_receiver, + refresh_receiver, protocols, embedder_proxy, ) @@ -241,11 +250,14 @@ fn create_http_states( } impl ResourceChannelManager { + #[expect(clippy::too_many_arguments)] fn start( &mut self, public_receiver: GenericReceiver, private_receiver: GenericReceiver, memory_reporter: GenericReceiver, + revoke_receiver: GenericReceiver, + refresh_receiver: GenericReceiver, protocols: Arc, embedder_proxy: GenericEmbedderProxy, ) { @@ -260,6 +272,8 @@ impl ResourceChannelManager { let private_id = rx_set.add(private_receiver); let public_id = rx_set.add(public_receiver); let reporter_id = rx_set.add(memory_reporter); + let revoker_id = rx_set.add(revoke_receiver); + let refresh_id = rx_set.add(refresh_receiver); loop { for received in rx_set.select().into_iter() { @@ -270,7 +284,31 @@ impl ResourceChannelManager { log::error!("Found selection error: {error}") }, GenericSelectionResult::MessageReceived(id, msg) => { - if id == reporter_id { + if id == revoker_id { + let CoreResourceMsg::RevokeTokenForFile(revocation_request) = msg + else { + log::error!("Blob revocation channel received unexpected message"); + continue; + }; + self.resource_manager.filemanager.invalidate_token( + &FileTokenCheck::Required(revocation_request.token), + &revocation_request.blob_id, + ) + } else if id == refresh_id { + let CoreResourceMsg::RefreshTokenForFile(refresh_request) = msg else { + log::error!("Blob revocation channel received unexpected message"); + continue; + }; + + let FileTokenCheck::Required(refreshed_token) = self + .resource_manager + .filemanager + .get_token_for_file(&refresh_request.blob_id, true) + else { + unreachable!(); + }; + let _ = refresh_request.new_token_sender.send(refreshed_token); + } else if id == reporter_id { if let CoreResourceMsg::CollectMemoryReport(report_chan) = msg { self.process_report( report_chan, @@ -607,8 +645,10 @@ impl ResourceChannelManager { let _ = sender.send(()); return false; }, - // Ignore this message as we handle it only in the reporter chan - CoreResourceMsg::CollectMemoryReport(_) => {}, + // Ignore these messages as they are only sent on very specific channels. + CoreResourceMsg::CollectMemoryReport(_) | + CoreResourceMsg::RevokeTokenForFile(..) | + CoreResourceMsg::RefreshTokenForFile(..) => {}, } true } @@ -654,11 +694,12 @@ impl CoreResourceManager { embedder_proxy: GenericEmbedderProxy, ca_certificates: CACertificates<'static>, ignore_certificate_errors: bool, + blob_token_communicator: Arc>, ) -> CoreResourceManager { CoreResourceManager { devtools_sender, sw_managers: Default::default(), - filemanager: FileManager::new(embedder_proxy.clone()), + filemanager: FileManager::new(embedder_proxy.clone(), blob_token_communicator), request_interceptor: RequestInterceptor::new(embedder_proxy), ca_certificates, ignore_certificate_errors, @@ -717,17 +758,18 @@ impl CoreResourceManager { // In the case of a valid blob URL, acquiring a token granting access to a file, // regardless if the URL is revoked after token acquisition. // - // TODO: to make more tests pass, acquire this token earlier, - // probably in a separate message flow. - // - // In such a setup, the token would not be acquired here, - // but could instead be contained in the actual CoreResourceMsg::Fetch message. - // - // See https://github.com/servo/servo/issues/25226 + // Ideally all callers should have claimed the blob entry themselves, but we're not there + // yet. let (file_token, blob_url_file_id) = match url.scheme() { "blob" => { - if let Ok((id, _)) = parse_blob_url(&url) { - (self.filemanager.get_token_for_file(&id), Some(id)) + if let Some(token) = request.current_url_with_blob_claim().token() { + (FileTokenCheck::Required(token.token), Some(token.file_id)) + } else if let Ok((id, _)) = parse_blob_url(&url) { + // See https://github.com/servo/servo/issues/25226 + log::warn!( + "Failed to claim blob URL entry of valid blob URL before passing it to `net`. This causes race conditions." + ); + (self.filemanager.get_token_for_file(&id, false), Some(id)) } else { (FileTokenCheck::ShouldFail, None) } diff --git a/components/net/test_util.rs b/components/net/test_util.rs index b5aa4254b36..a7be6c14911 100644 --- a/components/net/test_util.rs +++ b/components/net/test_util.rs @@ -20,6 +20,7 @@ use hyper::service::service_fn; use hyper::{Request as HyperRequest, Response as HyperResponse}; use hyper_util::rt::tokio::TokioIo; use net_traits::AsyncRuntime; +use net_traits::blob_url_store::UrlWithBlobClaim; use rustls_pki_types::pem::PemObject; use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; use servo_default_resources as _; @@ -79,7 +80,7 @@ impl Server { } } -pub fn make_server(handler: H) -> (Server, ServoUrl) +pub fn make_server(handler: H) -> (Server, UrlWithBlobClaim) where H: Fn(HyperRequest, &mut HyperResponse>) + Send @@ -99,7 +100,7 @@ where ); let url_string = format!("http://localhost:{}", listener.local_addr().unwrap().port()); - let url = ServoUrl::parse(&url_string).unwrap(); + let url = UrlWithBlobClaim::new(ServoUrl::parse(&url_string).unwrap(), None); let graceful = hyper_util::server::graceful::GracefulShutdown::new(); @@ -175,7 +176,7 @@ fn load_private_key_from_file( } } -pub fn make_ssl_server(handler: H) -> (Server, ServoUrl) +pub fn make_ssl_server(handler: H) -> (Server, UrlWithBlobClaim) where H: Fn(HyperRequest, &mut HyperResponse>) + Send @@ -194,7 +195,7 @@ where ); let url_string = format!("http://localhost:{}", listener.local_addr().unwrap().port()); - let url = ServoUrl::parse(&url_string).unwrap(); + let url = UrlWithBlobClaim::new(ServoUrl::parse(&url_string).unwrap(), None); let cert_path = Path::new("../../resources/self_signed_certificate_for_testing.crt") .canonicalize() diff --git a/components/net/tests/data_loader.rs b/components/net/tests/data_loader.rs index 5fc7d42db36..fcda310fb4f 100644 --- a/components/net/tests/data_loader.rs +++ b/components/net/tests/data_loader.rs @@ -7,6 +7,7 @@ use std::ops::Deref; use headers::{ContentType, HeaderMapExt}; use hyper_serde::Serde; use mime::{self, Mime}; +use net_traits::blob_url_store::UrlWithBlobClaim; use net_traits::request::Referrer; use net_traits::response::ResponseBody; use net_traits::{FetchMetadata, FilteredMetadata, NetworkError}; @@ -24,7 +25,7 @@ fn assert_parse( ) { use net_traits::request::RequestBuilder; - let url = ServoUrl::parse(url).unwrap(); + let url = UrlWithBlobClaim::new(ServoUrl::parse(url).unwrap(), None); let request = RequestBuilder::new(Some(TEST_WEBVIEW_ID), url.clone(), Referrer::NoReferrer) .origin(url.origin()) .pipeline_id(None) diff --git a/components/net/tests/fetch.rs b/components/net/tests/fetch.rs index d1b581c69e7..fe595bc7303 100644 --- a/components/net/tests/fetch.rs +++ b/components/net/tests/fetch.rs @@ -32,6 +32,7 @@ use net::filemanager_thread::FileManager; use net::hsts::HstsEntry; use net::protocols::ProtocolRegistry; use net::request_interceptor::RequestInterceptor; +use net_traits::blob_url_store::{BlobTokenCommunicator, UrlWithBlobClaim}; use net_traits::filemanager_thread::FileTokenCheck; use net_traits::http_status::HttpStatus; use net_traits::request::{ @@ -83,10 +84,14 @@ fn test_fetch_response_is_not_network_error() { #[test] fn test_fetch_on_bad_port_is_network_error() { let url = ServoUrl::parse("http://www.example.org:6667").unwrap(); - let request = RequestBuilder::new(Some(TEST_WEBVIEW_ID), url.clone(), Referrer::NoReferrer) - .origin(url.origin()) - .policy_container(Default::default()) - .build(); + let request = RequestBuilder::new( + Some(TEST_WEBVIEW_ID), + UrlWithBlobClaim::new(url.clone(), None), + Referrer::NoReferrer, + ) + .origin(url.origin()) + .policy_container(Default::default()) + .build(); let fetch_response = fetch(request, None); assert!(fetch_response.is_network_error()); let fetch_error = fetch_response.get_network_error().unwrap(); @@ -124,10 +129,14 @@ fn test_fetch_response_body_matches_const_message() { #[test] fn test_fetch_aboutblank() { let url = ServoUrl::parse("about:blank").unwrap(); - let request = RequestBuilder::new(Some(TEST_WEBVIEW_ID), url.clone(), Referrer::NoReferrer) - .origin(url.origin()) - .policy_container(Default::default()) - .build(); + let request = RequestBuilder::new( + Some(TEST_WEBVIEW_ID), + UrlWithBlobClaim::new(url.clone(), None), + Referrer::NoReferrer, + ) + .origin(url.origin()) + .policy_container(Default::default()) + .build(); let fetch_response = fetch(request, None); // We should see an opaque-filtered response. @@ -187,10 +196,14 @@ fn test_fetch_blob() { .promote_memory(id.clone(), blob_buf, true, origin.origin()); let url = ServoUrl::parse(&format!("blob:{}{}", origin.as_str(), id.simple())).unwrap(); - let request = RequestBuilder::new(Some(TEST_WEBVIEW_ID), url.clone(), Referrer::NoReferrer) - .origin(origin.origin()) - .policy_container(Default::default()) - .build(); + let request = RequestBuilder::new( + Some(TEST_WEBVIEW_ID), + UrlWithBlobClaim::from_url_without_having_claimed_blob(url.clone()), + Referrer::NoReferrer, + ) + .origin(origin.origin()) + .policy_container(Default::default()) + .build(); let (sender, receiver) = unbounded(); @@ -227,10 +240,14 @@ fn test_file() { .unwrap(); let url = ServoUrl::from_file_path(path.clone()).unwrap(); - let request = RequestBuilder::new(Some(TEST_WEBVIEW_ID), url.clone(), Referrer::NoReferrer) - .origin(url.origin()) - .policy_container(Default::default()) - .build(); + let request = RequestBuilder::new( + Some(TEST_WEBVIEW_ID), + UrlWithBlobClaim::new(url.clone(), None), + Referrer::NoReferrer, + ) + .origin(url.origin()) + .policy_container(Default::default()) + .build(); let mut context = new_fetch_context(None, None); let fetch_response = fetch_with_context(request, &mut context); @@ -269,10 +286,14 @@ fn test_file() { #[test] fn test_fetch_ftp() { let url = ServoUrl::parse("ftp://not-supported").unwrap(); - let request = RequestBuilder::new(Some(TEST_WEBVIEW_ID), url.clone(), Referrer::NoReferrer) - .origin(url.origin()) - .policy_container(Default::default()) - .build(); + let request = RequestBuilder::new( + Some(TEST_WEBVIEW_ID), + UrlWithBlobClaim::new(url.clone(), None), + Referrer::NoReferrer, + ) + .origin(url.origin()) + .policy_container(Default::default()) + .build(); let fetch_response = fetch(request, None); assert!(fetch_response.is_network_error()); } @@ -280,10 +301,14 @@ fn test_fetch_ftp() { #[test] fn test_fetch_bogus_scheme() { let url = ServoUrl::parse("bogus://whatever").unwrap(); - let request = RequestBuilder::new(Some(TEST_WEBVIEW_ID), url.clone(), Referrer::NoReferrer) - .origin(url.origin()) - .policy_container(Default::default()) - .build(); + let request = RequestBuilder::new( + Some(TEST_WEBVIEW_ID), + UrlWithBlobClaim::new(url.clone(), None), + Referrer::NoReferrer, + ) + .origin(url.origin()) + .policy_container(Default::default()) + .build(); let fetch_response = fetch(request, None); assert!(fetch_response.is_network_error()); } @@ -699,7 +724,7 @@ fn test_fetch_with_local_urls_only() { }; let (server, server_url) = make_server(handler); - let do_fetch = |url: ServoUrl| { + let do_fetch = |url: UrlWithBlobClaim| { let mut request = RequestBuilder::new(Some(TEST_WEBVIEW_ID), url.clone(), Referrer::NoReferrer) .origin(url.origin()) @@ -712,7 +737,7 @@ fn test_fetch_with_local_urls_only() { fetch(request, None) }; - let local_url = ServoUrl::parse("about:blank").unwrap(); + let local_url = UrlWithBlobClaim::new(ServoUrl::parse("about:blank").unwrap(), None); let local_response = do_fetch(local_url); let server_response = do_fetch(server_url); @@ -745,7 +770,10 @@ fn test_fetch_with_hsts() { state: Arc::new(create_http_state(None)), user_agent: DEFAULT_USER_AGENT.into(), devtools_chan: None, - filemanager: FileManager::new(embedder_proxy.clone()), + filemanager: FileManager::new( + embedder_proxy.clone(), + BlobTokenCommunicator::stub_for_testing(), + ), file_token: FileTokenCheck::NotRequired, request_interceptor: Arc::new(TokioMutex::new(RequestInterceptor::new(embedder_proxy))), cancellation_listener: Arc::new(Default::default()), @@ -808,7 +836,10 @@ fn test_load_adds_host_to_hsts_list_when_url_is_https() { state: Arc::new(create_http_state(None)), user_agent: DEFAULT_USER_AGENT.into(), devtools_chan: None, - filemanager: FileManager::new(embedder_proxy.clone()), + filemanager: FileManager::new( + embedder_proxy.clone(), + BlobTokenCommunicator::stub_for_testing(), + ), file_token: FileTokenCheck::NotRequired, request_interceptor: Arc::new(TokioMutex::new(RequestInterceptor::new(embedder_proxy))), cancellation_listener: Arc::new(Default::default()), @@ -876,7 +907,10 @@ fn test_fetch_self_signed() { state: Arc::new(create_http_state(None)), user_agent: DEFAULT_USER_AGENT.into(), devtools_chan: None, - filemanager: FileManager::new(embedder_proxy.clone()), + filemanager: FileManager::new( + embedder_proxy.clone(), + BlobTokenCommunicator::stub_for_testing(), + ), file_token: FileTokenCheck::NotRequired, request_interceptor: Arc::new(TokioMutex::new(RequestInterceptor::new(embedder_proxy))), cancellation_listener: Arc::new(Default::default()), @@ -1439,7 +1473,7 @@ fn test_fetch_with_devtools() { ); let httprequest = DevtoolsHttpRequest { - url: url, + url: url.url(), method: Method::GET, headers: headers, body: Some(vec![].into()), @@ -1522,7 +1556,10 @@ fn test_fetch_request_intercepted() { state: Arc::new(create_http_state(None)), user_agent: DEFAULT_USER_AGENT.into(), devtools_chan: None, - filemanager: FileManager::new(embedder_proxy.clone()), + filemanager: FileManager::new( + embedder_proxy.clone(), + BlobTokenCommunicator::stub_for_testing(), + ), file_token: FileTokenCheck::NotRequired, request_interceptor: Arc::new(TokioMutex::new(RequestInterceptor::new(embedder_proxy))), cancellation_listener: Arc::new(Default::default()), @@ -1538,10 +1575,14 @@ fn test_fetch_request_intercepted() { }; let url = ServoUrl::parse("http://www.example.org").unwrap(); - let request = RequestBuilder::new(Some(TEST_WEBVIEW_ID), url.clone(), Referrer::NoReferrer) - .origin(url.origin()) - .policy_container(Default::default()) - .build(); + let request = RequestBuilder::new( + Some(TEST_WEBVIEW_ID), + UrlWithBlobClaim::new(url.clone(), None), + Referrer::NoReferrer, + ) + .origin(url.origin()) + .policy_container(Default::default()) + .build(); let response = fetch_with_context(request, &mut context); assert!( diff --git a/components/net/tests/filemanager_thread.rs b/components/net/tests/filemanager_thread.rs index e23b04a93a6..b6fb188d303 100644 --- a/components/net/tests/filemanager_thread.rs +++ b/components/net/tests/filemanager_thread.rs @@ -13,7 +13,7 @@ use ipc_channel::ipc; use net::async_runtime::init_async_runtime; use net::embedder::NetToEmbedderMsg; use net::filemanager_thread::FileManager; -use net_traits::blob_url_store::BlobURLStoreError; +use net_traits::blob_url_store::{BlobTokenCommunicator, BlobURLStoreError}; use net_traits::filemanager_thread::{ FileManagerThreadError, FileManagerThreadMsg, ReadFileProgress, }; @@ -32,7 +32,7 @@ fn test_filemanager() { servo_config::prefs::set(preferences); let (embedder_proxy, embedder_receiver) = create_generic_embedder_proxy_and_receiver(); - let filemanager = FileManager::new(embedder_proxy); + let filemanager = FileManager::new(embedder_proxy, BlobTokenCommunicator::stub_for_testing()); // Try to open a dummy file "components/net/tests/test.jpeg" in tree let mut handler = File::open("tests/test.jpeg").expect("test.jpeg is stolen"); diff --git a/components/net/tests/http_cache.rs b/components/net/tests/http_cache.rs index 684cabb280a..c7947c1b484 100644 --- a/components/net/tests/http_cache.rs +++ b/components/net/tests/http_cache.rs @@ -5,6 +5,7 @@ use http::header::{CONTENT_LENGTH, CONTENT_RANGE, EXPIRES, HeaderValue, RANGE}; use http::{HeaderMap, StatusCode}; use net::http_cache::{CacheKey, HttpCache, refresh}; +use net_traits::blob_url_store::UrlWithBlobClaim; use net_traits::request::{Referrer, RequestBuilder}; use net_traits::response::{Response, ResponseBody}; use net_traits::{ResourceFetchTiming, ResourceTimingType}; @@ -20,10 +21,14 @@ async fn test_refreshing_resource_sets_done_chan_the_appropriate_value() { ResponseBody::Done(vec![]), ]; let url = ServoUrl::parse("https://servo.org").unwrap(); - let request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer) - .pipeline_id(Some(TEST_PIPELINE_ID)) - .origin(url.origin()) - .build(); + let request = RequestBuilder::new( + None, + UrlWithBlobClaim::new(url.clone(), None), + Referrer::NoReferrer, + ) + .pipeline_id(Some(TEST_PIPELINE_ID)) + .origin(url.origin()) + .build(); let timing = ResourceFetchTiming::new(ResourceTimingType::Navigation); let mut response = Response::new(url.clone(), timing); // Expires header makes the response cacheable. @@ -71,11 +76,15 @@ async fn test_skip_incomplete_cache_for_range_request_with_no_end_bound() { RANGE, HeaderValue::from_str(&format!("bytes={}-", 0)).unwrap(), ); - let request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer) - .pipeline_id(Some(TEST_PIPELINE_ID)) - .origin(url.origin()) - .headers(headers) - .build(); + let request = RequestBuilder::new( + None, + UrlWithBlobClaim::new(url.clone(), None), + Referrer::NoReferrer, + ) + .pipeline_id(Some(TEST_PIPELINE_ID)) + .origin(url.origin()) + .headers(headers) + .build(); // Store incomplete response to http_cache let timing = ResourceFetchTiming::new(ResourceTimingType::Navigation); diff --git a/components/net/tests/http_loader.rs b/components/net/tests/http_loader.rs index 94df61dd703..db0d9f01039 100644 --- a/components/net/tests/http_loader.rs +++ b/components/net/tests/http_loader.rs @@ -34,6 +34,7 @@ use net::fetch::methods::{self}; use net::http_loader::{determine_requests_referrer, serialize_origin}; use net::resource_thread::AuthCacheEntry; use net::test::DECODER_BUFFER_SIZE; +use net_traits::blob_url_store::UrlWithBlobClaim; use net_traits::http_status::HttpStatus; use net_traits::request::{ CredentialsMode, Destination, Referrer, Request, RequestBuilder, RequestMode, @@ -387,7 +388,7 @@ fn test_request_and_response_data_with_network_messages() { ); let httprequest = DevtoolsHttpRequest { - url: url, + url: url.url(), method: Method::GET, headers: headers, body: Some(vec![].into()), @@ -501,7 +502,7 @@ fn test_redirected_request_to_devtools() { let first_response = expect_response(&mut events); assert_eq!(first_request.method, Method::POST); - assert_eq!(first_request.url, pre_url); + assert_eq!(first_request.url, pre_url.url()); assert_eq!( first_response.status, HttpStatus::from(StatusCode::MOVED_PERMANENTLY) @@ -514,7 +515,7 @@ fn test_redirected_request_to_devtools() { let second_response = expect_response(&mut events); assert_eq!(second_request.method, Method::GET); - assert_eq!(second_request.url, post_url); + assert_eq!(second_request.url, post_url.url()); assert_eq!(second_response.status, HttpStatus::default()); assert_eq!(second_request.method, second_request_update.method); assert_eq!(second_request.url, second_request_update.url); @@ -1194,7 +1195,7 @@ fn test_load_errors_when_there_a_redirect_loop() { }; let (server_b, url_b) = make_server(handler_b); - *url_b_for_a.lock() = Some(url_b.clone()); + *url_b_for_a.lock() = Some(url_b.url()); let request = RequestBuilder::new(None, url_a.clone(), Referrer::NoReferrer) .method(Method::GET) @@ -1248,7 +1249,7 @@ fn test_load_succeeds_with_a_redirect_loop() { }; let (server_b, url_b) = make_server(handler_b); - *url_b_for_a.lock() = Some(url_b.clone()); + *url_b_for_a.lock() = Some(url_b.url()); let request = RequestBuilder::new(None, url_a.clone(), Referrer::NoReferrer) .method(Method::GET) @@ -1264,7 +1265,7 @@ fn test_load_succeeds_with_a_redirect_loop() { let _ = server_b.close(); let response = response.to_actual(); - assert_eq!(response.url_list, [url_a.clone(), url_b, url_a]); + assert_eq!(response.url_list, [url_a.url(), url_b.url(), url_a.url()]); assert_eq!( *response.body.lock(), ResponseBody::Done(b"Success".to_vec()) @@ -1380,14 +1381,18 @@ fn test_redirect_from_x_to_y_provides_y_cookies_from_y() { cookie_jar.push(cookie_y, &url_y, CookieSource::HTTP); } - let request = RequestBuilder::new(None, url_x.clone(), Referrer::NoReferrer) - .method(Method::GET) - .destination(Destination::Document) - .origin(mock_origin()) - .pipeline_id(Some(TEST_PIPELINE_ID)) - .credentials_mode(CredentialsMode::Include) - .policy_container(Default::default()) - .build(); + let request = RequestBuilder::new( + None, + UrlWithBlobClaim::new(url_x.clone(), None), + Referrer::NoReferrer, + ) + .method(Method::GET) + .destination(Destination::Document) + .origin(mock_origin()) + .pipeline_id(Some(TEST_PIPELINE_ID)) + .credentials_mode(CredentialsMode::Include) + .policy_container(Default::default()) + .build(); let response = fetch_with_context(request, &mut context); @@ -1432,14 +1437,18 @@ fn test_redirect_from_x_to_x_provides_x_with_cookie_from_first_response() { let url = url.join("/initial/").unwrap(); - let request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer) - .method(Method::GET) - .destination(Destination::Document) - .origin(mock_origin()) - .pipeline_id(Some(TEST_PIPELINE_ID)) - .credentials_mode(CredentialsMode::Include) - .policy_container(Default::default()) - .build(); + let request = RequestBuilder::new( + None, + UrlWithBlobClaim::new(url.clone(), None), + Referrer::NoReferrer, + ) + .method(Method::GET) + .destination(Destination::Document) + .origin(mock_origin()) + .pipeline_id(Some(TEST_PIPELINE_ID)) + .credentials_mode(CredentialsMode::Include) + .policy_container(Default::default()) + .build(); let response = fetch(request, None); diff --git a/components/net/tests/main.rs b/components/net/tests/main.rs index 40f60cb158e..14d77ef8977 100644 --- a/components/net/tests/main.rs +++ b/components/net/tests/main.rs @@ -39,6 +39,7 @@ use net::test::HttpState; use net::test_util::{ create_generic_embedder_proxy, make_body, make_server, make_ssl_server, replace_host_table, }; +use net_traits::blob_url_store::BlobTokenCommunicator; use net_traits::filemanager_thread::FileTokenCheck; use net_traits::request::Request; use net_traits::response::Response; @@ -135,7 +136,7 @@ fn new_fetch_context( state: Arc::new(create_http_state(Some(sender.clone()))), user_agent: DEFAULT_USER_AGENT.into(), devtools_chan, - filemanager: FileManager::new(sender.clone()), + filemanager: FileManager::new(sender.clone(), BlobTokenCommunicator::stub_for_testing()), file_token: FileTokenCheck::NotRequired, request_interceptor: Arc::new(TokioMutex::new(RequestInterceptor::new(sender))), cancellation_listener: Arc::new(Default::default()), diff --git a/components/script/dom/html/htmllinkelement.rs b/components/script/dom/html/htmllinkelement.rs index 3895241b8a8..99ca17c9f62 100644 --- a/components/script/dom/html/htmllinkelement.rs +++ b/components/script/dom/html/htmllinkelement.rs @@ -639,7 +639,7 @@ impl HTMLLinkElement { // Step 5. If request is null, then return. return; }; - let url = request.url.clone(); + let url = request.url.url(); // Step 6. Set request's initiator to "prefetch". let request = request.initiator(Initiator::Prefetch); diff --git a/components/script/dom/html/htmlvideoelement.rs b/components/script/dom/html/htmlvideoelement.rs index 5922718090d..8469a26008d 100644 --- a/components/script/dom/html/htmlvideoelement.rs +++ b/components/script/dom/html/htmlvideoelement.rs @@ -10,6 +10,7 @@ use euclid::default::Size2D; use html5ever::{LocalName, Prefix, local_name, ns}; use js::rust::HandleObject; use layout_api::{HTMLMediaData, MediaMetadata}; +use net_traits::blob_url_store::UrlWithBlobClaim; use net_traits::image_cache::{ ImageCache, ImageCacheResult, ImageLoadListener, ImageOrMetadataAvailable, ImageResponse, PendingImageId, @@ -232,7 +233,7 @@ impl HTMLVideoElement { let global = self.owner_global(); let request = RequestBuilder::new( Some(document.webview_id()), - poster_url.clone(), + UrlWithBlobClaim::from_url_without_having_claimed_blob(poster_url.clone()), global.get_referrer(), ) .destination(Destination::Image) diff --git a/components/script/dom/navigator.rs b/components/script/dom/navigator.rs index c4aabe0b005..715b7b35b70 100644 --- a/components/script/dom/navigator.rs +++ b/components/script/dom/navigator.rs @@ -13,6 +13,7 @@ use embedder_traits::{EmbedderMsg, ProtocolHandlerUpdateRegistration, RegisterOr use headers::HeaderMap; use http::header::{self, HeaderValue}; use js::rust::MutableHandleValue; +use net_traits::blob_url_store::UrlWithBlobClaim; use net_traits::request::{ CredentialsMode, Destination, RequestBuilder, RequestId, RequestMode, is_cors_safelisted_request_content_type, @@ -532,15 +533,19 @@ impl NavigatorMethods for Navigator { request_body = Some(extracted_body.into_net_request_body().0); } // Step 7.1. Let req be a new request, initialized as follows: - let request = RequestBuilder::new(None, url.clone(), global.get_referrer()) - .mode(cors_mode) - .destination(Destination::None) - .with_global_scope(&global) - .method(http::Method::POST) - .body(request_body) - .keep_alive(true) - .credentials_mode(CredentialsMode::Include) - .headers(headers); + let request = RequestBuilder::new( + None, + UrlWithBlobClaim::from_url_without_having_claimed_blob(url.clone()), + global.get_referrer(), + ) + .mode(cors_mode) + .destination(Destination::None) + .with_global_scope(&global) + .method(http::Method::POST) + .body(request_body) + .keep_alive(true) + .credentials_mode(CredentialsMode::Include) + .headers(headers); // Step 7.2. Fetch req. global.fetch( request, diff --git a/components/script/dom/notification.rs b/components/script/dom/notification.rs index 75dae350025..b02685f8a46 100644 --- a/components/script/dom/notification.rs +++ b/components/script/dom/notification.rs @@ -894,7 +894,7 @@ impl Notification { let request_id = request.id; let cache_result = global.image_cache().get_cached_image_status( - request.url.clone(), + request.url.url(), global.origin().immutable().clone(), None, // TODO: check which CORS should be used ); @@ -1021,7 +1021,7 @@ impl Notification { pending_image_id, image_cache: global.image_cache(), notification: Trusted::new(self), - url: request.url.clone(), + url: request.url.url(), status: Ok(()), }; diff --git a/components/script/dom/processingoptions.rs b/components/script/dom/processingoptions.rs index c006c52f3a4..1a82b75d0ff 100644 --- a/components/script/dom/processingoptions.rs +++ b/components/script/dom/processingoptions.rs @@ -306,7 +306,7 @@ impl LinkProcessingOptions { // Step 9. Let controller be null. // Step 10. Let reportTiming given a Document document be to report timing for controller // given document's relevant global object. - let url = request.url.clone(); + let url = request.url.url(); let fetch_context = LinkFetchContext { url, link, diff --git a/components/script/dom/request.rs b/components/script/dom/request.rs index 2b568f184f6..e133bbaaba0 100644 --- a/components/script/dom/request.rs +++ b/components/script/dom/request.rs @@ -12,6 +12,7 @@ use http::header::{HeaderName, HeaderValue}; use http::method::InvalidMethod; use js::rust::HandleObject; use net_traits::ReferrerPolicy as MsgReferrerPolicy; +use net_traits::blob_url_store::UrlWithBlobClaim; use net_traits::fetch::headers::is_forbidden_method; use net_traits::request::{ CacheMode as NetTraitsRequestCache, CredentialsMode as NetTraitsRequestCredentials, @@ -573,9 +574,13 @@ impl Request { } fn net_request_from_global(global: &GlobalScope, url: ServoUrl) -> NetTraitsRequest { - RequestBuilder::new(global.webview_id(), url, global.get_referrer()) - .with_global_scope(global) - .build() + RequestBuilder::new( + global.webview_id(), + UrlWithBlobClaim::from_url_without_having_claimed_blob(url), + global.get_referrer(), + ) + .with_global_scope(global) + .build() } /// diff --git a/components/script/dom/websocket.rs b/components/script/dom/websocket.rs index 6be6b3f3495..d180e1df48a 100644 --- a/components/script/dom/websocket.rs +++ b/components/script/dom/websocket.rs @@ -13,6 +13,7 @@ use js::jsval::UndefinedValue; use js::realm::AutoRealm; use js::rust::{CustomAutoRooterGuard, HandleObject}; use js::typedarray::{ArrayBuffer, ArrayBufferView, CreateWith}; +use net_traits::blob_url_store::UrlWithBlobClaim; use net_traits::request::{ CacheMode, CredentialsMode, RedirectMode, Referrer, RequestBuilder, RequestMode, ServiceWorkersMode, @@ -278,7 +279,7 @@ impl WebSocketMethods for WebSocket { // "include", cache mode is "no-store" , and redirect mode is "error" let request = RequestBuilder::new( global.webview_id(), - url_record.clone(), + UrlWithBlobClaim::from_url_without_having_claimed_blob(url_record.clone()), Referrer::NoReferrer, ) .with_global_scope(global) diff --git a/components/script/dom/workers/dedicatedworkerglobalscope.rs b/components/script/dom/workers/dedicatedworkerglobalscope.rs index 724cc7b0e2d..4c52e65fda6 100644 --- a/components/script/dom/workers/dedicatedworkerglobalscope.rs +++ b/components/script/dom/workers/dedicatedworkerglobalscope.rs @@ -14,6 +14,7 @@ use js::context::JSContext; use js::jsapi::{Heap, JSContext as RawJSContext, JSObject}; use js::jsval::UndefinedValue; use js::rust::{CustomAutoRooter, CustomAutoRooterGuard, HandleValue}; +use net_traits::blob_url_store::UrlWithBlobClaim; use net_traits::image_cache::ImageCache; use net_traits::policy_container::{PolicyContainer, RequestPolicyContainer}; use net_traits::request::{ @@ -359,7 +360,7 @@ impl DedicatedWorkerGlobalScope { pub(crate) fn run_worker_scope( mut init: WorkerGlobalScopeInit, webview_id: WebViewId, - worker_url: ServoUrl, + worker_url: UrlWithBlobClaim, from_devtools_receiver: GenericReceiver, worker: TrustedWorkerAddress, parent_event_loop_sender: ScriptEventLoopSender, @@ -474,7 +475,7 @@ impl DedicatedWorkerGlobalScope { webview_id, worker_name.into(), worker_type, - worker_url.clone(), + worker_url.url(), devtools_mpsc_port, runtime, parent_event_loop_sender, @@ -526,7 +527,7 @@ impl DedicatedWorkerGlobalScope { WorkerType::Module => { fetch_a_module_worker_script_graph( cx, - worker_url, + worker_url.url(), fetch_client, ModuleOwner::Worker(Trusted::new(scope)), referrer, @@ -804,14 +805,14 @@ pub(crate) unsafe extern "C" fn interrupt_callback(cx: *mut RawJSContext) -> boo /// fn fetch_a_classic_worker_script( workerscope: &WorkerGlobalScope, - url: ServoUrl, + url_with_blob_lock: UrlWithBlobClaim, fetch_client: ModuleFetchClient, destination: Destination, webview_id: WebViewId, referrer: Referrer, ) { // Step 1. Let request be a new request whose URL is url, - let request = RequestBuilder::new(Some(webview_id), url.clone(), referrer) + let request = RequestBuilder::new(Some(webview_id), url_with_blob_lock.clone(), referrer) // client is fetchClient, .insecure_requests_policy(fetch_client.insecure_requests_policy) .has_trustworthy_ancestor_origin(fetch_client.has_trustworthy_ancestor_origin) @@ -834,7 +835,7 @@ fn fetch_a_classic_worker_script( let context = ScriptFetchContext::new( Trusted::new(workerscope), - url, + url_with_blob_lock.url(), fetch_client.policy_container, ); let global = workerscope.upcast::(); diff --git a/components/script/dom/workers/serviceworkerglobalscope.rs b/components/script/dom/workers/serviceworkerglobalscope.rs index a0868680804..64e3d805cd0 100644 --- a/components/script/dom/workers/serviceworkerglobalscope.rs +++ b/components/script/dom/workers/serviceworkerglobalscope.rs @@ -14,6 +14,7 @@ use fonts::FontContext; use js::jsapi::{JS_AddInterruptCallback, JSContext}; use js::jsval::UndefinedValue; use net_traits::CustomResponseMediator; +use net_traits::blob_url_store::UrlWithBlobClaim; use net_traits::request::{ CredentialsMode, Destination, InsecureRequestsPolicy, ParserMetadata, Referrer, RequestBuilder, }; @@ -409,17 +410,21 @@ impl ServiceWorkerGlobalScope { .map(Referrer::ReferrerUrl) .unwrap_or_else(|| global_scope.get_referrer()); - let request = RequestBuilder::new(None, script_url, referrer) - .destination(Destination::ServiceWorker) - .credentials_mode(CredentialsMode::Include) - .parser_metadata(ParserMetadata::NotParserInserted) - .use_url_credentials(true) - .pipeline_id(Some(pipeline_id)) - .referrer_policy(referrer_policy) - .insecure_requests_policy(worker_scope.insecure_requests_policy()) - // TODO: Use policy container from ScopeThings - .policy_container(global_scope.policy_container()) - .origin(origin); + let request = RequestBuilder::new( + None, + UrlWithBlobClaim::from_url_without_having_claimed_blob(script_url), + referrer, + ) + .destination(Destination::ServiceWorker) + .credentials_mode(CredentialsMode::Include) + .parser_metadata(ParserMetadata::NotParserInserted) + .use_url_credentials(true) + .pipeline_id(Some(pipeline_id)) + .referrer_policy(referrer_policy) + .insecure_requests_policy(worker_scope.insecure_requests_policy()) + // TODO: Use policy container from ScopeThings + .policy_container(global_scope.policy_container()) + .origin(origin); let (url, source) = match load_whole_resource( request, diff --git a/components/script/dom/workers/worker.rs b/components/script/dom/workers/worker.rs index 3cd1337ca78..a5414c975a4 100644 --- a/components/script/dom/workers/worker.rs +++ b/components/script/dom/workers/worker.rs @@ -42,6 +42,7 @@ use crate::dom::workerglobalscope::prepare_workerscope_init; use crate::realms::enter_auto_realm; use crate::script_runtime::{CanGc, ThreadSafeJSContext}; use crate::task::TaskOnce; +use crate::url::ensure_blob_referenced_by_url_is_kept_alive; pub(crate) type TrustedWorkerAddress = Trusted; @@ -161,9 +162,9 @@ impl Worker { } impl WorkerMethods for Worker { - // https://html.spec.whatwg.org/multipage/#dom-worker + /// fn Constructor( - cx: &mut js::context::JSContext, + cx: &mut JSContext, global: &GlobalScope, proto: Option, script_url: TrustedScriptURLOrUSVString, @@ -178,10 +179,17 @@ impl WorkerMethods for Worker { script_url, "Worker constructor", )?; - // Step 2-4. - let worker_url = match global.encoding_parse_a_url(&compliant_script_url.str()) { - Ok(url) => url, - Err(_) => return Err(Error::Syntax(None)), + // Step 2. Let outsideSettings be this's relevant settings object. + // Step 3. Let workerURL be the result of encoding-parsing a URL given compliantScriptURL, + // relative to outsideSettings. + // TODO: Locking the URL should eventually happen inside encoding_parse_a_url, since most callers + // will expect their blobs to be kept alive... + let Ok(worker_url) = global + .encoding_parse_a_url(&compliant_script_url.str()) + .map(|url| ensure_blob_referenced_by_url_is_kept_alive(global, url)) + else { + // Step 4. If workerURL is failure, then throw a "SyntaxError" DOMException. + return Err(Error::Syntax(None)); }; let (sender, receiver) = unbounded(); @@ -221,11 +229,11 @@ impl WorkerMethods for Worker { let worker_id = WorkerId(Uuid::new_v4()); if let Some(chan) = global.devtools_chan() { let pipeline_id = global.pipeline_id(); - let title = format!("Worker for {}", worker_url); + let title = format!("Worker for {}", worker_url.url()); if let Some(browsing_context) = browsing_context { let page_info = DevtoolsPageInfo { title, - url: worker_url.clone(), + url: worker_url.url(), is_top_level_global: false, is_service_worker: false, }; diff --git a/components/script/dom/workers/workerglobalscope.rs b/components/script/dom/workers/workerglobalscope.rs index 8b29d40099a..798e547a0d4 100644 --- a/components/script/dom/workers/workerglobalscope.rs +++ b/components/script/dom/workers/workerglobalscope.rs @@ -18,6 +18,7 @@ use headers::{HeaderMapExt, ReferrerPolicy as ReferrerPolicyHeader}; use js::realm::CurrentRealm; use js::rust::{HandleValue, MutableHandleValue, ParentRuntime}; use mime::Mime; +use net_traits::blob_url_store::UrlWithBlobClaim; use net_traits::policy_container::PolicyContainer; use net_traits::request::{ CredentialsMode, Destination, InsecureRequestsPolicy, ParserMetadata, RequestBuilder, RequestId, @@ -696,7 +697,7 @@ impl WorkerGlobalScopeMethods for WorkerGlobalScope { let global_scope = self.upcast::(); let request = RequestBuilder::new( global_scope.webview_id(), - url.clone(), + UrlWithBlobClaim::from_url_without_having_claimed_blob(url.clone()), global_scope.get_referrer(), ) .destination(Destination::Script) diff --git a/components/script/dom/worklet.rs b/components/script/dom/worklet.rs index 8bf9f04aafc..3ed4b9325cc 100644 --- a/components/script/dom/worklet.rs +++ b/components/script/dom/worklet.rs @@ -23,6 +23,7 @@ use dom_struct::dom_struct; use js::jsapi::{GCReason, JSGCParamKey, JSTracer}; use js::rust::wrappers2::{JS_GC, JS_GetGCParameter}; use malloc_size_of::malloc_size_of_is_0; +use net_traits::blob_url_store::UrlWithBlobClaim; use net_traits::policy_container::PolicyContainer; use net_traits::request::{Destination, RequestBuilder, RequestMode}; use rustc_hash::FxHashMap; @@ -688,12 +689,16 @@ impl WorkletThread { // TODO: Caching. let global = global_scope.upcast::(); let resource_fetcher = self.global_init.resource_threads.sender(); - let request = RequestBuilder::new(None, script_url, global.get_referrer()) - .destination(Destination::Script) - .mode(RequestMode::CorsMode) - .credentials_mode(credentials.convert()) - .policy_container(policy_container) - .origin(origin); + let request = RequestBuilder::new( + None, + UrlWithBlobClaim::from_url_without_having_claimed_blob(script_url), + global.get_referrer(), + ) + .destination(Destination::Script) + .mode(RequestMode::CorsMode) + .credentials_mode(credentials.convert()) + .policy_container(policy_container) + .origin(origin); let script = load_whole_resource( request, diff --git a/components/script/dom/xmlhttprequest.rs b/components/script/dom/xmlhttprequest.rs index caf1e15c493..23d49595547 100644 --- a/components/script/dom/xmlhttprequest.rs +++ b/components/script/dom/xmlhttprequest.rs @@ -25,6 +25,7 @@ use js::jsval::{JSVal, NullValue}; use js::rust::wrappers::JS_ParseJSON; use js::rust::{HandleObject, MutableHandleValue}; use js::typedarray::{ArrayBufferU8, HeapArrayBuffer}; +use net_traits::blob_url_store::UrlWithBlobClaim; use net_traits::fetch::headers::extract_mime_type_as_dataurl_mime; use net_traits::http_status::HttpStatus; use net_traits::request::{CredentialsMode, Referrer, RequestBuilder, RequestId, RequestMode}; @@ -679,7 +680,9 @@ impl XMLHttpRequestMethods for XMLHttpRequest { let global = self.global(); let mut request = RequestBuilder::new( global.webview_id(), - self.request_url.borrow().clone().unwrap(), + UrlWithBlobClaim::from_url_without_having_claimed_blob( + self.request_url.borrow().clone().unwrap(), + ), self.referrer.clone(), ) .method(self.request_method.borrow().clone()) @@ -1585,7 +1588,7 @@ impl XMLHttpRequest { xhr, gen_id: self.generation_id.get(), sync_status: sync_status.clone(), - url: request_builder.url.clone(), + url: request_builder.url.url(), }; let (task_source, script_port) = if self.sync.get() { diff --git a/components/script/fetch.rs b/components/script/fetch.rs index de8f4a22399..43684dd706a 100644 --- a/components/script/fetch.rs +++ b/components/script/fetch.rs @@ -12,6 +12,7 @@ use js::jsval::UndefinedValue; use js::realm::CurrentRealm; use js::rust::HandleValue; use js::rust::wrappers::JS_SetPendingException; +use net_traits::blob_url_store::UrlWithBlobClaim; use net_traits::request::{ CorsSettings, CredentialsMode, Destination, Referrer, Request as NetTraitsRequest, RequestBuilder, RequestId, RequestMode, ServiceWorkersMode, @@ -143,31 +144,34 @@ pub(crate) struct FetchGroup { } fn request_init_from_request(request: NetTraitsRequest, global: &GlobalScope) -> RequestBuilder { - let mut builder = - RequestBuilder::new(request.target_webview_id, request.url(), request.referrer) - .method(request.method) - .headers(request.headers) - .unsafe_request(request.unsafe_request) - .body(request.body) - .destination(request.destination) - .synchronous(request.synchronous) - .mode(request.mode) - .cache_mode(request.cache_mode) - .use_cors_preflight(request.use_cors_preflight) - .credentials_mode(request.credentials_mode) - .use_url_credentials(request.use_url_credentials) - .referrer_policy(request.referrer_policy) - .pipeline_id(request.pipeline_id) - .redirect_mode(request.redirect_mode) - .integrity_metadata(request.integrity_metadata) - .cryptographic_nonce_metadata(request.cryptographic_nonce_metadata) - .parser_metadata(request.parser_metadata) - .initiator(request.initiator) - .client(global.request_client()) - .insecure_requests_policy(request.insecure_requests_policy) - .has_trustworthy_ancestor_origin(request.has_trustworthy_ancestor_origin) - .https_state(request.https_state) - .response_tainting(request.response_tainting); + let mut builder = RequestBuilder::new( + request.target_webview_id, + UrlWithBlobClaim::from_url_without_having_claimed_blob(request.url()), + request.referrer, + ) + .method(request.method) + .headers(request.headers) + .unsafe_request(request.unsafe_request) + .body(request.body) + .destination(request.destination) + .synchronous(request.synchronous) + .mode(request.mode) + .cache_mode(request.cache_mode) + .use_cors_preflight(request.use_cors_preflight) + .credentials_mode(request.credentials_mode) + .use_url_credentials(request.use_url_credentials) + .referrer_policy(request.referrer_policy) + .pipeline_id(request.pipeline_id) + .redirect_mode(request.redirect_mode) + .integrity_metadata(request.integrity_metadata) + .cryptographic_nonce_metadata(request.cryptographic_nonce_metadata) + .parser_metadata(request.parser_metadata) + .initiator(request.initiator) + .client(global.request_client()) + .insecure_requests_policy(request.insecure_requests_policy) + .has_trustworthy_ancestor_origin(request.has_trustworthy_ancestor_origin) + .https_state(request.https_state) + .response_tainting(request.response_tainting); builder.id = request.id; builder } @@ -276,7 +280,7 @@ pub(crate) fn Fetch( global: Trusted::new(global), locally_aborted: false, canceller: FetchCanceller::new(request_id, keep_alive, global.core_resource_thread()), - url: request_init.url.clone(), + url: request_init.url.url(), }; let network_listener = NetworkListener::new( fetch_context, @@ -741,7 +745,7 @@ pub(crate) fn load_whole_resource( ) -> Result<(Metadata, Vec, bool), NetworkError> { let request = request.https_state(global.get_https_state()); let (action_sender, action_receiver) = ipc::channel().unwrap(); - let url = request.url.clone(); + let url = request.url.url(); core_resource_thread .send(CoreResourceMsg::Fetch( request, @@ -805,22 +809,26 @@ pub(crate) fn create_a_potential_cors_request( same_origin_fallback: Option, referrer: Referrer, ) -> RequestBuilder { - RequestBuilder::new(webview_id, url, referrer) - // Step 1. Let mode be "no-cors" if corsAttributeState is No CORS, and "cors" otherwise. - .mode(match cors_setting { - Some(_) => RequestMode::CorsMode, - // Step 2. If same-origin fallback flag is set and mode is "no-cors", set mode to "same-origin". - None if same_origin_fallback == Some(true) => RequestMode::SameOrigin, - None => RequestMode::NoCors, - }) - .credentials_mode(match cors_setting { - // Step 4. If corsAttributeState is Anonymous, set credentialsMode to "same-origin". - Some(CorsSettings::Anonymous) => CredentialsMode::CredentialsSameOrigin, - // Step 3. Let credentialsMode be "include". - _ => CredentialsMode::Include, - }) - // Step 5. Return a new request whose URL is url, destination is destination, - // mode is mode, credentials mode is credentialsMode, and whose use-URL-credentials flag is set. - .destination(destination) - .use_url_credentials(true) + RequestBuilder::new( + webview_id, + UrlWithBlobClaim::from_url_without_having_claimed_blob(url), + referrer, + ) + // Step 1. Let mode be "no-cors" if corsAttributeState is No CORS, and "cors" otherwise. + .mode(match cors_setting { + Some(_) => RequestMode::CorsMode, + // Step 2. If same-origin fallback flag is set and mode is "no-cors", set mode to "same-origin". + None if same_origin_fallback == Some(true) => RequestMode::SameOrigin, + None => RequestMode::NoCors, + }) + .credentials_mode(match cors_setting { + // Step 4. If corsAttributeState is Anonymous, set credentialsMode to "same-origin". + Some(CorsSettings::Anonymous) => CredentialsMode::CredentialsSameOrigin, + // Step 3. Let credentialsMode be "include". + _ => CredentialsMode::Include, + }) + // Step 5. Return a new request whose URL is url, destination is destination, + // mode is mode, credentials mode is credentialsMode, and whose use-URL-credentials flag is set. + .destination(destination) + .use_url_credentials(true) } diff --git a/components/script/layout_image.rs b/components/script/layout_image.rs index 970b0cbf89d..facbc44dc64 100644 --- a/components/script/layout_image.rs +++ b/components/script/layout_image.rs @@ -8,6 +8,7 @@ use std::sync::Arc; +use net_traits::blob_url_store::UrlWithBlobClaim; use net_traits::image_cache::{ImageCache, PendingImageId}; use net_traits::request::{Destination, RequestBuilder, RequestId}; use net_traits::{FetchMetadata, FetchResponseMsg, NetworkError, ResourceFetchTiming}; @@ -102,9 +103,13 @@ pub(crate) fn fetch_image_for_layout( }; let global = node.owner_global(); - let request = RequestBuilder::new(Some(document.webview_id()), url, global.get_referrer()) - .destination(Destination::Image) - .with_global_scope(&global); + let request = RequestBuilder::new( + Some(document.webview_id()), + UrlWithBlobClaim::from_url_without_having_claimed_blob(url), + global.get_referrer(), + ) + .destination(Destination::Image) + .with_global_scope(&global); // Layout image loads do not delay the document load event. document.fetch_background(request, context); diff --git a/components/script/lib.rs b/components/script/lib.rs index 82e61808738..a570cfccc77 100644 --- a/components/script/lib.rs +++ b/components/script/lib.rs @@ -37,6 +37,7 @@ pub(crate) mod fetch; pub(crate) mod indexeddb; mod init; mod layout_image; +mod url; pub(crate) mod document_collection; pub(crate) mod iframe_collection; diff --git a/components/script/navigation.rs b/components/script/navigation.rs index bfd09380cf0..ab11a65252a 100644 --- a/components/script/navigation.rs +++ b/components/script/navigation.rs @@ -14,6 +14,7 @@ use embedder_traits::user_contents::UserContentManagerId; use embedder_traits::{Theme, ViewportDetails, WebDriverLoadStatus}; use http::header; use js::context::JSContext; +use net_traits::blob_url_store::UrlWithBlobClaim; use net_traits::policy_container::RequestPolicyContainer; use net_traits::request::{ CredentialsMode, InsecureRequestsPolicy, Origin, PreloadedResources, RedirectMode, @@ -236,7 +237,7 @@ impl InProgressLoad { let mut request_builder = RequestBuilder::new( Some(webview_id), - self.load_data.url.clone(), + UrlWithBlobClaim::from_url_without_having_claimed_blob(self.load_data.url.clone()), self.load_data.referrer.clone(), ) .method(self.load_data.method.clone()) diff --git a/components/script/script_module.rs b/components/script/script_module.rs index 810a5125759..e953459d8ac 100644 --- a/components/script/script_module.rs +++ b/components/script/script_module.rs @@ -41,6 +41,7 @@ use js::rust::{ transform_str_to_source_text, }; use mime::Mime; +use net_traits::blob_url_store::UrlWithBlobClaim; use net_traits::http_status::HttpStatus; use net_traits::mime_classifier::MimeClassifier; use net_traits::policy_container::PolicyContainer; @@ -1592,21 +1593,25 @@ pub(crate) fn fetch_a_single_module_script( // TODO Step 11. Set request's initiator type to "script". // Step 12. Set up the module script request given request and options. - let request = RequestBuilder::new(webview_id, url.clone(), referrer) - .destination(destination) - .parser_metadata(options.parser_metadata) - .integrity_metadata(options.integrity_metadata.clone()) - .credentials_mode(options.credentials_mode) - .referrer_policy(options.referrer_policy) - .mode(mode) - .cryptographic_nonce_metadata(options.cryptographic_nonce.clone()) - .insecure_requests_policy(fetch_client.insecure_requests_policy) - .has_trustworthy_ancestor_origin(fetch_client.has_trustworthy_ancestor_origin) - .policy_container(fetch_client.policy_container) - .client(fetch_client.client) - .pipeline_id(Some(fetch_client.pipeline_id)) - .origin(fetch_client.origin) - .https_state(fetch_client.https_state); + let request = RequestBuilder::new( + webview_id, + UrlWithBlobClaim::from_url_without_having_claimed_blob(url.clone()), + referrer, + ) + .destination(destination) + .parser_metadata(options.parser_metadata) + .integrity_metadata(options.integrity_metadata.clone()) + .credentials_mode(options.credentials_mode) + .referrer_policy(options.referrer_policy) + .mode(mode) + .cryptographic_nonce_metadata(options.cryptographic_nonce.clone()) + .insecure_requests_policy(fetch_client.insecure_requests_policy) + .has_trustworthy_ancestor_origin(fetch_client.has_trustworthy_ancestor_origin) + .policy_container(fetch_client.policy_container) + .client(fetch_client.client) + .pipeline_id(Some(fetch_client.pipeline_id)) + .origin(fetch_client.origin) + .https_state(fetch_client.https_state); let context = ModuleContext { owner, diff --git a/components/script/url.rs b/components/script/url.rs new file mode 100644 index 00000000000..5002cbd9776 --- /dev/null +++ b/components/script/url.rs @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use net_traits::blob_url_store::{BlobResolver, UrlWithBlobClaim}; +use servo_url::ServoUrl; + +use crate::dom::globalscope::GlobalScope; + +pub(crate) fn ensure_blob_referenced_by_url_is_kept_alive( + global: &GlobalScope, + url: ServoUrl, +) -> UrlWithBlobClaim { + match UrlWithBlobClaim::for_url(url) { + Ok(lock) => lock, + Err(url) => { + let token = BlobResolver { + origin: global.api_base_url().origin(), + resource_threads: global.resource_threads(), + } + .acquire_blob_token_for(&url); + + UrlWithBlobClaim::new(url, token) + }, + } +} diff --git a/components/servo/tests/network_manager.rs b/components/servo/tests/network_manager.rs index 3889252b5d6..4bbf44e752d 100644 --- a/components/servo/tests/network_manager.rs +++ b/components/servo/tests/network_manager.rs @@ -46,7 +46,7 @@ fn test_cache_entries() { let port = url.port().unwrap(); delegate.reset(); - webview.load(url.clone().into_url()); + webview.load(url.as_url().clone()); let delegate_clone = delegate.clone(); servo_test.spin(move || !delegate_clone.url_changed.get()); @@ -80,7 +80,7 @@ fn test_clear_cache() { let _webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone()) .delegate(delegate.clone()) - .url(url.into_url()) + .url(url.as_url().clone()) .build(); servo_test.spin(move || !delegate.url_changed.get()); diff --git a/components/servo/tests/site_data_manager.rs b/components/servo/tests/site_data_manager.rs index 0ee0040d445..7377d62dfef 100644 --- a/components/servo/tests/site_data_manager.rs +++ b/components/servo/tests/site_data_manager.rs @@ -15,6 +15,7 @@ use hyper::body::{Bytes, Incoming}; use hyper::{Request as HyperRequest, Response as HyperResponse}; use net::test_util::{Server, make_body, make_server, replace_host_table}; use net_traits::CookieSource; +use net_traits::blob_url_store::UrlWithBlobClaim; use servo::{JSValue, Servo, ServoUrl, SiteData, StorageType, WebView, WebViewBuilder}; use crate::common::{ServoTest, WebViewDelegateImpl, evaluate_javascript}; @@ -79,7 +80,7 @@ fn run_test_site_data_steps(webview_test: &WebViewTest, steps: &[TestSiteDataSte *response.body_mut() = make_body(MESSAGE.to_vec()); }; - let mut servers: Vec<(Server, ServoUrl)> = + let mut servers: Vec<(Server, UrlWithBlobClaim)> = (0..steps.len()).map(|_| make_server(handler)).collect(); servers.sort_by(|(_, a), (_, b)| a.cmp(b)); @@ -588,7 +589,7 @@ fn test_clear_cookies() { let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone()) .delegate(delegate.clone()) - .url(url.into_url()) + .url(url.as_url().clone()) .build(); servo_test.spin(move || !delegate.url_changed.get()); @@ -633,7 +634,7 @@ fn test_get_cookie() { let delegate = Rc::new(WebViewDelegateImpl::default()); let _webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone()) .delegate(delegate.clone()) - .url(url.clone().into_url()) + .url(url.url().into_url()) .build(); // Wait for LoadStatus::Complete to ensure the HTTP response and Set-Cookie header are processed. @@ -643,7 +644,7 @@ fn test_get_cookie() { let cookies = servo_test .servo() .site_data_manager() - .cookies_for_url(url.into_url(), CookieSource::NonHTTP); + .cookies_for_url(url.url().into_url(), CookieSource::NonHTTP); assert_eq!(cookies.len(), 1); assert_eq!(cookies[0].name(), "foo"); assert_eq!(cookies[0].value(), "bar"); @@ -666,7 +667,7 @@ fn test_set_cookie() { *response.body_mut() = make_body(b"

hi

".to_vec()); }; let (server, url) = make_server(handler); - let page_url = url.clone().into_url(); + let page_url = url.url().into_url(); let delegate = Rc::new(WebViewDelegateImpl::default()); let delegate_clone = delegate.clone(); diff --git a/components/servo/tests/user_content_manager.rs b/components/servo/tests/user_content_manager.rs index 35c38f28168..dcbaa485c95 100644 --- a/components/servo/tests/user_content_manager.rs +++ b/components/servo/tests/user_content_manager.rs @@ -47,7 +47,7 @@ fn test_user_content_manager_user_script() { let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone()) .user_content_manager(user_content_manager.clone()) - .url(url.into_url()) + .url(url.as_url().clone()) .build(); let result = evaluate_javascript(&servo_test, webview.clone(), "window.fromUserContentScript"); diff --git a/components/servo/tests/webview.rs b/components/servo/tests/webview.rs index b7e9068edf4..1eda0e5efbd 100644 --- a/components/servo/tests/webview.rs +++ b/components/servo/tests/webview.rs @@ -96,7 +96,7 @@ fn test_create_webview_http() { let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone()) .delegate(delegate.clone()) - .url(url.into_url()) + .url(url.as_url().clone()) .build(); servo_test.spin(move || !delegate.url_changed.get()); @@ -957,7 +957,7 @@ fn test_webview_load_with_headers() { let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone()) .delegate(delegate.clone()) - .url(url.clone().into_url()) + .url(url.url().into_url()) .build(); { @@ -965,7 +965,7 @@ fn test_webview_load_with_headers() { servo_test.spin(move || !delegate.url_changed.get()); } - let urlrequest = UrlRequest::new(url.into_url()).headers(headers); + let urlrequest = UrlRequest::new(url.url().into_url()).headers(headers); webview.load_request(urlrequest); { let request_count = request_count.clone(); diff --git a/components/shared/net/blob_url_store.rs b/components/shared/net/blob_url_store.rs index 2e408c5eac0..d345318a168 100644 --- a/components/shared/net/blob_url_store.rs +++ b/components/shared/net/blob_url_store.rs @@ -2,13 +2,24 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use std::fmt; +use std::ops::{Deref, DerefMut}; use std::str::FromStr; +use std::sync::Arc; +use malloc_size_of_derive::MallocSizeOf; +use parking_lot::Mutex; use serde::{Deserialize, Serialize}; +use servo_base::generic_channel::{self, GenericSend, GenericSender}; use servo_url::{ImmutableOrigin, ServoUrl}; use url::Url; use uuid::Uuid; +use crate::{ + BlobTokenRefreshRequest, BlobTokenRevocationRequest, CoreResourceMsg, FileManagerThreadMsg, + ResourceThreads, +}; + /// Errors returned to Blob URL Store request #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub enum BlobURLStoreError { @@ -39,6 +50,11 @@ pub struct BlobBuf { /// Parse URL as Blob URL scheme's definition /// /// +/// +/// FIXME: This function should never be used to obtain the origin of a blob url, because +/// it doesn't consider [blob URL entries]. +/// +/// [blob URL entries]: https://url.spec.whatwg.org/#concept-url-blob-entry pub fn parse_blob_url(url: &ServoUrl) -> Result<(Uuid, ImmutableOrigin), &'static str> { if url.query().is_some() { return Err("URL should not contain a query"); @@ -60,3 +76,261 @@ pub fn parse_blob_url(url: &ServoUrl) -> Result<(Uuid, ImmutableOrigin), &'stati Ok((id, origin)) } + +/// This type upholds the variant that if the URL is a valid `blob` URL, then it has +/// a token. Violating this invariant causes logic errors, but no unsafety. +#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)] +pub struct UrlWithBlobClaim { + url: ServoUrl, + token: Option, +} + +impl UrlWithBlobClaim { + pub fn new(url: ServoUrl, token: Option) -> Self { + debug_assert_eq!(parse_blob_url(&url).is_ok(), token.is_some()); + + Self { url, token } + } + + pub fn token(&self) -> Option<&BlobToken> { + self.token.as_ref().map(|guard| guard.token.as_ref()) + } + + pub fn blob_id(&self) -> Option { + self.token.as_ref().map(|guard| guard.token.file_id) + } + + pub fn origin(&self) -> ImmutableOrigin { + if let Some(guard) = self.token.as_ref() { + return guard.token.origin.clone(); + } + + self.url.origin() + } + + /// Constructs a [UrlWithBlobClaim] for URLs that are not `blob` URLs + /// (Such URLs don't need to claim anything). + /// + /// Returns an `Err` containing the original URL if it's a `blob` URL, + /// so it can be reused without cloning. + pub fn for_url(url: ServoUrl) -> Result { + if url.scheme() == "blob" { + return Err(url); + } + + Ok(Self { url, token: None }) + } + + /// This method should only exist temporarily, and all callers should either + /// claim the blob or guarantee that the URL is not a `blob` URL. + pub fn from_url_without_having_claimed_blob(url: ServoUrl) -> Self { + if url.scheme() == "blob" { + // See https://github.com/servo/servo/issues/25226 for more details + log::warn!( + "Creating blob URL without claiming its associated blob entry. This might cause race conditions if the URL is revoked." + ); + } + Self { url, token: None } + } + + pub fn url(&self) -> ServoUrl { + self.url.clone() + } +} + +impl Deref for UrlWithBlobClaim { + type Target = ServoUrl; + + fn deref(&self) -> &Self::Target { + &self.url + } +} + +impl DerefMut for UrlWithBlobClaim { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.url + } +} + +/// Guarantees that blob entries kept alive the contained token are not deallocated even +/// if this token is serialized, dropped, and then later deserialized (possibly in a different thread). +#[derive(Clone, Debug, MallocSizeOf)] +pub struct TokenSerializationGuard { + #[conditional_malloc_size_of] + token: Arc, +} + +impl serde::Serialize for TokenSerializationGuard { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut new_token = self.token.refresh(); + let result = new_token.serialize(serializer); + if result.is_ok() { + // This token belongs to whoever receives the serialized message, so don't free it. + new_token.disown(); + } + result + } +} + +impl<'a> serde::Deserialize<'a> for TokenSerializationGuard { + fn deserialize(de: D) -> Result>::Error> + where + D: serde::Deserializer<'a>, + { + struct TokenGuardVisitor; + + impl<'de> serde::de::Visitor<'de> for TokenGuardVisitor { + type Value = TokenSerializationGuard; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a TokenSerializationGuard") + } + + fn visit_newtype_struct( + self, + deserializer: D, + ) -> Result>::Error> + where + D: serde::Deserializer<'de>, + { + Ok(TokenSerializationGuard { + token: Arc::new(BlobToken::deserialize(deserializer)?), + }) + } + } + + de.deserialize_newtype_struct("TokenSerializationGuard", TokenGuardVisitor) + } +} + +#[derive(Clone, MallocSizeOf)] +pub struct BlobResolver<'a> { + pub origin: ImmutableOrigin, + pub resource_threads: &'a ResourceThreads, +} + +#[derive(Clone, Deserialize, MallocSizeOf, Serialize)] +/// A reference to a blob URL that will revoke the blob when dropped, +/// unless the `disown` method is invoked. +pub struct BlobToken { + pub token: Uuid, + pub file_id: Uuid, + pub disowned: bool, + pub origin: ImmutableOrigin, + // We need a mutex here because BlobTokens are shared among threads, and accessing + // `GenericSender` from different threads is not safe. + // + // We need a Arc because the Communicator is shared among different BlobTokens. + #[conditional_malloc_size_of] + pub communicator: Arc>, +} + +#[derive(Clone, Deserialize, MallocSizeOf, Serialize)] +pub struct BlobTokenCommunicator { + pub revoke_sender: GenericSender, + pub refresh_token_sender: GenericSender, +} + +impl BlobTokenCommunicator { + pub fn stub_for_testing() -> Arc> { + Arc::new(Mutex::new(Self { + revoke_sender: generic_channel::channel().unwrap().0, + refresh_token_sender: generic_channel::channel().unwrap().0, + })) + } +} + +impl BlobToken { + fn refresh(&self) -> Self { + let (new_token_sender, new_token_receiver) = generic_channel::channel().unwrap(); + let refresh_request = BlobTokenRefreshRequest { + blob_id: self.file_id, + new_token_sender, + }; + self.communicator + .lock() + .refresh_token_sender + .send(CoreResourceMsg::RefreshTokenForFile(refresh_request)) + .unwrap(); + let new_token = new_token_receiver.recv().unwrap(); + + BlobToken { + token: new_token, + file_id: self.file_id, + communicator: self.communicator.clone(), + disowned: false, + origin: self.origin.clone(), + } + } + + /// Prevents this token from revoking itself when it is dropped. + fn disown(&mut self) { + self.disowned = true; + } +} + +impl<'a> BlobResolver<'a> { + pub fn acquire_blob_token_for(&self, url: &ServoUrl) -> Option { + if url.scheme() != "blob" { + return None; + } + let (file_id, origin) = parse_blob_url(url) + .inspect_err(|error| log::warn!("Failed to acquire token for {url}: {error}")) + .ok()?; + let (sender, receiver) = generic_channel::channel().unwrap(); + self.resource_threads + .send(CoreResourceMsg::ToFileManager( + FileManagerThreadMsg::GetTokenForFile(file_id, origin, sender), + )) + .ok()?; + let reply = receiver.recv().ok()?; + reply.token.map(|token_id| { + let token = BlobToken { + token: token_id, + file_id, + communicator: Arc::new(Mutex::new(BlobTokenCommunicator { + revoke_sender: reply.revoke_sender, + refresh_token_sender: reply.refresh_sender, + })), + disowned: false, + origin: self.origin.clone(), + }; + + TokenSerializationGuard { + token: Arc::new(token), + } + }) + } +} + +impl Drop for BlobToken { + fn drop(&mut self) { + if self.disowned { + return; + } + + let revocation_request = BlobTokenRevocationRequest { + token: self.token, + blob_id: self.file_id, + }; + let _ = self + .communicator + .lock() + .revoke_sender + .send(CoreResourceMsg::RevokeTokenForFile(revocation_request)); + } +} + +impl fmt::Debug for BlobToken { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("BlobToken") + .field("token", &self.token) + .field("file_id", &self.file_id) + .field("disowned", &self.disowned) + .field("origin", &self.origin) + .finish() + } +} diff --git a/components/shared/net/filemanager_thread.rs b/components/shared/net/filemanager_thread.rs index 89a0de11f72..140b54ba3ac 100644 --- a/components/shared/net/filemanager_thread.rs +++ b/components/shared/net/filemanager_thread.rs @@ -15,10 +15,11 @@ use servo_base::generic_channel::GenericSender; use servo_url::ImmutableOrigin; use uuid::Uuid; +use crate::CoreResourceMsg; use crate::blob_url_store::{BlobBuf, BlobURLStoreError}; /// A token modulating access to a file for a blob URL. -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum FileTokenCheck { /// Checking against a token not required, /// used for accessing a file @@ -166,6 +167,16 @@ pub enum FileManagerThreadMsg { ImmutableOrigin, GenericSender>, ), + + GetTokenForFile(Uuid, ImmutableOrigin, GenericSender), + RevokeTokenForFile(Uuid, Uuid), +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct GetTokenForFileReply { + pub token: Option, + pub revoke_sender: GenericSender, + pub refresh_sender: GenericSender, } #[derive(Debug, Deserialize, Serialize)] diff --git a/components/shared/net/lib.rs b/components/shared/net/lib.rs index c836eefb504..8538e5281a9 100644 --- a/components/shared/net/lib.rs +++ b/components/shared/net/lib.rs @@ -33,6 +33,7 @@ use servo_base::generic_channel::{ }; use servo_base::id::{CookieStoreId, HistoryStateId, PipelineId}; use servo_url::{ImmutableOrigin, ServoUrl}; +use uuid::Uuid; use crate::fetch::headers::determine_nosniff; use crate::filemanager_thread::FileManagerThreadMsg; @@ -703,6 +704,20 @@ pub enum CoreResourceMsg { /// and exit Exit(GenericOneshotSender<()>), CollectMemoryReport(ReportsChan), + RevokeTokenForFile(BlobTokenRevocationRequest), + RefreshTokenForFile(BlobTokenRefreshRequest), +} + +#[derive(Debug, Deserialize, MallocSizeOf, Serialize)] +pub struct BlobTokenRevocationRequest { + pub blob_id: Uuid, + pub token: Uuid, +} + +#[derive(Debug, Deserialize, MallocSizeOf, Serialize)] +pub struct BlobTokenRefreshRequest { + pub blob_id: Uuid, + pub new_token_sender: GenericSender, } #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/components/shared/net/request.rs b/components/shared/net/request.rs index a9cb2f9ec0a..1777df7ddc3 100644 --- a/components/shared/net/request.rs +++ b/components/shared/net/request.rs @@ -22,6 +22,7 @@ use tokio::sync::oneshot::Sender as TokioSender; use url::Position; use uuid::Uuid; +use crate::blob_url_store::UrlWithBlobClaim; use crate::policy_container::{PolicyContainer, RequestPolicyContainer}; use crate::pub_domains::is_same_site; use crate::response::{HttpsState, RedirectTaint, Response}; @@ -149,7 +150,7 @@ pub struct PreloadKey { impl PreloadKey { pub fn new(request: &RequestBuilder) -> Self { Self { - url: request.url.clone(), + url: request.url.url(), destination: request.destination, mode: request.mode.clone(), credentials_mode: request.credentials_mode, @@ -429,7 +430,7 @@ pub struct RequestBuilder { pub method: Method, /// - pub url: ServoUrl, + pub url: UrlWithBlobClaim, /// #[serde( @@ -505,7 +506,11 @@ pub struct RequestBuilder { } impl RequestBuilder { - pub fn new(webview_id: Option, url: ServoUrl, referrer: Referrer) -> RequestBuilder { + pub fn new( + webview_id: Option, + url: UrlWithBlobClaim, + referrer: Referrer, + ) -> RequestBuilder { RequestBuilder { id: RequestId::default(), preload_id: None, @@ -744,7 +749,11 @@ impl RequestBuilder { request.cache_mode = self.cache_mode; request.referrer_policy = self.referrer_policy; request.redirect_mode = self.redirect_mode; - let mut url_list = self.url_list; + let mut url_list: Vec<_> = self + .url_list + .into_iter() + .map(UrlWithBlobClaim::from_url_without_having_claimed_blob) + .collect(); if url_list.is_empty() { url_list.push(self.url); } @@ -830,7 +839,7 @@ pub struct Request { // Use the last method on url_list to act as spec current url field, and // first method to act as spec url field /// - pub url_list: Vec, + pub url_list: Vec, /// pub redirect_count: u32, /// @@ -850,7 +859,7 @@ pub struct Request { impl Request { pub fn new( id: RequestId, - url: ServoUrl, + url: UrlWithBlobClaim, origin: Option, referrer: Referrer, pipeline_id: Option, @@ -899,7 +908,7 @@ impl Request { /// pub fn url(&self) -> ServoUrl { - self.url_list.first().unwrap().clone() + self.url_list.first().unwrap().url() } pub fn original_url(&self) -> ServoUrl { @@ -914,6 +923,11 @@ impl Request { /// pub fn current_url(&self) -> ServoUrl { + self.current_url_with_blob_claim().url() + } + + /// + pub fn current_url_with_blob_claim(&self) -> UrlWithBlobClaim { self.url_list.last().unwrap().clone() } diff --git a/tests/wpt/meta/workers/dedicated-worker-from-blob-url.window.js.ini b/tests/wpt/meta/workers/dedicated-worker-from-blob-url.window.js.ini deleted file mode 100644 index d0154d3e5d7..00000000000 --- a/tests/wpt/meta/workers/dedicated-worker-from-blob-url.window.js.ini +++ /dev/null @@ -1,3 +0,0 @@ -[dedicated-worker-from-blob-url.window.html] - [Creating a dedicated worker from a blob URL works immediately before revoking.] - expected: FAIL