blob: ff14d90257b320203e7deb72a458d17024273a05 [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 "content/renderer/local_resource_url_loader_factory.h"
#include <cstdint>
#include <memory>
#include <utility>
#include "base/check.h"
#include "base/containers/span.h"
#include "base/memory/ref_counted_memory.h"
#include "base/memory/scoped_refptr.h"
#include "base/notimplemented.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/types/expected.h"
#include "content/common/web_ui_loading_util.h"
#include "content/public/common/content_client.h"
#include "content/public/common/url_constants.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "net/base/mime_util.h"
#include "net/socket/socket.h"
#include "services/network/public/cpp/parsed_headers.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_factory.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/blink/public/mojom/loader/local_resource_loader_config.mojom.h"
#include "ui/base/template_expressions.h"
#include "url/origin.h"
namespace content {
namespace {
std::map<url::Origin, LocalResourceURLLoaderFactory::Source>
ConvertConfigToSourcesMap(blink::mojom::LocalResourceLoaderConfigPtr config) {
std::map<url::Origin, LocalResourceURLLoaderFactory::Source> sources;
// TODO(https://crbug.com/384765582) This manual copy is only necessary
// because ui::ReplaceTemplateExpressions uses an unconventional map type.
// Remove this when that is fixed.
for (const auto& source : config->sources) {
const url::Origin origin = source.first;
const blink::mojom::LocalResourceSourcePtr& mojo_source = source.second;
const std::map<std::string, std::string> replacement_strings(
mojo_source->replacement_strings.begin(),
mojo_source->replacement_strings.end());
LocalResourceURLLoaderFactory::Source local_source(
mojo_source.Clone(), std::move(replacement_strings));
sources.insert({std::move(origin), std::move(local_source)});
}
return sources;
}
} // namespace
LocalResourceURLLoaderFactory::Source::Source(
blink::mojom::LocalResourceSourcePtr source,
std::map<std::string, std::string> replacement_strings)
: source(std::move(source)),
replacement_strings(std::move(replacement_strings)) {}
LocalResourceURLLoaderFactory::Source::Source(Source&& other) = default;
LocalResourceURLLoaderFactory::Source&
LocalResourceURLLoaderFactory::Source::operator=(Source&& other) = default;
LocalResourceURLLoaderFactory::Source::~Source() = default;
LocalResourceURLLoaderFactory::LocalResourceURLLoaderFactory(
blink::mojom::LocalResourceLoaderConfigPtr config,
mojo::PendingRemote<network::mojom::URLLoaderFactory> fallback)
: sources_(base::MakeRefCounted<
base::RefCountedData<std::map<url::Origin, Source>>>(
ConvertConfigToSourcesMap(std::move(config)))),
fallback_(std::move(fallback)) {}
LocalResourceURLLoaderFactory::~LocalResourceURLLoaderFactory() = default;
bool LocalResourceURLLoaderFactory::CanServe(
const network::ResourceRequest& request) const {
const url::Origin origin = url::Origin::Create(request.url);
auto it = sources_->data.find(origin);
// The renderer process may not have metadata for the data source. This can
// happen if the data source isn't a WebUIDataSource, in which case the
// browser process doesn't send metadata for it.
// Example: chrome://theme/colors.css
if (it == sources_->data.end()) {
return false;
}
// Get the resource ID corresponding to the URL path.
const blink::mojom::LocalResourceSourcePtr& source = it->second.source;
std::string_view path = request.url.path_piece().substr(1);
auto resource_it = source->path_to_resource_id_map.find(path);
// The path-to-ID map may not have an entry for the given path. This can
// happen for resources that are generated on-the-fly in the browser process.
// Example: chrome://my-webui/strings.m.js
if (resource_it == source->path_to_resource_id_map.end()) {
return false;
}
int resource_id = resource_it->second;
// Return true if the in-process ResourceBundle has the resource for this ID.
return GetContentClient()->HasDataResource(resource_id);
}
void LocalResourceURLLoaderFactory::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) {
CHECK(fallback_);
if (!CanServe(request)) {
fallback_->CreateLoaderAndStart(std::move(loader), request_id, options,
request, std::move(client),
traffic_annotation);
return;
}
// Only the "chrome" scheme is supported.
CHECK(request.url.scheme() == kChromeUIScheme);
// Parallelize calls to GetResourceAndRespond across multiple threads.
// 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(GetResourceAndRespond, sources_,
request, std::move(client)));
}
void LocalResourceURLLoaderFactory::Clone(
mojo::PendingReceiver<network::mojom::URLLoaderFactory> receiver) {
receivers_.Add(this, std::move(receiver));
}
// static
void LocalResourceURLLoaderFactory::GetResourceAndRespond(
const scoped_refptr<base::RefCountedData<std::map<url::Origin, Source>>>
sources,
const network::ResourceRequest& request,
mojo::PendingRemote<network::mojom::URLLoaderClient> client) {
const url::Origin origin = url::Origin::Create(request.url);
auto it = sources->data.find(origin);
// CanServe should have been called before this point, which would have
// confirmed that there exists a source corresponding to the URL origin.
CHECK(it != sources->data.end());
const blink::mojom::LocalResourceSourcePtr& source = it->second.source;
const std::map<std::string, std::string>& replacement_strings =
it->second.replacement_strings;
// Get resource id.
std::string_view path = request.url.path_piece().substr(1);
auto resource_it = source->path_to_resource_id_map.find(path);
// CanServe should have been called before this point, which would have
// confirmed that there exists a resource ID corresponding to the URL path.
CHECK(resource_it != source->path_to_resource_id_map.end());
int resource_id = resource_it->second;
// Load bytes.
scoped_refptr<base::RefCountedMemory> raw_bytes =
GetContentClient()->GetDataResourceBytes(resource_id);
// CanServe should have been called before this point, which would have
// confirmed that the ResourceBundle will return non-null for the given
// resource ID.
CHECK(raw_bytes);
std::string_view bytes(base::as_string_view(*raw_bytes));
auto url_response_head = network::mojom::URLResponseHead::New();
// Mime type.
std::string mime_type;
if (net::GetMimeTypeFromFile(
base::FilePath::FromASCII(request.url.ExtractFileName()),
&mime_type)) {
url_response_head->mime_type = mime_type;
} else {
url_response_head->mime_type = "text/html";
}
scoped_refptr<base::RefCountedMemory> bytes_after_replacement = raw_bytes;
if (source->replacement_strings.size() > 0 &&
(url_response_head->mime_type == "text/html" ||
url_response_head->mime_type == "text/css" ||
(source->should_replace_i18n_in_js &&
url_response_head->mime_type == "text/javascript"))) {
std::string replaced_string;
if (url_response_head->mime_type == "text/javascript") {
CHECK(ui::ReplaceTemplateExpressionsInJS(bytes, replacement_strings,
&replaced_string));
} else {
replaced_string =
ui::ReplaceTemplateExpressions(bytes, replacement_strings);
}
bytes_after_replacement = base::MakeRefCounted<base::RefCountedString>(
std::move(replaced_string));
}
// Other headers.
scoped_refptr<net::HttpResponseHeaders> headers =
base::MakeRefCounted<net::HttpResponseHeaders>(source->headers);
headers->SetHeader(net::HttpRequestHeaders::kContentType, mime_type);
url_response_head->headers = headers;
url_response_head->parsed_headers = network::PopulateParsedHeaders(
url_response_head->headers.get(), request.url);
// Handle Range header if request.
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),
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;
webui::SendData(std::move(url_response_head), std::move(client), maybe_range,
bytes_after_replacement);
}
} // namespace content