blob: 9da7690305486f4672df2767c4bf717493437a84 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/webapps/isolated_web_apps/url_loading/utils.h"
#include <optional>
#include <string>
#include <string_view>
#include "base/containers/span.h"
#include "base/logging.h"
#include "base/memory/ref_counted.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "components/web_package/signed_web_bundles/signed_web_bundle_id.h"
#include "components/webapps/isolated_web_apps/client.h"
#include "components/webapps/isolated_web_apps/types/storage_location.h"
#include "content/public/browser/frame_tree_node_id.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/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 "services/network/public/cpp/constants.h"
#include "services/network/public/cpp/resource_request.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/blink/public/mojom/devtools/console_message.mojom.h"
namespace web_app {
namespace {
GURL ConstructProxyUrl(const IwaSourceProxy& proxy,
const GURL& resource_request_url) {
GURL::Replacements replacements;
replacements.SetPathStr(resource_request_url.path_piece());
if (resource_request_url.has_query()) {
replacements.SetQueryStr(resource_request_url.query_piece());
}
return proxy.proxy_url().GetURL().ReplaceComponents(replacements);
}
network::ResourceRequest ConstructProxyRequest(
const IwaSourceProxy& proxy,
const network::ResourceRequest& resource_request) {
// Create a new ResourceRequest with the proxy URL.
network::ResourceRequest proxy_request;
proxy_request.url = ConstructProxyUrl(proxy, resource_request.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 =
resource_request.headers.GetHeader(net::HttpRequestHeaders::kAccept)
.value_or(network::kDefaultAcceptHeaderValue);
proxy_request.headers.SetHeader(net::HttpRequestHeaders::kAccept,
accept_header_value);
proxy_request.headers.SetHeader(net::HttpRequestHeaders::kCacheControl,
"no-cache");
return proxy_request;
}
content::WebContents* FromFrameTreeNodeID(
const std::optional<content::FrameTreeNodeId>& frame_tree_node_id) {
if (!frame_tree_node_id) {
return nullptr;
}
return content::WebContents::FromFrameTreeNodeId(*frame_tree_node_id);
}
} // namespace
void CompleteWithGeneratedResponse(
mojo::Remote<network::mojom::URLLoaderClient> loader_client,
net::HttpStatusCode http_status_code,
std::optional<std::string> body,
std::string_view content_type) {
size_t content_length = body.has_value() ? body->size() : 0;
std::string headers = base::StringPrintf(
"HTTP/1.1 %d %s\n"
"Content-Type: %s;charset=utf-8\n"
"Content-Length: %s\n\n",
static_cast<int>(http_status_code),
net::GetHttpReasonPhrase(http_status_code), content_type.data(),
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=*/std::nullopt);
if (body.has_value()) {
size_t actually_written_bytes = 0;
MojoResult write_result = producer_handle->WriteData(
base::as_byte_span(*body), MOJO_WRITE_DATA_FLAG_NONE,
actually_written_bytes);
if (write_result != MOJO_RESULT_OK ||
actually_written_bytes != body->size()) {
loader_client->OnComplete(
network::URLLoaderCompletionStatus(net::ERR_FAILED));
return;
}
} 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(
std::optional<content::FrameTreeNodeId> frame_tree_node_id,
const std::string& error_message) {
content::WebContents* web_contents = 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;
}
// TODO(crbug.com/40239529): 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.
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;
});
}
void LogErrorAndFail(
const std::string& error_message,
const std::optional<content::FrameTreeNodeId>& frame_tree_node_id,
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));
}
void HandleProxy(
content::BrowserContext* browser_context,
const web_package::SignedWebBundleId& web_bundle_id,
const IwaSourceProxy& proxy,
mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver,
mojo::PendingRemote<network::mojom::URLLoaderClient> loader_client,
const network::ResourceRequest& resource_request,
const net::MutableNetworkTrafficAnnotationTag& traffic_annotation,
const std::optional<content::FrameTreeNodeId>& frame_tree_node_id) {
DCHECK(!proxy.proxy_url().opaque());
content::StoragePartition* storage_partition =
IwaClient::GetInstance()->GetStoragePartition(browser_context,
web_bundle_id);
if (!storage_partition) {
LogErrorAndFail("Storage not found for Isolated Web App: " +
resource_request.url.spec(),
frame_tree_node_id, std::move(loader_client));
return;
}
storage_partition->GetURLLoaderFactoryForBrowserProcess()
->CreateLoaderAndStart(std::move(loader_receiver),
/*request_id=*/0,
network::mojom::kURLLoadOptionNone,
ConstructProxyRequest(proxy, resource_request),
std::move(loader_client), traffic_annotation);
}
} // namespace web_app