| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #ifdef UNSAFE_BUFFERS_BUILD |
| // TODO(crbug.com/342213636): Remove this and spanify to fix the errors. |
| #pragma allow_unsafe_buffers |
| #endif |
| |
| #include <stdint.h> |
| |
| #include <optional> |
| #include <string> |
| |
| #include "base/containers/span.h" |
| #include "base/run_loop.h" |
| #include "base/strings/strcat.h" |
| #include "base/test/bind.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/test_future.h" |
| #include "build/build_config.h" |
| #include "content/browser/direct_sockets/direct_sockets_service_impl.h" |
| #include "content/browser/direct_sockets/direct_sockets_test_utils.h" |
| #include "content/browser/renderer_host/frame_tree_node.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/content_browser_test.h" |
| #include "content/public/test/content_browser_test_utils.h" |
| #include "content/shell/browser/shell.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "mojo/public/cpp/system/data_pipe.h" |
| #include "net/base/ip_address.h" |
| #include "net/base/ip_endpoint.h" |
| #include "net/base/net_errors.h" |
| #include "net/net_buildflags.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "net/traffic_annotation/network_traffic_annotation_test_helper.h" |
| #include "services/network/public/cpp/permissions_policy/permissions_policy_declaration.h" |
| #include "services/network/public/mojom/clear_data_filter.mojom.h" |
| #include "services/network/public/mojom/host_resolver.mojom.h" |
| #include "services/network/public/mojom/network_context.mojom.h" |
| #include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom-shared.h" |
| #include "services/network/public/mojom/tcp_socket.mojom.h" |
| #include "testing/gmock/include/gmock/gmock-matchers.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "url/gurl.h" |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| #include "chromeos/dbus/permission_broker/fake_permission_broker_client.h" // nogncheck |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| // The tests in this file use the Network Service implementation of |
| // NetworkContext, to test sending and receiving of data over TCP sockets. |
| |
| using testing::StartsWith; |
| |
| namespace content { |
| |
| namespace { |
| |
| constexpr char kLocalhostAddress[] = "127.0.0.1"; |
| |
| class ReadWriteWaiter { |
| public: |
| ReadWriteWaiter( |
| size_t required_receive_bytes, |
| size_t required_send_bytes, |
| mojo::Remote<network::mojom::TCPServerSocket>& tcp_server_socket) |
| : required_receive_bytes_(required_receive_bytes), |
| required_send_bytes_(required_send_bytes) { |
| tcp_server_socket->Accept( |
| /*observer=*/mojo::NullRemote(), |
| base::BindRepeating(&ReadWriteWaiter::OnAccept, |
| base::Unretained(this))); |
| } |
| |
| void Await() { run_loop_.Run(); } |
| |
| private: |
| void OnAccept( |
| int result, |
| const std::optional<net::IPEndPoint>& remote_addr, |
| mojo::PendingRemote<network::mojom::TCPConnectedSocket> accepted_socket, |
| mojo::ScopedDataPipeConsumerHandle consumer_handle, |
| mojo::ScopedDataPipeProducerHandle producer_handle) { |
| DCHECK_EQ(result, net::OK); |
| DCHECK(!accepted_socket_); |
| accepted_socket_.Bind(std::move(accepted_socket)); |
| |
| if (required_receive_bytes_ > 0) { |
| receive_stream_ = std::move(consumer_handle); |
| read_watcher_ = std::make_unique<mojo::SimpleWatcher>( |
| FROM_HERE, mojo::SimpleWatcher::ArmingPolicy::MANUAL); |
| read_watcher_->Watch( |
| receive_stream_.get(), |
| MOJO_HANDLE_SIGNAL_READABLE | MOJO_HANDLE_SIGNAL_PEER_CLOSED, |
| MOJO_TRIGGER_CONDITION_SIGNALS_SATISFIED, |
| base::BindRepeating(&ReadWriteWaiter::OnReadReady, |
| base::Unretained(this))); |
| read_watcher_->ArmOrNotify(); |
| } |
| |
| if (required_send_bytes_ > 0) { |
| send_stream_ = std::move(producer_handle); |
| write_watcher_ = std::make_unique<mojo::SimpleWatcher>( |
| FROM_HERE, mojo::SimpleWatcher::ArmingPolicy::MANUAL); |
| write_watcher_->Watch( |
| send_stream_.get(), |
| MOJO_HANDLE_SIGNAL_WRITABLE | MOJO_HANDLE_SIGNAL_PEER_CLOSED, |
| MOJO_TRIGGER_CONDITION_SIGNALS_SATISFIED, |
| base::BindRepeating(&ReadWriteWaiter::OnWriteReady, |
| base::Unretained(this))); |
| write_watcher_->ArmOrNotify(); |
| } |
| } |
| |
| void OnReadReady(MojoResult result, const mojo::HandleSignalsState& state) { |
| ReadData(); |
| } |
| |
| void OnWriteReady(MojoResult result, const mojo::HandleSignalsState& state) { |
| WriteData(); |
| } |
| |
| void ReadData() { |
| while (true) { |
| DCHECK(receive_stream_.is_valid()); |
| DCHECK_LT(bytes_received_, required_receive_bytes_); |
| base::span<const uint8_t> buffer; |
| MojoResult mojo_result = |
| receive_stream_->BeginReadData(MOJO_READ_DATA_FLAG_NONE, buffer); |
| if (mojo_result == MOJO_RESULT_SHOULD_WAIT) { |
| read_watcher_->ArmOrNotify(); |
| return; |
| } |
| DCHECK_EQ(mojo_result, MOJO_RESULT_OK); |
| |
| // This is guaranteed by Mojo. |
| DCHECK_GT(buffer.size(), 0u); |
| |
| for (uint8_t current : buffer) { |
| EXPECT_EQ(current, bytes_received_ % 256); |
| ++bytes_received_; |
| } |
| |
| mojo_result = receive_stream_->EndReadData(buffer.size()); |
| DCHECK_EQ(mojo_result, MOJO_RESULT_OK); |
| |
| if (bytes_received_ == required_receive_bytes_) { |
| if (bytes_sent_ == required_send_bytes_) { |
| run_loop_.Quit(); |
| } |
| return; |
| } |
| } |
| } |
| |
| void WriteData() { |
| while (true) { |
| DCHECK(send_stream_.is_valid()); |
| DCHECK_LT(bytes_sent_, required_send_bytes_); |
| base::span<uint8_t> buffer; |
| size_t size_hint = required_send_bytes_ - bytes_sent_; |
| MojoResult mojo_result = send_stream_->BeginWriteData( |
| size_hint, MOJO_WRITE_DATA_FLAG_NONE, buffer); |
| if (mojo_result == MOJO_RESULT_SHOULD_WAIT) { |
| write_watcher_->ArmOrNotify(); |
| return; |
| } |
| DCHECK_EQ(mojo_result, MOJO_RESULT_OK); |
| |
| // This is guaranteed by Mojo. |
| DCHECK_GT(buffer.size(), 0u); |
| |
| buffer = buffer.first( |
| std::min(buffer.size(), required_send_bytes_ - bytes_sent_)); |
| |
| for (char& c : base::as_writable_chars(buffer)) { |
| c = bytes_sent_ % 256; |
| ++bytes_sent_; |
| } |
| |
| mojo_result = send_stream_->EndWriteData(buffer.size()); |
| DCHECK_EQ(mojo_result, MOJO_RESULT_OK); |
| |
| if (bytes_sent_ == required_send_bytes_) { |
| if (bytes_received_ == required_receive_bytes_) { |
| run_loop_.Quit(); |
| } |
| return; |
| } |
| } |
| } |
| |
| const size_t required_receive_bytes_; |
| const size_t required_send_bytes_; |
| base::RunLoop run_loop_; |
| mojo::Remote<network::mojom::TCPConnectedSocket> accepted_socket_; |
| mojo::ScopedDataPipeConsumerHandle receive_stream_; |
| mojo::ScopedDataPipeProducerHandle send_stream_; |
| std::unique_ptr<mojo::SimpleWatcher> read_watcher_; |
| std::unique_ptr<mojo::SimpleWatcher> write_watcher_; |
| size_t bytes_received_ = 0; |
| size_t bytes_sent_ = 0; |
| }; |
| |
| } // anonymous namespace |
| |
| class DirectSocketsTcpBrowserTest : public ContentBrowserTest { |
| public: |
| GURL GetTestOpenPageURL() { |
| return embedded_test_server()->GetURL("/direct_sockets/open.html"); |
| } |
| |
| GURL GetTestPageURL() { |
| return embedded_test_server()->GetURL("/direct_sockets/tcp.html"); |
| } |
| |
| network::mojom::NetworkContext* GetNetworkContext() { |
| return browser_context()->GetDefaultStoragePartition()->GetNetworkContext(); |
| } |
| |
| // Returns the port listening for TCP connections. |
| uint16_t StartTcpServer() { |
| base::test::TestFuture<int32_t, const std::optional<net::IPEndPoint>&> |
| future; |
| auto options = network::mojom::TCPServerSocketOptions::New(); |
| options->backlog = 5; |
| GetNetworkContext()->CreateTCPServerSocket( |
| net::IPEndPoint(net::IPAddress::IPv4Localhost(), |
| /*port=*/0), |
| std::move(options), |
| net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS), |
| tcp_server_socket_.BindNewPipeAndPassReceiver(), future.GetCallback()); |
| auto local_addr = future.Get<std::optional<net::IPEndPoint>>(); |
| DCHECK(local_addr); |
| return local_addr->port(); |
| } |
| |
| mojo::Remote<network::mojom::TCPServerSocket>& tcp_server_socket() { |
| return tcp_server_socket_; |
| } |
| |
| raw_ptr<content::test::AsyncJsRunner> GetAsyncJsRunner() const { |
| return runner_.get(); |
| } |
| |
| void ConnectJsSocket(int port = 0) const { |
| const std::string open_socket = JsReplace( |
| R"( |
| socket = new TCPSocket($1, $2); |
| await socket.opened; |
| )", |
| kLocalhostAddress, port); |
| |
| ASSERT_TRUE( |
| EvalJs(shell(), content::test::WrapAsync(open_socket)).value.is_none()); |
| } |
| |
| protected: |
| void SetUpOnMainThread() override { |
| ContentBrowserTest::SetUpOnMainThread(); |
| |
| client_ = CreateContentBrowserClient(); |
| runner_ = |
| std::make_unique<content::test::AsyncJsRunner>(shell()->web_contents()); |
| |
| ASSERT_TRUE(NavigateToURL(shell(), GetTestPageURL())); |
| } |
| |
| void SetUp() override { |
| embedded_test_server()->AddDefaultHandlers(GetTestDataFilePath()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| ContentBrowserTest::SetUp(); |
| } |
| |
| virtual std::unique_ptr<ContentBrowserClient> CreateContentBrowserClient() { |
| return std::make_unique<test::IsolatedWebAppContentBrowserClient>( |
| url::Origin::Create(GetTestPageURL())); |
| } |
| |
| private: |
| BrowserContext* browser_context() { |
| return shell()->web_contents()->GetBrowserContext(); |
| } |
| |
| private: |
| mojo::Remote<network::mojom::TCPServerSocket> tcp_server_socket_; |
| |
| std::unique_ptr<ContentBrowserClient> client_; |
| std::unique_ptr<content::test::AsyncJsRunner> runner_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(DirectSocketsTcpBrowserTest, OpenTcp_Success) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetTestOpenPageURL())); |
| |
| const int listening_port = StartTcpServer(); |
| const std::string script = |
| JsReplace("openTcp($1, $2)", net::IPAddress::IPv4Localhost().ToString(), |
| listening_port); |
| |
| EXPECT_THAT(EvalJs(shell(), script).ExtractString(), |
| StartsWith("openTcp succeeded")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DirectSocketsTcpBrowserTest, OpenTcp_Success_Global) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetTestOpenPageURL())); |
| |
| const int listening_port = StartTcpServer(); |
| const std::string script = |
| JsReplace("openTcp($1, $2)", net::IPAddress::IPv4Localhost().ToString(), |
| listening_port); |
| |
| EXPECT_THAT(EvalJs(shell(), script).ExtractString(), |
| StartsWith("openTcp succeeded")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DirectSocketsTcpBrowserTest, CloseTcp) { |
| const int listening_port = StartTcpServer(); |
| const std::string script = |
| JsReplace("closeTcp($1, $2)", net::IPAddress::IPv4Localhost().ToString(), |
| listening_port); |
| |
| EXPECT_EQ("closeTcp succeeded", EvalJs(shell(), script)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DirectSocketsTcpBrowserTest, WriteTcp) { |
| constexpr int32_t kRequiredBytes = 10000; |
| |
| const int listening_port = StartTcpServer(); |
| ReadWriteWaiter waiter(/*required_receive_bytes=*/kRequiredBytes, |
| /*required_send_bytes=*/0, tcp_server_socket()); |
| |
| const std::string script = JsReplace( |
| "writeTcp($1, $2, {}, $3)", net::IPAddress::IPv4Localhost().ToString(), |
| listening_port, kRequiredBytes); |
| |
| EXPECT_EQ("write succeeded", EvalJs(shell(), script)); |
| waiter.Await(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DirectSocketsTcpBrowserTest, WriteLargeTcpPacket) { |
| // The default capacity of TCPSocket mojo pipe is 65536 bytes. This test |
| // verifies that out asynchronous writing logic actually works. |
| constexpr uint32_t defaultMojoPipeCapacity = (1 << 16); |
| constexpr int32_t kRequiredBytes = 3 * defaultMojoPipeCapacity + 1; |
| |
| const int listening_port = StartTcpServer(); |
| ReadWriteWaiter waiter(/*required_receive_bytes=*/kRequiredBytes, |
| /*required_send_bytes=*/0, tcp_server_socket()); |
| |
| const std::string script = |
| JsReplace("writeLargeTcpPacket($1, $2, $3)", |
| net::IPAddress::IPv4Localhost().ToString(), listening_port, |
| kRequiredBytes); |
| |
| EXPECT_EQ("writeLargeTcpPacket succeeded", EvalJs(shell(), script)); |
| waiter.Await(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DirectSocketsTcpBrowserTest, ReadTcp) { |
| constexpr int32_t kRequiredBytes = 150000; |
| |
| const int listening_port = StartTcpServer(); |
| ReadWriteWaiter waiter(/*required_receive_bytes=*/0, |
| /*required_send_bytes=*/kRequiredBytes, |
| tcp_server_socket()); |
| |
| const std::string script = JsReplace( |
| "readTcp($1, $2, {}, $3)", net::IPAddress::IPv4Localhost().ToString(), |
| listening_port, kRequiredBytes); |
| EXPECT_EQ("read succeeded", EvalJs(shell(), script)); |
| waiter.Await(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DirectSocketsTcpBrowserTest, ReadWriteTcp) { |
| constexpr int32_t kRequiredBytes = 1000; |
| |
| const int listening_port = StartTcpServer(); |
| ReadWriteWaiter waiter(/*required_receive_bytes=*/kRequiredBytes, |
| /*required_send_bytes=*/kRequiredBytes, |
| tcp_server_socket()); |
| const std::string script = |
| JsReplace("readWriteTcp($1, $2, {}, $3)", |
| net::IPAddress::IPv4Localhost().ToString(), listening_port, |
| kRequiredBytes); |
| EXPECT_EQ("readWrite succeeded", EvalJs(shell(), script)); |
| waiter.Await(); |
| } |
| |
| class MockTcpNetworkContext : public content::test::MockNetworkContext { |
| public: |
| MockTcpNetworkContext() : pipe_capacity_(1) {} |
| explicit MockTcpNetworkContext(uint32_t pipe_capacity) |
| : pipe_capacity_(pipe_capacity) {} |
| ~MockTcpNetworkContext() override = default; |
| |
| // network::TestNetworkContext: |
| void CreateTCPConnectedSocket( |
| const std::optional<net::IPEndPoint>& local_addr, |
| const net::AddressList& remote_addr_list, |
| network::mojom::TCPConnectedSocketOptionsPtr tcp_connected_socket_options, |
| const net::MutableNetworkTrafficAnnotationTag& traffic_annotation, |
| mojo::PendingReceiver<network::mojom::TCPConnectedSocket> socket, |
| mojo::PendingRemote<network::mojom::SocketObserver> observer, |
| CreateTCPConnectedSocketCallback callback) override { |
| const net::IPEndPoint& peer_addr = remote_addr_list.front(); |
| |
| mojo::ScopedDataPipeProducerHandle producer; |
| MojoResult producer_result = |
| mojo::CreateDataPipe(pipe_capacity_, producer, producer_complement_); |
| DCHECK_EQ(MOJO_RESULT_OK, producer_result); |
| |
| mojo::ScopedDataPipeConsumerHandle consumer; |
| MojoResult consumer_result = |
| CreateDataPipe(nullptr, consumer_complement_, consumer); |
| DCHECK_EQ(MOJO_RESULT_OK, consumer_result); |
| |
| observer_.Bind(std::move(observer)); |
| |
| std::move(callback).Run( |
| net::OK, net::IPEndPoint{net::IPAddress::IPv4Localhost(), 0}, peer_addr, |
| std::move(consumer), std::move(producer)); |
| } |
| |
| mojo::Remote<network::mojom::SocketObserver>& get_observer() { |
| return observer_; |
| } |
| |
| mojo::ScopedDataPipeProducerHandle& get_consumer_complement() { |
| return consumer_complement_; |
| } |
| mojo::ScopedDataPipeConsumerHandle& get_producer_complement() { |
| return producer_complement_; |
| } |
| |
| private: |
| mojo::ScopedDataPipeProducerHandle consumer_complement_; |
| mojo::ScopedDataPipeConsumerHandle producer_complement_; |
| |
| mojo::Remote<network::mojom::SocketObserver> observer_; |
| |
| const uint32_t pipe_capacity_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(DirectSocketsTcpBrowserTest, ReadTcpOnReadError) { |
| MockTcpNetworkContext mock_network_context; |
| DirectSocketsServiceImpl::SetNetworkContextForTesting(&mock_network_context); |
| |
| ConnectJsSocket(); |
| |
| const std::string async_script = |
| "readTcpOnError(socket, /*expected_read_success=*/false);"; |
| base::test::TestFuture<std::string> future = |
| GetAsyncJsRunner()->RunScript(async_script); |
| |
| { |
| // Simulate pipe shutdown on read error. Read requests must reject. |
| mock_network_context.get_observer()->OnReadError(net::ERR_NOT_IMPLEMENTED); |
| mock_network_context.get_consumer_complement().reset(); |
| } |
| |
| EXPECT_THAT(future.Get(), ::testing::HasSubstr("readTcpOnError succeeded.")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DirectSocketsTcpBrowserTest, ReadTcpOnPeerClosed) { |
| MockTcpNetworkContext mock_network_context; |
| DirectSocketsServiceImpl::SetNetworkContextForTesting(&mock_network_context); |
| |
| ConnectJsSocket(); |
| |
| const std::string async_script = |
| "readTcpOnError(socket, /*expected_read_success=*/true);"; |
| base::test::TestFuture<std::string> future = |
| GetAsyncJsRunner()->RunScript(async_script); |
| |
| { |
| // Simulate pipe shutdown on peer closed. Read requests must resolve with |
| // done = true. |
| mock_network_context.get_observer()->OnReadError(net::OK); |
| mock_network_context.get_consumer_complement().reset(); |
| } |
| |
| EXPECT_THAT(future.Get(), ::testing::HasSubstr("readTcpOnError succeeded.")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DirectSocketsTcpBrowserTest, WriteTcpOnWriteError) { |
| MockTcpNetworkContext mock_network_context; |
| DirectSocketsServiceImpl::SetNetworkContextForTesting(&mock_network_context); |
| |
| ConnectJsSocket(); |
| |
| const std::string async_script = "writeTcpOnError(socket);"; |
| base::test::TestFuture<std::string> future = |
| GetAsyncJsRunner()->RunScript(async_script); |
| |
| { |
| // Simulate pipe shutdown on write error. |
| mock_network_context.get_observer()->OnWriteError(net::ERR_NOT_IMPLEMENTED); |
| mock_network_context.get_producer_complement().reset(); |
| } |
| |
| EXPECT_THAT(future.Get(), ::testing::HasSubstr("writeTcpOnError succeeded.")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DirectSocketsTcpBrowserTest, |
| ReadWriteTcpOnSocketObserverError) { |
| MockTcpNetworkContext mock_network_context; |
| DirectSocketsServiceImpl::SetNetworkContextForTesting(&mock_network_context); |
| |
| ConnectJsSocket(); |
| |
| const std::string async_script = "readWriteTcpOnError(socket);"; |
| base::test::TestFuture<std::string> future = |
| GetAsyncJsRunner()->RunScript(async_script); |
| |
| mock_network_context.get_observer().reset(); |
| mock_network_context.get_consumer_complement().reset(); |
| mock_network_context.get_producer_complement().reset(); |
| |
| EXPECT_THAT(future.Get(), |
| ::testing::HasSubstr("readWriteTcpOnError succeeded.")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DirectSocketsTcpBrowserTest, |
| BarrierCallbackFiresWithErrorOnReadWriteError) { |
| MockTcpNetworkContext mock_network_context; |
| DirectSocketsServiceImpl::SetNetworkContextForTesting(&mock_network_context); |
| |
| ConnectJsSocket(); |
| |
| const std::string async_script = |
| "waitForClosedPromise(socket, /*expected_closed_result=*/false);"; |
| base::test::TestFuture<std::string> future = |
| GetAsyncJsRunner()->RunScript(async_script); |
| |
| { |
| mock_network_context.get_observer()->OnReadError(net::ERR_UNEXPECTED); |
| mock_network_context.get_consumer_complement().reset(); |
| mock_network_context.get_producer_complement().reset(); |
| mock_network_context.get_observer()->OnWriteError(net::ERR_UNEXPECTED); |
| } |
| |
| EXPECT_THAT(future.Get(), |
| ::testing::HasSubstr("waitForClosedPromise succeeded.")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DirectSocketsTcpBrowserTest, |
| BarrierCallbackFiresWithOkOnReaderAndWriterClose) { |
| MockTcpNetworkContext mock_network_context; |
| DirectSocketsServiceImpl::SetNetworkContextForTesting(&mock_network_context); |
| |
| ConnectJsSocket(); |
| |
| const std::string async_script = |
| "waitForClosedPromise(socket, /*expected_closed_result=*/true, " |
| "/*cancel_reader=*/true, /*close_writer=*/true);"; |
| base::test::TestFuture<std::string> future = |
| GetAsyncJsRunner()->RunScript(async_script); |
| |
| EXPECT_THAT(future.Get(), |
| ::testing::HasSubstr("waitForClosedPromise succeeded.")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DirectSocketsTcpBrowserTest, |
| BarrierCallbackFiresWithOkOnPeerAndWriterClose) { |
| MockTcpNetworkContext mock_network_context; |
| DirectSocketsServiceImpl::SetNetworkContextForTesting(&mock_network_context); |
| |
| ConnectJsSocket(); |
| |
| const std::string async_script = |
| "waitForClosedPromise(socket, /*expected_closed_result=*/true, " |
| "/*cancel_reader=*/false, /*close_writer=*/true);"; |
| base::test::TestFuture<std::string> future = |
| GetAsyncJsRunner()->RunScript(async_script); |
| |
| // Simulate peer closed event. |
| mock_network_context.get_observer()->OnReadError(net::OK); |
| mock_network_context.get_consumer_complement().reset(); |
| |
| EXPECT_THAT(future.Get(), |
| ::testing::HasSubstr("waitForClosedPromise succeeded.")); |
| } |
| |
| class DirectSocketsTcpServerBrowserTest : public DirectSocketsTcpBrowserTest { |
| public: |
| #if BUILDFLAG(IS_CHROMEOS) |
| DirectSocketsTcpServerBrowserTest() { |
| chromeos::PermissionBrokerClient::InitializeFake(); |
| DirectSocketsServiceImpl::SetAlwaysOpenFirewallHoleForTesting(); |
| } |
| |
| ~DirectSocketsTcpServerBrowserTest() override { |
| chromeos::PermissionBrokerClient::Shutdown(); |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(DirectSocketsTcpServerBrowserTest, ExchangeTcpServer) { |
| ASSERT_THAT(EvalJs(shell(), "exchangeSingleTcpPacketBetweenClientAndServer()") |
| .ExtractString(), |
| testing::HasSubstr("succeeded")); |
| } |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| IN_PROC_BROWSER_TEST_F(DirectSocketsTcpServerBrowserTest, HasFirewallHole) { |
| class DelegateImpl : public chromeos::FakePermissionBrokerClient::Delegate { |
| public: |
| DelegateImpl(uint16_t port, base::OnceClosure quit_closure) |
| : port_(port), quit_closure_(std::move(quit_closure)) {} |
| |
| void OnTcpPortReleased(uint16_t port, |
| const std::string& interface) override { |
| if (port == port_) { |
| ASSERT_EQ(interface, ""); |
| ASSERT_TRUE(quit_closure_); |
| std::move(quit_closure_).Run(); |
| } |
| } |
| |
| private: |
| uint16_t port_; |
| base::OnceClosure quit_closure_; |
| }; |
| |
| auto* client = chromeos::FakePermissionBrokerClient::Get(); |
| |
| const std::string open_script = R"( |
| (async () => { |
| socket = new TCPServerSocket('127.0.0.1'); |
| const { localPort } = await socket.opened; |
| return localPort; |
| })(); |
| )"; |
| |
| const int32_t local_port = EvalJs(shell(), open_script).ExtractInt(); |
| ASSERT_TRUE(client->HasTcpHole(local_port, "" /* all interfaces */)); |
| |
| base::RunLoop run_loop; |
| auto delegate = |
| std::make_unique<DelegateImpl>(local_port, run_loop.QuitClosure()); |
| client->AttachDelegate(delegate.get()); |
| |
| EXPECT_TRUE(EvalJs(shell(), content::test::WrapAsync("socket.close()")) |
| .error.empty()); |
| run_loop.Run(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DirectSocketsTcpServerBrowserTest, FirewallHoleDenied) { |
| auto* client = chromeos::FakePermissionBrokerClient::Get(); |
| client->SetTcpDenyAll(); |
| |
| const std::string open_script = R"( |
| (async () => { |
| socket = new TCPServerSocket('127.0.0.1'); |
| return await socket.opened.catch(err => err.message); |
| })(); |
| )"; |
| |
| EXPECT_THAT(EvalJs(shell(), open_script).ExtractString(), |
| testing::HasSubstr("Firewall")); |
| } |
| |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| IN_PROC_BROWSER_TEST_F(DirectSocketsTcpServerBrowserTest, OkOnClose) { |
| ASSERT_EQ(true, EvalJs(shell(), R"( |
| (async () => { |
| socket = new TCPServerSocket('127.0.0.1'); |
| await socket.opened; |
| socket.close(); |
| return await socket.closed.then(() => true); |
| })(); |
| )")); |
| } |
| |
| class MockNetworkContextWithTCPServerSocketReceiver |
| : public network::TestNetworkContext { |
| public: |
| void CreateTCPServerSocket( |
| const net::IPEndPoint& local_addr, |
| network::mojom::TCPServerSocketOptionsPtr options, |
| const net::MutableNetworkTrafficAnnotationTag& traffic_annotation, |
| mojo::PendingReceiver<network::mojom::TCPServerSocket> socket, |
| CreateTCPServerSocketCallback callback) override { |
| receiver_.Bind(std::move(socket)); |
| std::move(callback).Run(net::OK, /*local_addr=*/net::IPEndPoint( |
| net::IPAddress::IPv4Localhost(), 0)); |
| } |
| |
| void ResetSocketReceiver() { receiver_.reset(); } |
| |
| private: |
| mojo::Receiver<network::mojom::TCPServerSocket> receiver_{nullptr}; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(DirectSocketsTcpServerBrowserTest, ErrorOnRemoteReset) { |
| MockNetworkContextWithTCPServerSocketReceiver mock_network_context; |
| DirectSocketsServiceImpl::SetNetworkContextForTesting(&mock_network_context); |
| |
| ASSERT_EQ(true, EvalJs(shell(), R"( |
| (async () => { |
| socket = new TCPServerSocket('127.0.0.1'); |
| await socket.opened; |
| return true; |
| })(); |
| )")); |
| |
| base::test::TestFuture<std::string> future = GetAsyncJsRunner()->RunScript( |
| test::WrapAsync("return socket.closed.catch(() => 'ok')")); |
| mock_network_context.ResetSocketReceiver(); |
| |
| ASSERT_EQ("ok", future.Get()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DirectSocketsTcpServerBrowserTest, Ipv6Only) { |
| // Should be able to connect as mapped IPv4 with |ipv6Only| = false. |
| EXPECT_EQ( |
| true, |
| EvalJs(shell(), |
| "connectToServerWithIPv6Only(/*ipv6Only=*/false, '127.0.0.1')")); |
| |
| // Connection to IPv4 loopback is rejected with |ipv6Only| = true. |
| EXPECT_EQ( |
| false, |
| EvalJs(shell(), |
| "connectToServerWithIPv6Only(/*ipv6Only=*/true, '127.0.0.1')")); |
| |
| // Connection to IPv6 loopback succeeds. |
| EXPECT_EQ( |
| true, |
| EvalJs(shell(), "connectToServerWithIPv6Only(/*ipv6Only=*/true, '::1')")); |
| } |
| |
| // A ContentBrowserClient that grants Isolated Web Apps the "direct-sockets" |
| // permission, but not "cross-origin-isolated", which should result in Direct |
| // Sockets being disabled. |
| class NoCoiPermissionIsolatedWebAppContentBrowserClient |
| : public test::IsolatedWebAppContentBrowserClient { |
| public: |
| explicit NoCoiPermissionIsolatedWebAppContentBrowserClient( |
| const url::Origin& isolated_app_origin) |
| : IsolatedWebAppContentBrowserClient(isolated_app_origin) {} |
| |
| std::optional<network::ParsedPermissionsPolicy> |
| GetPermissionsPolicyForIsolatedWebApp( |
| WebContents* web_contents, |
| const url::Origin& app_origin) override { |
| return {{network::ParsedPermissionsPolicyDeclaration( |
| network::mojom::PermissionsPolicyFeature::kDirectSockets, |
| /*allowed_origins=*/{}, |
| /*self_if_matches=*/app_origin, |
| /*matches_all_origins=*/false, /*matches_opaque_src=*/false)}}; |
| } |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(DirectSocketsTcpBrowserTest, NoCoiPermission) { |
| NoCoiPermissionIsolatedWebAppContentBrowserClient client( |
| url::Origin::Create(GetTestPageURL())); |
| |
| ASSERT_TRUE(NavigateToURL(shell(), GetTestOpenPageURL())); |
| |
| EXPECT_EQ(false, EvalJs(shell(), "self.crossOriginIsolated")); |
| |
| const int listening_port = StartTcpServer(); |
| const std::string script = |
| JsReplace("openTcp($1, $2)", net::IPAddress::IPv4Localhost().ToString(), |
| listening_port); |
| |
| EXPECT_THAT(EvalJs(shell(), script).ExtractString(), |
| StartsWith("openTcp failed: NotAllowedError")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DirectSocketsTcpBrowserTest, NotInCrossOriginIframe) { |
| net::EmbeddedTestServer test_server2{net::EmbeddedTestServer::TYPE_HTTPS}; |
| test_server2.AddDefaultHandlers(); |
| net::test_server::EmbeddedTestServerHandle server_handle = |
| test_server2.StartAndReturnHandle(); |
| |
| ASSERT_TRUE(NavigateToURL(shell(), GetTestOpenPageURL())); |
| |
| EXPECT_EQ(true, EvalJs(shell(), "self.crossOriginIsolated")); |
| EXPECT_TRUE(ExecJs(shell(), "TCPSocket !== undefined")); |
| |
| constexpr const char kCreateIframeJs[] = R"( |
| new Promise(resolve => { |
| let f = document.createElement('iframe'); |
| f.src = $1; |
| f.allow = 'cross-origin-isolated'; |
| f.addEventListener('load', () => resolve()); |
| document.body.appendChild(f); |
| }); |
| )"; |
| GURL cross_origin_corp_url = test_server2.GetURL( |
| "/set-header?" |
| "Cross-Origin-Opener-Policy: same-origin&" |
| "Cross-Origin-Embedder-Policy: require-corp&" |
| "Cross-Origin-Resource-Policy: cross-origin&"); |
| ASSERT_TRUE(content::ExecJs( |
| shell(), content::JsReplace(kCreateIframeJs, cross_origin_corp_url))); |
| |
| content::RenderFrameHost* iframe_rfh = content::ChildFrameAt(shell(), 0); |
| EXPECT_FALSE(iframe_rfh->IsErrorDocument()); |
| EXPECT_EQ(cross_origin_corp_url, iframe_rfh->GetLastCommittedURL()); |
| EXPECT_EQ(true, EvalJs(iframe_rfh, "self.crossOriginIsolated")); |
| EXPECT_TRUE(ExecJs(shell(), "TCPSocket === undefined")); |
| } |
| |
| class IsolatedContextContentBrowserClient |
| : public ContentBrowserTestContentBrowserClient { |
| public: |
| bool IsIsolatedContextAllowedForUrl(BrowserContext* browser_context, |
| const GURL& lock_url) override { |
| return lock_url.is_valid(); |
| } |
| }; |
| |
| class DirectSocketsIsolatedContextTcpBrowserTest |
| : public DirectSocketsTcpBrowserTest { |
| protected: |
| std::unique_ptr<ContentBrowserClient> CreateContentBrowserClient() override { |
| return std::make_unique<IsolatedContextContentBrowserClient>(); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_{ |
| blink::features::kIsolateSandboxedIframes}; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(DirectSocketsIsolatedContextTcpBrowserTest, |
| NotAvailableInSandboxedIframes) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetTestOpenPageURL())); |
| |
| ASSERT_EQ(true, EvalJs(shell(), "'TCPSocket' in window")); |
| |
| // Verify that non-sandboxed iframes have TCPSocket. |
| ASSERT_TRUE(ExecJs(shell(), content::JsReplace(R"( |
| new Promise(resolve => { |
| let f = document.createElement('iframe'); |
| f.src = $1; |
| f.addEventListener('load', () => resolve()); |
| document.body.appendChild(f); |
| }); |
| )", |
| GetTestOpenPageURL()))); |
| content::RenderFrameHost* iframe1_rfh = content::ChildFrameAt(shell(), 0); |
| |
| ASSERT_EQ(true, EvalJs(iframe1_rfh, "'TCPSocket' in window")); |
| |
| // Verify that sandboxed iframes don't have TCPSocket. |
| ASSERT_TRUE(ExecJs(shell(), content::JsReplace(R"( |
| new Promise(resolve => { |
| let f = document.createElement('iframe'); |
| f.src = $1; |
| f.sandbox = 'allow-scripts'; |
| f.addEventListener('load', () => resolve()); |
| document.body.appendChild(f); |
| }); |
| )", |
| GetTestOpenPageURL()))); |
| content::RenderFrameHost* iframe2_rfh = content::ChildFrameAt(shell(), 1); |
| |
| ASSERT_EQ(false, EvalJs(iframe2_rfh, "'TCPSocket' in window")); |
| } |
| |
| } // namespace content |