// 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