blob: ba0bffb10946ab500b91ff434b584c9b07d8a173 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/network/cors/cors_url_loader.h"
#include "mojo/public/cpp/system/data_pipe_utils.h"
#include "net/cookies/site_for_cookies.h"
#include "services/network/cors/cors_url_loader_test_util.h"
#include "services/network/network_context.h"
#include "services/network/public/cpp/features.h"
#include "services/network/shared_dictionary/shared_dictionary_constants.h"
#include "services/network/shared_dictionary/shared_dictionary_manager.h"
#include "services/network/shared_dictionary/shared_dictionary_storage.h"
#include "services/network/shared_dictionary/shared_dictionary_storage_in_memory.h"
#include "services/network/test/client_security_state_builder.h"
#include "services/network/test/test_url_loader_client.h"
#include "url/scheme_host_port.h"
namespace network::cors {
namespace {
const std::string kTestData = "hello world";
const std::string kTestOriginString = "https://origin.test/";
} // namespace
class CorsURLLoaderSharedDictionaryTest : public CorsURLLoaderTestBase {
public:
CorsURLLoaderSharedDictionaryTest()
: CorsURLLoaderTestBase(/*shared_dictionary_enabled*/ true) {
const url::Origin kOrigin = url::Origin::Create(GURL(kTestOriginString));
isolation_info_ = net::IsolationInfo::Create(
net::IsolationInfo::RequestType::kOther, kOrigin, kOrigin,
net::SiteForCookies::FromOrigin(kOrigin));
}
~CorsURLLoaderSharedDictionaryTest() override = default;
CorsURLLoaderSharedDictionaryTest(const CorsURLLoaderSharedDictionaryTest&) =
delete;
CorsURLLoaderSharedDictionaryTest& operator=(
const CorsURLLoaderSharedDictionaryTest&) = delete;
protected:
void ResetFactory(bool is_web_secure_context = true) {
ResetFactoryParams factory_params;
factory_params.isolation_info = isolation_info_;
factory_params.client_security_state =
ClientSecurityStateBuilder()
.WithIsSecureContext(is_web_secure_context)
.Build();
CorsURLLoaderTestBase::ResetFactory(isolation_info_.frame_origin(),
kRendererProcessId, factory_params);
}
ResourceRequest CreateResourceRequest(
bool shared_dictionary_writer_enabled = true) {
ResourceRequest request;
request.method = "GET";
request.mode = mojom::RequestMode::kCors;
request.url = GURL("https://origin.test/test");
request.request_initiator = isolation_info_.frame_origin();
request.shared_dictionary_writer_enabled = shared_dictionary_writer_enabled;
return request;
}
void CreateDataPipe() {
MojoCreateDataPipeOptions options;
options.struct_size = sizeof(MojoCreateDataPipeOptions);
options.flags = MOJO_CREATE_DATA_PIPE_FLAG_NONE;
options.element_num_bytes = 1;
options.capacity_num_bytes =
network::features::GetDataPipeDefaultAllocationSize(
features::DataPipeAllocationSize::kLargerSizeIfPossible);
ASSERT_EQ(MOJO_RESULT_OK, mojo::CreateDataPipe(&options, producer_handle_,
consumer_handle_));
}
void CreateDataPipeAndWriteTestData() {
CreateDataPipe();
ASSERT_TRUE(mojo::BlockingCopyFromString(kTestData, producer_handle_));
}
void NotifyLoaderClientOnReceiveResponseWithUseAsDictionaryHeader(
std::vector<std::pair<std::string, std::string>> extra_headers = {}) {
extra_headers.emplace_back(
network::shared_dictionary::kUseAsDictionaryHeaderName,
"match=\"/path*\"");
NotifyLoaderClientOnReceiveResponse(extra_headers,
std::move(consumer_handle_));
}
void CallOnReceiveResponseAndOnCompleteAndFinishBody(
const std::vector<std::pair<std::string, std::string>>& extra_headers =
{}) {
NotifyLoaderClientOnReceiveResponseWithUseAsDictionaryHeader(extra_headers);
NotifyLoaderClientOnComplete(net::OK);
producer_handle_.reset();
}
void CheckDictionaryInStorage(
bool expect_exists,
const GURL& dictionary_url = GURL("https://origin.test/test")) {
ASSERT_TRUE(isolation_info_.frame_origin());
absl::optional<net::SharedDictionaryStorageIsolationKey> isolation_key =
net::SharedDictionaryStorageIsolationKey::MaybeCreate(isolation_info_);
ASSERT_TRUE(isolation_key);
scoped_refptr<SharedDictionaryStorage> storage =
network_context()->GetSharedDictionaryManager()->GetStorage(
*isolation_key);
const auto& dictionary_map = GetInMemoryDictionaryMap(storage.get());
if (!expect_exists) {
EXPECT_TRUE(dictionary_map.empty());
return;
}
ASSERT_EQ(1u, dictionary_map.size());
EXPECT_EQ(url::SchemeHostPort(dictionary_url),
dictionary_map.begin()->first);
ASSERT_EQ(1u, dictionary_map.begin()->second.size());
EXPECT_EQ("/path*", dictionary_map.begin()->second.begin()->first);
const auto& dictionary_info =
dictionary_map.begin()->second.begin()->second;
EXPECT_EQ(dictionary_url, dictionary_info.url());
EXPECT_EQ(shared_dictionary::kDefaultExpiration,
dictionary_info.expiration());
EXPECT_EQ("/path*", dictionary_info.match());
EXPECT_EQ(kTestData.size(), dictionary_info.size());
EXPECT_EQ(kTestData, std::string(dictionary_info.data()->data(),
dictionary_info.size()));
}
const std::map<
url::SchemeHostPort,
std::map<std::string, SharedDictionaryStorageInMemory::DictionaryInfo>>&
GetInMemoryDictionaryMap(SharedDictionaryStorage* storage) {
return static_cast<SharedDictionaryStorageInMemory*>(storage)
->GetDictionaryMapForTesting();
}
net::IsolationInfo isolation_info_;
mojo::ScopedDataPipeProducerHandle producer_handle_;
mojo::ScopedDataPipeConsumerHandle consumer_handle_;
};
TEST_F(CorsURLLoaderSharedDictionaryTest, SameOriginUrlSameOriginModeRequest) {
ResetFactory();
ResourceRequest request = CreateResourceRequest();
request.mode = mojom::RequestMode::kSameOrigin;
CreateLoaderAndStart(request);
RunUntilCreateLoaderAndStartCalled();
CreateDataPipeAndWriteTestData();
CallOnReceiveResponseAndOnCompleteAndFinishBody();
RunUntilComplete();
EXPECT_EQ(net::OK, client().completion_status().error_code);
// The response of SameOrigin mode request should not be stored to the
// dictionary storage.
CheckDictionaryInStorage(/*expect_exists=*/false);
}
TEST_F(CorsURLLoaderSharedDictionaryTest, SameOriginUrlNoCorsModeRequest) {
ResetFactory();
ResourceRequest request = CreateResourceRequest();
request.mode = mojom::RequestMode::kNoCors;
CreateLoaderAndStart(request);
RunUntilCreateLoaderAndStartCalled();
CreateDataPipeAndWriteTestData();
CallOnReceiveResponseAndOnCompleteAndFinishBody();
RunUntilComplete();
EXPECT_EQ(net::OK, client().completion_status().error_code);
// The response of NoCors mode request should not be stored to the dictionary
// storage.
CheckDictionaryInStorage(/*expect_exists=*/false);
}
TEST_F(CorsURLLoaderSharedDictionaryTest, SameOriginUrlCorsModeRequest) {
ResetFactory();
ResourceRequest request = CreateResourceRequest();
EXPECT_EQ(mojom::RequestMode::kCors, request.mode);
CreateLoaderAndStart(request);
RunUntilCreateLoaderAndStartCalled();
CreateDataPipeAndWriteTestData();
CallOnReceiveResponseAndOnCompleteAndFinishBody();
RunUntilComplete();
EXPECT_EQ(net::OK, client().completion_status().error_code);
// The response of Cors request should be stored to the dictionary storage.
CheckDictionaryInStorage(/*expect_exists=*/true);
}
TEST_F(CorsURLLoaderSharedDictionaryTest,
SameOriginUrlCorsWithForcedPreflightModeRequest) {
ResetFactory();
ResourceRequest request = CreateResourceRequest();
request.mode = mojom::RequestMode::kCorsWithForcedPreflight;
CreateLoaderAndStart(request);
RunUntilCreateLoaderAndStartCalled();
CreateDataPipeAndWriteTestData();
CallOnReceiveResponseAndOnCompleteAndFinishBody();
RunUntilComplete();
EXPECT_EQ(net::OK, client().completion_status().error_code);
// The response of CorsWithForcedPreflight mode request should be stored to
// to the dictionary storage.
CheckDictionaryInStorage(/*expect_exists=*/true);
}
TEST_F(CorsURLLoaderSharedDictionaryTest, SameOriginUrlNavigateModeRequest) {
ResetFactory();
ResourceRequest request = CreateResourceRequest();
request.mode = mojom::RequestMode::kNavigate;
request.redirect_mode = mojom::RedirectMode::kManual;
request.navigation_redirect_chain.push_back(request.url);
CreateLoaderAndStart(request);
RunUntilCreateLoaderAndStartCalled();
CreateDataPipeAndWriteTestData();
CallOnReceiveResponseAndOnCompleteAndFinishBody();
RunUntilComplete();
EXPECT_EQ(net::OK, client().completion_status().error_code);
// The response of Navigation mode request should not be stored to the
// dictionary storage.
CheckDictionaryInStorage(/*expect_exists=*/false);
}
TEST_F(CorsURLLoaderSharedDictionaryTest,
CrossOriginUrlCorsModeOmitCredentialRequest) {
ResetFactory();
ResourceRequest request = CreateResourceRequest();
request.url = GURL("https://crossorigin.test/test");
EXPECT_EQ(mojom::RequestMode::kCors, request.mode);
request.credentials_mode = mojom::CredentialsMode::kOmit;
CreateLoaderAndStart(request);
RunUntilCreateLoaderAndStartCalled();
CreateDataPipeAndWriteTestData();
CallOnReceiveResponseAndOnCompleteAndFinishBody(
{{"Access-Control-Allow-Origin", "https://origin.test"}});
RunUntilComplete();
EXPECT_EQ(net::OK, client().completion_status().error_code);
// The successful response of Cors request should be stored to the dictionary
// storage.
CheckDictionaryInStorage(/*expect_exists=*/true, request.url);
}
TEST_F(CorsURLLoaderSharedDictionaryTest,
CrossOriginUrlCorsModeOmitCredentialRequestAsteriskACAO) {
ResetFactory();
ResourceRequest request = CreateResourceRequest();
request.url = GURL("https://crossorigin.test/test");
EXPECT_EQ(mojom::RequestMode::kCors, request.mode);
request.credentials_mode = mojom::CredentialsMode::kOmit;
CreateLoaderAndStart(request);
RunUntilCreateLoaderAndStartCalled();
CreateDataPipeAndWriteTestData();
CallOnReceiveResponseAndOnCompleteAndFinishBody(
{{"Access-Control-Allow-Origin", "*"}});
RunUntilComplete();
EXPECT_EQ(net::OK, client().completion_status().error_code);
// The successful response of Cors request should be stored to the dictionary
// storage.
CheckDictionaryInStorage(/*expect_exists=*/true, request.url);
}
TEST_F(CorsURLLoaderSharedDictionaryTest,
CrossOriginUrlCorsModeIncludeCredentialRequest) {
ResetFactory();
ResourceRequest request = CreateResourceRequest();
request.url = GURL("https://crossorigin.test/test");
EXPECT_EQ(mojom::RequestMode::kCors, request.mode);
request.credentials_mode = mojom::CredentialsMode::kInclude;
CreateLoaderAndStart(request);
RunUntilCreateLoaderAndStartCalled();
CreateDataPipeAndWriteTestData();
CallOnReceiveResponseAndOnCompleteAndFinishBody({
{"Access-Control-Allow-Origin", "https://origin.test"},
{"Access-Control-Allow-Credentials", "true"},
});
RunUntilComplete();
EXPECT_EQ(net::OK, client().completion_status().error_code);
// The successful response of Cors request should be stored to the dictionary
// storage.
CheckDictionaryInStorage(/*expect_exists=*/true, request.url);
}
TEST_F(CorsURLLoaderSharedDictionaryTest,
OnCompleteAfterClosingBodyDataHandle) {
ResetFactory();
CreateLoaderAndStart(CreateResourceRequest());
RunUntilCreateLoaderAndStartCalled();
CreateDataPipeAndWriteTestData();
NotifyLoaderClientOnReceiveResponseWithUseAsDictionaryHeader();
producer_handle_.reset();
base::RunLoop().RunUntilIdle();
// Call OnComplete after closing the body data handle.
NotifyLoaderClientOnComplete(net::OK);
RunUntilComplete();
EXPECT_EQ(net::OK, client().completion_status().error_code);
CheckDictionaryInStorage(/*expect_exists=*/true);
}
TEST_F(CorsURLLoaderSharedDictionaryTest,
ResetClientRemoteFromNetworkWithoutOnCompleteCalled) {
ResetFactory();
CreateLoaderAndStart(CreateResourceRequest());
RunUntilCreateLoaderAndStartCalled();
CreateDataPipeAndWriteTestData();
NotifyLoaderClientOnReceiveResponseWithUseAsDictionaryHeader();
base::RunLoop().RunUntilIdle();
// Simulate an abort from the network side.
ResetClientRemote();
RunUntilComplete();
EXPECT_EQ(net::ERR_ABORTED, client().completion_status().error_code);
// When the requesta was aborted from the network side, the response should
// not be stored to the dictionary storage.
CheckDictionaryInStorage(/*expect_exists=*/false);
}
TEST_F(CorsURLLoaderSharedDictionaryTest,
ResetClientRemoteFromNetworkAfterOnCompleteCalled) {
ResetFactory();
CreateLoaderAndStart(CreateResourceRequest());
RunUntilCreateLoaderAndStartCalled();
CreateDataPipeAndWriteTestData();
NotifyLoaderClientOnReceiveResponseWithUseAsDictionaryHeader();
NotifyLoaderClientOnComplete(net::OK);
base::RunLoop().RunUntilIdle();
// Simulate an abort from the network side.
ResetClientRemote();
producer_handle_.reset();
RunUntilComplete();
EXPECT_EQ(net::OK, client().completion_status().error_code);
// When the request was aborted from the network side after OnComplete() is
// called, the response should be stored to the dictionary storage.
CheckDictionaryInStorage(/*expect_exists=*/true);
}
TEST_F(CorsURLLoaderSharedDictionaryTest, InsecureContext) {
ResetFactory(/*is_web_secure_context=*/false);
ResourceRequest request = CreateResourceRequest();
EXPECT_EQ(mojom::RequestMode::kCors, request.mode);
CreateLoaderAndStart(request);
RunUntilCreateLoaderAndStartCalled();
CreateDataPipeAndWriteTestData();
CallOnReceiveResponseAndOnCompleteAndFinishBody();
RunUntilComplete();
EXPECT_EQ(net::OK, client().completion_status().error_code);
// The response of should be stored to the dictionary storage because the web
// context is not secure.
CheckDictionaryInStorage(/*expect_exists=*/false);
}
TEST_F(CorsURLLoaderSharedDictionaryTest, SharedDictionaryWriterDisabled) {
ResetFactory();
ResourceRequest request =
CreateResourceRequest(/*shared_dictionary_writer_enabled=*/false);
EXPECT_EQ(mojom::RequestMode::kCors, request.mode);
CreateLoaderAndStart(request);
RunUntilCreateLoaderAndStartCalled();
CreateDataPipeAndWriteTestData();
CallOnReceiveResponseAndOnCompleteAndFinishBody();
RunUntilComplete();
EXPECT_EQ(net::OK, client().completion_status().error_code);
// The response of should be stored to the dictionary storage because shared
// dictionary writer is disabled.
CheckDictionaryInStorage(/*expect_exists=*/false);
}
} // namespace network::cors