Commit 6dc6ba26 authored by Chris Mumford's avatar Chris Mumford Committed by Commit Bot

Fix loading of file://.../*.svgz files w/network service.

Chrome supports loading gzipped SVG files from the file scheme
which was unsupported with the network service implementation.

Note: The non-network service implementation of *.svgz loading
is currently broken on Linux, but works on Windows and macOS.
This change works on both platforms.

Bug: 857117
Change-Id: Id90deba4761ea67d4ac7bef681051ea9326e113b
Reviewed-on: https://chromium-review.googlesource.com/c/1268960
Commit-Queue: Chris Mumford <cmumford@chromium.org>
Reviewed-by: default avatarMatt Menke <mmenke@chromium.org>
Reviewed-by: default avatarAlex Moshchuk <alexmos@chromium.org>
Cr-Commit-Position: refs/heads/master@{#605136}
parent 9f440c86
......@@ -615,7 +615,11 @@ class FileURLLoader : public network::mojom::URLLoader {
total_bytes_to_send -= write_size;
}
if (!net::GetMimeTypeFromFile(path, &head.mime_type)) {
if (path.MatchesExtension(FILE_PATH_LITERAL(".svgz"))) {
head.mime_type = "image/svg+xml";
// Don't know what the uncompressed content length will be.
head.content_length = -1;
} else if (!net::GetMimeTypeFromFile(path, &head.mime_type)) {
net::SniffMimeType(
initial_read_buffer, initial_read_result, request.url, head.mime_type,
GetContentClient()->browser()->ForceSniffingFileUrlsForHtml()
......@@ -624,7 +628,7 @@ class FileURLLoader : public network::mojom::URLLoader {
&head.mime_type);
head.did_mime_sniff = true;
}
if (head.headers) {
if (head.headers && !head.mime_type.empty()) {
head.headers->AddHeader(
base::StringPrintf("%s: %s", net::HttpRequestHeaders::kContentType,
head.mime_type.c_str()));
......
......@@ -13,6 +13,7 @@
#include "content/public/renderer/content_renderer_client.h"
#include "content/renderer/loader/resource_dispatcher.h"
#include "content/renderer/loader/url_response_body_consumer.h"
#include "net/base/filename_util.h"
#include "net/url_request/redirect_info.h"
#include "services/network/public/cpp/features.h"
......@@ -308,8 +309,14 @@ void URLLoaderClientImpl::OnStartLoadingResponseBody(
return;
}
// Special handling for *.svgz files loaded from the file scheme.
base::FilePath file_path;
bool inflate_response =
net::FileURLToFilePath(last_loaded_url_, &file_path) &&
file_path.MatchesExtension(FILE_PATH_LITERAL(".svgz"));
body_consumer_ = new URLResponseBodyConsumer(
request_id_, resource_dispatcher_, std::move(body), task_runner_);
request_id_, resource_dispatcher_, std::move(body), inflate_response,
task_runner_);
if (NeedsStoringMessage()) {
body_consumer_->SetDefersLoading();
......
......@@ -9,30 +9,56 @@
#include "base/macros.h"
#include "content/public/renderer/request_peer.h"
#include "content/renderer/loader/resource_dispatcher.h"
#include "net/base/io_buffer.h"
#include "net/filter/zlib_stream_wrapper.h"
#include "services/network/public/cpp/url_loader_completion_status.h"
namespace content {
constexpr uint32_t URLResponseBodyConsumer::kMaxNumConsumedBytesInTask;
const size_t kInflateBufferSize = 4 * 1024;
class URLResponseBodyConsumer::ReclaimAccountant
: public base::RefCounted<URLResponseBodyConsumer::ReclaimAccountant> {
public:
explicit ReclaimAccountant(scoped_refptr<URLResponseBodyConsumer> consumer)
: consumer_(consumer) {}
void Reclaim(uint32_t reclaim_bytes) { reclaim_length_ += reclaim_bytes; }
private:
friend class base::RefCounted<URLResponseBodyConsumer::ReclaimAccountant>;
~ReclaimAccountant() { consumer_->Reclaim(reclaim_length_); }
scoped_refptr<URLResponseBodyConsumer> consumer_;
uint32_t reclaim_length_ = 0;
DISALLOW_COPY_AND_ASSIGN(ReclaimAccountant);
};
class URLResponseBodyConsumer::ReceivedData final
: public RequestPeer::ReceivedData {
public:
ReceivedData(const char* payload,
int length,
scoped_refptr<URLResponseBodyConsumer> consumer)
: payload_(payload), length_(length), consumer_(consumer) {}
int payload_length,
int reclaim_length,
scoped_refptr<ReclaimAccountant> reclaim_accountant)
: payload_(payload),
payload_length_(payload_length),
reclaim_length_(reclaim_length),
reclaim_accountant_(reclaim_accountant) {}
~ReceivedData() override { consumer_->Reclaim(length_); }
~ReceivedData() override { reclaim_accountant_->Reclaim(reclaim_length_); }
const char* payload() const override { return payload_; }
int length() const override { return length_; }
int length() const override { return payload_length_; }
private:
const char* const payload_;
const uint32_t length_;
scoped_refptr<URLResponseBodyConsumer> consumer_;
const uint32_t payload_length_;
const uint32_t reclaim_length_;
scoped_refptr<ReclaimAccountant> reclaim_accountant_;
DISALLOW_COPY_AND_ASSIGN(ReceivedData);
};
......@@ -41,6 +67,7 @@ URLResponseBodyConsumer::URLResponseBodyConsumer(
int request_id,
ResourceDispatcher* resource_dispatcher,
mojo::ScopedDataPipeConsumerHandle handle,
bool inflate_response,
scoped_refptr<base::SingleThreadTaskRunner> task_runner)
: request_id_(request_id),
resource_dispatcher_(resource_dispatcher),
......@@ -49,7 +76,19 @@ URLResponseBodyConsumer::URLResponseBodyConsumer(
mojo::SimpleWatcher::ArmingPolicy::MANUAL,
task_runner),
task_runner_(task_runner),
zlib_wrapper_(inflate_response
? new net::ZLibStreamWrapper(
net::ZLibStreamWrapper::SourceType::kGzip)
: nullptr),
inflate_buffer_(inflate_response ? new net::IOBuffer(kInflateBufferSize)
: nullptr),
has_seen_end_of_data_(!handle_.is_valid()) {
if (zlib_wrapper_ && !zlib_wrapper_->Init()) {
// If zlib can't be initialized then release the wrapper which will result
// in the compressed response being received and unable to be processed.
zlib_wrapper_.release();
inflate_buffer_.reset();
}
handle_watcher_.Watch(
handle_.get(), MOJO_HANDLE_SIGNAL_READABLE,
base::Bind(&URLResponseBodyConsumer::OnReadable, base::Unretained(this)));
......@@ -146,8 +185,42 @@ void URLResponseBodyConsumer::OnReadable(MojoResult unused) {
resource_dispatcher_->GetPendingRequestInfo(request_id_);
DCHECK(request_info);
scoped_refptr<ReclaimAccountant> reclaim_accountant =
new ReclaimAccountant(this);
if (zlib_wrapper_) {
const char* input_buffer_ptr = static_cast<const char*>(buffer);
int input_buffer_available = available;
while (input_buffer_available) {
scoped_refptr<net::WrappedIOBuffer> input_buffer =
new net::WrappedIOBuffer(input_buffer_ptr);
int consumed_bytes = 0;
int output_bytes_written = zlib_wrapper_->FilterData(
inflate_buffer_.get(), kInflateBufferSize, input_buffer.get(),
input_buffer_available, &consumed_bytes, has_seen_end_of_data_);
if (output_bytes_written < 0) {
status_.error_code = output_bytes_written;
has_seen_end_of_data_ = true;
has_received_completion_ = true;
NotifyCompletionIfAppropriate();
return;
}
input_buffer_ptr += consumed_bytes;
input_buffer_available -= consumed_bytes;
DCHECK_GE(input_buffer_available, 0);
if (output_bytes_written > 0) {
request_info->peer->OnReceivedData(std::make_unique<ReceivedData>(
inflate_buffer_->data(), output_bytes_written, consumed_bytes,
reclaim_accountant));
}
}
} else {
request_info->peer->OnReceivedData(std::make_unique<ReceivedData>(
static_cast<const char*>(buffer), available, this));
static_cast<const char*>(buffer), available, available,
reclaim_accountant));
}
reclaim_accountant.reset();
}
}
......
......@@ -9,7 +9,6 @@
#include <stdint.h>
#include <utility>
#include <vector>
#include "base/macros.h"
#include "base/memory/ref_counted.h"
......@@ -20,6 +19,11 @@
#include "mojo/public/cpp/system/simple_watcher.h"
#include "services/network/public/mojom/url_loader.mojom.h"
namespace net {
class IOBuffer;
class ZLibStreamWrapper;
} // namespace net
namespace network {
struct URLLoaderCompletionStatus;
} // namespace network
......@@ -34,10 +38,13 @@ class CONTENT_EXPORT URLResponseBodyConsumer final
: public base::RefCounted<URLResponseBodyConsumer>,
public base::SupportsWeakPtr<URLResponseBodyConsumer> {
public:
// If |inflate_response| is true then the response body will be inflated
// using zlib.
URLResponseBodyConsumer(
int request_id,
ResourceDispatcher* resource_dispatcher,
mojo::ScopedDataPipeConsumerHandle handle,
bool inflate_response,
scoped_refptr<base::SingleThreadTaskRunner> task_runner);
// Sets the completion status. The completion status is dispatched to the
......@@ -71,6 +78,8 @@ class CONTENT_EXPORT URLResponseBodyConsumer final
~URLResponseBodyConsumer();
class ReceivedData;
class ReclaimAccountant;
void Reclaim(uint32_t size);
void NotifyCompletionIfAppropriate();
......@@ -81,6 +90,10 @@ class CONTENT_EXPORT URLResponseBodyConsumer final
mojo::SimpleWatcher handle_watcher_;
network::URLLoaderCompletionStatus status_;
scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
// zlib_wrapper_ and inflate_buffer_ will only be non-null when inflating
// the response body.
std::unique_ptr<net::ZLibStreamWrapper> zlib_wrapper_;
scoped_refptr<net::IOBuffer> inflate_buffer_;
bool has_received_completion_ = false;
bool has_been_cancelled_ = false;
......
......@@ -15,6 +15,7 @@
#include "content/renderer/loader/request_extra_data.h"
#include "content/renderer/loader/resource_dispatcher.h"
#include "net/base/request_priority.h"
#include "net/filter/filter_source_stream_test_util.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/url_loader_completion_status.h"
......@@ -183,6 +184,7 @@ TEST_F(URLResponseBodyConsumerTest, ReceiveData) {
scoped_refptr<URLResponseBodyConsumer> consumer(new URLResponseBodyConsumer(
request_id, dispatcher_.get(), std::move(data_pipe.consumer_handle),
/*inflate_response=*/false,
blink::scheduler::GetSingleThreadTaskRunnerForTesting()));
consumer->ArmOrNotify();
......@@ -200,6 +202,77 @@ TEST_F(URLResponseBodyConsumerTest, ReceiveData) {
EXPECT_EQ("hello", context.data);
}
TEST_F(URLResponseBodyConsumerTest, ReceiveValidGzipData) {
TestRequestPeer::Context context;
std::unique_ptr<network::ResourceRequest> request(CreateResourceRequest());
int request_id = SetUpRequestPeer(std::move(request), &context);
mojo::DataPipe data_pipe(CreateDataPipeOptions());
scoped_refptr<URLResponseBodyConsumer> consumer(new URLResponseBodyConsumer(
request_id, dispatcher_.get(), std::move(data_pipe.consumer_handle),
/*inflate_response=*/true,
blink::scheduler::GetSingleThreadTaskRunnerForTesting()));
consumer->ArmOrNotify();
mojo::ScopedDataPipeProducerHandle writer =
std::move(data_pipe.producer_handle);
const std::string uncompressed_data = "hello";
size_t compressed_data_len = 2048;
auto compressed_data = std::make_unique<char[]>(compressed_data_len);
net::CompressGzip(uncompressed_data.data(), uncompressed_data.size(),
compressed_data.get(), &compressed_data_len,
/*gzip_framing=*/true);
uint32_t num_bytes = compressed_data_len;
MojoResult result =
writer->WriteData(compressed_data.get(), &num_bytes, kNone);
ASSERT_EQ(MOJO_RESULT_OK, result);
ASSERT_EQ(compressed_data_len, num_bytes);
Run(&context);
EXPECT_FALSE(context.complete);
EXPECT_EQ(uncompressed_data, context.data);
}
TEST_F(URLResponseBodyConsumerTest, ReceiveInvalidGzipData) {
TestRequestPeer::Context context;
std::unique_ptr<network::ResourceRequest> request(CreateResourceRequest());
int request_id = SetUpRequestPeer(std::move(request), &context);
mojo::DataPipe data_pipe(CreateDataPipeOptions());
scoped_refptr<URLResponseBodyConsumer> consumer(new URLResponseBodyConsumer(
request_id, dispatcher_.get(), std::move(data_pipe.consumer_handle),
/*inflate_response=*/true,
blink::scheduler::GetSingleThreadTaskRunnerForTesting()));
consumer->ArmOrNotify();
mojo::ScopedDataPipeProducerHandle writer =
std::move(data_pipe.producer_handle);
const std::string uncompressed_data = "hello";
size_t compressed_data_len = 2048;
auto compressed_data = std::make_unique<char[]>(compressed_data_len);
net::CompressGzip(uncompressed_data.data(), uncompressed_data.size(),
compressed_data.get(), &compressed_data_len,
/*gzip_framing=*/true);
// Corrupt the Gzip data stream.
compressed_data[1] = 'A';
uint32_t num_bytes = compressed_data_len;
MojoResult result =
writer->WriteData(compressed_data.get(), &num_bytes, kNone);
ASSERT_EQ(MOJO_RESULT_OK, result);
ASSERT_EQ(compressed_data_len, num_bytes);
Run(&context);
EXPECT_TRUE(context.complete);
EXPECT_EQ("", context.data);
}
TEST_F(URLResponseBodyConsumerTest, OnCompleteThenClose) {
TestRequestPeer::Context context;
std::unique_ptr<network::ResourceRequest> request(CreateResourceRequest());
......@@ -208,6 +281,7 @@ TEST_F(URLResponseBodyConsumerTest, OnCompleteThenClose) {
scoped_refptr<URLResponseBodyConsumer> consumer(new URLResponseBodyConsumer(
request_id, dispatcher_.get(), std::move(data_pipe.consumer_handle),
/*inflate_response=*/false,
blink::scheduler::GetSingleThreadTaskRunnerForTesting()));
consumer->ArmOrNotify();
......@@ -243,6 +317,7 @@ TEST_F(URLResponseBodyConsumerTest, OnCompleteThenCloseWithAsyncRelease) {
scoped_refptr<URLResponseBodyConsumer> consumer(new URLResponseBodyConsumer(
request_id, dispatcher_.get(), std::move(data_pipe.consumer_handle),
/*inflate_response=*/false,
blink::scheduler::GetSingleThreadTaskRunnerForTesting()));
consumer->ArmOrNotify();
......@@ -275,6 +350,7 @@ TEST_F(URLResponseBodyConsumerTest, CloseThenOnComplete) {
scoped_refptr<URLResponseBodyConsumer> consumer(new URLResponseBodyConsumer(
request_id, dispatcher_.get(), std::move(data_pipe.consumer_handle),
/*inflate_response=*/false,
blink::scheduler::GetSingleThreadTaskRunnerForTesting()));
consumer->ArmOrNotify();
......@@ -318,6 +394,7 @@ TEST_F(URLResponseBodyConsumerTest, TooBigChunkShouldBeSplit) {
scoped_refptr<URLResponseBodyConsumer> consumer(new URLResponseBodyConsumer(
request_id, dispatcher_.get(), std::move(data_pipe.consumer_handle),
/*inflate_response=*/false,
blink::scheduler::GetSingleThreadTaskRunnerForTesting()));
consumer->ArmOrNotify();
......
......@@ -732,6 +732,8 @@ component("net") {
"filter/source_stream.cc",
"filter/source_stream.h",
"filter/source_stream_type_list.h",
"filter/zlib_stream_wrapper.cc",
"filter/zlib_stream_wrapper.h",
"http/bidirectional_stream.cc",
"http/bidirectional_stream.h",
"http/bidirectional_stream_impl.cc",
......@@ -4779,6 +4781,7 @@ test("net_unittests") {
"filter/brotli_source_stream_unittest.cc",
"filter/filter_source_stream_unittest.cc",
"filter/gzip_source_stream_unittest.cc",
"filter/zlib_stream_wrapper_unittest.cc",
"ftp/ftp_auth_cache_unittest.cc",
"ftp/ftp_ctrl_response_buffer_unittest.cc",
"ftp/ftp_directory_listing_parser_ls_unittest.cc",
......
......@@ -4,14 +4,8 @@
#include "net/filter/gzip_source_stream.h"
#include <algorithm>
#include <utility>
#include "base/bind.h"
#include "base/bit_cast.h"
#include "base/logging.h"
#include "base/memory/ref_counted.h"
#include "net/base/io_buffer.h"
#include "third_party/zlib/zlib.h"
namespace net {
......@@ -21,20 +15,9 @@ namespace {
const char kDeflate[] = "DEFLATE";
const char kGzip[] = "GZIP";
// For deflate streams, if more than this many bytes have been received without
// an error and without adding a Zlib header, assume the original stream had a
// Zlib header. In practice, don't need nearly this much data, but since the
// detection logic is a heuristic, best to be safe. Data is freed once it's been
// determined whether the stream has a zlib header or not, so larger values
// shouldn't affect memory usage, in practice.
const int kMaxZlibHeaderSniffBytes = 1000;
} // namespace
GzipSourceStream::~GzipSourceStream() {
if (zlib_stream_)
inflateEnd(zlib_stream_.get());
}
GzipSourceStream::~GzipSourceStream() = default;
std::unique_ptr<GzipSourceStream> GzipSourceStream::Create(
std::unique_ptr<SourceStream> upstream,
......@@ -51,24 +34,12 @@ std::unique_ptr<GzipSourceStream> GzipSourceStream::Create(
GzipSourceStream::GzipSourceStream(std::unique_ptr<SourceStream> upstream,
SourceStream::SourceType type)
: FilterSourceStream(type, std::move(upstream)),
gzip_footer_bytes_left_(0),
input_state_(STATE_START),
replay_state_(STATE_COMPRESSED_BODY) {}
zlib_stream_(type == TYPE_DEFLATE
? ZLibStreamWrapper::SourceType::kDeflate
: ZLibStreamWrapper::SourceType::kGzip) {}
bool GzipSourceStream::Init() {
zlib_stream_.reset(new z_stream);
if (!zlib_stream_)
return false;
memset(zlib_stream_.get(), 0, sizeof(z_stream));
int ret;
if (type() == TYPE_GZIP) {
ret = inflateInit2(zlib_stream_.get(), -MAX_WBITS);
} else {
ret = inflateInit(zlib_stream_.get());
}
DCHECK_NE(Z_VERSION_ERROR, ret);
return ret == Z_OK;
return zlib_stream_.Init();
}
std::string GzipSourceStream::GetTypeAsString() const {
......@@ -89,178 +60,9 @@ int GzipSourceStream::FilterData(IOBuffer* output_buffer,
int input_buffer_size,
int* consumed_bytes,
bool upstream_end_reached) {
*consumed_bytes = 0;
char* input_data = input_buffer->data();
int input_data_size = input_buffer_size;
int bytes_out = 0;
bool state_compressed_entered = false;
while (input_data_size > 0 && bytes_out < output_buffer_size) {
InputState state = input_state_;
switch (state) {
case STATE_START: {
if (type() == TYPE_DEFLATE) {
input_state_ = STATE_SNIFFING_DEFLATE_HEADER;
break;
}
DCHECK_LT(0, input_data_size);
input_state_ = STATE_GZIP_HEADER;
break;
}
case STATE_GZIP_HEADER: {
DCHECK_NE(TYPE_DEFLATE, type());
const size_t kGzipFooterBytes = 8;
const char* end = nullptr;
GZipHeader::Status status =
gzip_header_.ReadMore(input_data, input_data_size, &end);
if (status == GZipHeader::INCOMPLETE_HEADER) {
input_data += input_data_size;
input_data_size = 0;
} else if (status == GZipHeader::COMPLETE_HEADER) {
// If there is a valid header, there should also be a valid footer.
gzip_footer_bytes_left_ = kGzipFooterBytes;
int bytes_consumed = end - input_data;
input_data += bytes_consumed;
input_data_size -= bytes_consumed;
input_state_ = STATE_COMPRESSED_BODY;
} else if (status == GZipHeader::INVALID_HEADER) {
return ERR_CONTENT_DECODING_FAILED;
}
break;
}
case STATE_SNIFFING_DEFLATE_HEADER: {
DCHECK_EQ(TYPE_DEFLATE, type());
zlib_stream_.get()->next_in = bit_cast<Bytef*>(input_data);
zlib_stream_.get()->avail_in = input_data_size;
zlib_stream_.get()->next_out = bit_cast<Bytef*>(output_buffer->data());
zlib_stream_.get()->avail_out = output_buffer_size;
int ret = inflate(zlib_stream_.get(), Z_NO_FLUSH);
// On error, try adding a zlib header and replaying the response. Note
// that data just received doesn't have to be replayed, since it hasn't
// been removed from input_data yet, only data from previous FilterData
// calls needs to be replayed.
if (ret != Z_STREAM_END && ret != Z_OK) {
if (!InsertZlibHeader())
return ERR_CONTENT_DECODING_FAILED;
input_state_ = STATE_REPLAY_DATA;
// |replay_state_| should still have its initial value.
DCHECK_EQ(STATE_COMPRESSED_BODY, replay_state_);
break;
}
int bytes_used = input_data_size - zlib_stream_.get()->avail_in;
bytes_out = output_buffer_size - zlib_stream_.get()->avail_out;
// If any bytes are output, enough total bytes have been received, or at
// the end of the stream, assume the response had a valid Zlib header.
if (bytes_out > 0 ||
bytes_used + replay_data_.size() >= kMaxZlibHeaderSniffBytes ||
ret == Z_STREAM_END) {
replay_data_.clear();
if (ret == Z_STREAM_END) {
input_state_ = STATE_GZIP_FOOTER;
} else {
input_state_ = STATE_COMPRESSED_BODY;
}
} else {
replay_data_.append(input_data, bytes_used);
}
input_data_size -= bytes_used;
input_data += bytes_used;
break;
}
case STATE_REPLAY_DATA: {
DCHECK_EQ(TYPE_DEFLATE, type());
if (replay_data_.empty()) {
input_state_ = replay_state_;
break;
}
// Call FilterData recursively, after updating |input_state_|, with
// |replay_data_|. This recursive call makes handling data from
// |replay_data_| and |input_buffer| much simpler than the alternative
// operations, though it's not pretty.
input_state_ = replay_state_;
int bytes_used;
scoped_refptr<IOBuffer> replay_buffer =
base::MakeRefCounted<WrappedIOBuffer>(replay_data_.data());
int result =
FilterData(output_buffer, output_buffer_size, replay_buffer.get(),
replay_data_.size(), &bytes_used, upstream_end_reached);
replay_data_.erase(0, bytes_used);
// Back up resulting state, and return state to STATE_REPLAY_DATA.
replay_state_ = input_state_;
input_state_ = STATE_REPLAY_DATA;
// On error, or if bytes were read, just return result immediately.
// Could continue consuming data in the success case, but simplest not
// to.
if (result != 0)
return result;
break;
}
case STATE_COMPRESSED_BODY: {
DCHECK(!state_compressed_entered);
DCHECK_LE(0, input_data_size);
state_compressed_entered = true;
zlib_stream_.get()->next_in = bit_cast<Bytef*>(input_data);
zlib_stream_.get()->avail_in = input_data_size;
zlib_stream_.get()->next_out = bit_cast<Bytef*>(output_buffer->data());
zlib_stream_.get()->avail_out = output_buffer_size;
int ret = inflate(zlib_stream_.get(), Z_NO_FLUSH);
if (ret != Z_STREAM_END && ret != Z_OK)
return ERR_CONTENT_DECODING_FAILED;
int bytes_used = input_data_size - zlib_stream_.get()->avail_in;
bytes_out = output_buffer_size - zlib_stream_.get()->avail_out;
input_data_size -= bytes_used;
input_data += bytes_used;
if (ret == Z_STREAM_END)
input_state_ = STATE_GZIP_FOOTER;
// zlib has written as much data to |output_buffer| as it could.
// There might still be some unconsumed data in |input_buffer| if there
// is no space in |output_buffer|.
break;
}
case STATE_GZIP_FOOTER: {
size_t to_read = std::min(gzip_footer_bytes_left_,
base::checked_cast<size_t>(input_data_size));
gzip_footer_bytes_left_ -= to_read;
input_data_size -= to_read;
input_data += to_read;
if (gzip_footer_bytes_left_ == 0)
input_state_ = STATE_IGNORING_EXTRA_BYTES;
break;
}
case STATE_IGNORING_EXTRA_BYTES: {
input_data_size = 0;
break;
}
}
}
*consumed_bytes = input_buffer_size - input_data_size;
return bytes_out;
}
bool GzipSourceStream::InsertZlibHeader() {
char dummy_header[] = {0x78, 0x01};
char dummy_output[4];
inflateReset(zlib_stream_.get());
zlib_stream_.get()->next_in = bit_cast<Bytef*>(&dummy_header[0]);
zlib_stream_.get()->avail_in = sizeof(dummy_header);
zlib_stream_.get()->next_out = bit_cast<Bytef*>(&dummy_output[0]);
zlib_stream_.get()->avail_out = sizeof(dummy_output);
int ret = inflate(zlib_stream_.get(), Z_NO_FLUSH);
return ret == Z_OK;
return zlib_stream_.FilterData(output_buffer, output_buffer_size,
input_buffer, input_buffer_size,
consumed_bytes, upstream_end_reached);
}
} // namespace net
......@@ -11,9 +11,7 @@
#include "base/macros.h"
#include "net/base/net_export.h"
#include "net/filter/filter_source_stream.h"
#include "net/filter/gzip_header.h"
typedef struct z_stream_s z_stream;
#include "net/filter/zlib_stream_wrapper.h"
namespace net {
......@@ -36,33 +34,6 @@ class NET_EXPORT_PRIVATE GzipSourceStream : public FilterSourceStream {
SourceStream::SourceType type);
private:
enum InputState {
// Starts processing the input stream. Checks whether the stream is valid
// and whether a fallback to plain data is needed.
STATE_START,
// Gzip header of the input stream is being processed.
STATE_GZIP_HEADER,
// Deflate responses may or may not have a zlib header. In this state until
// enough has been inflated that this stream most likely has a zlib header,
// or until a zlib header has been added. Data is appended to |replay_data_|
// in case it needs to be replayed after adding a header.
STATE_SNIFFING_DEFLATE_HEADER,
// If a zlib header has to be added to the response, this state will replay
// data passed to inflate before it was determined that no zlib header was
// present.
// See https://crbug.com/677001
STATE_REPLAY_DATA,
// The input stream is being decoded.
STATE_COMPRESSED_BODY,
// Gzip footer of the input stream is being processed.
STATE_GZIP_FOOTER,
// The end of the gzipped body has been reached. If any extra bytes are
// received, just silently ignore them. Doing this, rather than failing the
// request or passing the extra bytes alone with the rest of the response
// body, matches the behavior of other browsers.
STATE_IGNORING_EXTRA_BYTES,
};
GzipSourceStream(std::unique_ptr<SourceStream> previous,
SourceStream::SourceType type);
......@@ -80,33 +51,7 @@ class NET_EXPORT_PRIVATE GzipSourceStream : public FilterSourceStream {
int* consumed_bytes,
bool upstream_end_reached) override;
// Inserts a zlib header to the data stream before calling zlib inflate.
// This is used to work around server bugs. The function returns true on
// success.
bool InsertZlibHeader();
// The control block of zlib which actually does the decoding.
// This data structure is initialized by Init and updated only by
// FilterData(), with InsertZlibHeader() being the exception as a workaround.
std::unique_ptr<z_stream> zlib_stream_;
// While in STATE_SNIFFING_DEFLATE_HEADER, it may be determined that a zlib
// header needs to be added, and all received data needs to be replayed. In
// that case, this buffer holds the data to be replayed.
std::string replay_data_;
// Used to parse the gzip header in gzip stream.
// It is used when the decoding mode is GZIP_SOURCE_STREAM_GZIP.
GZipHeader gzip_header_;
// Tracks how many bytes of gzip footer are yet to be filtered.
size_t gzip_footer_bytes_left_;
// Tracks the state of the input stream.
InputState input_state_;
// Used when replaying data.
InputState replay_state_;
ZLibStreamWrapper zlib_stream_;
DISALLOW_COPY_AND_ASSIGN(GzipSourceStream);
};
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/filter/zlib_stream_wrapper.h"
#include <algorithm>
#include "base/bit_cast.h"
#include "base/logging.h"
#include "net/base/io_buffer.h"
#include "net/base/net_errors.h"
namespace net {
namespace {
// For deflate streams, if more than this many bytes have been received without
// an error and without adding a Zlib header, assume the original stream had a
// Zlib header. In practice, don't need nearly this much data, but since the
// detection logic is a heuristic, best to be safe. Data is freed once it's been
// determined whether the stream has a zlib header or not, so larger values
// shouldn't affect memory usage, in practice.
const int kMaxZlibHeaderSniffBytes = 1000;
} // anonymous namespace
ZLibStreamWrapper::ZLibStreamWrapper(SourceType type)
: type_(type),
input_state_(STATE_START),
replay_state_(STATE_COMPRESSED_BODY),
gzip_footer_bytes_left_(0) {}
bool ZLibStreamWrapper::Init() {
memset(&zlib_stream_, 0, sizeof(z_stream));
int ret;
if (type_ == SourceType::kGzip) {
ret = inflateInit2(&zlib_stream_, -MAX_WBITS);
} else {
ret = inflateInit(&zlib_stream_);
}
DCHECK_NE(Z_VERSION_ERROR, ret);
return ret == Z_OK;
}
ZLibStreamWrapper::~ZLibStreamWrapper() {
inflateEnd(&zlib_stream_);
}
bool ZLibStreamWrapper::InsertZlibHeader() {
char dummy_header[] = {0x78, 0x01};
char dummy_output[4];
inflateReset(&zlib_stream_);
zlib_stream_.next_in = bit_cast<Bytef*>(&dummy_header[0]);
zlib_stream_.avail_in = sizeof(dummy_header);
zlib_stream_.next_out = bit_cast<Bytef*>(&dummy_output[0]);
zlib_stream_.avail_out = sizeof(dummy_output);
int ret = inflate(&zlib_stream_, Z_NO_FLUSH);
return ret == Z_OK;
}
int ZLibStreamWrapper::FilterData(IOBuffer* output_buffer,
int output_buffer_size,
IOBuffer* input_buffer,
int input_buffer_size,
int* consumed_bytes,
bool upstream_end_reached) {
*consumed_bytes = 0;
char* input_data = input_buffer->data();
int input_data_size = input_buffer_size;
int bytes_out = 0;
bool state_compressed_entered = false;
while (input_data_size > 0 && bytes_out < output_buffer_size) {
InputState state = input_state_;
switch (state) {
case STATE_START: {
if (type_ == SourceType::kDeflate) {
input_state_ = STATE_SNIFFING_DEFLATE_HEADER;
break;
}
DCHECK_LT(0, input_data_size);
input_state_ = STATE_GZIP_HEADER;
break;
}
case STATE_GZIP_HEADER: {
DCHECK_NE(SourceType::kDeflate, type_);
const size_t kGzipFooterBytes = 8;
const char* end = nullptr;
GZipHeader::Status status =
gzip_header_.ReadMore(input_data, input_data_size, &end);
if (status == GZipHeader::INCOMPLETE_HEADER) {
input_data += input_data_size;
input_data_size = 0;
} else if (status == GZipHeader::COMPLETE_HEADER) {
// If there is a valid header, there should also be a valid footer.
gzip_footer_bytes_left_ = kGzipFooterBytes;
int bytes_consumed = end - input_data;
input_data += bytes_consumed;
input_data_size -= bytes_consumed;
input_state_ = STATE_COMPRESSED_BODY;
} else if (status == GZipHeader::INVALID_HEADER) {
return ERR_CONTENT_DECODING_FAILED;
}
break;
}
case STATE_SNIFFING_DEFLATE_HEADER: {
DCHECK_EQ(SourceType::kDeflate, type_);
zlib_stream_.next_in = bit_cast<Bytef*>(input_data);
zlib_stream_.avail_in = input_data_size;
zlib_stream_.next_out = bit_cast<Bytef*>(output_buffer->data());
zlib_stream_.avail_out = output_buffer_size;
int ret = inflate(&zlib_stream_, Z_NO_FLUSH);
// On error, try adding a zlib header and replaying the response. Note
// that data just received doesn't have to be replayed, since it hasn't
// been removed from input_data yet, only data from previous FilterData
// calls needs to be replayed.
if (ret != Z_STREAM_END && ret != Z_OK) {
if (!InsertZlibHeader())
return ERR_CONTENT_DECODING_FAILED;
input_state_ = STATE_REPLAY_DATA;
// |replay_state_| should still have its initial value.
DCHECK_EQ(STATE_COMPRESSED_BODY, replay_state_);
break;
}
int bytes_used = input_data_size - zlib_stream_.avail_in;
bytes_out = output_buffer_size - zlib_stream_.avail_out;
// If any bytes are output, enough total bytes have been received, or at
// the end of the stream, assume the response had a valid Zlib header.
if (bytes_out > 0 ||
bytes_used + replay_data_.size() >= kMaxZlibHeaderSniffBytes ||
ret == Z_STREAM_END) {
replay_data_.clear();
if (ret == Z_STREAM_END) {
input_state_ = STATE_GZIP_FOOTER;
} else {
input_state_ = STATE_COMPRESSED_BODY;
}
} else {
replay_data_.append(input_data, bytes_used);
}
input_data_size -= bytes_used;
input_data += bytes_used;
break;
}
case STATE_REPLAY_DATA: {
DCHECK_EQ(SourceType::kDeflate, type_);
if (replay_data_.empty()) {
input_state_ = replay_state_;
break;
}
// Call FilterData recursively, after updating |input_state_|, with
// |replay_data_|. This recursive call makes handling data from
// |replay_data_| and |input_buffer| much simpler than the alternative
// operations, though it's not pretty.
input_state_ = replay_state_;
int bytes_used;
scoped_refptr<IOBuffer> replay_buffer =
base::MakeRefCounted<WrappedIOBuffer>(replay_data_.data());
int result =
FilterData(output_buffer, output_buffer_size, replay_buffer.get(),
replay_data_.size(), &bytes_used, upstream_end_reached);
replay_data_.erase(0, bytes_used);
// Back up resulting state, and return state to STATE_REPLAY_DATA.
replay_state_ = input_state_;
input_state_ = STATE_REPLAY_DATA;
// On error, or if bytes were read, just return result immediately.
// Could continue consuming data in the success case, but simplest not
// to.
if (result != 0)
return result;
break;
}
case STATE_COMPRESSED_BODY: {
DCHECK(!state_compressed_entered);
DCHECK_LE(0, input_data_size);
state_compressed_entered = true;
zlib_stream_.next_in = bit_cast<Bytef*>(input_data);
zlib_stream_.avail_in = input_data_size;
zlib_stream_.next_out = bit_cast<Bytef*>(output_buffer->data());
zlib_stream_.avail_out = output_buffer_size;
int ret = inflate(&zlib_stream_, Z_NO_FLUSH);
if (ret != Z_STREAM_END && ret != Z_OK)
return ERR_CONTENT_DECODING_FAILED;
int bytes_used = input_data_size - zlib_stream_.avail_in;
bytes_out = output_buffer_size - zlib_stream_.avail_out;
input_data_size -= bytes_used;
input_data += bytes_used;
if (ret == Z_STREAM_END)
input_state_ = STATE_GZIP_FOOTER;
// zlib has written as much data to |output_buffer| as it could.
// There might still be some unconsumed data in |input_buffer| if there
// is no space in |output_buffer|.
break;
}
case STATE_GZIP_FOOTER: {
size_t to_read = std::min(gzip_footer_bytes_left_,
base::checked_cast<size_t>(input_data_size));
gzip_footer_bytes_left_ -= to_read;
input_data_size -= to_read;
input_data += to_read;
if (gzip_footer_bytes_left_ == 0)
input_state_ = STATE_IGNORING_EXTRA_BYTES;
break;
}
case STATE_IGNORING_EXTRA_BYTES: {
input_data_size = 0;
break;
}
}
}
*consumed_bytes = input_buffer_size - input_data_size;
return bytes_out;
}
} // namespace net
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef NET_FILTER_ZLIB_STREAM_WRAPPER_H_
#define NET_FILTER_ZLIB_STREAM_WRAPPER_H_
#include <memory>
#include <string>
#include "base/macros.h"
#include "net/base/net_export.h"
#include "net/filter/gzip_header.h"
#include "third_party/zlib/zlib.h"
namespace net {
class IOBuffer;
// Class to wrap and provide an easier to use interface to zlib for inflating
// zlib or gzip wrapped deflated data streams.
class NET_EXPORT ZLibStreamWrapper {
public:
// The stream source type to be inflated.
enum class SourceType {
// Source stream is a gzip-wrapped deflate data.
kGzip,
// Source stream is a zlib-wrapped deflate data.
kDeflate
};
explicit ZLibStreamWrapper(SourceType type);
~ZLibStreamWrapper();
// Returns true if initialization is successful, false otherwise.
// For instance, this method returns false if there is not enough memory or
// if there is a version mismatch.
// Note: Must be called before FilterData.
bool Init();
// Filter (i.e. inflate) the data from |input_buffer| into |output_buffer|.
// Will return the number bytes written to the output buffer or a net::Error.
// |consumed_bytes| will be set to the number of bytes read from
// |input_buffer| when expanding.
// Note: Must call Init() before using this method.
int FilterData(net::IOBuffer* output_buffer,
int output_buffer_size,
net::IOBuffer* input_buffer,
int input_buffer_size,
int* consumed_bytes,
bool upstream_end_reached);
private:
enum InputState {
// Starts processing the input stream. Checks whether the stream is valid
// and whether a fallback to plain data is needed.
STATE_START,
// Gzip header of the input stream is being processed.
STATE_GZIP_HEADER,
// Deflate responses may or may not have a zlib header. In this state until
// enough has been inflated that this stream most likely has a zlib header,
// or until a zlib header has been added. Data is appended to |replay_data_|
// in case it needs to be replayed after adding a header.
STATE_SNIFFING_DEFLATE_HEADER,
// If a zlib header has to be added to the response, this state will replay
// data passed to inflate before it was determined that no zlib header was
// present.
// See https://crbug.com/677001
STATE_REPLAY_DATA,
// The input stream is being decoded.
STATE_COMPRESSED_BODY,
// Gzip footer of the input stream is being processed.
STATE_GZIP_FOOTER,
// The end of the gzipped body has been reached. If any extra bytes are
// received, just silently ignore them. Doing this, rather than failing the
// request or passing the extra bytes alone with the rest of the response
// body, matches the behavior of other browsers.
STATE_IGNORING_EXTRA_BYTES,
};
bool InsertZlibHeader();
SourceType type_;
InputState input_state_;
InputState replay_state_;
z_stream_s zlib_stream_;
// Used to parse the gzip header in gzip stream.
// It is used when the decoding mode is SourceType::kDeflate.
net::GZipHeader gzip_header_;
// While in STATE_SNIFFING_DEFLATE_HEADER, it may be determined that a zlib
// header needs to be added, and all received data needs to be replayed. In
// that case, this buffer holds the data to be replayed.
std::string replay_data_;
// Tracks how many bytes of gzip footer are yet to be filtered.
size_t gzip_footer_bytes_left_;
DISALLOW_COPY_AND_ASSIGN(ZLibStreamWrapper);
};
} // namespace net
#endif // NET_FILTER_ZLIB_STREAM_WRAPPER_H_
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/filter/zlib_stream_wrapper.h"
#include "base/memory/scoped_refptr.h"
#include "net/base/io_buffer.h"
#include "net/base/net_errors.h"
#include "net/filter/filter_source_stream_test_util.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace net {
class ZLibStreamWrapperTest
: public testing::Test,
public testing::WithParamInterface<ZLibStreamWrapper::SourceType> {
public:
ZLibStreamWrapper::SourceType source_type() { return GetParam(); }
bool compress_with_gzip_framing() {
return GetParam() == ZLibStreamWrapper::SourceType::kGzip;
}
};
TEST_P(ZLibStreamWrapperTest, SmallInflate) {
const std::string uncompressed_data = "hello";
size_t compressed_data_len = 2048;
auto compressed_data = std::make_unique<char[]>(compressed_data_len);
net::CompressGzip(uncompressed_data.data(), uncompressed_data.size(),
compressed_data.get(), &compressed_data_len,
compress_with_gzip_framing());
ZLibStreamWrapper zlib_wrapper(source_type());
ASSERT_TRUE(zlib_wrapper.Init());
scoped_refptr<WrappedIOBuffer> input_buffer =
new WrappedIOBuffer(compressed_data.get());
scoped_refptr<IOBuffer> output_buffer =
new IOBuffer(uncompressed_data.size());
int consumed_bytes(0);
int bytes_written = zlib_wrapper.FilterData(
output_buffer.get(), uncompressed_data.size(), input_buffer.get(),
compressed_data_len, &consumed_bytes,
/*upstream_end_reached=*/true);
ASSERT_EQ(static_cast<int>(uncompressed_data.size()), bytes_written);
EXPECT_EQ(static_cast<int>(compressed_data_len), consumed_bytes);
EXPECT_EQ(0, memcmp(output_buffer->data(), uncompressed_data.data(),
uncompressed_data.size()));
}
TEST_P(ZLibStreamWrapperTest, SmallCorruptedInflate) {
const std::string uncompressed_data = "hello";
size_t compressed_data_len = 2048;
auto compressed_data = std::make_unique<char[]>(compressed_data_len);
net::CompressGzip(uncompressed_data.data(), uncompressed_data.size(),
compressed_data.get(), &compressed_data_len,
compress_with_gzip_framing());
// Corrupt the compressed data stream.
compressed_data[1] = 'X';
ZLibStreamWrapper zlib_wrapper(source_type());
ASSERT_TRUE(zlib_wrapper.Init());
scoped_refptr<WrappedIOBuffer> input_buffer =
new WrappedIOBuffer(compressed_data.get());
scoped_refptr<IOBuffer> output_buffer =
new IOBuffer(uncompressed_data.size());
int consumed_bytes(0);
int bytes_written = zlib_wrapper.FilterData(
output_buffer.get(), uncompressed_data.size(), input_buffer.get(),
compressed_data_len, &consumed_bytes,
/*upstream_end_reached=*/true);
EXPECT_EQ(net::ERR_CONTENT_DECODING_FAILED, bytes_written);
EXPECT_EQ(0, consumed_bytes);
}
INSTANTIATE_TEST_CASE_P(
,
ZLibStreamWrapperTest,
testing::Values(ZLibStreamWrapper::SourceType::kGzip,
ZLibStreamWrapper::SourceType::kDeflate));
} // namespace net
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment