blob: 80d42e0bb941c8875285700c5a63cb43b3348fef [file] [log] [blame]
// Copyright 2021 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 "services/network/public/cpp/opaque_response_blocking.h"
#include "base/strings/string_util.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "net/base/mime_sniffer.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_util.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
namespace network {
namespace {
// ResourceType::kImage from resource_load_info.mojom
constexpr mojom::RequestDestination kDefaultRequestDestination =
mojom::RequestDestination::kImage;
const char kDefaultRequestUrl[] = "https://target.example.com/foo";
struct TestInput {
GURL request_url;
base::Optional<url::Origin> request_initiator;
mojom::RequestMode request_mode;
mojom::RequestDestination request_destination;
mojom::URLResponseHeadPtr response;
};
class TestInputBuilder {
public:
TestInputBuilder() = default;
// No copy constructor or assignment operator.
TestInputBuilder(const TestInputBuilder&) = delete;
TestInputBuilder& operator=(const TestInputBuilder&) = delete;
TestInput Build() const {
std::string raw_headers = base::ReplaceStringPlaceholders(
"$1\nContent-Type: $2\n", {http_status_line_, mime_type_}, nullptr);
if (no_sniff_)
raw_headers += "X-Content-Type-Options: nosniff\n";
mojom::URLResponseHeadPtr response = mojom::URLResponseHead::New();
response->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
net::HttpUtil::AssembleRawHeaders(raw_headers));
response->was_fetched_via_service_worker = false;
response->response_type = mojom::FetchResponseType::kDefault;
return TestInput{
.request_url = request_url_,
.request_initiator = request_initiator_,
.request_mode = request_mode_,
.request_destination = kDefaultRequestDestination,
.response = std::move(response),
};
}
TestInputBuilder& WithHttpStatus(std::string status_line) {
http_status_line_ = status_line;
return *this;
}
TestInputBuilder& WithMimeType(std::string mime_type) {
mime_type_ = mime_type;
return *this;
}
TestInputBuilder& WithInitiator(
const base::Optional<url::Origin>& initiator) {
request_initiator_ = initiator;
return *this;
}
TestInputBuilder& WithMode(const mojom::RequestMode& mode) {
request_mode_ = mode;
return *this;
}
TestInputBuilder& WithNoSniff() {
no_sniff_ = true;
return *this;
}
private:
std::string http_status_line_ = "HTTP/1.1 200 OK";
std::string mime_type_ = "application/octet-stream";
GURL request_url_ = GURL(kDefaultRequestUrl);
base::Optional<url::Origin> request_initiator_ =
url::Origin::Create(GURL("https://initiator.example.com"));
mojom::RequestMode request_mode_ = mojom::RequestMode::kNoCors;
bool no_sniff_ = false;
};
void LogUmaForOpaqueResponseBlocking(const TestInput& test_input) {
network::LogUmaForOpaqueResponseBlocking(
test_input.request_url, test_input.request_initiator,
test_input.request_mode, test_input.request_destination,
*test_input.response);
}
void LogUmaForOpaqueResponseBlocking(
const TestInputBuilder& test_input_builder) {
LogUmaForOpaqueResponseBlocking(test_input_builder.Build());
}
void TestUma(const TestInput& test_input,
const ResponseHeadersHeuristicForUma& expected_uma_decision) {
base::HistogramTester histograms;
LogUmaForOpaqueResponseBlocking(test_input);
// Verify that the ...Heuristic.Decision UMA has only a single/unique bucket
// (for `expected_uma_decision`) and that only 1 sample has been logged for
// this bucket.
histograms.ExpectUniqueSample(
"SiteIsolation.ORB.ResponseHeadersHeuristic.Decision",
expected_uma_decision, 1 /* expected_count */);
}
void TestUma(const TestInputBuilder& test_input_builder,
const ResponseHeadersHeuristicForUma& expected_uma_decision) {
TestUma(test_input_builder.Build(), expected_uma_decision);
}
TEST(OpaqueResponseBlocking, DefaultTestInput) {
// Verify properties of the default test input: renderer-initiated,
// cross-origin, no-cors request with a successful application/octet-stream
// response.
TestInput default_test_input = TestInputBuilder().Build();
EXPECT_TRUE(default_test_input.request_initiator.has_value());
EXPECT_FALSE(default_test_input.request_initiator->IsSameOriginWith(
url::Origin::Create(default_test_input.request_url)));
EXPECT_EQ(mojom::RequestMode::kNoCors, default_test_input.request_mode);
EXPECT_TRUE(default_test_input.response);
EXPECT_TRUE(default_test_input.response->headers);
EXPECT_EQ(200, default_test_input.response->headers->response_code());
std::string mime_type;
EXPECT_TRUE(default_test_input.response->headers->GetMimeType(&mime_type));
EXPECT_EQ("application/octet-stream", mime_type);
// Verify that the right UMA is logged for the default test input.
TestUma(default_test_input,
ResponseHeadersHeuristicForUma::kRequiresJavascriptParsing);
}
TEST(OpaqueResponseBlocking, ResourceTypeUma) {
// RequiresJavascriptParsing case.
{
base::HistogramTester histograms;
TestUma(TestInputBuilder(),
ResponseHeadersHeuristicForUma::kRequiresJavascriptParsing);
histograms.ExpectTotalCount(
"SiteIsolation.ORB.ResponseHeadersHeuristic.ProcessedBasedOnHeaders",
0);
histograms.ExpectUniqueSample(
"SiteIsolation.ORB.ResponseHeadersHeuristic.RequiresJavascriptParsing",
kDefaultRequestDestination,
1 /* `expected_count` of `kDefaultRequestDestination` */);
}
// ProcessedBasedOnHeaders case.
{
base::HistogramTester histograms;
TestUma(TestInputBuilder().WithNoSniff(),
ResponseHeadersHeuristicForUma::kProcessedBasedOnHeaders);
histograms.ExpectUniqueSample(
"SiteIsolation.ORB.ResponseHeadersHeuristic.ProcessedBasedOnHeaders",
kDefaultRequestDestination,
1 /* `expected_count` of `kDefaultRequestDestination` */);
histograms.ExpectTotalCount(
"SiteIsolation.ORB.ResponseHeadersHeuristic.RequiresJavascriptParsing",
0);
}
// Non-opaque case..
{
base::HistogramTester histograms;
LogUmaForOpaqueResponseBlocking(
TestInputBuilder().WithInitiator(base::nullopt));
histograms.ExpectTotalCount(
"SiteIsolation.ORB.ResponseHeadersHeuristic.ProcessedBasedOnHeaders",
0);
histograms.ExpectTotalCount(
"SiteIsolation.ORB.ResponseHeadersHeuristic.RequiresJavascriptParsing",
0);
}
LogUmaForOpaqueResponseBlocking(TestInputBuilder().WithNoSniff());
}
TEST(OpaqueResponseBlocking, NonOpaqueRequests) {
// Browser-initiated request.
TestUma(TestInputBuilder().WithInitiator(base::nullopt),
ResponseHeadersHeuristicForUma::kNonOpaqueResponse);
// Mode != no-cors.
TestUma(TestInputBuilder().WithMode(mojom::RequestMode::kCors),
ResponseHeadersHeuristicForUma::kNonOpaqueResponse);
TestUma(TestInputBuilder().WithMode(mojom::RequestMode::kNavigate),
ResponseHeadersHeuristicForUma::kNonOpaqueResponse);
}
TEST(OpaqueResponseBlocking, SameOrigin) {
TestUma(TestInputBuilder().WithInitiator(
url::Origin::Create(GURL(kDefaultRequestUrl))),
ResponseHeadersHeuristicForUma::kProcessedBasedOnHeaders);
}
TEST(OpaqueResponseBlocking, NoSniff) {
TestUma(TestInputBuilder().WithNoSniff(),
ResponseHeadersHeuristicForUma::kProcessedBasedOnHeaders);
}
TEST(OpaqueResponseBlocking, MimeTypes) {
// Unrecognized type:
TestUma(TestInputBuilder().WithMimeType("application/octet-stream"),
ResponseHeadersHeuristicForUma::kRequiresJavascriptParsing);
// opaque-safelisted MIME types:
TestUma(TestInputBuilder().WithMimeType("application/dash+xml"),
ResponseHeadersHeuristicForUma::kProcessedBasedOnHeaders);
TestUma(TestInputBuilder().WithMimeType("application/javascript"),
ResponseHeadersHeuristicForUma::kProcessedBasedOnHeaders);
TestUma(TestInputBuilder().WithMimeType("image/svg+xml"),
ResponseHeadersHeuristicForUma::kProcessedBasedOnHeaders);
TestUma(TestInputBuilder().WithMimeType("text/css"),
ResponseHeadersHeuristicForUma::kProcessedBasedOnHeaders);
// Example of a opaque-blocklisted-never-sniffed MIME type:
TestUma(TestInputBuilder().WithMimeType("application/pdf"),
ResponseHeadersHeuristicForUma::kProcessedBasedOnHeaders);
// Future multimedia types:
TestUma(TestInputBuilder().WithMimeType("audio/some-future-type"),
ResponseHeadersHeuristicForUma::kProcessedBasedOnHeaders);
TestUma(TestInputBuilder().WithMimeType("image/some-future-type"),
ResponseHeadersHeuristicForUma::kProcessedBasedOnHeaders);
TestUma(TestInputBuilder().WithMimeType("video/some-future-type"),
ResponseHeadersHeuristicForUma::kProcessedBasedOnHeaders);
}
TEST(OpaqueResponseBlocking, HttpStatusCodes) {
TestUma(TestInputBuilder().WithHttpStatus("HTTP/1.1 200 OK"),
ResponseHeadersHeuristicForUma::kRequiresJavascriptParsing);
TestUma(TestInputBuilder().WithHttpStatus("HTTP/1.1 206 Range"),
ResponseHeadersHeuristicForUma::kProcessedBasedOnHeaders);
TestUma(TestInputBuilder().WithHttpStatus("HTTP/1.1 300 Redirect"),
ResponseHeadersHeuristicForUma::kProcessedBasedOnHeaders);
TestUma(TestInputBuilder().WithHttpStatus("HTTP/1.1 400 NotFound"),
ResponseHeadersHeuristicForUma::kProcessedBasedOnHeaders);
TestUma(TestInputBuilder().WithHttpStatus("HTTP/1.1 500 ServerError"),
ResponseHeadersHeuristicForUma::kProcessedBasedOnHeaders);
}
} // namespace
} // namespace network