blob: e119c658aae2cef5bc6e70e869009edb88681b91 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_reader_registry.h"
#include <memory>
#include <utility>
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/overloaded.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/ranges/algorithm.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/time/time.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader_factory.h"
#include "chrome/browser/web_applications/isolated_web_apps/signed_web_bundle_reader.h"
#include "chrome/common/url_constants.h"
#include "components/web_package/mojom/web_bundle_parser.mojom.h"
#include "components/web_package/signed_web_bundles/signed_web_bundle_id.h"
#include "components/web_package/signed_web_bundles/signed_web_bundle_signature_verifier.h"
#include "services/network/public/cpp/resource_request.h"
#include "url/url_constants.h"
namespace web_app {
namespace {
// References to `SignedWebBundleReader`s that are not used for the provided
// time interval will be removed from the cache. This is important so that the
// cache doesn't grow forever, given that each `SignedWebBundleReader` requires
// some memory and an open file handle.
//
// Note: Depending on when during the interval a new `SignedWebBundleReader` is
// accessed, the worst-case time until it is cleaned up can be up to two times
// `kCleanupInterval`, since the logic for cleaning up `SignedWebBundleReader`s
// is as follows: Every `kCleanupInterval`, remove references to all
// `SignedWebBundleReader`s that haven't been accessed for at least
// `kCleanupInterval`.
// We could run a separate timer per `SignedWebBundleReader` to more accurately
// respect `kCleanupInterval`, but this feels like unnecessary overhead.
base::TimeDelta kCleanupInterval = base::Minutes(10);
} // namespace
IsolatedWebAppReaderRegistry::IsolatedWebAppReaderRegistry(
std::unique_ptr<IsolatedWebAppValidator> validator,
base::RepeatingCallback<
std::unique_ptr<web_package::SignedWebBundleSignatureVerifier>()>
signature_verifier_factory)
: reader_factory_(std::make_unique<IsolatedWebAppResponseReaderFactory>(
std::move(validator),
std::move(signature_verifier_factory))) {}
IsolatedWebAppReaderRegistry::~IsolatedWebAppReaderRegistry() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
void IsolatedWebAppReaderRegistry::ReadResponse(
const base::FilePath& web_bundle_path,
const web_package::SignedWebBundleId& web_bundle_id,
const network::ResourceRequest& resource_request,
ReadResponseCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_EQ(web_bundle_id.type(),
web_package::SignedWebBundleId::Type::kEd25519PublicKey);
{
auto cache_entry_it = reader_cache_.Find(web_bundle_path);
bool found = cache_entry_it != reader_cache_.End();
base::UmaHistogramEnumeration(
"WebApp.Isolated.ResponseReaderCacheState",
found ? cache_entry_it->second.AsReaderCacheState()
: ReaderCacheState::kNotCached);
if (found) {
switch (cache_entry_it->second.state()) {
case Cache::Entry::State::kPending:
// If integrity block and metadata are still being read, then the
// `SignedWebBundleReader` is not yet ready to be used for serving
// responses. Queue the request and callback in this case.
cache_entry_it->second.pending_requests.emplace_back(
resource_request, std::move(callback));
return;
case Cache::Entry::State::kReady:
// If integrity block and metadata have already been read, read
// the response from the cached `SignedWebBundleReader`.
DoReadResponse(cache_entry_it->second.GetReader(), resource_request,
std::move(callback));
return;
}
}
}
GURL base_url(
base::StrCat({chrome::kIsolatedAppScheme, url::kStandardSchemeSeparator,
web_bundle_id.id()}));
auto [cache_entry_it, was_insertion] =
reader_cache_.Emplace(web_bundle_path, Cache::Entry());
DCHECK(was_insertion);
cache_entry_it->second.pending_requests.emplace_back(resource_request,
std::move(callback));
// If we already verified the signatures of this Signed Web Bundle during
// the current browser session, we trust that the Signed Web Bundle has not
// been tampered with and don't re-verify signatures.
//
// TODO(crbug.com/1366309): On ChromeOS, we should only verify signatures at
// install-time. Until this is implemented, we will verify signatures on
// ChromeOS once per session.
bool skip_signature_verification = verified_files_.contains(web_bundle_path);
reader_factory_->CreateResponseReader(
web_bundle_path, web_bundle_id, skip_signature_verification,
base::BindOnce(&IsolatedWebAppReaderRegistry::OnResponseReaderCreated,
// `base::Unretained` can be used here since `this` owns
// `reader_factory`.
base::Unretained(this), web_bundle_path, web_bundle_id));
}
void IsolatedWebAppReaderRegistry::OnResponseReaderCreated(
const base::FilePath& web_bundle_path,
const web_package::SignedWebBundleId& web_bundle_id,
base::expected<std::unique_ptr<IsolatedWebAppResponseReader>,
IsolatedWebAppResponseReaderFactory::Error> reader) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto cache_entry_it = reader_cache_.Find(web_bundle_path);
DCHECK(cache_entry_it != reader_cache_.End());
DCHECK_EQ(cache_entry_it->second.state(), Cache::Entry::State::kPending);
std::vector<std::pair<network::ResourceRequest, ReadResponseCallback>>
pending_requests =
std::exchange(cache_entry_it->second.pending_requests, {});
if (!reader.has_value()) {
for (auto& [resource_request, callback] : pending_requests) {
std::move(callback).Run(
base::unexpected(ReadResponseError::ForError(reader.error())));
}
reader_cache_.Erase(cache_entry_it);
return;
}
// The `SignedWebBundleReader` is now ready to read responses. Inform all
// consumers that were waiting for this `SignedWebBundleReader` to become
// available.
verified_files_.insert(cache_entry_it->first);
cache_entry_it->second.set_reader(std::move(*reader));
for (auto& [resource_request, callback] : pending_requests) {
DoReadResponse(cache_entry_it->second.GetReader(), resource_request,
std::move(callback));
}
}
void IsolatedWebAppReaderRegistry::DoReadResponse(
IsolatedWebAppResponseReader& reader,
network::ResourceRequest resource_request,
ReadResponseCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Remove query parameters from the request URL, if it has any.
// Resources within Signed Web Bundles used for Isolated Web Apps never have
// username, password, or fragment, just like resources within Signed Web
// Bundles and normal Web Bundles. Removing these from request URLs is done by
// the `SignedWebBundleReader`. However, in addition, resources in Signed Web
// Bundles used for Isolated Web Apps can also never have query parameters,
// which we need to remove here.
//
// Conceptually, we treat the resources in Signed Web Bundles for Isolated Web
// Apps more like files served by a file server (which also strips query
// parameters before looking up the file), and not like HTTP exchanges as they
// are used for Signed Exchanges (SXG).
if (resource_request.url.has_query()) {
GURL::Replacements replacements;
replacements.ClearQuery();
resource_request.url = resource_request.url.ReplaceComponents(replacements);
}
reader.ReadResponse(
resource_request,
base::BindOnce(
&IsolatedWebAppReaderRegistry::OnResponseRead,
// `base::Unretained` can be used here since `this` owns `reader`.
base::Unretained(this), std::move(callback)));
}
void IsolatedWebAppReaderRegistry::OnResponseRead(
ReadResponseCallback callback,
base::expected<IsolatedWebAppResponseReader::Response,
IsolatedWebAppResponseReader::Error> response) {
base::UmaHistogramEnumeration("WebApp.Isolated.ReadResponseHeadStatus",
response.has_value()
? ReadResponseHeadStatus::kSuccess
: GetStatusFromError(response.error()));
if (!response.has_value()) {
std::move(callback).Run(
base::unexpected(ReadResponseError::ForError(response.error())));
return;
}
std::move(callback).Run(std::move(*response));
}
IsolatedWebAppReaderRegistry::ReadResponseHeadStatus
IsolatedWebAppReaderRegistry::GetStatusFromError(
const IsolatedWebAppResponseReader::Error& error) {
switch (error.type) {
case IsolatedWebAppResponseReader::Error::Type::kParserInternalError:
return ReadResponseHeadStatus::kResponseHeadParserInternalError;
case IsolatedWebAppResponseReader::Error::Type::kFormatError:
return ReadResponseHeadStatus::kResponseHeadParserFormatError;
case IsolatedWebAppResponseReader::Error::Type::kResponseNotFound:
return ReadResponseHeadStatus::kResponseNotFoundError;
}
}
// static
IsolatedWebAppReaderRegistry::ReadResponseError
IsolatedWebAppReaderRegistry::ReadResponseError::ForError(
const IsolatedWebAppResponseReaderFactory::Error& error) {
return ForOtherError(absl::visit(
base::Overloaded{
[](const web_package::mojom::BundleIntegrityBlockParseErrorPtr&
error) {
return base::StringPrintf("Failed to parse integrity block: %s",
error->message.c_str());
},
[](const IntegrityBlockError& error) {
return base::StringPrintf("Failed to validate integrity block: %s",
error.message.c_str());
},
[](const web_package::SignedWebBundleSignatureVerifier::Error&
error) {
return base::StringPrintf("Failed to verify signatures: %s",
error.message.c_str());
},
[](const web_package::mojom::BundleMetadataParseErrorPtr& error) {
return base::StringPrintf("Failed to parse metadata: %s",
error->message.c_str());
},
[](const MetadataError& error) {
return base::StringPrintf("Failed to validate metadata: %s",
error.message.c_str());
}},
error));
}
// static
IsolatedWebAppReaderRegistry::ReadResponseError
IsolatedWebAppReaderRegistry::ReadResponseError::ForError(
const IsolatedWebAppResponseReader::Error& error) {
switch (error.type) {
case IsolatedWebAppResponseReader::Error::Type::kParserInternalError:
return ForOtherError(base::StringPrintf(
"Failed to parse response head: %s", error.message.c_str()));
case IsolatedWebAppResponseReader::Error::Type::kFormatError:
return ForOtherError(base::StringPrintf(
"Failed to parse response head: %s", error.message.c_str()));
case IsolatedWebAppResponseReader::Error::Type::kResponseNotFound:
return ForResponseNotFound(base::StringPrintf(
"Failed to read response: %s", error.message.c_str()));
}
}
IsolatedWebAppReaderRegistry::Cache::Cache() = default;
IsolatedWebAppReaderRegistry::Cache::~Cache() = default;
base::flat_map<base::FilePath,
IsolatedWebAppReaderRegistry::Cache::Entry>::iterator
IsolatedWebAppReaderRegistry::Cache::Find(const base::FilePath& file_path) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return cache_.find(file_path);
}
base::flat_map<base::FilePath,
IsolatedWebAppReaderRegistry::Cache::Entry>::iterator
IsolatedWebAppReaderRegistry::Cache::End() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return cache_.end();
}
template <class... Args>
std::pair<base::flat_map<base::FilePath,
IsolatedWebAppReaderRegistry::Cache::Entry>::iterator,
bool>
IsolatedWebAppReaderRegistry::Cache::Emplace(Args&&... args) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto result = cache_.emplace(std::forward<Args>(args)...);
StartCleanupTimerIfNotRunning();
return result;
}
void IsolatedWebAppReaderRegistry::Cache::Erase(
base::flat_map<base::FilePath, Entry>::iterator iterator) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
cache_.erase(iterator);
StopCleanupTimerIfCacheIsEmpty();
}
void IsolatedWebAppReaderRegistry::Cache::StartCleanupTimerIfNotRunning() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!cache_.empty());
if (cleanup_timer_.IsRunning()) {
return;
}
cleanup_timer_.Start(
FROM_HERE, kCleanupInterval,
base::BindRepeating(&Cache::CleanupOldEntries,
// It is safe to use `base::Unretained` here, because
// `cache_cleanup_timer_` will be deleted before
// `this` is deleted.
base::Unretained(this)));
}
void IsolatedWebAppReaderRegistry::Cache::StopCleanupTimerIfCacheIsEmpty() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (cache_.empty()) {
cleanup_timer_.AbandonAndStop();
}
}
void IsolatedWebAppReaderRegistry::Cache::CleanupOldEntries() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::TimeTicks now = base::TimeTicks::Now();
cache_.erase(
base::ranges::remove_if(
cache_,
[&now](const Entry& cache_entry) -> bool {
// If a `SignedWebBundleReader` is ready to read responses and has
// not been used for at least `kCleanupInterval`, remove it from the
// cache.
return cache_entry.state() == Entry::State::kReady &&
now - cache_entry.last_access() > kCleanupInterval;
},
[](const std::pair<base::FilePath, Entry>& entry) -> const Entry& {
return entry.second;
}),
cache_.end());
StopCleanupTimerIfCacheIsEmpty();
}
IsolatedWebAppReaderRegistry::Cache::Entry::Entry() = default;
IsolatedWebAppReaderRegistry::Cache::Entry::~Entry() = default;
IsolatedWebAppReaderRegistry::Cache::Entry::Entry(Entry&& other) = default;
IsolatedWebAppReaderRegistry::Cache::Entry&
IsolatedWebAppReaderRegistry::Cache::Entry::operator=(Entry&& other) = default;
} // namespace web_app