| // Copyright (c) 2012 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/callback.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram.h" |
| #include "base/values.h" |
| #include "net/base/address_list.h" |
| #include "net/http/http_network_session.h" |
| #include "net/http/http_server_properties.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; |
| |
| SpdySessionPool::SpdySessionPool( |
| HostResolver* resolver, |
| SSLConfigService* ssl_config_service, |
| HttpServerProperties* http_server_properties, |
| size_t max_sessions_per_domain, |
| bool force_single_domain, |
| bool enable_ip_pooling, |
| bool enable_credential_frames, |
| bool enable_compression, |
| bool enable_ping_based_connection_checking, |
| NextProto default_protocol, |
| size_t initial_recv_window_size, |
| size_t initial_max_concurrent_streams, |
| size_t max_concurrent_streams_limit, |
| SpdySessionPool::TimeFunc time_func, |
| const std::string& trusted_spdy_proxy) |
| : http_server_properties_(http_server_properties), |
| ssl_config_service_(ssl_config_service), |
| resolver_(resolver), |
| verify_domain_authentication_(true), |
| enable_sending_initial_settings_(true), |
| max_sessions_per_domain_(max_sessions_per_domain == 0 ? |
| kMaxSessionsPerDomain : |
| max_sessions_per_domain), |
| force_single_domain_(force_single_domain), |
| enable_ip_pooling_(enable_ip_pooling), |
| enable_credential_frames_(enable_credential_frames), |
| enable_compression_(enable_compression), |
| enable_ping_based_connection_checking_( |
| enable_ping_based_connection_checking), |
| default_protocol_(default_protocol), |
| initial_recv_window_size_(initial_recv_window_size), |
| initial_max_concurrent_streams_(initial_max_concurrent_streams), |
| max_concurrent_streams_limit_(max_concurrent_streams_limit), |
| time_func_(time_func), |
| trusted_spdy_proxy_( |
| HostPortPair::FromString(trusted_spdy_proxy)) { |
| NetworkChangeNotifier::AddIPAddressObserver(this); |
| if (ssl_config_service_) |
| ssl_config_service_->AddObserver(this); |
| CertDatabase::GetInstance()->AddObserver(this); |
| } |
| |
| SpdySessionPool::~SpdySessionPool() { |
| CloseAllSessions(); |
| |
| if (ssl_config_service_) |
| ssl_config_service_->RemoveObserver(this); |
| NetworkChangeNotifier::RemoveIPAddressObserver(this); |
| CertDatabase::GetInstance()->RemoveObserver(this); |
| } |
| |
| scoped_refptr<SpdySession> SpdySessionPool::Get( |
| const HostPortProxyPair& host_port_proxy_pair, |
| const BoundNetLog& net_log) { |
| return GetInternal(host_port_proxy_pair, net_log, false); |
| } |
| |
| scoped_refptr<SpdySession> SpdySessionPool::GetIfExists( |
| const HostPortProxyPair& host_port_proxy_pair, |
| const BoundNetLog& net_log) { |
| return GetInternal(host_port_proxy_pair, net_log, true); |
| } |
| |
| scoped_refptr<SpdySession> SpdySessionPool::GetInternal( |
| const HostPortProxyPair& host_port_proxy_pair, |
| const BoundNetLog& net_log, |
| bool only_use_existing_sessions) { |
| 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, |
| spdy_session->net_log().source().ToEventParametersCallback()); |
| // Add this session to the map so that we can find it next time. |
| list = AddSessionList(host_port_proxy_pair); |
| list->push_back(spdy_session); |
| spdy_session->AddPooledAlias(host_port_proxy_pair); |
| return spdy_session; |
| } else if (only_use_existing_sessions) { |
| return NULL; |
| } |
| list = AddSessionList(host_port_proxy_pair); |
| } |
| |
| DCHECK(list); |
| if (list->size() && list->size() == 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, |
| spdy_session->net_log().source().ToEventParametersCallback()); |
| return spdy_session; |
| } |
| |
| DCHECK(!only_use_existing_sessions); |
| |
| spdy_session = new SpdySession(host_port_proxy_pair, this, |
| http_server_properties_, |
| verify_domain_authentication_, |
| enable_sending_initial_settings_, |
| enable_credential_frames_, |
| enable_compression_, |
| enable_ping_based_connection_checking_, |
| default_protocol_, |
| initial_recv_window_size_, |
| initial_max_concurrent_streams_, |
| max_concurrent_streams_limit_, |
| time_func_, |
| trusted_spdy_proxy_, |
| 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, |
| spdy_session->net_log().source().ToEventParametersCallback()); |
| DCHECK_LE(list->size(), 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, |
| http_server_properties_, |
| verify_domain_authentication_, |
| enable_sending_initial_settings_, |
| enable_credential_frames_, |
| enable_compression_, |
| enable_ping_based_connection_checking_, |
| default_protocol_, |
| initial_recv_window_size_, |
| initial_max_concurrent_streams_, |
| max_concurrent_streams_limit_, |
| time_func_, |
| trusted_spdy_proxy_, |
| 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, |
| (*spdy_session)->net_log().source().ToEventParametersCallback()); |
| |
| // We have a new session. Lookup the IP address for this session so that we |
| // can match future Sessions (potentially to different domains) which can |
| // potentially be pooled with this one. Because GetPeerAddress() reports the |
| // proxy's address instead of the origin server, check to see if this is a |
| // direct connection. |
| if (enable_ip_pooling_ && host_port_proxy_pair.second.is_direct()) { |
| IPEndPoint address; |
| if (connection->socket()->GetPeerAddress(&address) == OK) |
| AddAlias(address, host_port_proxy_pair); |
| } |
| |
| // 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) { |
| bool ok = RemoveFromSessionList(session, session->host_port_proxy_pair()); |
| DCHECK(ok); |
| session->net_log().AddEvent( |
| NetLog::TYPE_SPDY_SESSION_POOL_REMOVE_SESSION, |
| session->net_log().source().ToEventParametersCallback()); |
| |
| const std::set<HostPortProxyPair>& aliases = session->pooled_aliases(); |
| for (std::set<HostPortProxyPair>::const_iterator it = aliases.begin(); |
| it != aliases.end(); ++it) { |
| ok = RemoveFromSessionList(session, *it); |
| DCHECK(ok); |
| } |
| } |
| |
| bool SpdySessionPool::RemoveFromSessionList( |
| const scoped_refptr<SpdySession>& session, |
| const HostPortProxyPair& pair) { |
| SpdySessionList* list = GetSessionList(pair); |
| if (!list) |
| return false; |
| list->remove(session); |
| if (list->empty()) |
| RemoveSessionList(pair); |
| return true; |
| } |
| |
| Value* SpdySessionPool::SpdySessionPoolInfoToValue() const { |
| ListValue* list = new ListValue(); |
| |
| 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) { |
| // Only add the session if the key in the map matches the main |
| // host_port_proxy_pair (not an alias). |
| const HostPortProxyPair& key = it->first; |
| const HostPortProxyPair& pair = session->get()->host_port_proxy_pair(); |
| if (key.first.Equals(pair.first) && key.second == pair.second) |
| list->Append(session->get()->GetInfoAsValue()); |
| } |
| } |
| return list; |
| } |
| |
| void SpdySessionPool::OnIPAddressChanged() { |
| CloseCurrentSessions(ERR_NETWORK_CHANGED); |
| http_server_properties_->ClearSpdySettings(); |
| } |
| |
| void SpdySessionPool::OnSSLConfigChanged() { |
| CloseCurrentSessions(ERR_NETWORK_CHANGED); |
| } |
| |
| 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 (!enable_ip_pooling_) |
| return NULL; |
| |
| AddressList addresses; |
| if (!LookupAddresses(host_port_proxy_pair, net_log, &addresses)) |
| return NULL; |
| for (AddressList::const_iterator iter = addresses.begin(); |
| iter != addresses.end(); |
| ++iter) { |
| SpdyAliasMap::const_iterator alias_iter = aliases_.find(*iter); |
| if (alias_iter == aliases_.end()) |
| continue; |
| |
| // We found an alias. |
| const HostPortProxyPair& alias_pair = alias_iter->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::OnCertAdded(const X509Certificate* cert) { |
| CloseCurrentSessions(ERR_NETWORK_CHANGED); |
| } |
| |
| 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(ERR_NETWORK_CHANGED); |
| } |
| |
| const HostPortProxyPair& SpdySessionPool::NormalizeListPair( |
| const HostPortProxyPair& host_port_proxy_pair) const { |
| if (!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; |
| 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, |
| const BoundNetLog& net_log, |
| AddressList* addresses) const { |
| net::HostResolver::RequestInfo resolve_info(pair.first); |
| int rv = resolver_->ResolveFromCache(resolve_info, addresses, net_log); |
| DCHECK_NE(ERR_IO_PENDING, rv); |
| return rv == OK; |
| } |
| |
| void SpdySessionPool::AddAlias(const IPEndPoint& endpoint, |
| const HostPortProxyPair& pair) { |
| DCHECK(enable_ip_pooling_); |
| aliases_[endpoint] = pair; |
| } |
| |
| 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, "Closing all sessions."); |
| } |
| } |
| |
| void SpdySessionPool::CloseCurrentSessions(net::Error error) { |
| 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(error, false, "Closing current sessions."); |
| 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, "Closing idle sessions."); |
| } |
| } |
| } |
| |
| } // namespace net |