blob: dca1ddda83bc64203eb79b7252a5efe73a9b77b1 [file] [log] [blame]
// Copyright 2021 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/pdf/browser/plugin_response_writer.h"
#include <memory>
#include <string>
#include <utility>
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "components/pdf/browser/pdf_stream_delegate.h"
#include "mojo/public/c/system/types.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/system/data_pipe.h"
#include "mojo/public/cpp/system/data_pipe_producer.h"
#include "mojo/public/cpp/system/string_data_source.h"
#include "net/base/net_errors.h"
#include "net/http/http_response_headers.h"
#include "pdf/pdf_features.h"
#include "services/network/public/cpp/url_loader_completion_status.h"
#include "services/network/public/mojom/url_loader.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "url/gurl.h"
namespace pdf {
namespace {
// Generates a response ready to be used for creating the PDF loader. The
// returned value is a raw string in which the escape characters are not
// processed.
// Note: This function is security sensitive since it defines the boundary of
// HTML and the embedded PDF. Must limit information shared with the PDF plugin
// process through this response.
std::string GenerateResponse(const PdfStreamDelegate::StreamInfo& stream_info) {
// TODO(crbug.com/40189769): This script in this response is never executed
// when JavaScript is blocked throughout the browser (set in
// chrome://settings/content/javascript). A permanent solution would likely
// have to hook into postMessage() natively.
static constexpr char kResponseTemplate[] = R"(<!DOCTYPE html>
<style>
body,
embed,
html {
height: 100%;
margin: 0;
width: 100%;
}
embed {
left: 0;
position: fixed;
top: 0;
}
/* Hide scrollbars when in Presentation mode. */
.fullscreen {
overflow: hidden;
}
</style>
<div id="sizer"></div>
<embed type="application/x-google-chrome-pdf" src="$1" original-url="$2"
background-color="$4" javascript="$5"$6$7>
<script type="module">
$3
</script>
)";
// TODO(crbug.com/40792950): We should load the injected scripts as network
// resources instead. Until then, feel free to raise this limit as necessary.
if (stream_info.injected_script)
DCHECK_LE(stream_info.injected_script->size(), 16'384u);
return base::ReplaceStringPlaceholders(
kResponseTemplate,
{stream_info.stream_url.spec(), stream_info.original_url.spec(),
stream_info.injected_script ? *stream_info.injected_script : "",
base::NumberToString(stream_info.background_color),
stream_info.allow_javascript ? "allow" : "block",
stream_info.full_frame ? " full-frame" : "",
stream_info.use_skia ? " use-skia" : ""},
/*offsets=*/nullptr);
}
} // namespace
PluginResponseWriter::PluginResponseWriter(
const PdfStreamDelegate::StreamInfo& stream_info,
mojo::PendingRemote<network::mojom::URLLoaderClient> client)
: body_(GenerateResponse(stream_info)),
client_(std::move(client)),
coep_header_(stream_info.coep_header) {}
PluginResponseWriter::~PluginResponseWriter() = default;
void PluginResponseWriter::Start(base::OnceClosure done_callback) {
auto response = network::mojom::URLResponseHead::New();
response->headers =
base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK");
// Allow the PDF plugin to be embedded in cross-origin sites if the original
// PDF has a COEP: require-corp or COEP: credentialless header.
if (chrome_pdf::features::IsOopifPdfEnabled() &&
(coep_header_ == "require-corp" || coep_header_ == "credentialless")) {
response->headers->AddHeader("Cross-Origin-Embedder-Policy", coep_header_);
response->headers->AddHeader("Cross-Origin-Resource-Policy",
"cross-origin");
}
response->mime_type = "text/html";
mojo::ScopedDataPipeProducerHandle producer;
mojo::ScopedDataPipeConsumerHandle consumer;
if (mojo::CreateDataPipe(nullptr, producer, consumer) != MOJO_RESULT_OK) {
client_->OnComplete(
network::URLLoaderCompletionStatus(net::ERR_INSUFFICIENT_RESOURCES));
std::move(done_callback).Run();
return;
}
client_->OnReceiveResponse(std::move(response), std::move(consumer),
std::nullopt);
producer_ = std::make_unique<mojo::DataPipeProducer>(std::move(producer));
// Caller is required to keep `this` alive until `done_callback` is called, so
// `base::Unretained(this)` should be safe.
producer_->Write(
std::make_unique<mojo::StringDataSource>(
body_, mojo::StringDataSource::AsyncWritingMode::
STRING_STAYS_VALID_UNTIL_COMPLETION),
base::BindOnce(&PluginResponseWriter::OnWrite, base::Unretained(this),
std::move(done_callback)));
}
void PluginResponseWriter::OnWrite(base::OnceClosure done_callback,
MojoResult result) {
producer_.reset();
if (result == MOJO_RESULT_OK) {
network::URLLoaderCompletionStatus status(net::OK);
status.encoded_data_length = body_.size();
status.encoded_body_length = body_.size();
status.decoded_body_length = body_.size();
client_->OnComplete(status);
} else {
client_->OnComplete(network::URLLoaderCompletionStatus(net::ERR_FAILED));
}
std::move(done_callback).Run();
}
} // namespace pdf