blob: 6d41b04c5f3b74ca856ac79b536ece23798cfbc6 [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_url_loader_factory.h"
#include <memory>
#include <vector>
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/overloaded.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/types/expected.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_reader_registry.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_reader_registry_factory.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
#include "chrome/browser/web_applications/isolated_web_apps/pending_install_info.h"
#include "chrome/browser/web_applications/isolation_data.h"
#include "chrome/browser/web_applications/web_app_provider.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/web_bundle_utils.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "mojo/public/cpp/system/data_pipe.h"
#include "net/base/net_errors.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "net/http/http_util.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/constants.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/mojom/fetch_api.mojom.h"
#include "services/network/public/mojom/url_loader.mojom.h"
#include "services/network/public/mojom/url_loader_completion_status.mojom.h"
#include "services/network/public/mojom/url_loader_factory.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/abseil-cpp/absl/types/variant.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom.h"
#include "url/gurl.h"
namespace web_app {
namespace {
const char kInstallPagePath[] = "/.well-known/_generated_install_page.html";
const char kInstallPageContent[] = R"(
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
<link rel="manifest" href="/manifest.webmanifest" />
</head>
</html>
)";
bool IsSupportedHttpMethod(const std::string& method) {
return method == net::HttpRequestHeaders::kGetMethod ||
method == net::HttpRequestHeaders::kHeadMethod;
}
void CompleteWithGeneratedHtmlResponse(
mojo::Remote<network::mojom::URLLoaderClient> loader_client,
net::HttpStatusCode http_status_code,
absl::optional<std::string> body) {
size_t content_length = body.has_value() ? body->size() : 0;
std::string headers = base::StringPrintf(
"HTTP/1.1 %d %s\n"
"Content-Type: text/html;charset=utf-8\n"
"Content-Length: %s\n\n",
static_cast<int>(http_status_code),
net::GetHttpReasonPhrase(http_status_code),
base::NumberToString(content_length).c_str());
auto response_head = network::mojom::URLResponseHead::New();
response_head->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
net::HttpUtil::AssembleRawHeaders(headers));
response_head->headers->GetMimeTypeAndCharset(&response_head->mime_type,
&response_head->charset);
response_head->content_length = content_length;
mojo::ScopedDataPipeConsumerHandle consumer_handle;
mojo::ScopedDataPipeProducerHandle producer_handle;
auto result = mojo::CreateDataPipe(nullptr, producer_handle, consumer_handle);
if (result != MOJO_RESULT_OK) {
loader_client->OnComplete(
network::URLLoaderCompletionStatus(net::ERR_INSUFFICIENT_RESOURCES));
return;
}
loader_client->OnReceiveResponse(std::move(response_head),
std::move(consumer_handle),
/*cached_metadata=*/absl::nullopt);
if (body.has_value()) {
uint32_t write_size = body->size();
MojoResult write_result = producer_handle->WriteData(
body->c_str(), &write_size, MOJO_WRITE_DATA_FLAG_NONE);
if (write_result != MOJO_RESULT_OK || write_size != body->size()) {
loader_client->OnComplete(
network::URLLoaderCompletionStatus(net::ERR_FAILED));
}
} else {
producer_handle.reset();
}
network::URLLoaderCompletionStatus status(net::OK);
status.encoded_data_length = headers.size() + content_length;
status.encoded_body_length = content_length;
status.decoded_body_length = content_length;
loader_client->OnComplete(status);
}
void LogErrorMessageToConsole(absl::optional<int> frame_tree_node_id,
const std::string& error_message) {
if (!frame_tree_node_id.has_value()) {
LOG(ERROR) << error_message;
return;
}
// TODO(crbug.com/1365850): The console message will vanish from the console
// if the user does not have the `Preserve Log` option enabled, since it is
// triggered before the navigation commits. We should try to use a similar
// approach as in crrev.com/c/3397976, but `FrameTreeNode` is not part of
// content/public.
// Find the `RenderFrameHost` associated with the `FrameTreeNode`
// corresponding to the `frame_tree_node_id`, and then log the message.
content::WebContents* web_contents =
content::WebContents::FromFrameTreeNodeId(*frame_tree_node_id);
if (!web_contents) {
// Log to the terminal if we can't log to the console.
LOG(ERROR) << error_message;
return;
}
web_contents->ForEachRenderFrameHostWithAction(
[frame_tree_node_id,
&error_message](content::RenderFrameHost* render_frame_host) {
if (render_frame_host->GetFrameTreeNodeId() == frame_tree_node_id) {
render_frame_host->AddMessageToConsole(
blink::mojom::ConsoleMessageLevel::kError, error_message);
return content::RenderFrameHost::FrameIterationAction::kStop;
}
return content::RenderFrameHost::FrameIterationAction::kContinue;
});
}
base::expected<std::reference_wrapper<const WebApp>, std::string>
FindIsolatedWebApp(Profile* profile, const IsolatedWebAppUrlInfo& url_info) {
// TODO(b/242738845): Defer navigation in IsolatedWebAppThrottle until
// WebAppProvider is ready to ensure we never fail this DCHECK.
auto* web_app_provider = WebAppProvider::GetForWebApps(profile);
DCHECK(web_app_provider->is_registry_ready());
const WebAppRegistrar& registrar = web_app_provider->registrar_unsafe();
const WebApp* iwa = registrar.GetAppById(url_info.app_id());
if (iwa == nullptr || !iwa->is_locally_installed()) {
return base::unexpected(base::StrCat(
{"Isolated Web App not installed: ", url_info.origin().Serialize()}));
}
if (!iwa->isolation_data().has_value()) {
return base::unexpected(base::StrCat(
{"App is not an Isolated Web App: ", url_info.origin().Serialize()}));
}
return *iwa;
}
class IsolatedWebAppURLLoader : public network::mojom::URLLoader {
public:
IsolatedWebAppURLLoader(
IsolatedWebAppReaderRegistry* isolated_web_app_reader_registry,
const base::FilePath& web_bundle_path,
web_package::SignedWebBundleId web_bundle_id,
mojo::PendingRemote<network::mojom::URLLoaderClient> loader_client,
const network::ResourceRequest& resource_request,
absl::optional<int> frame_tree_node_id)
: loader_client_(std::move(loader_client)),
resource_request_(resource_request),
frame_tree_node_id_(frame_tree_node_id) {
isolated_web_app_reader_registry->ReadResponse(
web_bundle_path, web_bundle_id, resource_request,
base::BindOnce(&IsolatedWebAppURLLoader::OnResponseRead,
weak_factory_.GetWeakPtr()));
}
IsolatedWebAppURLLoader(const IsolatedWebAppURLLoader&) = delete;
IsolatedWebAppURLLoader& operator=(const IsolatedWebAppURLLoader&) = delete;
IsolatedWebAppURLLoader(IsolatedWebAppURLLoader&&) = delete;
IsolatedWebAppURLLoader& operator=(IsolatedWebAppURLLoader&&) = delete;
private:
void OnResponseRead(
base::expected<IsolatedWebAppReaderRegistry::Response,
IsolatedWebAppReaderRegistry::ReadResponseError>
response) {
if (!loader_client_.is_connected()) {
return;
}
if (!response.has_value()) {
LogErrorMessageToConsole(
frame_tree_node_id_,
base::StringPrintf(
"Failed to read response from Signed Web Bundle: %s",
response.error().message.c_str()));
switch (response.error().type) {
case IsolatedWebAppReaderRegistry::ReadResponseError::Type::kOtherError:
loader_client_->OnComplete(
network::URLLoaderCompletionStatus(net::ERR_INVALID_WEB_BUNDLE));
return;
case IsolatedWebAppReaderRegistry::ReadResponseError::Type::
kResponseNotFound:
// Return a synthetic 404 response.
CompleteWithGeneratedHtmlResponse(std::move(loader_client_),
net::HTTP_NOT_FOUND,
/*body=*/absl::nullopt);
return;
}
}
// TODO(crbug.com/990733): For the initial implementation, we allow only
// net::HTTP_OK, but we should clarify acceptable status code in the spec.
if (response->head()->response_code != net::HTTP_OK) {
LogErrorMessageToConsole(
frame_tree_node_id_,
base::StringPrintf(
"Failed to read response from Signed Web Bundle: The response "
"has an unsupported HTTP status code: %d (only status code %d is "
"allowed).",
response->head()->response_code, net::HTTP_OK));
loader_client_->OnComplete(
network::URLLoaderCompletionStatus(net::ERR_INVALID_WEB_BUNDLE));
return;
}
std::string header_string =
web_package::CreateHeaderString(response->head());
auto response_head =
web_package::CreateResourceResponseFromHeaderString(header_string);
mojo::ScopedDataPipeProducerHandle producer_handle;
mojo::ScopedDataPipeConsumerHandle consumer_handle;
MojoCreateDataPipeOptions options;
options.struct_size = sizeof(MojoCreateDataPipeOptions);
options.flags = MOJO_CREATE_DATA_PIPE_FLAG_NONE;
options.element_num_bytes = 1;
options.capacity_num_bytes =
std::min(base::strict_cast<uint64_t>(
network::features::GetDataPipeDefaultAllocationSize()),
response->head()->payload_length);
auto result =
mojo::CreateDataPipe(&options, producer_handle, consumer_handle);
if (result != MOJO_RESULT_OK) {
loader_client_->OnComplete(
network::URLLoaderCompletionStatus(net::ERR_INSUFFICIENT_RESOURCES));
return;
}
header_length_ = header_string.size();
body_length_ = response->head()->payload_length;
loader_client_->OnReceiveResponse(
std::move(response_head), std::move(consumer_handle), absl::nullopt);
response->ReadBody(
std::move(producer_handle),
base::BindOnce(&IsolatedWebAppURLLoader::FinishReadingBody,
weak_factory_.GetWeakPtr()));
}
void FinishReadingBody(net::Error net_error) {
if (!loader_client_.is_connected()) {
return;
}
network::URLLoaderCompletionStatus status(net_error);
// For these values we use the same `body_length_` as we don't currently
// provide encoding in Web Bundles.
status.encoded_data_length = body_length_ + header_length_;
status.encoded_body_length = body_length_;
status.decoded_body_length = body_length_;
loader_client_->OnComplete(status);
}
// network::mojom::URLLoader implementation
void FollowRedirect(
const std::vector<std::string>& removed_headers,
const net::HttpRequestHeaders& modified_headers,
const net::HttpRequestHeaders& modified_cors_exempt_headers,
const absl::optional<GURL>& new_url) override {
NOTREACHED();
}
void SetPriority(net::RequestPriority priority,
int intra_priority_value) override {}
void PauseReadingBodyFromNet() override {}
void ResumeReadingBodyFromNet() override {}
mojo::Remote<network::mojom::URLLoaderClient> loader_client_;
int64_t header_length_;
int64_t body_length_;
const network::ResourceRequest resource_request_;
absl::optional<int> frame_tree_node_id_;
base::WeakPtrFactory<IsolatedWebAppURLLoader> weak_factory_{this};
};
} // namespace
IsolatedWebAppURLLoaderFactory::IsolatedWebAppURLLoaderFactory(
absl::optional<int> frame_tree_node_id,
Profile* profile,
mojo::PendingReceiver<network::mojom::URLLoaderFactory> factory_receiver)
: network::SelfDeletingURLLoaderFactory(std::move(factory_receiver)),
frame_tree_node_id_(frame_tree_node_id),
profile_(profile) {
profile_observation_.Observe(profile);
}
IsolatedWebAppURLLoaderFactory::~IsolatedWebAppURLLoaderFactory() = default;
void IsolatedWebAppURLLoaderFactory::CreateLoaderAndStart(
mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver,
int32_t request_id,
uint32_t options,
const network::ResourceRequest& resource_request,
mojo::PendingRemote<network::mojom::URLLoaderClient> loader_client,
const net::MutableNetworkTrafficAnnotationTag& traffic_annotation) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DCHECK(resource_request.url.SchemeIs(chrome::kIsolatedAppScheme));
DCHECK(resource_request.url.IsStandard());
base::expected<IsolatedWebAppUrlInfo, std::string> url_info =
IsolatedWebAppUrlInfo::Create(resource_request.url);
if (!url_info.has_value()) {
LogErrorAndFail(url_info.error(), std::move(loader_client));
return;
}
auto forward_request_to_isolation_data_content =
[&](const IsolationData& isolation_data) {
if (!IsSupportedHttpMethod(resource_request.method)) {
CompleteWithGeneratedHtmlResponse(
mojo::Remote<network::mojom::URLLoaderClient>(
std::move(loader_client)),
net::HTTP_METHOD_NOT_ALLOWED, /*body=*/absl::nullopt);
return;
}
absl::visit(
base::Overloaded{
[&](const IsolationData::InstalledBundle& content) {
DCHECK_EQ(
url_info->web_bundle_id().type(),
web_package::SignedWebBundleId::Type::kEd25519PublicKey);
HandleSignedBundle(content.path, url_info->web_bundle_id(),
std::move(loader_receiver),
resource_request,
std::move(loader_client));
},
[&](const IsolationData::DevModeBundle& content) {
DCHECK_EQ(
url_info->web_bundle_id().type(),
web_package::SignedWebBundleId::Type::kEd25519PublicKey);
// A Signed Web Bundle installed in dev mode is treated just
// like a properly installed Signed Web Bundle, with the only
// difference being that we implicitly trust its public
// key(s).
HandleSignedBundle(content.path, url_info->web_bundle_id(),
std::move(loader_receiver),
resource_request,
std::move(loader_client));
},
[&](const IsolationData::DevModeProxy& content) {
DCHECK_EQ(url_info->web_bundle_id().type(),
web_package::SignedWebBundleId::Type::kDevelopment);
HandleDevModeProxy(*url_info, content,
std::move(loader_receiver),
resource_request, std::move(loader_client),
traffic_annotation);
}},
isolation_data.content);
};
absl::optional<IsolationData> pending_install_isolation_data = absl::nullopt;
if (frame_tree_node_id_.has_value()) {
pending_install_isolation_data =
IsolatedWebAppPendingInstallInfo::FromWebContents(
*content::WebContents::FromFrameTreeNodeId(*frame_tree_node_id_))
.isolation_data();
}
if (pending_install_isolation_data.has_value()) {
if (resource_request.url.path() == kInstallPagePath &&
IsSupportedHttpMethod(resource_request.method)) {
CompleteWithGeneratedHtmlResponse(
mojo::Remote<network::mojom::URLLoaderClient>(
std::move(loader_client)),
net::HTTP_OK, kInstallPageContent);
return;
}
forward_request_to_isolation_data_content(*pending_install_isolation_data);
return;
}
base::expected<std::reference_wrapper<const WebApp>, std::string> iwa =
FindIsolatedWebApp(profile_, *url_info);
if (!iwa.has_value()) {
LogErrorAndFail(iwa.error(), std::move(loader_client));
return;
}
forward_request_to_isolation_data_content(*iwa->get().isolation_data());
}
void IsolatedWebAppURLLoaderFactory::OnProfileWillBeDestroyed(
Profile* profile) {
if (profile == profile_) {
// When `profile_` gets destroyed, `this` factory is not able to serve any
// more requests.
profile_observation_.Reset();
DisconnectReceiversAndDestroy();
}
}
void IsolatedWebAppURLLoaderFactory::HandleSignedBundle(
const base::FilePath& path,
const web_package::SignedWebBundleId& web_bundle_id,
mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver,
const network::ResourceRequest& resource_request,
mojo::PendingRemote<network::mojom::URLLoaderClient> loader_client) {
auto* isolated_web_app_reader_registry =
IsolatedWebAppReaderRegistryFactory::GetForProfile(profile_);
if (!isolated_web_app_reader_registry) {
LogErrorAndFail("Support for Isolated Web Apps is not enabled.",
std::move(loader_client));
return;
}
auto loader = std::make_unique<IsolatedWebAppURLLoader>(
isolated_web_app_reader_registry, path, web_bundle_id,
std::move(loader_client), resource_request, frame_tree_node_id_);
mojo::MakeSelfOwnedReceiver(std::move(std::move(loader)),
mojo::PendingReceiver<network::mojom::URLLoader>(
std::move(loader_receiver)));
}
void IsolatedWebAppURLLoaderFactory::HandleDevModeProxy(
const IsolatedWebAppUrlInfo& url_info,
const IsolationData::DevModeProxy& dev_mode_proxy,
mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver,
const network::ResourceRequest& resource_request,
mojo::PendingRemote<network::mojom::URLLoaderClient> loader_client,
const net::MutableNetworkTrafficAnnotationTag& traffic_annotation) {
DCHECK(!dev_mode_proxy.proxy_url.opaque());
GURL proxy_url =
dev_mode_proxy.proxy_url.GetURL().Resolve(resource_request.url.path());
// Create a new ResourceRequest with the proxy URL.
network::ResourceRequest proxy_request;
proxy_request.url = proxy_url;
proxy_request.method = net::HttpRequestHeaders::kGetMethod;
// Don't send cookies or HTTP authentication to the proxy server.
proxy_request.credentials_mode = network::mojom::CredentialsMode::kOmit;
std::string accept_header_value = network::kDefaultAcceptHeaderValue;
resource_request.headers.GetHeader(net::HttpRequestHeaders::kAccept,
&accept_header_value);
proxy_request.headers.SetHeader(net::HttpRequestHeaders::kAccept,
accept_header_value);
proxy_request.headers.SetHeader(net::HttpRequestHeaders::kCacheControl,
"no-cache");
content::StoragePartition* storage_partition = profile_->GetStoragePartition(
url_info.storage_partition_config(profile_), /*can_create=*/false);
if (storage_partition == nullptr) {
LogErrorAndFail(base::StrCat({"Storage not found for Isolated Web App: ",
resource_request.url.spec()}),
std::move(loader_client));
return;
}
storage_partition->GetURLLoaderFactoryForBrowserProcess()
->CreateLoaderAndStart(std::move(loader_receiver),
/*request_id=*/0,
network::mojom::kURLLoadOptionNone, proxy_request,
std::move(loader_client), traffic_annotation);
}
void IsolatedWebAppURLLoaderFactory::LogErrorAndFail(
const std::string& error_message,
mojo::PendingRemote<network::mojom::URLLoaderClient> client) {
LogErrorMessageToConsole(frame_tree_node_id_, error_message);
mojo::Remote<network::mojom::URLLoaderClient>(std::move(client))
->OnComplete(network::URLLoaderCompletionStatus(net::ERR_FAILED));
}
// static
mojo::PendingRemote<network::mojom::URLLoaderFactory>
IsolatedWebAppURLLoaderFactory::Create(
int frame_tree_node_id,
content::BrowserContext* browser_context) {
return CreateInternal(frame_tree_node_id, browser_context);
}
// static
mojo::PendingRemote<network::mojom::URLLoaderFactory>
IsolatedWebAppURLLoaderFactory::CreateForServiceWorker(
content::BrowserContext* browser_context) {
return CreateInternal(/*frame_tree_node_id=*/absl::nullopt, browser_context);
}
// static
mojo::PendingRemote<network::mojom::URLLoaderFactory>
IsolatedWebAppURLLoaderFactory::CreateInternal(
absl::optional<int> frame_tree_node_id,
content::BrowserContext* browser_context) {
DCHECK(browser_context);
DCHECK(!browser_context->ShutdownStarted());
mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote;
// The IsolatedWebAppURLLoaderFactory will delete itself when there are no
// more receivers - see the
// network::SelfDeletingURLLoaderFactory::OnDisconnect method.
new IsolatedWebAppURLLoaderFactory(
/*frame_tree_node_id=*/frame_tree_node_id,
Profile::FromBrowserContext(browser_context),
pending_remote.InitWithNewPipeAndPassReceiver());
return pending_remote;
}
} // namespace web_app