blob: 25adf3996961a2aff06a9b62ae53f9b900353fab [file] [log] [blame]
// 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.
#include "content/browser/direct_sockets/direct_sockets_service_impl.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/weak_ptr.h"
#include "build/build_config.h"
#include "content/browser/process_lock.h"
#include "content/browser/renderer_host/isolated_context_util.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/direct_sockets_delegate.h"
#include "content/public/browser/document_service.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/common/content_client.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/unique_receiver_set.h"
#include "net/base/host_port_pair.h"
#include "net/base/ip_address.h"
#include "net/base/ip_endpoint.h"
#include "net/base/network_anonymization_key.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/simple_host_resolver.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "services/network/public/mojom/restricted_udp_socket.mojom.h"
#include "services/network/public/mojom/tcp_socket.mojom.h"
#include "services/network/public/mojom/udp_socket.mojom.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/public/mojom/direct_sockets/direct_sockets.mojom.h"
#include "third_party/blink/public/mojom/permissions_policy/permissions_policy_feature.mojom-shared.h"
#if BUILDFLAG(IS_WIN)
#include <winsock2.h>
#endif // BUILDFLAG(IS_WIN)
#if BUILDFLAG(IS_POSIX)
#include <sys/socket.h>
#endif // BUILDFLAG(IS_POSIX)
#if BUILDFLAG(IS_CHROMEOS)
#include "content/public/browser/firewall_hole_proxy.h"
#include "services/network/public/mojom/socket_connection_tracker.mojom.h"
#endif // BUILDFLAG(IS_CHROMEOS)
namespace content {
namespace {
#if BUILDFLAG(IS_CHROMEOS)
bool g_always_open_firewall_hole_for_testing = false;
#endif // BUILDFLAG(IS_CHROMEOS)
constexpr net::NetworkTrafficAnnotationTag kDirectSocketsTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("direct_sockets", R"(
semantics {
sender: "Direct Sockets API"
description: "Web app request to communicate with network device"
trigger: "User completes network connection dialog"
data: "Any data sent by web app"
destination: OTHER
destination_other: "Address entered by user in connection dialog"
}
policy {
cookies_allowed: NO
setting: "This feature cannot yet be controlled by settings."
policy_exception_justification: "To be implemented"
}
)");
constexpr int32_t kMaxBufferSize = 32 * 1024 * 1024;
network::mojom::NetworkContext*& GetNetworkContextForTesting() {
static network::mojom::NetworkContext* network_context = nullptr;
return network_context;
}
network::mojom::TCPConnectedSocketOptionsPtr CreateTCPConnectedSocketOptions(
blink::mojom::DirectTCPSocketOptionsPtr options) {
network::mojom::TCPConnectedSocketOptionsPtr tcp_connected_socket_options =
network::mojom::TCPConnectedSocketOptions::New();
if (options->send_buffer_size > 0) {
tcp_connected_socket_options->send_buffer_size =
std::min(options->send_buffer_size, kMaxBufferSize);
}
if (options->receive_buffer_size > 0) {
tcp_connected_socket_options->receive_buffer_size =
std::min(options->receive_buffer_size, kMaxBufferSize);
}
tcp_connected_socket_options->no_delay = options->no_delay;
if (options->keep_alive_options) {
// options->keep_alive_options will be invalidated.
tcp_connected_socket_options->keep_alive_options =
std::move(options->keep_alive_options);
}
return tcp_connected_socket_options;
}
#if BUILDFLAG(ENABLE_MDNS)
bool ResemblesMulticastDNSName(base::StringPiece hostname) {
return base::EndsWith(hostname, ".local") ||
base::EndsWith(hostname, ".local.");
}
#endif // !BUILDFLAG(ENABLE_MDNS)
bool ValidateAddressAndPort(RenderFrameHost& rfh,
const std::string& address,
uint16_t port,
DirectSocketsDelegate::ProtocolType protocol) {
auto* delegate = GetContentClient()->browser()->GetDirectSocketsDelegate();
if (!delegate) {
// No additional rules from the embedder.
return true;
}
return delegate->ValidateAddressAndPort(
rfh.GetBrowserContext(), rfh.GetProcess()->GetProcessLock().lock_url(),
address, port, protocol);
}
bool ValidateAddressAndPort(RenderFrameHost& rfh,
const net::IPEndPoint& ip_endpoint,
DirectSocketsDelegate::ProtocolType protocol) {
return ValidateAddressAndPort(rfh, ip_endpoint.address().ToString(),
ip_endpoint.port(), protocol);
}
bool ValidateAddressAndPort(RenderFrameHost& rfh,
const net::HostPortPair& host_port_pair,
DirectSocketsDelegate::ProtocolType protocol) {
return ValidateAddressAndPort(rfh, host_port_pair.host(),
host_port_pair.port(), protocol);
}
#if BUILDFLAG(IS_CHROMEOS)
bool ShouldOpenFirewallHole(const net::IPAddress& address) {
if (g_always_open_firewall_hole_for_testing) {
return true;
}
return !address.IsLoopback();
}
#endif // BUILDFLAG(IS_CHROMEOS)
} // namespace
#if BUILDFLAG(IS_CHROMEOS)
// This class inherits from SocketConnectionTracker so that all stored firewall
// hole handles reference |this| in the internal ReceiverSet.
class DirectSocketsServiceImpl::FirewallHoleDelegate
: public network::mojom::SocketConnectionTracker {
public:
void OpenTCPFirewallHole(
mojo::PendingReceiver<network::mojom::SocketConnectionTracker>
connection_tracker,
OpenTCPServerSocketCallback callback,
int32_t result,
const absl::optional<net::IPEndPoint>& local_addr) {
if (result != net::OK) {
std::move(callback).Run(result, /*local_addr=*/absl::nullopt);
return;
}
if (!ShouldOpenFirewallHole(local_addr->address())) {
std::move(callback).Run(net::OK, *local_addr);
return;
}
auto [callback_a, callback_b] =
base::SplitOnceCallback(std::move(callback));
content::OpenTCPFirewallHole(
"" /*all interfaces*/, local_addr->port(),
base::BindOnce(
&FirewallHoleDelegate::OnFirewallHoleOpened, GetWeakPtr(),
std::move(connection_tracker),
/*on_success=*/
base::BindOnce(std::move(callback_a), net::OK, *local_addr),
/*on_failure=*/
base::BindOnce(std::move(callback_b),
net::ERR_NETWORK_ACCESS_DENIED, absl::nullopt)));
}
void OpenUDPFirewallHole(
mojo::PendingReceiver<network::mojom::SocketConnectionTracker>
connection_tracker,
OpenBoundUDPSocketCallback callback,
int32_t result,
const absl::optional<net::IPEndPoint>& local_addr) {
if (result != net::OK) {
std::move(callback).Run(result, /*local_addr=*/absl::nullopt);
return;
}
if (!ShouldOpenFirewallHole(local_addr->address())) {
std::move(callback).Run(net::OK, *local_addr);
return;
}
auto [callback_a, callback_b] =
base::SplitOnceCallback(std::move(callback));
content::OpenUDPFirewallHole(
"" /*all interfaces*/, local_addr->port(),
base::BindOnce(
&FirewallHoleDelegate::OnFirewallHoleOpened, GetWeakPtr(),
std::move(connection_tracker),
/*on_success=*/
base::BindOnce(std::move(callback_a), net::OK, *local_addr),
/*on_failure=*/
base::BindOnce(std::move(callback_b),
net::ERR_NETWORK_ACCESS_DENIED, absl::nullopt)));
}
base::WeakPtr<FirewallHoleDelegate> GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
private:
void OnFirewallHoleOpened(
mojo::PendingReceiver<network::mojom::SocketConnectionTracker>
connection_tracker,
base::OnceClosure on_success,
base::OnceClosure on_failure,
std::unique_ptr<content::FirewallHoleProxy> firewall_hole_proxy) {
if (!firewall_hole_proxy) {
std::move(on_failure).Run();
return;
}
receivers_.Add(this, std::move(connection_tracker),
std::move(firewall_hole_proxy));
std::move(on_success).Run();
}
mojo::ReceiverSet<network::mojom::SocketConnectionTracker,
std::unique_ptr<content::FirewallHoleProxy>>
receivers_;
base::WeakPtrFactory<FirewallHoleDelegate> weak_factory_{this};
};
#endif // BUILDFLAG(IS_CHROMEOS)
DirectSocketsServiceImpl::DirectSocketsServiceImpl(
RenderFrameHost* render_frame_host,
mojo::PendingReceiver<blink::mojom::DirectSocketsService> receiver)
: DocumentService(*render_frame_host, std::move(receiver)),
resolver_(network::SimpleHostResolver::Create(GetNetworkContext())) {
#if BUILDFLAG(IS_CHROMEOS)
firewall_hole_delegate_ = std::make_unique<FirewallHoleDelegate>();
#endif // BUILDFLAG(IS_CHROMEOS)
}
DirectSocketsServiceImpl::~DirectSocketsServiceImpl() = default;
// static
void DirectSocketsServiceImpl::CreateForFrame(
RenderFrameHost* render_frame_host,
mojo::PendingReceiver<blink::mojom::DirectSocketsService> receiver) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!render_frame_host->IsFeatureEnabled(
blink::mojom::PermissionsPolicyFeature::kDirectSockets)) {
mojo::ReportBadMessage(
"Permissions policy blocks access to Direct Sockets.");
}
if (!IsFrameSufficientlyIsolated(render_frame_host)) {
mojo::ReportBadMessage(
"Frame is not sufficiently isolated to use Direct Sockets.");
return;
}
new DirectSocketsServiceImpl(render_frame_host, std::move(receiver));
}
void DirectSocketsServiceImpl::OpenTCPSocket(
blink::mojom::DirectTCPSocketOptionsPtr options,
mojo::PendingReceiver<network::mojom::TCPConnectedSocket> receiver,
mojo::PendingRemote<network::mojom::SocketObserver> observer,
OpenTCPSocketCallback callback) {
net::HostPortPair remote_addr = options->remote_addr;
if (!ValidateAddressAndPort(render_frame_host(), remote_addr,
DirectSocketsDelegate::ProtocolType::kTcp)) {
std::move(callback).Run(net::ERR_ACCESS_DENIED, absl::nullopt,
absl::nullopt, mojo::ScopedDataPipeConsumerHandle(),
mojo::ScopedDataPipeProducerHandle());
return;
}
network::mojom::ResolveHostParametersPtr parameters =
network::mojom::ResolveHostParameters::New();
parameters->dns_query_type = options->dns_query_type;
#if BUILDFLAG(ENABLE_MDNS)
if (ResemblesMulticastDNSName(remote_addr.host())) {
parameters->source = net::HostResolverSource::MULTICAST_DNS;
}
#endif // !BUILDFLAG(ENABLE_MDNS)
// Unretained(this) is safe here because the callback will be owned by
// |resolver_| which in turn is owned by |this|.
resolver_->ResolveHost(
network::mojom::HostResolverHost::NewHostPortPair(std::move(remote_addr)),
net::NetworkAnonymizationKey::CreateTransient(), std::move(parameters),
base::BindOnce(&DirectSocketsServiceImpl::OnResolveCompleteForTCPSocket,
base::Unretained(this), std::move(options),
std::move(receiver), std::move(observer),
std::move(callback)));
}
void DirectSocketsServiceImpl::OpenConnectedUDPSocket(
blink::mojom::DirectConnectedUDPSocketOptionsPtr options,
mojo::PendingReceiver<network::mojom::RestrictedUDPSocket> receiver,
mojo::PendingRemote<network::mojom::UDPSocketListener> listener,
OpenConnectedUDPSocketCallback callback) {
net::HostPortPair remote_addr = options->remote_addr;
if (!ValidateAddressAndPort(
render_frame_host(), remote_addr,
DirectSocketsDelegate::ProtocolType::kConnectedUdp)) {
std::move(callback).Run(net::ERR_ACCESS_DENIED, absl::nullopt,
absl::nullopt);
return;
}
network::mojom::ResolveHostParametersPtr parameters =
network::mojom::ResolveHostParameters::New();
parameters->dns_query_type = options->dns_query_type;
#if BUILDFLAG(ENABLE_MDNS)
if (ResemblesMulticastDNSName(remote_addr.host())) {
parameters->source = net::HostResolverSource::MULTICAST_DNS;
}
#endif // !BUILDFLAG(ENABLE_MDNS)
// Unretained(this) is safe here because the callback will be owned by
// |resolver_| which in turn is owned by |this|.
resolver_->ResolveHost(
network::mojom::HostResolverHost::NewHostPortPair(std::move(remote_addr)),
net::NetworkAnonymizationKey::CreateTransient(), std::move(parameters),
base::BindOnce(&DirectSocketsServiceImpl::OnResolveCompleteForUDPSocket,
base::Unretained(this), std::move(options),
std::move(receiver), std::move(listener),
std::move(callback)));
}
void DirectSocketsServiceImpl::OpenBoundUDPSocket(
blink::mojom::DirectBoundUDPSocketOptionsPtr options,
mojo::PendingReceiver<network::mojom::RestrictedUDPSocket> receiver,
mojo::PendingRemote<network::mojom::UDPSocketListener> listener,
OpenBoundUDPSocketCallback callback) {
if (!ValidateAddressAndPort(render_frame_host(), options->local_addr,
DirectSocketsDelegate::ProtocolType::kBoundUdp)) {
std::move(callback).Run(net::ERR_ACCESS_DENIED,
/*local_addr=*/absl::nullopt);
return;
}
auto socket_options = network::mojom::UDPSocketOptions::New();
socket_options->send_buffer_size = options->send_buffer_size;
socket_options->receive_buffer_size = options->receive_buffer_size;
auto params = network::mojom::RestrictedUDPSocketParams::New();
params->socket_options = std::move(socket_options);
#if BUILDFLAG(IS_CHROMEOS)
mojo::PendingReceiver<network::mojom::SocketConnectionTracker>
connection_tracker;
params->connection_tracker =
connection_tracker.InitWithNewPipeAndPassRemote();
#endif // BUILDFLAG(IS_CHROMEOS)
GetNetworkContext()->CreateRestrictedUDPSocket(
options->local_addr,
/*mode=*/network::mojom::RestrictedUDPSocketMode::BOUND,
/*traffic_annotation=*/
net::MutableNetworkTrafficAnnotationTag(kDirectSocketsTrafficAnnotation),
/*params=*/std::move(params), std::move(receiver), std::move(listener),
#if !BUILDFLAG(IS_CHROMEOS)
std::move(callback)
#else // BUILDFLAG(IS_CHROMEOS)
// On ChromeOS the original callback will be invoked after punching a
// firewall hole.
base::BindOnce(&FirewallHoleDelegate::OpenUDPFirewallHole,
firewall_hole_delegate_->GetWeakPtr(),
std::move(connection_tracker), std::move(callback))
#endif // BUILDFLAG(IS_CHROMEOS)
);
}
void DirectSocketsServiceImpl::OpenTCPServerSocket(
blink::mojom::DirectTCPServerSocketOptionsPtr options,
mojo::PendingReceiver<network::mojom::TCPServerSocket> socket,
OpenTCPServerSocketCallback callback) {
if (!ValidateAddressAndPort(
render_frame_host(), options->local_addr,
DirectSocketsDelegate::ProtocolType::kTcpServer)) {
std::move(callback).Run(net::ERR_ACCESS_DENIED,
/*local_addr=*/absl::nullopt);
return;
}
auto server_options = network::mojom::TCPServerSocketOptions::New();
// Default if |options->backlog| is 0.
server_options->backlog = SOMAXCONN;
if (options->backlog > 0) {
// Truncate the provided value if it is larger than allowed by the platform.
server_options->backlog = std::min<uint32_t>(options->backlog, SOMAXCONN);
}
#if BUILDFLAG(IS_CHROMEOS)
mojo::PendingReceiver<network::mojom::SocketConnectionTracker>
connection_tracker;
server_options->connection_tracker =
connection_tracker.InitWithNewPipeAndPassRemote();
#endif // BUILDFLAG(IS_CHROMEOS)
GetNetworkContext()->CreateTCPServerSocket(
options->local_addr, std::move(server_options),
net::MutableNetworkTrafficAnnotationTag(kDirectSocketsTrafficAnnotation),
std::move(socket),
#if !BUILDFLAG(IS_CHROMEOS)
std::move(callback)
#else // BUILDFLAG(IS_CHROMEOS)
// On ChromeOS the original callback will be invoked after punching a
// firewall hole.
base::BindOnce(&FirewallHoleDelegate::OpenTCPFirewallHole,
firewall_hole_delegate_->GetWeakPtr(),
std::move(connection_tracker), std::move(callback))
#endif // BUILDFLAG(IS_CHROMEOS)
);
}
// static
void DirectSocketsServiceImpl::SetNetworkContextForTesting(
network::mojom::NetworkContext* network_context) {
GetNetworkContextForTesting() = network_context;
}
#if BUILDFLAG(IS_CHROMEOS)
// static
void DirectSocketsServiceImpl::SetAlwaysOpenFirewallHoleForTesting(bool value) {
g_always_open_firewall_hole_for_testing = value;
}
#endif // BUILDFLAG(IS_CHROMEOS)
network::mojom::NetworkContext* DirectSocketsServiceImpl::GetNetworkContext()
const {
if (auto* network_context = GetNetworkContextForTesting()) {
return network_context;
}
return render_frame_host().GetStoragePartition()->GetNetworkContext();
}
void DirectSocketsServiceImpl::OnResolveCompleteForTCPSocket(
blink::mojom::DirectTCPSocketOptionsPtr options,
mojo::PendingReceiver<network::mojom::TCPConnectedSocket> socket,
mojo::PendingRemote<network::mojom::SocketObserver> observer,
OpenTCPSocketCallback callback,
int result,
const net::ResolveErrorInfo&,
const absl::optional<net::AddressList>& resolved_addresses,
const absl::optional<net::HostResolverEndpointResults>&) {
if (result != net::OK) {
std::move(callback).Run(result, absl::nullopt, absl::nullopt,
mojo::ScopedDataPipeConsumerHandle(),
mojo::ScopedDataPipeProducerHandle());
return;
}
DCHECK(resolved_addresses && !resolved_addresses->empty());
absl::optional<net::IPEndPoint> local_addr = options->local_addr;
GetNetworkContext()->CreateTCPConnectedSocket(
/*local_addr=*/std::move(local_addr),
/*remote_addr_list=*/*resolved_addresses,
CreateTCPConnectedSocketOptions(std::move(options)),
net::MutableNetworkTrafficAnnotationTag(kDirectSocketsTrafficAnnotation),
std::move(socket), std::move(observer), std::move(callback));
}
void DirectSocketsServiceImpl::OnResolveCompleteForUDPSocket(
blink::mojom::DirectConnectedUDPSocketOptionsPtr options,
mojo::PendingReceiver<network::mojom::RestrictedUDPSocket>
restricted_udp_socket_receiver,
mojo::PendingRemote<network::mojom::UDPSocketListener> listener,
OpenConnectedUDPSocketCallback callback,
int result,
const net::ResolveErrorInfo&,
const absl::optional<net::AddressList>& resolved_addresses,
const absl::optional<net::HostResolverEndpointResults>&) {
if (result != net::OK) {
std::move(callback).Run(result, /*local_addr=*/absl::nullopt,
/*peer_addr=*/absl::nullopt);
return;
}
DCHECK(resolved_addresses && !resolved_addresses->empty());
auto socket_options = network::mojom::UDPSocketOptions::New();
socket_options->send_buffer_size = options->send_buffer_size;
socket_options->receive_buffer_size = options->receive_buffer_size;
auto params = network::mojom::RestrictedUDPSocketParams::New();
params->socket_options = std::move(socket_options);
const net::IPEndPoint& peer_addr = resolved_addresses->front();
GetNetworkContext()->CreateRestrictedUDPSocket(
peer_addr,
/*mode=*/network::mojom::RestrictedUDPSocketMode::CONNECTED,
/*traffic_annotation=*/
net::MutableNetworkTrafficAnnotationTag(kDirectSocketsTrafficAnnotation),
std::move(params), std::move(restricted_udp_socket_receiver),
std::move(listener),
base::BindOnce(
[](OpenConnectedUDPSocketCallback callback, net::IPEndPoint peer_addr,
int result, const absl::optional<net::IPEndPoint>& local_addr) {
std::move(callback).Run(result, local_addr, peer_addr);
},
std::move(callback), peer_addr));
}
} // namespace content