blob: aaa07d54618e7c3183eef123a44556040b9fe6d7 [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/public/browser/web_ui_url_loader_factory.h"
#include <algorithm>
#include <optional>
#include <string_view>
#include <utility>
#include <vector>
#include "base/containers/span.h"
#include "base/debug/crash_logging.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/ref_counted_memory.h"
#include "base/metrics/histogram_macros.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_view_util.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/timer/elapsed_timer.h"
#include "base/trace_event/trace_event.h"
#include "base/types/expected.h"
#include "content/browser/bad_message.h"
#include "content/browser/blob_storage/blob_internals_url_loader.h"
#include "content/browser/blob_storage/chrome_blob_storage_context.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/webui/network_error_url_loader.h"
#include "content/browser/webui/url_data_manager_backend.h"
#include "content/browser/webui/url_data_source_impl.h"
#include "content/common/web_ui_loading_util.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/url_data_source.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/url_constants.h"
#include "mojo/public/cpp/bindings/message.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/http/http_byte_range.h"
#include "services/network/public/cpp/parsed_headers.h"
#include "services/network/public/cpp/self_deleting_url_loader_factory.h"
#include "services/network/public/mojom/network_service.mojom.h"
#include "ui/base/template_expressions.h"
namespace content {
namespace {
class WebUIURLLoaderFactory;
void ReadData(
network::mojom::URLResponseHeadPtr headers,
const ui::TemplateReplacements* replacements,
bool replace_in_js,
scoped_refptr<URLDataSourceImpl> data_source,
mojo::PendingRemote<network::mojom::URLLoaderClient> client_remote,
std::optional<net::HttpByteRange> requested_range,
base::ElapsedTimer url_request_elapsed_timer,
scoped_refptr<base::RefCountedMemory> bytes) {
TRACE_EVENT0("ui", "WebUIURLLoader::ReadData");
if (!bytes) {
webui::CallOnError(std::move(client_remote), net::ERR_FAILED);
return;
}
if (replacements && !replacements->empty()) {
// We won't know the the final output size ahead of time, so we have to
// use an intermediate string.
auto input = base::as_string_view(*bytes);
std::string temp_str;
if (replace_in_js) {
CHECK(
ui::ReplaceTemplateExpressionsInJS(input, *replacements, &temp_str));
} else {
temp_str = ui::ReplaceTemplateExpressions(input, *replacements);
}
bytes = base::MakeRefCounted<base::RefCountedString>(std::move(temp_str));
}
// Send the bytes to the client. Failed requests do not count towards load
// time metrics.
if (!webui::SendData(std::move(headers), std::move(client_remote),
std::move(requested_range), bytes)) {
return;
}
UMA_HISTOGRAM_TIMES("WebUI.WebUIURLLoaderFactory.URLRequestLoadTime",
url_request_elapsed_timer.Elapsed());
}
void DataAvailable(
network::mojom::URLResponseHeadPtr headers,
const ui::TemplateReplacements* replacements,
bool replace_in_js,
scoped_refptr<URLDataSourceImpl> source,
mojo::PendingRemote<network::mojom::URLLoaderClient> client_remote,
std::optional<net::HttpByteRange> requested_range,
base::ElapsedTimer url_request_elapsed_timer,
scoped_refptr<base::RefCountedMemory> bytes) {
TRACE_EVENT0("ui", "WebUIURLLoader::DataAvailable");
// Since the bytes are from the memory mapped resource file, copying the
// data can lead to disk access. Needs to be posted to a SequencedTaskRunner
// as Mojo requires a SequencedTaskRunner::CurrentDefaultHandle in scope.
base::ThreadPool::CreateSequencedTaskRunner(
{base::TaskPriority::USER_BLOCKING, base::MayBlock(),
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})
->PostTask(FROM_HERE,
base::BindOnce(ReadData, std::move(headers), replacements,
replace_in_js, source, std::move(client_remote),
std::move(requested_range),
std::move(url_request_elapsed_timer), bytes));
}
void StartURLLoader(
const network::ResourceRequest& request,
FrameTreeNodeId frame_tree_node_id,
mojo::PendingRemote<network::mojom::URLLoaderClient> client_remote,
BrowserContext* browser_context) {
base::ElapsedTimer url_request_elapsed_timer;
// NOTE: this duplicates code in URLDataManagerBackend::StartRequest.
if (!URLDataManagerBackend::CheckURLIsValid(request.url)) {
webui::CallOnError(std::move(client_remote), net::ERR_INVALID_URL);
return;
}
URLDataSourceImpl* source =
URLDataManagerBackend::GetForBrowserContext(browser_context)
->GetDataSourceFromURL(request.url);
if (!source) {
webui::CallOnError(std::move(client_remote), net::ERR_INVALID_URL);
return;
}
if (!source->source()->ShouldServiceRequest(request.url, browser_context,
-1)) {
webui::CallOnError(std::move(client_remote), net::ERR_INVALID_URL);
return;
}
// Load everything by default, but respect the Range header if present.
base::expected<net::HttpByteRange, webui::GetRequestedRangeError>
range_or_error = webui::GetRequestedRange(request.headers);
// Errors (aside from 'no Range header') should be surfaced to the client.
if (!range_or_error.has_value() &&
range_or_error.error() != webui::GetRequestedRangeError::kNoRanges) {
webui::CallOnError(std::move(client_remote),
net::ERR_REQUEST_RANGE_NOT_SATISFIABLE);
return;
}
std::optional<net::HttpByteRange> maybe_range =
range_or_error.has_value() ? std::make_optional(range_or_error.value())
: std::nullopt;
std::string path = URLDataSource::URLToRequestPath(request.url);
std::string origin_header =
request.headers.GetHeader(net::HttpRequestHeaders::kOrigin)
.value_or(std::string());
scoped_refptr<net::HttpResponseHeaders> headers =
URLDataManagerBackend::GetHeaders(source, request.url, origin_header);
auto resource_response = network::mojom::URLResponseHead::New();
resource_response->headers = headers;
// Headers from WebUI are trusted, so parsing can happen from a non-sandboxed
// process.
resource_response->parsed_headers = network::PopulateParsedHeaders(
resource_response->headers.get(), request.url);
resource_response->mime_type = source->source()->GetMimeType(request.url);
// TODO: fill all the time related field i.e. request_time response_time
// request_start response_start
WebContents::Getter wc_getter;
// Service Workers factories have no associated frame.
if (frame_tree_node_id.is_null()) {
wc_getter = base::BindRepeating([]() -> WebContents* { return nullptr; });
} else {
wc_getter = base::BindRepeating(WebContents::FromFrameTreeNodeId,
frame_tree_node_id);
}
bool replace_in_js =
source->source()->ShouldReplaceI18nInJS() &&
source->source()->GetMimeType(request.url) == "application/javascript";
const ui::TemplateReplacements* replacements = nullptr;
const std::string mime_type = source->source()->GetMimeType(request.url);
if (mime_type == "text/html" || mime_type == "text/css" || replace_in_js)
replacements = source->source()->GetReplacements();
// To keep the same behavior as the old WebUI code, we call the source to get
// the value for |replacements| on the IO thread. Since |replacements| is
// owned by |source| keep a reference to it in the callback.
URLDataSource::GotDataCallback data_available_callback = base::BindOnce(
DataAvailable, std::move(resource_response), replacements, replace_in_js,
base::RetainedRef(source), std::move(client_remote),
std::move(maybe_range), std::move(url_request_elapsed_timer));
source->source()->StartDataRequest(request.url, std::move(wc_getter),
std::move(data_available_callback));
}
class WebUIURLLoaderFactory : public network::SelfDeletingURLLoaderFactory {
public:
// Returns mojo::PendingRemote to a newly constructed WebUIURLLoaderFactory.
// The factory is self-owned - it will delete itself once there are no more
// receivers (including the receiver associated with the returned
// mojo::PendingRemote and the receivers bound by the Clone method).
//
// |allowed_hosts| is an optional set of allowed host names. If empty then
// all hosts are allowed.
static mojo::PendingRemote<network::mojom::URLLoaderFactory> CreateForFrame(
FrameTreeNode* ftn,
const std::string& scheme,
base::flat_set<std::string> allowed_hosts) {
mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote;
// The WebUIURLLoaderFactory will delete itself when there are no more
// receivers - see the
// network::SelfDeletingURLLoaderFactory::OnDisconnect method.
new WebUIURLLoaderFactory(ftn->current_frame_host()->GetBrowserContext(),
ftn->frame_tree_node_id(), scheme,
std::move(allowed_hosts),
pending_remote.InitWithNewPipeAndPassReceiver());
return pending_remote;
}
static mojo::PendingRemote<network::mojom::URLLoaderFactory>
CreateForServiceWorker(BrowserContext* browser_context,
const std::string& scheme,
base::flat_set<std::string> allowed_hosts) {
mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote;
// The WebUIURLLoaderFactory will delete itself when there are no more
// receivers - see the
// network::SelfDeletingURLLoaderFactory::OnDisconnect method.
new WebUIURLLoaderFactory(browser_context, FrameTreeNodeId(), scheme,
std::move(allowed_hosts),
pending_remote.InitWithNewPipeAndPassReceiver());
return pending_remote;
}
WebUIURLLoaderFactory(const WebUIURLLoaderFactory&) = delete;
WebUIURLLoaderFactory& operator=(const WebUIURLLoaderFactory&) = delete;
private:
~WebUIURLLoaderFactory() override = default;
// network::mojom::URLLoaderFactory implementation:
void CreateLoaderAndStart(
mojo::PendingReceiver<network::mojom::URLLoader> loader,
int32_t request_id,
uint32_t options,
const network::ResourceRequest& request,
mojo::PendingRemote<network::mojom::URLLoaderClient> client,
const net::MutableNetworkTrafficAnnotationTag& traffic_annotation)
override {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (browser_context_.WasInvalidated()) {
DVLOG(1) << "Context has been destroyed";
webui::CallOnError(std::move(client), net::ERR_FAILED);
DisconnectReceiversAndDestroy();
return;
}
if (frame_tree_node_id_ &&
!FrameTreeNode::GloballyFindByID(frame_tree_node_id_)) {
webui::CallOnError(std::move(client), net::ERR_FAILED);
return;
}
if (request.url.GetScheme() != scheme_) {
DVLOG(1) << "Bad scheme: " << request.url.GetScheme();
SCOPED_CRASH_KEY_STRING32("WebUI", "actual_scheme",
request.url.GetScheme());
SCOPED_CRASH_KEY_STRING32("WebUI", "expected_scheme", scheme_);
SCOPED_CRASH_KEY_STRING64("WebUI", "requested_url", request.url.spec());
SCOPED_CRASH_KEY_STRING64(
"WebUI", "initiator_origin",
request.request_initiator.has_value()
? request.request_initiator->GetDebugString(false)
: "nullopt");
mojo::ReportBadMessage("Incorrect scheme");
mojo::Remote<network::mojom::URLLoaderClient>(std::move(client))
->OnComplete(network::URLLoaderCompletionStatus(net::ERR_FAILED));
return;
}
CHECK(allowed_hosts_.empty() ||
(request.url.has_host() &&
allowed_hosts_.find(request.url.GetHost()) != allowed_hosts_.end()))
<< "Incorrect host: " << request.url.GetHost();
if (request.url.host() == kChromeUIBlobInternalsHost) {
GetIOThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(
&StartBlobInternalsURLLoader, request, std::move(client),
base::Unretained(
ChromeBlobStorageContext::GetFor(browser_context_.get()))));
return;
}
// This path is entered on user-trigger navigations (e.g. from omnibox or
// links) to chrome://network-error or chrome://dino. Actual network error
// does not trigger this path.
if (request.url.host() == kChromeUINetworkErrorHost ||
request.url.host() == kChromeUIDinoHost) {
// Simulate a network error.
StartNetworkErrorsURLLoader(request, std::move(client));
// Logs WebUI usage. These WebUIs don't create a WebUI object.
// TODO(crbug.com/40089364): all WebUIs should have a WebUI object.
WebContents* web_contents =
WebContents::FromFrameTreeNodeId(frame_tree_node_id_);
CHECK(web_contents);
GetContentClient()->browser()->LogWebUIUsage(request.url);
return;
}
// We pass the FrameTreeNode ID to get to the WebContents because requests
// from frames can happen while the RFH is changed for a cross-process
// navigation. The URLDataSources just need the WebContents; the specific
// frame doesn't matter.
StartURLLoader(request, frame_tree_node_id_, std::move(client),
browser_context_.get());
}
const std::string& scheme() const { return scheme_; }
WebUIURLLoaderFactory(
BrowserContext* browser_context,
FrameTreeNodeId frame_tree_node_id,
const std::string& scheme,
base::flat_set<std::string> allowed_hosts,
mojo::PendingReceiver<network::mojom::URLLoaderFactory> factory_receiver)
: network::SelfDeletingURLLoaderFactory(std::move(factory_receiver)),
browser_context_(browser_context->GetWeakPtr()),
frame_tree_node_id_(frame_tree_node_id),
scheme_(scheme),
allowed_hosts_(std::move(allowed_hosts)) {}
base::WeakPtr<BrowserContext> browser_context_;
const FrameTreeNodeId frame_tree_node_id_;
const std::string scheme_;
const base::flat_set<std::string> allowed_hosts_; // if empty all allowed.
};
} // namespace
mojo::PendingRemote<network::mojom::URLLoaderFactory>
CreateWebUIURLLoaderFactory(RenderFrameHost* render_frame_host,
const std::string& scheme,
base::flat_set<std::string> allowed_hosts) {
return WebUIURLLoaderFactory::CreateForFrame(
FrameTreeNode::From(render_frame_host), scheme, std::move(allowed_hosts));
}
mojo::PendingRemote<network::mojom::URLLoaderFactory>
CreateWebUIServiceWorkerLoaderFactory(
BrowserContext* browser_context,
const std::string& scheme,
base::flat_set<std::string> allowed_hosts) {
return WebUIURLLoaderFactory::CreateForServiceWorker(
browser_context, scheme, std::move(allowed_hosts));
}
} // namespace content