| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/web_package/bundled_exchanges_handle.h" |
| |
| #include "base/bind.h" |
| #include "base/feature_list.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/sequence_checker.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/task/post_task.h" |
| #include "content/browser/loader/navigation_loader_interceptor.h" |
| #include "content/browser/loader/single_request_url_loader_factory.h" |
| #include "content/browser/web_package/bundled_exchanges_handle_tracker.h" |
| #include "content/browser/web_package/bundled_exchanges_navigation_info.h" |
| #include "content/browser/web_package/bundled_exchanges_reader.h" |
| #include "content/browser/web_package/bundled_exchanges_source.h" |
| #include "content/browser/web_package/bundled_exchanges_url_loader_factory.h" |
| #include "content/browser/web_package/bundled_exchanges_utils.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/download_utils.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/content_features.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "mojo/public/cpp/bindings/receiver.h" |
| #include "mojo/public/cpp/bindings/self_owned_receiver.h" |
| #include "net/http/http_util.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "services/data_decoder/public/mojom/bundled_exchanges_parser.mojom.h" |
| #include "services/network/public/mojom/url_loader.mojom.h" |
| #include "third_party/blink/public/common/loader/throttling_url_loader.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| using DoneCallback = base::OnceCallback<void( |
| const GURL& target_inner_url, |
| std::unique_ptr<BundledExchangesURLLoaderFactory> url_loader_factory)>; |
| |
| const net::NetworkTrafficAnnotationTag kTrafficAnnotation = |
| net::DefineNetworkTrafficAnnotation("bundled_exchanges_start_url_loader", |
| R"( |
| semantics { |
| sender: "BundledExchanges primary_url Loader" |
| description: |
| "Navigation request for the primary_url provided by a BundledExchanges " |
| "that is requested by the user. This does not trigger any network " |
| "transaction directly, but access to an entry in a local file, or in a " |
| "previously fetched resource over network." |
| trigger: "The user navigates to a BundledExchanges." |
| data: "Nothing." |
| destination: LOCAL |
| } |
| policy { |
| cookies_allowed: NO |
| setting: "These requests cannot be disabled in settings." |
| policy_exception_justification: |
| "Not implemented. This request does not make any network transaction." |
| } |
| comments: |
| "Usually the request accesses an entry in a local file that contains " |
| "multiple archived entries. But once the feature is exposed to the public " |
| "web API, the archive file can be streamed over network. In such case, the " |
| "streaming should be provided by another URLLoader request that is issued " |
| "by Blink, but based on a user initiated navigation." |
| )"); |
| |
| void AddErrorMessageToConsole(int frame_tree_node_id, |
| const std::string& error_message) { |
| WebContents* web_contents = |
| WebContents::FromFrameTreeNodeId(frame_tree_node_id); |
| if (!web_contents) |
| return; |
| web_contents->GetMainFrame()->AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, error_message); |
| } |
| |
| void AddMetadataParseErrorMessageToConsole( |
| int frame_tree_node_id, |
| const data_decoder::mojom::BundleMetadataParseErrorPtr& metadata_error) { |
| AddErrorMessageToConsole( |
| frame_tree_node_id, |
| std::string("Failed to read metadata of Web Bundle file: ") + |
| metadata_error->message); |
| } |
| |
| // A class to provide a network::mojom::URLLoader interface to redirect a |
| // request to the BundledExchanges to the main resource url. |
| class PrimaryURLRedirectLoader final : public network::mojom::URLLoader { |
| public: |
| PrimaryURLRedirectLoader( |
| mojo::PendingRemote<network::mojom::URLLoaderClient> client) |
| : client_(std::move(client)) {} |
| |
| void OnReadyToRedirect(const network::ResourceRequest& resource_request, |
| const GURL& url) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(client_.is_connected()); |
| network::ResourceResponseHead response_head; |
| response_head.encoded_data_length = 0; |
| response_head.headers = base::MakeRefCounted<net::HttpResponseHeaders>( |
| net::HttpUtil::AssembleRawHeaders( |
| base::StringPrintf("HTTP/1.1 %d %s\r\n", 303, "See Other"))); |
| |
| net::RedirectInfo redirect_info = net::RedirectInfo::ComputeRedirectInfo( |
| "GET", resource_request.url, resource_request.request_initiator, |
| resource_request.site_for_cookies, |
| resource_request.update_first_party_url_on_redirect |
| ? net::URLRequest::FirstPartyURLPolicy:: |
| UPDATE_FIRST_PARTY_URL_ON_REDIRECT |
| : net::URLRequest::FirstPartyURLPolicy:: |
| NEVER_CHANGE_FIRST_PARTY_URL, |
| resource_request.referrer_policy, resource_request.referrer.spec(), 303, |
| url, /*referrer_policy_header=*/base::nullopt, |
| /*insecure_scheme_was_upgraded=*/false, /*copy_fragment=*/true, |
| /*is_signed_exchange_fallback_redirect=*/false); |
| client_->OnReceiveRedirect(redirect_info, response_head); |
| } |
| |
| private: |
| // mojom::URLLoader overrides: |
| void FollowRedirect(const std::vector<std::string>& removed_headers, |
| const net::HttpRequestHeaders& modified_headers, |
| const base::Optional<GURL>& new_url) override {} |
| void SetPriority(net::RequestPriority priority, |
| int intra_priority_value) override {} |
| void PauseReadingBodyFromNet() override {} |
| void ResumeReadingBodyFromNet() override {} |
| |
| SEQUENCE_CHECKER(sequence_checker_); |
| |
| mojo::Remote<network::mojom::URLLoaderClient> client_; |
| |
| DISALLOW_COPY_AND_ASSIGN(PrimaryURLRedirectLoader); |
| }; |
| |
| // A class to inherit NavigationLoaderInterceptor for a navigation to a |
| // untrustable BundledExchanges file (eg: "file:///tmp/a.wbn"). |
| // The overridden methods of NavigationLoaderInterceptor are called in the |
| // following sequence: |
| // [1] MaybeCreateLoader() is called for all navigation requests. It calls the |
| // |callback| with a null RequestHandler. |
| // [2] MaybeCreateLoaderForResponse() is called for all navigation responses. |
| // If the response mime type is not "application/webbundle", returns false. |
| // Otherwise starts reading the metadata and returns true. Once the metadata |
| // is read, OnMetadataReady() is called, and a redirect loader is |
| // created to redirect the navigation request to the Bundle's synthesized |
| // primary URL ("file:///tmp/a.wbn?https://example.com/a.html"). |
| // [3] MaybeCreateLoader() is called again for the redirect. It continues on |
| // StartResponse() to create the loader for the main resource. |
| class InterceptorForFile final : public NavigationLoaderInterceptor { |
| public: |
| explicit InterceptorForFile(DoneCallback done_callback, |
| int frame_tree_node_id) |
| : done_callback_(std::move(done_callback)), |
| frame_tree_node_id_(frame_tree_node_id) {} |
| ~InterceptorForFile() override { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| } |
| |
| private: |
| // NavigationLoaderInterceptor implementation |
| void MaybeCreateLoader( |
| const network::ResourceRequest& tentative_resource_request, |
| BrowserContext* browser_context, |
| LoaderCallback callback, |
| FallbackCallback fallback_callback) override { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // InterceptorForFile::MaybeCreateLoader() creates a loader only after |
| // recognising that the response is a bundled exchange file at |
| // MaybeCreateLoaderForResponse() and successfully created |
| // |url_loader_factory_|. |
| if (!url_loader_factory_) { |
| std::move(callback).Run({}); |
| return; |
| } |
| std::move(callback).Run( |
| base::MakeRefCounted<SingleRequestURLLoaderFactory>(base::BindOnce( |
| &InterceptorForFile::StartResponse, weak_factory_.GetWeakPtr()))); |
| } |
| |
| bool MaybeCreateLoaderForResponse( |
| const network::ResourceRequest& request, |
| const network::ResourceResponseHead& response_head, |
| mojo::ScopedDataPipeConsumerHandle* response_body, |
| network::mojom::URLLoaderPtr* loader, |
| mojo::PendingReceiver<network::mojom::URLLoaderClient>* client_receiver, |
| blink::ThrottlingURLLoader* url_loader, |
| bool* skip_other_interceptors, |
| bool* will_return_unsafe_redirect) override { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(bundled_exchanges_utils::IsSupprtedFileScheme(request.url)); |
| if (response_head.mime_type != |
| bundled_exchanges_utils:: |
| kBundledExchangesFileMimeTypeWithoutParameters) { |
| return false; |
| } |
| std::unique_ptr<BundledExchangesSource> source = |
| BundledExchangesSource::MaybeCreateFromFileUrl(request.url); |
| if (!source) |
| return false; |
| reader_ = base::MakeRefCounted<BundledExchangesReader>(std::move(source)); |
| reader_->ReadMetadata(base::BindOnce(&InterceptorForFile::OnMetadataReady, |
| weak_factory_.GetWeakPtr(), request)); |
| *client_receiver = forwarding_client_.BindNewPipeAndPassReceiver(); |
| *will_return_unsafe_redirect = true; |
| return true; |
| } |
| |
| void OnMetadataReady( |
| network::ResourceRequest request, |
| data_decoder::mojom::BundleMetadataParseErrorPtr metadata_error) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (metadata_error) { |
| AddMetadataParseErrorMessageToConsole(frame_tree_node_id_, |
| metadata_error); |
| std::move(forwarding_client_) |
| ->OnComplete(network::URLLoaderCompletionStatus( |
| net::ERR_INVALID_BUNDLED_EXCHANGES)); |
| return; |
| } |
| DCHECK(reader_); |
| primary_url_ = reader_->GetPrimaryURL(); |
| url_loader_factory_ = std::make_unique<BundledExchangesURLLoaderFactory>( |
| std::move(reader_), frame_tree_node_id_); |
| |
| const GURL new_url = |
| bundled_exchanges_utils::GetSynthesizedUrlForBundledExchanges( |
| request.url, primary_url_); |
| auto redirect_loader = |
| std::make_unique<PrimaryURLRedirectLoader>(forwarding_client_.Unbind()); |
| redirect_loader->OnReadyToRedirect(request, new_url); |
| } |
| |
| void StartResponse( |
| const network::ResourceRequest& resource_request, |
| mojo::PendingReceiver<network::mojom::URLLoader> receiver, |
| mojo::PendingRemote<network::mojom::URLLoaderClient> client) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| network::ResourceRequest new_resource_request = resource_request; |
| new_resource_request.url = primary_url_; |
| url_loader_factory_->CreateLoaderAndStart( |
| std::move(receiver), /*routing_id=*/0, /*request_id=*/0, /*options=*/0, |
| new_resource_request, std::move(client), |
| net::MutableNetworkTrafficAnnotationTag(kTrafficAnnotation)); |
| std::move(done_callback_).Run(primary_url_, std::move(url_loader_factory_)); |
| } |
| |
| DoneCallback done_callback_; |
| const int frame_tree_node_id_; |
| scoped_refptr<BundledExchangesReader> reader_; |
| GURL primary_url_; |
| std::unique_ptr<BundledExchangesURLLoaderFactory> url_loader_factory_; |
| |
| mojo::Remote<network::mojom::URLLoaderClient> forwarding_client_; |
| |
| SEQUENCE_CHECKER(sequence_checker_); |
| |
| base::WeakPtrFactory<InterceptorForFile> weak_factory_{this}; |
| |
| DISALLOW_COPY_AND_ASSIGN(InterceptorForFile); |
| }; |
| |
| // A class to inherit NavigationLoaderInterceptor for a navigation to a |
| // trustable BundledExchanges file (eg: "file:///tmp/a.wbn"). |
| // The overridden methods of NavigationLoaderInterceptor are called in the |
| // following sequence: |
| // [1] MaybeCreateLoader() is called for the navigation request to the trustable |
| // BundledExchanges file. It continues on CreateURLLoader() to create the |
| // loader for the main resource. |
| // - If OnMetadataReady() has not been called yet: |
| // Wait for OnMetadataReady() to be called. |
| // - If OnMetadataReady() was called with an error: |
| // Completes the request with ERR_INVALID_BUNDLED_EXCHANGES. |
| // - If OnMetadataReady() was called whthout errors: |
| // A redirect loader is created to redirect the navigation request to |
| // the Bundle's primary URL ("https://example.com/a.html"). |
| // [2] MaybeCreateLoader() is called again for the redirect. It continues on |
| // CreateURLLoader() to create the loader for the main resource. |
| class InterceptorForTrustableFile final : public NavigationLoaderInterceptor { |
| public: |
| InterceptorForTrustableFile(std::unique_ptr<BundledExchangesSource> source, |
| DoneCallback done_callback, |
| int frame_tree_node_id) |
| : source_(std::move(source)), |
| reader_(base::MakeRefCounted<BundledExchangesReader>(source_->Clone())), |
| done_callback_(std::move(done_callback)), |
| frame_tree_node_id_(frame_tree_node_id) { |
| reader_->ReadMetadata( |
| base::BindOnce(&InterceptorForTrustableFile::OnMetadataReady, |
| weak_factory_.GetWeakPtr())); |
| } |
| ~InterceptorForTrustableFile() override { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| } |
| |
| private: |
| // NavigationLoaderInterceptor implementation |
| void MaybeCreateLoader(const network::ResourceRequest& resource_request, |
| BrowserContext* browser_context, |
| LoaderCallback callback, |
| FallbackCallback fallback_callback) override { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| std::move(callback).Run(base::MakeRefCounted<SingleRequestURLLoaderFactory>( |
| base::BindOnce(&InterceptorForTrustableFile::CreateURLLoader, |
| weak_factory_.GetWeakPtr()))); |
| } |
| |
| void CreateURLLoader( |
| const network::ResourceRequest& resource_request, |
| mojo::PendingReceiver<network::mojom::URLLoader> receiver, |
| mojo::PendingRemote<network::mojom::URLLoaderClient> client) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (metadata_error_) { |
| mojo::Remote<network::mojom::URLLoaderClient>(std::move(client)) |
| ->OnComplete(network::URLLoaderCompletionStatus( |
| net::ERR_INVALID_BUNDLED_EXCHANGES)); |
| return; |
| } |
| |
| if (!url_loader_factory_) { |
| // This must be the first request to the bundled exchange file. |
| DCHECK_EQ(source_->url(), resource_request.url); |
| pending_resource_request_ = resource_request; |
| pending_receiver_ = std::move(receiver); |
| pending_client_ = std::move(client); |
| return; |
| } |
| |
| // Currently |source_| must be a local file. And the bundle's primary URL |
| // can't be a local file URL. So while handling redirected request to the |
| // primary URL, |resource_request.url| must not be same as the |source_|'s |
| // URL. |
| if (source_->url() != resource_request.url) { |
| url_loader_factory_->CreateLoaderAndStart( |
| std::move(receiver), /*routing_id=*/0, /*request_id=*/0, |
| /*options=*/0, resource_request, std::move(client), |
| net::MutableNetworkTrafficAnnotationTag(kTrafficAnnotation)); |
| std::move(done_callback_) |
| .Run(resource_request.url, std::move(url_loader_factory_)); |
| return; |
| } |
| |
| auto redirect_loader = |
| std::make_unique<PrimaryURLRedirectLoader>(std::move(client)); |
| redirect_loader->OnReadyToRedirect(resource_request, primary_url_); |
| mojo::MakeSelfOwnedReceiver( |
| std::move(redirect_loader), |
| mojo::PendingReceiver<network::mojom::URLLoader>(std::move(receiver))); |
| } |
| |
| void OnMetadataReady(data_decoder::mojom::BundleMetadataParseErrorPtr error) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(!url_loader_factory_); |
| |
| if (error) { |
| AddMetadataParseErrorMessageToConsole(frame_tree_node_id_, error); |
| metadata_error_ = std::move(error); |
| } else { |
| primary_url_ = reader_->GetPrimaryURL(); |
| url_loader_factory_ = std::make_unique<BundledExchangesURLLoaderFactory>( |
| std::move(reader_), frame_tree_node_id_); |
| } |
| |
| if (pending_receiver_) { |
| DCHECK(pending_client_); |
| CreateURLLoader(pending_resource_request_, std::move(pending_receiver_), |
| std::move(pending_client_)); |
| } |
| } |
| |
| std::unique_ptr<BundledExchangesSource> source_; |
| scoped_refptr<BundledExchangesReader> reader_; |
| DoneCallback done_callback_; |
| const int frame_tree_node_id_; |
| |
| network::ResourceRequest pending_resource_request_; |
| mojo::PendingReceiver<network::mojom::URLLoader> pending_receiver_; |
| mojo::PendingRemote<network::mojom::URLLoaderClient> pending_client_; |
| |
| std::unique_ptr<BundledExchangesURLLoaderFactory> url_loader_factory_; |
| |
| GURL primary_url_; |
| data_decoder::mojom::BundleMetadataParseErrorPtr metadata_error_; |
| |
| SEQUENCE_CHECKER(sequence_checker_); |
| |
| base::WeakPtrFactory<InterceptorForTrustableFile> weak_factory_{this}; |
| |
| DISALLOW_COPY_AND_ASSIGN(InterceptorForTrustableFile); |
| }; |
| |
| // A class to inherit NavigationLoaderInterceptor for a navigation to a |
| // BundledExchanges file on HTTPS server (eg: "https://example.com/a.wbn"). |
| // The overridden methods of NavigationLoaderInterceptor are called in the |
| // following sequence: |
| // [1] MaybeCreateLoader() is called for all navigation requests. It calls the |
| // |callback| with a null RequestHandler. |
| // [2] MaybeCreateLoaderForResponse() is called for all navigation responses. |
| // - If the response mime type is not "application/webbundle", or attachment |
| // Content-Disposition header is set, returns false. |
| // - If the URL isn't HTTPS nor localhost HTTP, or the Content-Length header |
| // is not a positive value, completes the requests with |
| // ERR_INVALID_BUNDLED_EXCHANGES and returns true. |
| // - Otherwise starts reading the metadata and returns true. Once the |
| // metadata is read, OnMetadataReady() is called, and a redirect loader is |
| // created to redirect the navigation request to the Bundle's primary URL |
| // ("https://example.com/a.html"). |
| // [3] MaybeCreateLoader() is called again for the redirect. It continues on |
| // StartResponse() to create the loader for the main resource. |
| class InterceptorForNetwork final : public NavigationLoaderInterceptor { |
| public: |
| InterceptorForNetwork(DoneCallback done_callback, |
| BrowserContext* browser_context, |
| int frame_tree_node_id) |
| : done_callback_(std::move(done_callback)), |
| browser_context_(browser_context), |
| frame_tree_node_id_(frame_tree_node_id) { |
| DCHECK(browser_context_); |
| } |
| ~InterceptorForNetwork() override { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| } |
| |
| private: |
| // NavigationLoaderInterceptor implementation |
| void MaybeCreateLoader( |
| const network::ResourceRequest& tentative_resource_request, |
| BrowserContext* browser_context, |
| LoaderCallback callback, |
| FallbackCallback fallback_callback) override { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!reader_) { |
| std::move(callback).Run({}); |
| return; |
| } |
| std::move(callback).Run(base::MakeRefCounted<SingleRequestURLLoaderFactory>( |
| base::BindOnce(&InterceptorForNetwork::StartResponse, |
| weak_factory_.GetWeakPtr()))); |
| } |
| |
| bool MaybeCreateLoaderForResponse( |
| const network::ResourceRequest& request, |
| const network::ResourceResponseHead& response_head, |
| mojo::ScopedDataPipeConsumerHandle* response_body, |
| network::mojom::URLLoaderPtr* loader, |
| mojo::PendingReceiver<network::mojom::URLLoaderClient>* client_receiver, |
| blink::ThrottlingURLLoader* url_loader, |
| bool* skip_other_interceptors, |
| bool* will_return_and_handle_unsafe_redirect) override { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (response_head.mime_type != |
| bundled_exchanges_utils:: |
| kBundledExchangesFileMimeTypeWithoutParameters) { |
| return false; |
| } |
| if (download_utils::MustDownload(request.url, response_head.headers.get(), |
| response_head.mime_type)) { |
| return false; |
| } |
| |
| auto source = |
| BundledExchangesSource::MaybeCreateFromNetworkUrl(request.url); |
| if (!source) { |
| AddErrorMessageToConsole( |
| frame_tree_node_id_, |
| "Web Bundle response must be served from HTTPS or localhost HTTP."); |
| *client_receiver = forwarding_client_.BindNewPipeAndPassReceiver(); |
| std::move(forwarding_client_) |
| ->OnComplete(network::URLLoaderCompletionStatus( |
| net::ERR_INVALID_BUNDLED_EXCHANGES)); |
| return true; |
| } |
| |
| if (response_head.content_length <= 0) { |
| AddErrorMessageToConsole( |
| frame_tree_node_id_, |
| "Web Bundle response must have valid Content-Length header."); |
| *client_receiver = forwarding_client_.BindNewPipeAndPassReceiver(); |
| std::move(forwarding_client_) |
| ->OnComplete(network::URLLoaderCompletionStatus( |
| net::ERR_INVALID_BUNDLED_EXCHANGES)); |
| return true; |
| } |
| |
| // TODO(crbug.com/1018640): Check the special HTTP response header if we |
| // decided to require one for WBN navigation. |
| |
| reader_ = base::MakeRefCounted<BundledExchangesReader>( |
| std::move(source), response_head.content_length, |
| std::move(*response_body), url_loader->Unbind(), |
| BrowserContext::GetBlobStorageContext(browser_context_)); |
| reader_->ReadMetadata( |
| base::BindOnce(&InterceptorForNetwork::OnMetadataReady, |
| weak_factory_.GetWeakPtr(), request)); |
| *client_receiver = forwarding_client_.BindNewPipeAndPassReceiver(); |
| return true; |
| } |
| |
| void OnMetadataReady(network::ResourceRequest request, |
| data_decoder::mojom::BundleMetadataParseErrorPtr error) { |
| if (error) { |
| AddMetadataParseErrorMessageToConsole(frame_tree_node_id_, error); |
| std::move(forwarding_client_) |
| ->OnComplete(network::URLLoaderCompletionStatus( |
| net::ERR_INVALID_BUNDLED_EXCHANGES)); |
| return; |
| } |
| primary_url_ = reader_->GetPrimaryURL(); |
| if (!reader_->HasEntry(primary_url_)) { |
| AddErrorMessageToConsole( |
| frame_tree_node_id_, |
| "The primary URL resource is not found in the web bundle."); |
| std::move(forwarding_client_) |
| ->OnComplete(network::URLLoaderCompletionStatus( |
| net::ERR_INVALID_BUNDLED_EXCHANGES)); |
| return; |
| } |
| |
| // TODO(crbug.com/1018640): Check the scope to see if it's allowed to load |
| // the URL. |
| if (primary_url_.GetOrigin() != reader_->source().url().GetOrigin()) { |
| AddErrorMessageToConsole(frame_tree_node_id_, |
| "The origin of primary URL doesn't match with " |
| "the origin of the web bundle."); |
| std::move(forwarding_client_) |
| ->OnComplete(network::URLLoaderCompletionStatus( |
| net::ERR_INVALID_BUNDLED_EXCHANGES)); |
| return; |
| } |
| url_loader_factory_ = std::make_unique<BundledExchangesURLLoaderFactory>( |
| reader_, frame_tree_node_id_); |
| auto redirect_loader = |
| std::make_unique<PrimaryURLRedirectLoader>(forwarding_client_.Unbind()); |
| redirect_loader->OnReadyToRedirect(request, primary_url_); |
| } |
| |
| void StartResponse( |
| const network::ResourceRequest& resource_request, |
| mojo::PendingReceiver<network::mojom::URLLoader> receiver, |
| mojo::PendingRemote<network::mojom::URLLoaderClient> client) { |
| network::ResourceRequest new_resource_request = resource_request; |
| new_resource_request.url = primary_url_; |
| url_loader_factory_->CreateLoaderAndStart( |
| std::move(receiver), 0, 0, 0, new_resource_request, std::move(client), |
| net::MutableNetworkTrafficAnnotationTag(kTrafficAnnotation)); |
| std::move(done_callback_).Run(primary_url_, std::move(url_loader_factory_)); |
| } |
| |
| DoneCallback done_callback_; |
| BrowserContext* browser_context_; |
| const int frame_tree_node_id_; |
| scoped_refptr<BundledExchangesReader> reader_; |
| GURL primary_url_; |
| std::unique_ptr<BundledExchangesURLLoaderFactory> url_loader_factory_; |
| |
| mojo::Remote<network::mojom::URLLoaderClient> forwarding_client_; |
| |
| SEQUENCE_CHECKER(sequence_checker_); |
| |
| base::WeakPtrFactory<InterceptorForNetwork> weak_factory_{this}; |
| |
| DISALLOW_COPY_AND_ASSIGN(InterceptorForNetwork); |
| }; |
| |
| // A class to inherit NavigationLoaderInterceptor for a navigation within a |
| // trustable BundledExchanges file. |
| // For example: |
| // A user opened "file:///tmp/a.wbn", and InterceptorForTrustableFile |
| // redirected to "https://example.com/a.html" and "a.html" in "a.wbn" was |
| // loaded. And the user clicked a link to "https://example.com/b.html" from |
| // "a.html". |
| // In this case, this interceptor intecepts the navigation request to "b.html", |
| // and creates a URLLoader using the BundledExchangesURLLoaderFactory to load |
| // the response of "b.html" in "a.wbn". |
| class InterceptorForTrackedNavigationFromTrustableFile final |
| : public NavigationLoaderInterceptor { |
| public: |
| InterceptorForTrackedNavigationFromTrustableFile( |
| scoped_refptr<BundledExchangesReader> reader, |
| DoneCallback done_callback, |
| int frame_tree_node_id) |
| : url_loader_factory_(std::make_unique<BundledExchangesURLLoaderFactory>( |
| std::move(reader), |
| frame_tree_node_id)), |
| done_callback_(std::move(done_callback)) {} |
| ~InterceptorForTrackedNavigationFromTrustableFile() override { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| } |
| |
| private: |
| // NavigationLoaderInterceptor implementation |
| void MaybeCreateLoader(const network::ResourceRequest& resource_request, |
| BrowserContext* browser_context, |
| LoaderCallback callback, |
| FallbackCallback fallback_callback) override { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(url_loader_factory_->reader()->HasEntry(resource_request.url)); |
| std::move(callback).Run( |
| base::MakeRefCounted<SingleRequestURLLoaderFactory>(base::BindOnce( |
| &InterceptorForTrackedNavigationFromTrustableFile::CreateURLLoader, |
| weak_factory_.GetWeakPtr()))); |
| } |
| |
| void CreateURLLoader( |
| const network::ResourceRequest& resource_request, |
| mojo::PendingReceiver<network::mojom::URLLoader> receiver, |
| mojo::PendingRemote<network::mojom::URLLoaderClient> client) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| url_loader_factory_->CreateLoaderAndStart( |
| std::move(receiver), /*routing_id=*/0, /*request_id=*/0, /*options=*/0, |
| resource_request, std::move(client), |
| net::MutableNetworkTrafficAnnotationTag(kTrafficAnnotation)); |
| std::move(done_callback_) |
| .Run(resource_request.url, std::move(url_loader_factory_)); |
| } |
| |
| std::unique_ptr<BundledExchangesURLLoaderFactory> url_loader_factory_; |
| DoneCallback done_callback_; |
| |
| SEQUENCE_CHECKER(sequence_checker_); |
| |
| base::WeakPtrFactory<InterceptorForTrackedNavigationFromTrustableFile> |
| weak_factory_{this}; |
| |
| DISALLOW_COPY_AND_ASSIGN(InterceptorForTrackedNavigationFromTrustableFile); |
| }; |
| |
| // A class to inherit NavigationLoaderInterceptor for a navigation within a |
| // BundledExchanges file. |
| // For example: |
| // A user opened "file:///tmp/a.wbn", and InterceptorForFile redirected to |
| // "file:///tmp/a.wbn?https://example.com/a.html" and "a.html" in "a.wbn" was |
| // loaded. And the user clicked a link to "https://example.com/b.html" from |
| // "a.html". |
| // In this case, this interceptor intecepts the navigation request to "b.html", |
| // and redirect the navigation request to |
| // "file:///tmp/a.wbn?https://example.com/b.html" and creates a URLLoader using |
| // the BundledExchangesURLLoaderFactory to load the response of "b.html" in |
| // "a.wbn". |
| class InterceptorForTrackedNavigationFromFile final |
| : public NavigationLoaderInterceptor { |
| public: |
| InterceptorForTrackedNavigationFromFile( |
| scoped_refptr<BundledExchangesReader> reader, |
| DoneCallback done_callback, |
| int frame_tree_node_id) |
| : url_loader_factory_(std::make_unique<BundledExchangesURLLoaderFactory>( |
| std::move(reader), |
| frame_tree_node_id)), |
| done_callback_(std::move(done_callback)) {} |
| ~InterceptorForTrackedNavigationFromFile() override { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| } |
| |
| private: |
| // NavigationLoaderInterceptor implementation |
| void MaybeCreateLoader(const network::ResourceRequest& resource_request, |
| BrowserContext* browser_context, |
| LoaderCallback callback, |
| FallbackCallback fallback_callback) override { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| std::move(callback).Run( |
| base::MakeRefCounted<SingleRequestURLLoaderFactory>(base::BindOnce( |
| &InterceptorForTrackedNavigationFromFile::CreateURLLoader, |
| weak_factory_.GetWeakPtr()))); |
| } |
| |
| bool ShouldBypassRedirectChecks() override { return true; } |
| |
| void CreateURLLoader( |
| const network::ResourceRequest& resource_request, |
| mojo::PendingReceiver<network::mojom::URLLoader> receiver, |
| mojo::PendingRemote<network::mojom::URLLoaderClient> client) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!is_redirected_) { |
| DCHECK(url_loader_factory_->reader()->HasEntry(resource_request.url)); |
| is_redirected_ = true; |
| original_request_url_ = resource_request.url; |
| |
| GURL bundled_exchanges_url = |
| url_loader_factory_->reader()->source().url(); |
| const GURL new_url = |
| bundled_exchanges_utils::GetSynthesizedUrlForBundledExchanges( |
| bundled_exchanges_url, original_request_url_); |
| auto redirect_loader = |
| std::make_unique<PrimaryURLRedirectLoader>(std::move(client)); |
| redirect_loader->OnReadyToRedirect(resource_request, new_url); |
| mojo::MakeSelfOwnedReceiver( |
| std::move(redirect_loader), |
| mojo::PendingReceiver<network::mojom::URLLoader>( |
| std::move(receiver))); |
| return; |
| } |
| network::ResourceRequest new_resource_request = resource_request; |
| new_resource_request.url = original_request_url_; |
| url_loader_factory_->CreateLoaderAndStart( |
| std::move(receiver), /*routing_id=*/0, /*request_id=*/0, /*options=*/0, |
| new_resource_request, std::move(client), |
| net::MutableNetworkTrafficAnnotationTag(kTrafficAnnotation)); |
| std::move(done_callback_) |
| .Run(original_request_url_, std::move(url_loader_factory_)); |
| } |
| |
| std::unique_ptr<BundledExchangesURLLoaderFactory> url_loader_factory_; |
| DoneCallback done_callback_; |
| |
| bool is_redirected_ = false; |
| GURL original_request_url_; |
| |
| SEQUENCE_CHECKER(sequence_checker_); |
| |
| base::WeakPtrFactory<InterceptorForTrackedNavigationFromFile> weak_factory_{ |
| this}; |
| |
| DISALLOW_COPY_AND_ASSIGN(InterceptorForTrackedNavigationFromFile); |
| }; |
| |
| // A class to inherit NavigationLoaderInterceptor for the history navigation to |
| // a BundledExchanges file. |
| // - MaybeCreateLoader() is called for the history navigation request. It |
| // continues on CreateURLLoader() to create the loader for the main resource. |
| // - If OnMetadataReady() has not been called yet: |
| // Wait for OnMetadataReady() to be called. |
| // - If OnMetadataReady() was called with an error: |
| // Completes the request with ERR_INVALID_BUNDLED_EXCHANGES. |
| // - If OnMetadataReady() was called whthout errors: |
| // Creates the loader for the main resource. |
| class InterceptorForNavigationInfo final : public NavigationLoaderInterceptor { |
| public: |
| InterceptorForNavigationInfo( |
| std::unique_ptr<BundledExchangesNavigationInfo> navigation_info, |
| DoneCallback done_callback, |
| int frame_tree_node_id) |
| : reader_(base::MakeRefCounted<BundledExchangesReader>( |
| navigation_info->source().Clone())), |
| target_inner_url_(navigation_info->target_inner_url()), |
| done_callback_(std::move(done_callback)), |
| frame_tree_node_id_(frame_tree_node_id) { |
| reader_->ReadMetadata( |
| base::BindOnce(&InterceptorForNavigationInfo::OnMetadataReady, |
| weak_factory_.GetWeakPtr())); |
| } |
| ~InterceptorForNavigationInfo() override { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| } |
| |
| private: |
| // NavigationLoaderInterceptor implementation |
| void MaybeCreateLoader(const network::ResourceRequest& resource_request, |
| BrowserContext* browser_context, |
| LoaderCallback callback, |
| FallbackCallback fallback_callback) override { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| std::move(callback).Run(base::MakeRefCounted<SingleRequestURLLoaderFactory>( |
| base::BindOnce(&InterceptorForNavigationInfo::CreateURLLoader, |
| weak_factory_.GetWeakPtr()))); |
| } |
| |
| void CreateURLLoader( |
| const network::ResourceRequest& resource_request, |
| mojo::PendingReceiver<network::mojom::URLLoader> receiver, |
| mojo::PendingRemote<network::mojom::URLLoaderClient> client) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (metadata_error_) { |
| mojo::Remote<network::mojom::URLLoaderClient>(std::move(client)) |
| ->OnComplete(network::URLLoaderCompletionStatus( |
| net::ERR_INVALID_BUNDLED_EXCHANGES)); |
| return; |
| } |
| |
| if (!url_loader_factory_) { |
| pending_resource_request_ = resource_request; |
| pending_receiver_ = std::move(receiver); |
| pending_client_ = std::move(client); |
| return; |
| } |
| |
| network::ResourceRequest new_resource_request = resource_request; |
| new_resource_request.url = target_inner_url_; |
| url_loader_factory_->CreateLoaderAndStart( |
| std::move(receiver), /*routing_id=*/0, /*request_id=*/0, /*options=*/0, |
| new_resource_request, std::move(client), |
| net::MutableNetworkTrafficAnnotationTag(kTrafficAnnotation)); |
| std::move(done_callback_) |
| .Run(target_inner_url_, std::move(url_loader_factory_)); |
| } |
| |
| void OnMetadataReady(data_decoder::mojom::BundleMetadataParseErrorPtr error) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(!url_loader_factory_); |
| |
| if (error) { |
| AddMetadataParseErrorMessageToConsole(frame_tree_node_id_, error); |
| metadata_error_ = std::move(error); |
| } else { |
| url_loader_factory_ = std::make_unique<BundledExchangesURLLoaderFactory>( |
| std::move(reader_), frame_tree_node_id_); |
| } |
| |
| if (pending_receiver_) { |
| DCHECK(pending_client_); |
| CreateURLLoader(pending_resource_request_, std::move(pending_receiver_), |
| std::move(pending_client_)); |
| } |
| } |
| |
| scoped_refptr<BundledExchangesReader> reader_; |
| const GURL target_inner_url_; |
| DoneCallback done_callback_; |
| const int frame_tree_node_id_; |
| |
| network::ResourceRequest pending_resource_request_; |
| mojo::PendingReceiver<network::mojom::URLLoader> pending_receiver_; |
| mojo::PendingRemote<network::mojom::URLLoaderClient> pending_client_; |
| |
| std::unique_ptr<BundledExchangesURLLoaderFactory> url_loader_factory_; |
| data_decoder::mojom::BundleMetadataParseErrorPtr metadata_error_; |
| |
| SEQUENCE_CHECKER(sequence_checker_); |
| |
| base::WeakPtrFactory<InterceptorForNavigationInfo> weak_factory_{this}; |
| |
| DISALLOW_COPY_AND_ASSIGN(InterceptorForNavigationInfo); |
| }; |
| |
| } // namespace |
| |
| // static |
| std::unique_ptr<BundledExchangesHandle> BundledExchangesHandle::CreateForFile( |
| int frame_tree_node_id) { |
| auto handle = base::WrapUnique(new BundledExchangesHandle()); |
| handle->SetInterceptor(std::make_unique<InterceptorForFile>( |
| base::BindOnce(&BundledExchangesHandle::OnBundledExchangesFileLoaded, |
| handle->weak_factory_.GetWeakPtr()), |
| frame_tree_node_id)); |
| return handle; |
| } |
| |
| // static |
| std::unique_ptr<BundledExchangesHandle> |
| BundledExchangesHandle::CreateForTrustableFile( |
| std::unique_ptr<BundledExchangesSource> source, |
| int frame_tree_node_id) { |
| DCHECK(source->is_trusted_file()); |
| auto handle = base::WrapUnique(new BundledExchangesHandle()); |
| handle->SetInterceptor(std::make_unique<InterceptorForTrustableFile>( |
| std::move(source), |
| base::BindOnce(&BundledExchangesHandle::OnBundledExchangesFileLoaded, |
| handle->weak_factory_.GetWeakPtr()), |
| frame_tree_node_id)); |
| return handle; |
| } |
| |
| // static |
| std::unique_ptr<BundledExchangesHandle> |
| BundledExchangesHandle::CreateForNetwork(BrowserContext* browser_context, |
| int frame_tree_node_id) { |
| DCHECK(base::FeatureList::IsEnabled(features::kWebBundlesFromNetwork)); |
| auto handle = base::WrapUnique(new BundledExchangesHandle()); |
| handle->SetInterceptor(std::make_unique<InterceptorForNetwork>( |
| base::BindOnce(&BundledExchangesHandle::OnBundledExchangesFileLoaded, |
| handle->weak_factory_.GetWeakPtr()), |
| browser_context, frame_tree_node_id)); |
| return handle; |
| } |
| |
| // static |
| std::unique_ptr<BundledExchangesHandle> |
| BundledExchangesHandle::CreateForTrackedNavigation( |
| scoped_refptr<BundledExchangesReader> reader, |
| int frame_tree_node_id) { |
| auto handle = base::WrapUnique(new BundledExchangesHandle()); |
| switch (reader->source().type()) { |
| case BundledExchangesSource::Type::kTrustedFile: |
| handle->SetInterceptor( |
| std::make_unique<InterceptorForTrackedNavigationFromTrustableFile>( |
| std::move(reader), |
| base::BindOnce( |
| &BundledExchangesHandle::OnBundledExchangesFileLoaded, |
| handle->weak_factory_.GetWeakPtr()), |
| frame_tree_node_id)); |
| break; |
| case BundledExchangesSource::Type::kFile: |
| handle->SetInterceptor( |
| std::make_unique<InterceptorForTrackedNavigationFromFile>( |
| std::move(reader), |
| base::BindOnce( |
| &BundledExchangesHandle::OnBundledExchangesFileLoaded, |
| handle->weak_factory_.GetWeakPtr()), |
| frame_tree_node_id)); |
| break; |
| case BundledExchangesSource::Type::kNetwork: |
| // Currently navigation within web bundles from network is not supported. |
| // TODO(crbug.com/1018640): Implement this. |
| NOTREACHED(); |
| break; |
| } |
| return handle; |
| } |
| |
| // static |
| std::unique_ptr<BundledExchangesHandle> |
| BundledExchangesHandle::MaybeCreateForNavigationInfo( |
| std::unique_ptr<BundledExchangesNavigationInfo> navigation_info, |
| int frame_tree_node_id) { |
| // Currently history navigation is not supported for web bundles from network. |
| // TODO(crbug.com/1018640): Implement this. |
| if (navigation_info->source().is_network()) |
| return nullptr; |
| auto handle = base::WrapUnique(new BundledExchangesHandle()); |
| handle->SetInterceptor(std::make_unique<InterceptorForNavigationInfo>( |
| std::move(navigation_info), |
| base::BindOnce(&BundledExchangesHandle::OnBundledExchangesFileLoaded, |
| handle->weak_factory_.GetWeakPtr()), |
| frame_tree_node_id)); |
| return handle; |
| } |
| |
| BundledExchangesHandle::BundledExchangesHandle() = default; |
| |
| BundledExchangesHandle::~BundledExchangesHandle() = default; |
| |
| std::unique_ptr<NavigationLoaderInterceptor> |
| BundledExchangesHandle::TakeInterceptor() { |
| DCHECK(interceptor_); |
| return std::move(interceptor_); |
| } |
| |
| void BundledExchangesHandle::CreateURLLoaderFactory( |
| mojo::PendingReceiver<network::mojom::URLLoaderFactory> receiver, |
| mojo::Remote<network::mojom::URLLoaderFactory> fallback_factory) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(url_loader_factory_); |
| |
| url_loader_factory_->SetFallbackFactory(std::move(fallback_factory)); |
| url_loader_factory_->Clone(std::move(receiver)); |
| } |
| |
| std::unique_ptr<BundledExchangesHandleTracker> |
| BundledExchangesHandle::MaybeCreateTracker() { |
| if (!url_loader_factory_) |
| return nullptr; |
| return std::make_unique<BundledExchangesHandleTracker>( |
| url_loader_factory_->reader(), navigation_info_->target_inner_url()); |
| } |
| |
| bool BundledExchangesHandle::IsReadyForLoading() { |
| return !!url_loader_factory_; |
| } |
| |
| void BundledExchangesHandle::SetInterceptor( |
| std::unique_ptr<NavigationLoaderInterceptor> interceptor) { |
| interceptor_ = std::move(interceptor); |
| } |
| |
| void BundledExchangesHandle::OnBundledExchangesFileLoaded( |
| const GURL& target_inner_url, |
| std::unique_ptr<BundledExchangesURLLoaderFactory> url_loader_factory) { |
| auto source = url_loader_factory->reader()->source().Clone(); |
| if (source->is_file()) |
| base_url_override_ = target_inner_url; |
| navigation_info_ = std::make_unique<BundledExchangesNavigationInfo>( |
| std::move(source), target_inner_url); |
| url_loader_factory_ = std::move(url_loader_factory); |
| } |
| |
| } // namespace content |