blob: 1671db77037545c92541bc4bb0423d05e5aea90c [file] [log] [blame]
// Copyright 2022 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 "content/browser/direct_sockets/resolve_host_and_open_socket.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "content/browser/direct_sockets/direct_sockets_service_impl.h"
#include "net/base/ip_endpoint.h"
#include "net/http/http_response_headers.h"
#include "net/net_buildflags.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "services/network/public/mojom/url_loader_factory.mojom.h"
#include "third_party/blink/public/mojom/direct_sockets/direct_sockets.mojom-shared.h"
namespace {
constexpr char kPermissionDeniedHistogramName[] =
"DirectSockets.PermissionDeniedFailures";
constexpr base::TimeDelta kCorsRequestTimeout = base::Seconds(15);
constexpr int kHttpsPort = 443;
#if BUILDFLAG(ENABLE_MDNS)
bool ResemblesMulticastDNSName(const std::string& hostname) {
return base::EndsWith(hostname, ".local") ||
base::EndsWith(hostname, ".local.");
}
#endif // !BUILDFLAG(ENABLE_MDNS)
bool ContainNonPubliclyRoutableAddress(const net::AddressList& addresses) {
DCHECK(!addresses.empty());
for (auto ip : addresses) {
if (!ip.address().IsPubliclyRoutable())
return true;
}
return false;
}
absl::optional<net::IPEndPoint> GetLocalAddr(
const blink::mojom::DirectSocketOptions& options) {
absl::optional<net::IPEndPoint> local_addr = absl::nullopt;
if (!options.local_hostname) {
return {};
}
if (net::IPAddress local_address;
local_address.AssignFromIPLiteral(*options.local_hostname)) {
return net::IPEndPoint(local_address, options.local_port);
}
return {};
}
absl::optional<int> g_https_port_for_testing = {};
} // namespace
namespace content {
// ResolveHostAndOpenSocket implementation.
ResolveHostAndOpenSocket::ResolveHostAndOpenSocket(
base::WeakPtr<DirectSocketsServiceImpl> service,
blink::mojom::DirectSocketOptionsPtr options)
: service_(std::move(service)), options_(std::move(options)) {}
ResolveHostAndOpenSocket::~ResolveHostAndOpenSocket() = default;
void ResolveHostAndOpenSocket::Start() {
auto* network_context = service_->GetNetworkContext();
DCHECK(network_context);
DCHECK(!receiver_.is_bound());
DCHECK(!resolver_.is_bound());
if (net::IPAddress().AssignFromIPLiteral(*options_->remote_hostname)) {
is_raw_address_ = true;
}
network_context->CreateHostResolver(absl::nullopt,
resolver_.BindNewPipeAndPassReceiver());
network::mojom::ResolveHostParametersPtr parameters =
network::mojom::ResolveHostParameters::New();
#if BUILDFLAG(ENABLE_MDNS)
if (ResemblesMulticastDNSName(*options_->remote_hostname)) {
parameters->source = net::HostResolverSource::MULTICAST_DNS;
is_mdns_name_ = true;
}
#endif // !BUILDFLAG(ENABLE_MDNS)
// Intentionally using a HostPortPair because scheme isn't specified.
resolver_->ResolveHost(
network::mojom::HostResolverHost::NewHostPortPair(
net::HostPortPair(*options_->remote_hostname, options_->remote_port)),
net::NetworkIsolationKey::CreateTransient(), std::move(parameters),
receiver_.BindNewPipeAndPassRemote());
receiver_.set_disconnect_handler(
base::BindOnce(&ResolveHostAndOpenSocket::OnComplete,
base::Unretained(this), net::ERR_NAME_NOT_RESOLVED,
net::ResolveErrorInfo(net::ERR_FAILED), absl::nullopt));
}
// static
int ResolveHostAndOpenSocket::GetHttpsPort() {
if (g_https_port_for_testing) {
return *g_https_port_for_testing;
}
return kHttpsPort;
}
// static
void ResolveHostAndOpenSocket::SetHttpsPortForTesting(
absl::optional<int> https_port) {
g_https_port_for_testing = https_port;
}
void ResolveHostAndOpenSocket::OnComplete(
int result,
const net::ResolveErrorInfo& resolve_error_info,
const absl::optional<net::AddressList>& resolved_addresses) {
DCHECK(receiver_.is_bound());
receiver_.reset();
// Reject hostnames that resolve to non-public exception unless a raw IP
// address or a *.local hostname is entered by the user.
if (!is_raw_address_ && !is_mdns_name_ && resolved_addresses &&
ContainNonPubliclyRoutableAddress(*resolved_addresses)) {
base::UmaHistogramEnumeration(
kPermissionDeniedHistogramName,
blink::mojom::DirectSocketFailureType::kResolvingToNonPublic);
OpenSocket(net::ERR_NETWORK_ACCESS_DENIED, {});
return;
}
if (result == net::OK && resolved_addresses) {
if (options_->remote_port == GetHttpsPort()) {
// Delegates to OpenSocket(...) after the check.
// We cannot use the resolved address here since it causes problems
// with SSL :(
PerformCORSCheck(*options_->remote_hostname, *resolved_addresses);
return;
}
}
OpenSocket(result, resolved_addresses);
}
void ResolveHostAndOpenSocket::PerformCORSCheck(
const std::string& address,
net::AddressList resolved_addresses) {
if (!service_) {
OpenSocket(net::ERR_UNEXPECTED, {});
return;
}
auto* frame = service_->GetFrameHost();
if (!frame) {
OpenSocket(net::ERR_UNEXPECTED, {});
return;
}
mojo::Remote<network::mojom::URLLoaderFactory> factory;
frame->CreateNetworkServiceDefaultFactory(
factory.BindNewPipeAndPassReceiver());
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url =
url::SchemeHostPort(url::kHttpsScheme, address, GetHttpsPort()).GetURL();
if (!resource_request->url.is_valid()) {
OpenSocket(net::ERR_INVALID_URL, {});
return;
}
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
resource_request->method = net::HttpRequestHeaders::kGetMethod;
resource_request->mode =
network::mojom::RequestMode::kCorsWithForcedPreflight;
resource_request->request_initiator = frame->GetLastCommittedOrigin();
auto loader = network::SimpleURLLoader::Create(
std::move(resource_request),
DirectSocketsServiceImpl::TrafficAnnotation());
auto* loader_ptr = loader.get();
loader_ptr->SetTimeoutDuration(kCorsRequestTimeout);
loader_ptr->DownloadHeadersOnly(
factory.get(),
base::BindOnce(&ResolveHostAndOpenSocket::OnCORSCheckComplete,
base::Unretained(this), std::move(loader),
std::move(resolved_addresses)));
}
void ResolveHostAndOpenSocket::OnCORSCheckComplete(
std::unique_ptr<network::SimpleURLLoader> loader,
net::AddressList resolved_addresses,
scoped_refptr<net::HttpResponseHeaders>) {
if (const auto& completion_status = loader->CompletionStatus()) {
if (auto status = completion_status->cors_error_status) {
LOG(ERROR) << "Preflight failed: " << *status;
base::UmaHistogramEnumeration(
kPermissionDeniedHistogramName,
blink::mojom::DirectSocketFailureType::kCORS);
OpenSocket(net::ERR_BLOCKED_BY_RESPONSE, {});
return;
}
if (completion_status->error_code != net::OK) {
OpenSocket(completion_status->error_code, {});
return;
}
OpenSocket(net::OK, std::move(resolved_addresses));
return;
}
OpenSocket(net::ERR_TIMED_OUT, {});
}
// ResolveHostAndOpenTCPSocket implementation.
ResolveHostAndOpenTCPSocket::ResolveHostAndOpenTCPSocket(
base::WeakPtr<DirectSocketsServiceImpl> service,
blink::mojom::DirectSocketOptionsPtr options,
mojo::PendingReceiver<network::mojom::TCPConnectedSocket> receiver,
mojo::PendingRemote<network::mojom::SocketObserver> observer,
OpenTcpSocketCallback callback)
: ResolveHostAndOpenSocket(std::move(service), std::move(options)),
receiver_(std::move(receiver)),
observer_(std::move(observer)),
callback_(std::move(callback)) {}
ResolveHostAndOpenTCPSocket::~ResolveHostAndOpenTCPSocket() = default;
void ResolveHostAndOpenTCPSocket::OpenSocket(
int result,
const absl::optional<net::AddressList>& resolved_addresses) {
network::mojom::NetworkContext* network_context = nullptr;
if (service_) {
network_context = service_->GetNetworkContext();
}
if (!network_context) {
delete this;
return;
}
if (result != net::OK) {
std::move(callback_).Run(result, absl::nullopt, absl::nullopt,
mojo::ScopedDataPipeConsumerHandle(),
mojo::ScopedDataPipeProducerHandle());
delete this;
return;
}
DCHECK(resolved_addresses && !resolved_addresses->empty());
const absl::optional<net::IPEndPoint> local_addr = GetLocalAddr(*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,
DirectSocketsServiceImpl::GetMaxBufferSize());
}
if (options_->receive_buffer_size > 0) {
tcp_connected_socket_options->receive_buffer_size =
std::min(options_->receive_buffer_size,
DirectSocketsServiceImpl::GetMaxBufferSize());
}
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);
}
// invalidate options_.
options_.reset();
network_context->CreateTCPConnectedSocket(
local_addr, *resolved_addresses, std::move(tcp_connected_socket_options),
DirectSocketsServiceImpl::MutableTrafficAnnotation(),
std::move(receiver_), std::move(observer_), std::move(callback_));
delete this;
}
// ResolveHostAndOpenUDPSocket implementation.
ResolveHostAndOpenUDPSocket::ResolveHostAndOpenUDPSocket(
base::WeakPtr<DirectSocketsServiceImpl> service,
blink::mojom::DirectSocketOptionsPtr options,
mojo::PendingReceiver<blink::mojom::DirectUDPSocket> receiver,
mojo::PendingRemote<network::mojom::UDPSocketListener> listener,
OpenUdpSocketCallback callback)
: ResolveHostAndOpenSocket(std::move(service), std::move(options)),
receiver_(std::move(receiver)),
listener_(std::move(listener)),
callback_(std::move(callback)) {}
ResolveHostAndOpenUDPSocket::~ResolveHostAndOpenUDPSocket() = default;
void ResolveHostAndOpenUDPSocket::OpenSocket(
int result,
const absl::optional<net::AddressList>& resolved_addresses) {
network::mojom::NetworkContext* network_context = nullptr;
if (service_) {
network_context = service_->GetNetworkContext();
}
if (!network_context) {
delete this;
return;
}
if (result != net::OK) {
std::move(callback_).Run(result, absl::nullopt, absl::nullopt);
delete this;
return;
}
DCHECK(resolved_addresses && !resolved_addresses->empty());
network::mojom::UDPSocketOptionsPtr udp_socket_options =
network::mojom::UDPSocketOptions::New();
if (options_->send_buffer_size > 0) {
udp_socket_options->send_buffer_size =
std::min(options_->send_buffer_size,
DirectSocketsServiceImpl::GetMaxBufferSize());
}
if (options_->receive_buffer_size > 0) {
udp_socket_options->receive_buffer_size =
std::min(options_->receive_buffer_size,
DirectSocketsServiceImpl::GetMaxBufferSize());
}
net::IPEndPoint peer_addr = resolved_addresses->front();
auto direct_udp_socket = std::make_unique<DirectUDPSocketImpl>(
network_context, std::move(listener_));
direct_udp_socket->Connect(
peer_addr, std::move(udp_socket_options),
base::BindOnce(&ResolveHostAndOpenUDPSocket::OnUdpConnectCompleted,
base::Unretained(this), peer_addr));
service_->AddDirectUDPSocketReceiver(std::move(direct_udp_socket),
std::move(receiver_));
}
void ResolveHostAndOpenUDPSocket::OnUdpConnectCompleted(
net::IPEndPoint peer_addr,
int result,
const absl::optional<net::IPEndPoint>& local_addr) {
std::move(callback_).Run(result, local_addr, peer_addr);
delete this;
}
} // namespace content