// Copyright (c) 2011 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 "net/spdy/spdy_session_pool.h"
#include "base/logging.h"
#include "base/metrics/histogram.h"
#include "base/values.h"
#include "net/base/address_list.h"
#include "net/base/sys_addrinfo.h"
#include "net/http/http_network_session.h"
#include "net/spdy/spdy_session.h"
namespace net {
namespace {
enum SpdySessionGetTypes {
CREATED_NEW = 0,
FOUND_EXISTING = 1,
FOUND_EXISTING_FROM_IP_POOL = 2,
IMPORTED_FROM_SOCKET = 3,
SPDY_SESSION_GET_MAX = 4
};
bool HostPortProxyPairsAreEqual(const HostPortProxyPair& a,
const HostPortProxyPair& b) {
return a.first.Equals(b.first) && a.second == b.second;
}
}
// The maximum number of sessions to open to a single domain.
static const size_t kMaxSessionsPerDomain = 1;
size_t SpdySessionPool::g_max_sessions_per_domain = kMaxSessionsPerDomain;
bool SpdySessionPool::g_force_single_domain = false;
bool SpdySessionPool::g_enable_ip_pooling = true;
SpdySessionPool::SpdySessionPool(HostResolver* resolver,
SSLConfigService* ssl_config_service)
: ssl_config_service_(ssl_config_service),
resolver_(resolver) {
NetworkChangeNotifier::AddIPAddressObserver(this);
if (ssl_config_service_)
ssl_config_service_->AddObserver(this);
CertDatabase::AddObserver(this);
}
SpdySessionPool::~SpdySessionPool() {
CloseAllSessions();
if (ssl_config_service_)
ssl_config_service_->RemoveObserver(this);
NetworkChangeNotifier::RemoveIPAddressObserver(this);
CertDatabase::RemoveObserver(this);
}
scoped_refptr<SpdySession> SpdySessionPool::Get(
const HostPortProxyPair& host_port_proxy_pair,
const BoundNetLog& net_log) {
scoped_refptr<SpdySession> spdy_session;
SpdySessionList* list = GetSessionList(host_port_proxy_pair);
if (!list) {
// Check if we have a Session through a domain alias.
spdy_session = GetFromAlias(host_port_proxy_pair, net_log, true);
if (spdy_session) {
UMA_HISTOGRAM_ENUMERATION("Net.SpdySessionGet",
FOUND_EXISTING_FROM_IP_POOL,
SPDY_SESSION_GET_MAX);
net_log.AddEvent(
NetLog::TYPE_SPDY_SESSION_POOL_FOUND_EXISTING_SESSION_FROM_IP_POOL,
make_scoped_refptr(new NetLogSourceParameter(
"session", spdy_session->net_log().source())));
return spdy_session;
}
list = AddSessionList(host_port_proxy_pair);
}
DCHECK(list);
if (list->size() && list->size() == g_max_sessions_per_domain) {
UMA_HISTOGRAM_ENUMERATION("Net.SpdySessionGet",
FOUND_EXISTING,
SPDY_SESSION_GET_MAX);
spdy_session = GetExistingSession(list, net_log);
net_log.AddEvent(
NetLog::TYPE_SPDY_SESSION_POOL_FOUND_EXISTING_SESSION,
make_scoped_refptr(new NetLogSourceParameter(
"session", spdy_session->net_log().source())));
return spdy_session;
}
spdy_session = new SpdySession(host_port_proxy_pair, this, &spdy_settings_,
net_log.net_log());
UMA_HISTOGRAM_ENUMERATION("Net.SpdySessionGet",
CREATED_NEW,
SPDY_SESSION_GET_MAX);
list->push_back(spdy_session);
net_log.AddEvent(
NetLog::TYPE_SPDY_SESSION_POOL_CREATED_NEW_SESSION,
make_scoped_refptr(new NetLogSourceParameter(
"session", spdy_session->net_log().source())));
DCHECK_LE(list->size(), g_max_sessions_per_domain);
return spdy_session;
}
net::Error SpdySessionPool::GetSpdySessionFromSocket(
const HostPortProxyPair& host_port_proxy_pair,
ClientSocketHandle* connection,
const BoundNetLog& net_log,
int certificate_error_code,
scoped_refptr<SpdySession>* spdy_session,
bool is_secure) {
UMA_HISTOGRAM_ENUMERATION("Net.SpdySessionGet",
IMPORTED_FROM_SOCKET,
SPDY_SESSION_GET_MAX);
// Create the SPDY session and add it to the pool.
*spdy_session = new SpdySession(host_port_proxy_pair, this, &spdy_settings_,
net_log.net_log());
SpdySessionList* list = GetSessionList(host_port_proxy_pair);
if (!list)
list = AddSessionList(host_port_proxy_pair);
DCHECK(list->empty());
list->push_back(*spdy_session);
net_log.AddEvent(
NetLog::TYPE_SPDY_SESSION_POOL_IMPORTED_SESSION_FROM_SOCKET,
make_scoped_refptr(new NetLogSourceParameter(
"session", (*spdy_session)->net_log().source())));
// Now we can initialize the session with the SSL socket.
return (*spdy_session)->InitializeWithSocket(connection, is_secure,
certificate_error_code);
}
bool SpdySessionPool::HasSession(
const HostPortProxyPair& host_port_proxy_pair) const {
if (GetSessionList(host_port_proxy_pair))
return true;
// Check if we have a session via an alias.
scoped_refptr<SpdySession> spdy_session =
GetFromAlias(host_port_proxy_pair, BoundNetLog(), false);
return spdy_session.get() != NULL;
}
void SpdySessionPool::Remove(const scoped_refptr<SpdySession>& session) {
SpdySessionList* list = GetSessionList(session->host_port_proxy_pair());
DCHECK(list); // We really shouldn't remove if we've already been removed.
if (!list)
return;
list->remove(session);
session->net_log().AddEvent(
NetLog::TYPE_SPDY_SESSION_POOL_REMOVE_SESSION,
make_scoped_refptr(new NetLogSourceParameter(
"session", session->net_log().source())));
if (list->empty())
RemoveSessionList(session->host_port_proxy_pair());
}
Value* SpdySessionPool::SpdySessionPoolInfoToValue() const {
ListValue* list = new ListValue();
SpdySessionsMap::const_iterator spdy_session_pool_it = sessions_.begin();
for (SpdySessionsMap::const_iterator it = sessions_.begin();
it != sessions_.end(); ++it) {
SpdySessionList* sessions = it->second;
for (SpdySessionList::const_iterator session = sessions->begin();
session != sessions->end(); ++session) {
list->Append(session->get()->GetInfoAsValue());
}
}
return list;
}
void SpdySessionPool::OnIPAddressChanged() {
CloseCurrentSessions();
}
void SpdySessionPool::OnSSLConfigChanged() {
CloseCurrentSessions();
}
scoped_refptr<SpdySession> SpdySessionPool::GetExistingSession(
SpdySessionList* list,
const BoundNetLog& net_log) const {
DCHECK(list);
DCHECK_LT(0u, list->size());
scoped_refptr<SpdySession> spdy_session = list->front();
if (list->size() > 1) {
list->pop_front(); // Rotate the list.
list->push_back(spdy_session);
}
return spdy_session;
}
scoped_refptr<SpdySession> SpdySessionPool::GetFromAlias(
const HostPortProxyPair& host_port_proxy_pair,
const BoundNetLog& net_log,
bool record_histograms) const {
// We should only be checking aliases when there is no direct session.
DCHECK(!GetSessionList(host_port_proxy_pair));
if (!g_enable_ip_pooling)
return NULL;
AddressList addresses;
if (!LookupAddresses(host_port_proxy_pair, &addresses))
return NULL;
const addrinfo* address = addresses.head();
while (address) {
IPEndPoint endpoint;
endpoint.FromSockAddr(address->ai_addr, address->ai_addrlen);
address = address->ai_next;
SpdyAliasMap::const_iterator it = aliases_.find(endpoint);
if (it == aliases_.end())
continue;
// We found an alias.
const HostPortProxyPair& alias_pair = it->second;
// If the proxy settings match, we can reuse this session.
if (!(alias_pair.second == host_port_proxy_pair.second))
continue;
SpdySessionList* list = GetSessionList(alias_pair);
if (!list) {
NOTREACHED(); // It shouldn't be in the aliases table if we can't get it!
continue;
}
scoped_refptr<SpdySession> spdy_session = GetExistingSession(list, net_log);
// If the SPDY session is a secure one, we need to verify that the server
// is authenticated to serve traffic for |host_port_proxy_pair| too.
if (!spdy_session->VerifyDomainAuthentication(
host_port_proxy_pair.first.host())) {
if (record_histograms)
UMA_HISTOGRAM_ENUMERATION("Net.SpdyIPPoolDomainMatch", 0, 2);
continue;
}
if (record_histograms)
UMA_HISTOGRAM_ENUMERATION("Net.SpdyIPPoolDomainMatch", 1, 2);
return spdy_session;
}
return NULL;
}
void SpdySessionPool::OnUserCertAdded(const X509Certificate* cert) {
CloseCurrentSessions();
}
void SpdySessionPool::OnCertTrustChanged(const X509Certificate* cert) {
// Per wtc, we actually only need to CloseCurrentSessions when trust is
// reduced. CloseCurrentSessions now because OnCertTrustChanged does not
// tell us this.
// See comments in ClientSocketPoolManager::OnCertTrustChanged.
CloseCurrentSessions();
}
const HostPortProxyPair& SpdySessionPool::NormalizeListPair(
const HostPortProxyPair& host_port_proxy_pair) const {
if (!g_force_single_domain)
return host_port_proxy_pair;
static HostPortProxyPair* single_domain_pair = NULL;
if (!single_domain_pair) {
HostPortPair single_domain = HostPortPair("singledomain.com", 80);
single_domain_pair = new HostPortProxyPair(single_domain,
ProxyServer::Direct());
}
return *single_domain_pair;
}
SpdySessionPool::SpdySessionList*
SpdySessionPool::AddSessionList(
const HostPortProxyPair& host_port_proxy_pair) {
const HostPortProxyPair& pair = NormalizeListPair(host_port_proxy_pair);
DCHECK(sessions_.find(pair) == sessions_.end());
SpdySessionPool::SpdySessionList* list = new SpdySessionList();
sessions_[pair] = list;
// We have a new session. Lookup the IP addresses for this session so that
// we can match future Sessions (potentially to different domains) which can
// potentially be pooled with this one.
if (g_enable_ip_pooling) {
AddressList addresses;
if (LookupAddresses(host_port_proxy_pair, &addresses))
AddAliases(addresses, host_port_proxy_pair);
}
return list;
}
SpdySessionPool::SpdySessionList*
SpdySessionPool::GetSessionList(
const HostPortProxyPair& host_port_proxy_pair) const {
const HostPortProxyPair& pair = NormalizeListPair(host_port_proxy_pair);
SpdySessionsMap::const_iterator it = sessions_.find(pair);
if (it != sessions_.end())
return it->second;
return NULL;
}
void SpdySessionPool::RemoveSessionList(
const HostPortProxyPair& host_port_proxy_pair) {
const HostPortProxyPair& pair = NormalizeListPair(host_port_proxy_pair);
SpdySessionList* list = GetSessionList(pair);
if (list) {
delete list;
sessions_.erase(pair);
} else {
DCHECK(false) << "removing orphaned session list";
}
RemoveAliases(host_port_proxy_pair);
}
bool SpdySessionPool::LookupAddresses(const HostPortProxyPair& pair,
AddressList* addresses) const {
net::HostResolver::RequestInfo resolve_info(pair.first);
resolve_info.set_only_use_cached_response(true);
int rv = resolver_->Resolve(resolve_info,
addresses,
NULL,
NULL,
net::BoundNetLog());
DCHECK_NE(ERR_IO_PENDING, rv);
return rv == OK;
}
void SpdySessionPool::AddAliases(const AddressList& addresses,
const HostPortProxyPair& pair) {
// Note: it is possible to think of strange overlapping sets of ip addresses
// for hosts such that a new session can override the alias for an IP
// address that was previously aliased to a different host. This is probably
// undesirable, but seemingly unlikely and complicated to fix.
// Example:
// host1 = 1.1.1.1, 1.1.1.4
// host2 = 1.1.1.4, 1.1.1.5
// host3 = 1.1.1.3, 1.1.1.5
// Creating session1 (to host1), creates an alias for host2 to host1.
// Creating session2 (to host3), overrides the alias for host2 to host3.
const addrinfo* address = addresses.head();
while (address) {
IPEndPoint endpoint;
endpoint.FromSockAddr(address->ai_addr, address->ai_addrlen);
aliases_[endpoint] = pair;
address = address->ai_next;
}
}
void SpdySessionPool::RemoveAliases(const HostPortProxyPair& pair) {
// Walk the aliases map, find references to this pair.
// TODO(mbelshe): Figure out if this is too expensive.
SpdyAliasMap::iterator alias_it = aliases_.begin();
while (alias_it != aliases_.end()) {
if (HostPortProxyPairsAreEqual(alias_it->second, pair)) {
aliases_.erase(alias_it);
alias_it = aliases_.begin(); // Iterator was invalidated.
continue;
}
++alias_it;
}
}
void SpdySessionPool::CloseAllSessions() {
while (!sessions_.empty()) {
SpdySessionList* list = sessions_.begin()->second;
CHECK(list);
const scoped_refptr<SpdySession>& session = list->front();
CHECK(session);
// This call takes care of removing the session from the pool, as well as
// removing the session list if the list is empty.
session->CloseSessionOnError(net::ERR_ABORTED, true);
}
}
void SpdySessionPool::CloseCurrentSessions() {
SpdySessionsMap old_map;
old_map.swap(sessions_);
for (SpdySessionsMap::const_iterator it = old_map.begin();
it != old_map.end(); ++it) {
SpdySessionList* list = it->second;
CHECK(list);
const scoped_refptr<SpdySession>& session = list->front();
CHECK(session);
session->set_spdy_session_pool(NULL);
}
while (!old_map.empty()) {
SpdySessionList* list = old_map.begin()->second;
CHECK(list);
const scoped_refptr<SpdySession>& session = list->front();
CHECK(session);
session->CloseSessionOnError(net::ERR_ABORTED, false);
list->pop_front();
if (list->empty()) {
delete list;
RemoveAliases(old_map.begin()->first);
old_map.erase(old_map.begin()->first);
}
}
DCHECK(sessions_.empty());
DCHECK(aliases_.empty());
}
void SpdySessionPool::CloseIdleSessions() {
SpdySessionsMap::const_iterator map_it = sessions_.begin();
while (map_it != sessions_.end()) {
SpdySessionList* list = map_it->second;
++map_it;
CHECK(list);
// Assumes there is only 1 element in the list
SpdySessionList::iterator session_it = list->begin();
const scoped_refptr<SpdySession>& session = *session_it;
CHECK(session);
if (!session->is_active())
session->CloseSessionOnError(net::ERR_ABORTED, true);
}
}
} // namespace net