blob: 8c4418cfb677621f8fa701d4fdeeb31579206ae4 [file] [log] [blame]
// 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/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/web_package/bundled_exchanges_handle_tracker.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 "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.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"
namespace content {
namespace {
using DoneCallback = base::OnceCallback<void(
const GURL& base_url_override,
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."
)");
// 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.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 the 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 the 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)
: done_callback_(std::move(done_callback)) {}
~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::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,
network::mojom::URLLoaderClientRequest* client_request,
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_request = 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) {
forwarding_client_->OnComplete(network::URLLoaderCompletionStatus(
net::ERR_INVALID_BUNDLED_EXCHANGES));
forwarding_client_.reset();
return;
}
DCHECK(reader_);
primary_url_ = reader_->GetPrimaryURL();
url_loader_factory_ =
std::make_unique<BundledExchangesURLLoaderFactory>(std::move(reader_));
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,
network::mojom::URLLoaderRequest request,
network::mojom::URLLoaderClientPtr 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(request), /*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_;
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 the 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)
: source_(std::move(source)),
reader_(base::MakeRefCounted<BundledExchangesReader>(source_->Clone())),
done_callback_(std::move(done_callback)) {
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::BindOnce(&InterceptorForTrustableFile::CreateURLLoader,
weak_factory_.GetWeakPtr()));
}
void CreateURLLoader(const network::ResourceRequest& resource_request,
network::mojom::URLLoaderRequest request,
network::mojom::URLLoaderClientPtr client) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (metadata_error_) {
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_request_ = std::move(request);
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(request), /*routing_id=*/0, /*request_id=*/0, /*options=*/0,
resource_request, std::move(client),
net::MutableNetworkTrafficAnnotationTag(kTrafficAnnotation));
std::move(done_callback_)
.Run(
/*base_url_override=*/GURL(), std::move(url_loader_factory_));
return;
}
auto redirect_loader =
std::make_unique<PrimaryURLRedirectLoader>(client.PassInterface());
redirect_loader->OnReadyToRedirect(resource_request, primary_url_);
mojo::MakeSelfOwnedReceiver(
std::move(redirect_loader),
mojo::PendingReceiver<network::mojom::URLLoader>(std::move(request)));
}
void OnMetadataReady(data_decoder::mojom::BundleMetadataParseErrorPtr error) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!url_loader_factory_);
if (error) {
metadata_error_ = std::move(error);
} else {
primary_url_ = reader_->GetPrimaryURL();
url_loader_factory_ = std::make_unique<BundledExchangesURLLoaderFactory>(
std::move(reader_));
}
if (pending_request_) {
DCHECK(pending_client_);
CreateURLLoader(pending_resource_request_, std::move(pending_request_),
std::move(pending_client_));
}
}
std::unique_ptr<BundledExchangesSource> source_;
scoped_refptr<BundledExchangesReader> reader_;
DoneCallback done_callback_;
network::ResourceRequest pending_resource_request_;
network::mojom::URLLoaderRequest pending_request_;
network::mojom::URLLoaderClientPtr 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 the 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)
: url_loader_factory_(std::make_unique<BundledExchangesURLLoaderFactory>(
std::move(reader))),
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::BindOnce(
&InterceptorForTrackedNavigationFromTrustableFile::CreateURLLoader,
weak_factory_.GetWeakPtr()));
}
void CreateURLLoader(const network::ResourceRequest& resource_request,
network::mojom::URLLoaderRequest request,
network::mojom::URLLoaderClientPtr client) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
url_loader_factory_->CreateLoaderAndStart(
std::move(request), /*routing_id=*/0, /*request_id=*/0, /*options=*/0,
resource_request, std::move(client),
net::MutableNetworkTrafficAnnotationTag(kTrafficAnnotation));
std::move(done_callback_)
.Run(
/*base_url_override=*/GURL(), 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 the 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)
: url_loader_factory_(std::make_unique<BundledExchangesURLLoaderFactory>(
std::move(reader))),
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::BindOnce(
&InterceptorForTrackedNavigationFromFile::CreateURLLoader,
weak_factory_.GetWeakPtr()));
}
bool ShouldBypassRedirectChecks() override { return true; }
void CreateURLLoader(const network::ResourceRequest& resource_request,
network::mojom::URLLoaderRequest request,
network::mojom::URLLoaderClientPtr 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>(client.PassInterface());
redirect_loader->OnReadyToRedirect(resource_request, new_url);
mojo::MakeSelfOwnedReceiver(
std::move(redirect_loader),
mojo::PendingReceiver<network::mojom::URLLoader>(std::move(request)));
return;
}
network::ResourceRequest new_resource_request = resource_request;
new_resource_request.url = original_request_url_;
url_loader_factory_->CreateLoaderAndStart(
std::move(request), /*routing_id=*/0, /*request_id=*/0, /*options=*/0,
new_resource_request, std::move(client),
net::MutableNetworkTrafficAnnotationTag(kTrafficAnnotation));
std::move(done_callback_)
.Run(
/*base_url_override=*/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);
};
} // namespace
// static
std::unique_ptr<BundledExchangesHandle>
BundledExchangesHandle::CreateForFile() {
auto handle = base::WrapUnique(new BundledExchangesHandle());
handle->SetInterceptor(std::make_unique<InterceptorForFile>(
base::BindOnce(&BundledExchangesHandle::OnBundledExchangesFileLoaded,
handle->weak_factory_.GetWeakPtr())));
return handle;
}
// static
std::unique_ptr<BundledExchangesHandle>
BundledExchangesHandle::CreateForTrustableFile(
std::unique_ptr<BundledExchangesSource> source) {
DCHECK(source->is_trusted());
auto handle = base::WrapUnique(new BundledExchangesHandle());
handle->SetInterceptor(std::make_unique<InterceptorForTrustableFile>(
std::move(source),
base::BindOnce(&BundledExchangesHandle::OnBundledExchangesFileLoaded,
handle->weak_factory_.GetWeakPtr())));
return handle;
}
// static
std::unique_ptr<BundledExchangesHandle>
BundledExchangesHandle::CreateForTrackedNavigation(
scoped_refptr<BundledExchangesReader> reader) {
auto handle = base::WrapUnique(new BundledExchangesHandle());
if (reader->source().is_trusted()) {
handle->SetInterceptor(
std::make_unique<InterceptorForTrackedNavigationFromTrustableFile>(
std::move(reader),
base::BindOnce(
&BundledExchangesHandle::OnBundledExchangesFileLoaded,
handle->weak_factory_.GetWeakPtr())));
} else {
handle->SetInterceptor(
std::make_unique<InterceptorForTrackedNavigationFromFile>(
std::move(reader),
base::BindOnce(
&BundledExchangesHandle::OnBundledExchangesFileLoaded,
handle->weak_factory_.GetWeakPtr())));
}
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());
}
bool BundledExchangesHandle::IsReadyForLoading() {
return !!url_loader_factory_;
}
void BundledExchangesHandle::SetInterceptor(
std::unique_ptr<NavigationLoaderInterceptor> interceptor) {
interceptor_ = std::move(interceptor);
}
void BundledExchangesHandle::OnBundledExchangesFileLoaded(
const GURL& base_url_override,
std::unique_ptr<BundledExchangesURLLoaderFactory> url_loader_factory) {
base_url_override_ = base_url_override;
url_loader_factory_ = std::move(url_loader_factory);
}
} // namespace content