// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <optional>
#include "base/feature_list.h"
#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "content/browser/network/socket_broker_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/network_service_instance.h"
#include "content/public/browser/network_service_util.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/content_browser_test.h"
#include "content/shell/browser/shell.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "mojo/public/cpp/system/data_pipe_utils.h"
#include "mojo/public/cpp/system/handle.h"
#include "net/base/ip_endpoint.h"
#include "net/socket/tcp_server_socket.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "sandbox/policy/features.h"
#include "services/cert_verifier/public/mojom/cert_verifier_service_factory.mojom.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "services/network/public/mojom/tcp_socket.mojom.h"
namespace content {
namespace {
const char kTestResponse[] = "hello socket broker";
class SandboxedSocketBrokerBrowserTest : public ContentBrowserTest {
public:
SandboxedSocketBrokerBrowserTest() {
#if BUILDFLAG(IS_ANDROID)
// On older Android the Connect callback is not called with the featurelist
// enabled. Since it is not strictly necessary to test socket brokering core
// functionality with the featurelist we won't enable it on Android versions
// prior to R.
const int sdk_version = base::android::BuildInfo::GetInstance()->sdk_int();
check_sandbox_ = sdk_version >= base::android::SdkVersion::SDK_VERSION_R;
#endif // BUILDFLAG(IS_ANDROID)
#if BUILDFLAG(IS_WIN)
if (!sandbox::policy::features::IsNetworkSandboxSupported()) {
check_sandbox_ = false;
}
#endif // BUILDFLAG(IS_WIN)
if (check_sandbox_) {
#if !BUILDFLAG(IS_MAC) && !BUILDFLAG(IS_FUCHSIA)
// Network Service Sandboxing is unconditionally enabled on these
// platforms.
scoped_feature_list_.InitAndEnableFeature(
sandbox::policy::features::kNetworkServiceSandbox);
#endif // !BUILDFLAG(IS_MAC) && !BUILDFLAG(IS_FUCHSIA)
ForceOutOfProcessNetworkService();
}
}
void SetUp() override {
#if BUILDFLAG(IS_WIN)
if (check_sandbox_) {
ASSERT_TRUE(IsOutOfProcessNetworkService());
ASSERT_TRUE(sandbox::policy::features::IsNetworkSandboxEnabled());
}
embedded_test_server_.RegisterRequestHandler(
base::BindRepeating(&SandboxedSocketBrokerBrowserTest::HandleRequest,
base::Unretained(this)));
ASSERT_TRUE(embedded_test_server_.InitializeAndListen());
ContentBrowserTest::SetUp();
#else
GTEST_SKIP();
#endif
}
#if BUILDFLAG(IS_WIN)
void SetUpOnMainThread() override {
embedded_test_server_.StartAcceptingConnections();
}
#endif
std::unique_ptr<net::test_server::HttpResponse> HandleRequest(
const net::test_server::HttpRequest& request) {
GURL absolute_url = embedded_test_server_.GetURL(request.relative_url);
if (absolute_url.path() != "/test")
return nullptr;
auto http_response =
std::make_unique<net::test_server::BasicHttpResponse>();
http_response->set_code(net::HTTP_OK);
http_response->set_content(kTestResponse);
http_response->set_content_type("text/plain");
return http_response;
}
protected:
base::test::ScopedFeatureList scoped_feature_list_;
net::test_server::EmbeddedTestServer embedded_test_server_;
bool check_sandbox_ = true;
};
mojo::Remote<network::mojom::NetworkContext> CreateNetworkContext() {
mojo::Remote<network::mojom::NetworkContext> network_context;
network::mojom::NetworkContextParamsPtr context_params =
network::mojom::NetworkContextParams::New();
context_params->cert_verifier_params = GetCertVerifierParams(
cert_verifier::mojom::CertVerifierCreationParams::New());
CreateNetworkContextInNetworkService(
network_context.BindNewPipeAndPassReceiver(), std::move(context_params));
return network_context;
}
void OnConnected(base::OnceClosure quit_closure,
int result,
const std::optional<net::IPEndPoint>& local_addr,
const std::optional<net::IPEndPoint>& peer_addr,
mojo::ScopedDataPipeConsumerHandle receive_stream,
mojo::ScopedDataPipeProducerHandle send_stream) {
base::ScopedClosureRunner closure_runner(std::move(quit_closure));
ASSERT_EQ(result, net::OK);
const std::string request = "GET /test HTTP/1.0\r\n\r\n";
ASSERT_TRUE(BlockingCopyFromString(request, std::move(send_stream)));
std::string response;
ASSERT_TRUE(BlockingCopyToString(std::move(receive_stream), &response));
LOG(ERROR) << response;
EXPECT_NE(response.find(kTestResponse), std::string::npos);
}
// Creates a TCPConnectedSocket that attempts one GET request to
// `embedded_test_server`. If `use_options` is true then a set of options are
// set for the socket.
void RunTcpEndToEndTest(
network::mojom::NetworkContext* network_context,
net::test_server::EmbeddedTestServer& embedded_test_server,
bool use_options) {
mojo::PendingRemote<network::mojom::TCPConnectedSocket>
tcp_connected_socket_remote;
net::AddressList addr;
ASSERT_TRUE(embedded_test_server.GetAddressList(&addr));
network::mojom::TCPConnectedSocketOptionsPtr tcp_connected_socket_options =
network::mojom::TCPConnectedSocketOptions::New();
tcp_connected_socket_options->send_buffer_size = 32 * 1024;
tcp_connected_socket_options->receive_buffer_size = 64 * 1024;
tcp_connected_socket_options->no_delay = false;
base::RunLoop run_loop;
network_context->CreateTCPConnectedSocket(
std::nullopt, addr,
use_options ? std::move(tcp_connected_socket_options) : nullptr,
net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS),
tcp_connected_socket_remote.InitWithNewPipeAndPassReceiver(),
mojo::NullRemote(), base::BindOnce(&OnConnected, run_loop.QuitClosure()));
run_loop.Run();
}
IN_PROC_BROWSER_TEST_F(SandboxedSocketBrokerBrowserTest,
TcpEndToEndDefaultContext) {
network::mojom::NetworkContext* network_context =
shell()
->web_contents()
->GetBrowserContext()
->GetDefaultStoragePartition()
->GetNetworkContext();
ASSERT_TRUE(network_context);
RunTcpEndToEndTest(network_context, embedded_test_server_,
/*use_options=*/false);
}
IN_PROC_BROWSER_TEST_F(SandboxedSocketBrokerBrowserTest,
TcpEndToEndDefaultContextWithOptions) {
network::mojom::NetworkContext* network_context =
shell()
->web_contents()
->GetBrowserContext()
->GetDefaultStoragePartition()
->GetNetworkContext();
ASSERT_TRUE(network_context);
RunTcpEndToEndTest(network_context, embedded_test_server_,
/*use_options=*/true);
}
// Implementation of network::mojom::SocketBroker that tracks the number of
// times CreateTcpSocket has been called.
class CountingSocketBrokerImpl : public SocketBrokerImpl {
public:
void CreateTcpSocket(net::AddressFamily address_family,
CreateTcpSocketCallback callback) override {
++tcp_socket_count_;
SocketBrokerImpl::CreateTcpSocket(address_family, std::move(callback));
}
int tcp_socket_count() { return tcp_socket_count_; }
private:
int tcp_socket_count_ = 0;
};
IN_PROC_BROWSER_TEST_F(SandboxedSocketBrokerBrowserTest,
TcpEndToEndBrokeredContext) {
CountingSocketBrokerImpl socket_broker;
network::mojom::NetworkContextParamsPtr network_context_params =
network::mojom::NetworkContextParams::New();
network_context_params->socket_brokers =
network::mojom::SocketBrokerRemotes::New();
network_context_params->socket_brokers->client =
socket_broker.BindNewRemote();
network_context_params->socket_brokers->server =
socket_broker.BindNewRemote();
auto file_paths = network::mojom::NetworkContextFilePaths::New();
base::FilePath context_path =
shell()->web_contents()->GetBrowserContext()->GetPath().Append(
FILE_PATH_LITERAL("TestContext"));
file_paths->data_directory = context_path.Append(FILE_PATH_LITERAL("Data"));
file_paths->unsandboxed_data_path = context_path;
file_paths->trigger_migration = true;
network_context_params->file_paths = std::move(file_paths);
network_context_params->cert_verifier_params = GetCertVerifierParams(
cert_verifier::mojom::CertVerifierCreationParams::New());
network_context_params->http_cache_enabled = true;
network_context_params->file_paths->http_cache_directory =
shell()
->web_contents()
->GetBrowserContext()
->GetPath()
.Append(FILE_PATH_LITERAL("TestContext"))
.Append(FILE_PATH_LITERAL("Cache"));
mojo::Remote<network::mojom::NetworkContext> network_context;
CreateNetworkContextInNetworkService(
network_context.BindNewPipeAndPassReceiver(),
std::move(network_context_params));
RunTcpEndToEndTest(network_context.get(), embedded_test_server_,
/*use_options=*/false);
EXPECT_EQ(socket_broker.tcp_socket_count(), 1);
}
IN_PROC_BROWSER_TEST_F(SandboxedSocketBrokerBrowserTest,
TcpEndToEndCrashingService) {
if (IsInProcessNetworkService())
GTEST_SKIP();
auto network_context = CreateNetworkContext();
ASSERT_TRUE(network_context.is_bound());
// Run test on the first network context.
RunTcpEndToEndTest(network_context.get(), embedded_test_server_,
/*use_options=*/false);
SimulateNetworkServiceCrash();
auto network_context2 = CreateNetworkContext();
ASSERT_TRUE(network_context2.is_bound());
// Run the test again, in the new network service.
RunTcpEndToEndTest(network_context2.get(), embedded_test_server_,
/*use_options=*/false);
}
} // namespace
} // namespace content