// 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 #include #include "base/logging.h" #include "base/metrics/histogram_macros.h" #include "base/stl_util.h" #include "base/strings/stringprintf.h" #include "base/trace_event/memory_allocator_dump.h" #include "base/trace_event/memory_usage_estimator.h" #include "base/trace_event/process_memory_dump.h" #include "base/trace_event/trace_event.h" #include "base/values.h" #include "build/build_config.h" #include "net/base/address_list.h" #include "net/base/trace_constants.h" #include "net/http/http_network_session.h" #include "net/http/http_server_properties.h" #include "net/http/http_stream_request.h" #include "net/log/net_log_event_type.h" #include "net/log/net_log_source.h" #include "net/log/net_log_with_source.h" #include "net/spdy/bidirectional_stream_spdy_impl.h" #include "net/spdy/spdy_http_stream.h" #include "net/spdy/spdy_session.h" #include "net/third_party/spdy/core/hpack/hpack_constants.h" #include "net/third_party/spdy/core/hpack/hpack_huffman_table.h" #include "net/third_party/spdy/core/hpack/hpack_static_table.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 }; } // namespace SpdySessionPool::SpdySessionPool( HostResolver* resolver, SSLConfigService* ssl_config_service, HttpServerProperties* http_server_properties, TransportSecurityState* transport_security_state, const QuicTransportVersionVector& quic_supported_versions, bool enable_ping_based_connection_checking, bool support_ietf_format_quic_altsvc, size_t session_max_recv_window_size, const spdy::SettingsMap& initial_settings, SpdySessionPool::TimeFunc time_func) : http_server_properties_(http_server_properties), transport_security_state_(transport_security_state), ssl_config_service_(ssl_config_service), resolver_(resolver), quic_supported_versions_(quic_supported_versions), enable_sending_initial_data_(true), enable_ping_based_connection_checking_( enable_ping_based_connection_checking), support_ietf_format_quic_altsvc_(support_ietf_format_quic_altsvc), session_max_recv_window_size_(session_max_recv_window_size), initial_settings_(initial_settings), time_func_(time_func), push_delegate_(nullptr) { NetworkChangeNotifier::AddIPAddressObserver(this); if (ssl_config_service_.get()) ssl_config_service_->AddObserver(this); CertDatabase::GetInstance()->AddObserver(this); } SpdySessionPool::~SpdySessionPool() { DCHECK(spdy_session_request_map_.empty()); // TODO(bnc): CloseAllSessions() is also called in HttpNetworkSession // destructor, one of the two calls should be removed. CloseAllSessions(); while (!sessions_.empty()) { // Destroy sessions to enforce that lifetime is scoped to SpdySessionPool. // Write callbacks queued upon session drain are not invoked. RemoveUnavailableSession((*sessions_.begin())->GetWeakPtr()); } if (ssl_config_service_.get()) ssl_config_service_->RemoveObserver(this); NetworkChangeNotifier::RemoveIPAddressObserver(this); CertDatabase::GetInstance()->RemoveObserver(this); } base::WeakPtr SpdySessionPool::CreateAvailableSessionFromSocket( const SpdySessionKey& key, bool is_trusted_proxy, std::unique_ptr connection, const NetLogWithSource& net_log) { TRACE_EVENT0(kNetTracingCategory, "SpdySessionPool::CreateAvailableSessionFromSocket"); UMA_HISTOGRAM_ENUMERATION( "Net.SpdySessionGet", IMPORTED_FROM_SOCKET, SPDY_SESSION_GET_MAX); auto new_session = std::make_unique( key, http_server_properties_, transport_security_state_, quic_supported_versions_, enable_sending_initial_data_, enable_ping_based_connection_checking_, support_ietf_format_quic_altsvc_, is_trusted_proxy, session_max_recv_window_size_, initial_settings_, time_func_, push_delegate_, net_log.net_log()); new_session->InitializeWithSocket(std::move(connection), this); base::WeakPtr available_session = new_session->GetWeakPtr(); sessions_.insert(new_session.release()); MapKeyToAvailableSession(key, available_session); net_log.AddEvent( NetLogEventType::HTTP2_SESSION_POOL_IMPORTED_SESSION_FROM_SOCKET, available_session->net_log().source().ToEventParametersCallback()); // Look up 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 (key.proxy_server().is_direct()) { IPEndPoint address; if (available_session->GetPeerAddress(&address) == OK) aliases_.insert(AliasMap::value_type(address, key)); } return available_session; } base::WeakPtr SpdySessionPool::FindAvailableSession( const SpdySessionKey& key, bool enable_ip_based_pooling, bool is_websocket, const NetLogWithSource& net_log) { AvailableSessionMap::iterator it = LookupAvailableSessionByKey(key); if (it != available_sessions_.end() && (!is_websocket || it->second->support_websocket())) { if (key == it->second->spdy_session_key()) { UMA_HISTOGRAM_ENUMERATION("Net.SpdySessionGet", FOUND_EXISTING, SPDY_SESSION_GET_MAX); net_log.AddEvent( NetLogEventType::HTTP2_SESSION_POOL_FOUND_EXISTING_SESSION, it->second->net_log().source().ToEventParametersCallback()); return it->second; } if (enable_ip_based_pooling) { UMA_HISTOGRAM_ENUMERATION("Net.SpdySessionGet", FOUND_EXISTING_FROM_IP_POOL, SPDY_SESSION_GET_MAX); net_log.AddEvent( NetLogEventType:: HTTP2_SESSION_POOL_FOUND_EXISTING_SESSION_FROM_IP_POOL, it->second->net_log().source().ToEventParametersCallback()); return it->second; } // Remove session from available sessions and from aliases, and remove // key from the session's pooled alias set, so that a new session can be // created with this |key|. it->second->RemovePooledAlias(key); UnmapKey(key); RemoveAliases(key); return base::WeakPtr(); } if (!enable_ip_based_pooling) return base::WeakPtr(); // Look up IP addresses from resolver cache. HostResolver::RequestInfo resolve_info(key.host_port_pair()); AddressList addresses; int rv = resolver_->ResolveFromCache(resolve_info, &addresses, net_log); DCHECK_NE(rv, ERR_IO_PENDING); if (rv != OK) return base::WeakPtr(); // Check if we have a session through a domain alias. for (AddressList::const_iterator address_it = addresses.begin(); address_it != addresses.end(); ++address_it) { auto range = aliases_.equal_range(*address_it); for (auto alias_it = range.first; alias_it != range.second; ++alias_it) { // We found an alias. const SpdySessionKey& alias_key = alias_it->second; // We can reuse this session only if the proxy and privacy // settings match. if (!(alias_key.proxy_server() == key.proxy_server()) || !(alias_key.privacy_mode() == key.privacy_mode())) { continue; } AvailableSessionMap::iterator available_session_it = LookupAvailableSessionByKey(alias_key); if (available_session_it == available_sessions_.end()) { NOTREACHED(); // It shouldn't be in the aliases table if we can't get // it! continue; } // Make copy of WeakPtr as call to UnmapKey() will delete original. const base::WeakPtr available_session = available_session_it->second; DCHECK(base::ContainsKey(sessions_, available_session.get())); if (is_websocket && !available_session->support_websocket()) continue; // If the session is a secure one, we need to verify that the // server is authenticated to serve traffic for |host_port_proxy_pair| // too. if (!available_session->VerifyDomainAuthentication( key.host_port_pair().host())) { UMA_HISTOGRAM_ENUMERATION("Net.SpdyIPPoolDomainMatch", 0, 2); continue; } bool adding_pooled_alias = true; // If socket tags differ, see if session's socket tag can be changed. if (alias_key.socket_tag() != key.socket_tag()) { SpdySessionKey old_key = available_session->spdy_session_key(); if (!available_session->ChangeSocketTag(key.socket_tag())) continue; const SpdySessionKey& new_key = available_session->spdy_session_key(); // This isn't a pooled alias, it's the actual session. adding_pooled_alias = false; // Remap main session key. UnmapKey(old_key); MapKeyToAvailableSession(new_key, available_session); // Remap alias. aliases_.insert(AliasMap::value_type(alias_it->first, new_key)); aliases_.erase(alias_it); // Remap pooled session keys. const auto& aliases = available_session->pooled_aliases(); for (auto it = aliases.begin(); it != aliases.end();) { // Ignore aliases this loop is inserting. if (it->socket_tag() == key.socket_tag()) { ++it; continue; } UnmapKey(*it); SpdySessionKey new_pool_alias_key = SpdySessionKey(it->host_port_pair(), it->proxy_server(), it->privacy_mode(), key.socket_tag()); MapKeyToAvailableSession(new_pool_alias_key, available_session); auto old_it = it; ++it; available_session->RemovePooledAlias(*old_it); available_session->AddPooledAlias(new_pool_alias_key); } } UMA_HISTOGRAM_ENUMERATION("Net.SpdyIPPoolDomainMatch", 1, 2); UMA_HISTOGRAM_ENUMERATION("Net.SpdySessionGet", FOUND_EXISTING_FROM_IP_POOL, SPDY_SESSION_GET_MAX); net_log.AddEvent( NetLogEventType:: HTTP2_SESSION_POOL_FOUND_EXISTING_SESSION_FROM_IP_POOL, available_session->net_log().source().ToEventParametersCallback()); if (adding_pooled_alias) { // Add this session to the map so that we can find it next time. MapKeyToAvailableSession(key, available_session); available_session->AddPooledAlias(key); } return available_session; } } return base::WeakPtr(); } void SpdySessionPool::MakeSessionUnavailable( const base::WeakPtr& available_session) { UnmapKey(available_session->spdy_session_key()); RemoveAliases(available_session->spdy_session_key()); const std::set& aliases = available_session->pooled_aliases(); for (std::set::const_iterator it = aliases.begin(); it != aliases.end(); ++it) { UnmapKey(*it); RemoveAliases(*it); } DCHECK(!IsSessionAvailable(available_session)); } void SpdySessionPool::RemoveUnavailableSession( const base::WeakPtr& unavailable_session) { DCHECK(!IsSessionAvailable(unavailable_session)); unavailable_session->net_log().AddEvent( NetLogEventType::HTTP2_SESSION_POOL_REMOVE_SESSION); SessionSet::iterator it = sessions_.find(unavailable_session.get()); CHECK(it != sessions_.end()); std::unique_ptr owned_session(*it); sessions_.erase(it); } // Make a copy of |sessions_| in the Close* functions below to avoid // reentrancy problems. Since arbitrary functions get called by close // handlers, it doesn't suffice to simply increment the iterator // before closing. void SpdySessionPool::CloseCurrentSessions(Error error) { CloseCurrentSessionsHelper(error, "Closing current sessions.", false /* idle_only */); } void SpdySessionPool::CloseCurrentIdleSessions() { CloseCurrentSessionsHelper(ERR_ABORTED, "Closing idle sessions.", true /* idle_only */); } void SpdySessionPool::CloseAllSessions() { auto is_draining = [](const SpdySession* s) { return s->IsDraining(); }; // Repeat until every SpdySession owned by |this| is draining. while (!std::all_of(sessions_.begin(), sessions_.end(), is_draining)) { CloseCurrentSessionsHelper(ERR_ABORTED, "Closing all sessions.", false /* idle_only */); } } std::unique_ptr SpdySessionPool::SpdySessionPoolInfoToValue() const { auto list = std::make_unique(); for (AvailableSessionMap::const_iterator it = available_sessions_.begin(); it != available_sessions_.end(); ++it) { // Only add the session if the key in the map matches the main // host_port_proxy_pair (not an alias). const SpdySessionKey& key = it->first; const SpdySessionKey& session_key = it->second->spdy_session_key(); if (key == session_key) list->Append(it->second->GetInfoAsValue()); } return std::move(list); } void SpdySessionPool::OnIPAddressChanged() { WeakSessionList current_sessions = GetCurrentSessions(); for (WeakSessionList::const_iterator it = current_sessions.begin(); it != current_sessions.end(); ++it) { if (!*it) continue; // For OSs that terminate TCP connections upon relevant network changes, // attempt to preserve active streams by marking all sessions as going // away, rather than explicitly closing them. Streams may still fail due // to a generated TCP reset. #if defined(OS_ANDROID) || defined(OS_WIN) || defined(OS_IOS) (*it)->MakeUnavailable(); (*it)->StartGoingAway(kLastStreamId, ERR_NETWORK_CHANGED); (*it)->MaybeFinishGoingAway(); #else (*it)->CloseSessionOnError(ERR_NETWORK_CHANGED, "Closing current sessions."); DCHECK((*it)->IsDraining()); #endif // defined(OS_ANDROID) || defined(OS_WIN) || defined(OS_IOS) DCHECK(!IsSessionAvailable(*it)); } } void SpdySessionPool::OnSSLConfigChanged() { CloseCurrentSessions(ERR_NETWORK_CHANGED); } void SpdySessionPool::OnCertDBChanged() { CloseCurrentSessions(ERR_CERT_DATABASE_CHANGED); } void SpdySessionPool::OnNewSpdySessionReady( const base::WeakPtr& spdy_session, const SSLConfig& used_ssl_config, const ProxyInfo& used_proxy_info, bool was_alpn_negotiated, NextProto negotiated_protocol, bool using_spdy, NetLogSource source_dependency) { while (spdy_session) { const SpdySessionKey& spdy_session_key = spdy_session->spdy_session_key(); // Each iteration may empty out the RequestSet for |spdy_session_key| in // |spdy_session_request_map_|. So each time, check for RequestSet and use // the first one. // // TODO(willchan): If it's important, switch RequestSet out for a FIFO // queue (Order by priority first, then FIFO within same priority). Unclear // that it matters here. auto iter = spdy_session_request_map_.find(spdy_session_key); if (iter == spdy_session_request_map_.end()) return; HttpStreamRequest* request = *iter->second.begin(); request->Complete(was_alpn_negotiated, negotiated_protocol, using_spdy); RemoveRequestFromSpdySessionRequestMap(request); if (request->stream_type() == HttpStreamRequest::BIDIRECTIONAL_STREAM) { request->OnBidirectionalStreamImplReadyOnPooledConnection( used_ssl_config, used_proxy_info, std::make_unique(spdy_session, source_dependency)); } else { request->OnStreamReadyOnPooledConnection( used_ssl_config, used_proxy_info, std::make_unique(spdy_session, kNoPushedStreamFound, source_dependency)); } } // TODO(mbelshe): Alert other valid requests. } bool SpdySessionPool::StartRequest(const SpdySessionKey& spdy_session_key, const base::Closure& callback) { auto iter = spdy_session_pending_request_map_.find(spdy_session_key); if (iter == spdy_session_pending_request_map_.end()) { spdy_session_pending_request_map_.emplace(spdy_session_key, std::list{}); return true; } iter->second.push_back(callback); return false; } void SpdySessionPool::ResumePendingRequests( const SpdySessionKey& spdy_session_key) { auto iter = spdy_session_pending_request_map_.find(spdy_session_key); if (iter != spdy_session_pending_request_map_.end()) { for (auto callback : iter->second) { base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, callback); } spdy_session_pending_request_map_.erase(iter); } } void SpdySessionPool::AddRequestToSpdySessionRequestMap( const SpdySessionKey& spdy_session_key, HttpStreamRequest* request) { if (request->HasSpdySessionKey()) return; RequestSet& request_set = spdy_session_request_map_[spdy_session_key]; DCHECK(!base::ContainsKey(request_set, request)); request_set.insert(request); request->SetSpdySessionKey(spdy_session_key); } void SpdySessionPool::RemoveRequestFromSpdySessionRequestMap( HttpStreamRequest* request) { if (!request->HasSpdySessionKey()) return; const SpdySessionKey& spdy_session_key = request->GetSpdySessionKey(); // Resume all pending requests now that |request| is done/canceled. ResumePendingRequests(spdy_session_key); auto iter = spdy_session_request_map_.find(spdy_session_key); DCHECK(iter != spdy_session_request_map_.end()); RequestSet& request_set = iter->second; DCHECK(base::ContainsKey(request_set, request)); request_set.erase(request); if (request_set.empty()) spdy_session_request_map_.erase(spdy_session_key); // Resets |request|'s SpdySessionKey. This will invalid |spdy_session_key|. request->ResetSpdySessionKey(); } void SpdySessionPool::DumpMemoryStats( base::trace_event::ProcessMemoryDump* pmd, const std::string& parent_dump_absolute_name) const { if (sessions_.empty()) return; size_t total_size = 0; size_t buffer_size = 0; size_t cert_count = 0; size_t cert_size = 0; size_t num_active_sessions = 0; for (auto* session : sessions_) { StreamSocket::SocketMemoryStats stats; bool is_session_active = false; total_size += session->DumpMemoryStats(&stats, &is_session_active); buffer_size += stats.buffer_size; cert_count += stats.cert_count; cert_size += stats.cert_size; if (is_session_active) num_active_sessions++; } total_size += base::trace_event::EstimateMemoryUsage(spdy::ObtainHpackHuffmanTable()) + base::trace_event::EstimateMemoryUsage(spdy::ObtainHpackStaticTable()) + base::trace_event::EstimateMemoryUsage(push_promise_index_); base::trace_event::MemoryAllocatorDump* dump = pmd->CreateAllocatorDump(base::StringPrintf( "%s/spdy_session_pool", parent_dump_absolute_name.c_str())); dump->AddScalar(base::trace_event::MemoryAllocatorDump::kNameSize, base::trace_event::MemoryAllocatorDump::kUnitsBytes, total_size); dump->AddScalar(base::trace_event::MemoryAllocatorDump::kNameObjectCount, base::trace_event::MemoryAllocatorDump::kUnitsObjects, sessions_.size()); dump->AddScalar("active_session_count", base::trace_event::MemoryAllocatorDump::kUnitsObjects, num_active_sessions); dump->AddScalar("buffer_size", base::trace_event::MemoryAllocatorDump::kUnitsBytes, buffer_size); dump->AddScalar("cert_count", base::trace_event::MemoryAllocatorDump::kUnitsObjects, cert_count); dump->AddScalar("cert_size", base::trace_event::MemoryAllocatorDump::kUnitsBytes, cert_size); } bool SpdySessionPool::IsSessionAvailable( const base::WeakPtr& session) const { for (AvailableSessionMap::const_iterator it = available_sessions_.begin(); it != available_sessions_.end(); ++it) { if (it->second.get() == session.get()) return true; } return false; } void SpdySessionPool::MapKeyToAvailableSession( const SpdySessionKey& key, const base::WeakPtr& session) { DCHECK(base::ContainsKey(sessions_, session.get())); std::pair result = available_sessions_.insert(std::make_pair(key, session)); CHECK(result.second); } SpdySessionPool::AvailableSessionMap::iterator SpdySessionPool::LookupAvailableSessionByKey( const SpdySessionKey& key) { return available_sessions_.find(key); } void SpdySessionPool::UnmapKey(const SpdySessionKey& key) { AvailableSessionMap::iterator it = LookupAvailableSessionByKey(key); CHECK(it != available_sessions_.end()); available_sessions_.erase(it); } void SpdySessionPool::RemoveAliases(const SpdySessionKey& key) { // Walk the aliases map, find references to this pair. // TODO(mbelshe): Figure out if this is too expensive. for (AliasMap::iterator it = aliases_.begin(); it != aliases_.end(); ) { if (it->second == key) { AliasMap::iterator old_it = it; ++it; aliases_.erase(old_it); } else { ++it; } } } SpdySessionPool::WeakSessionList SpdySessionPool::GetCurrentSessions() const { WeakSessionList current_sessions; for (SessionSet::const_iterator it = sessions_.begin(); it != sessions_.end(); ++it) { current_sessions.push_back((*it)->GetWeakPtr()); } return current_sessions; } void SpdySessionPool::CloseCurrentSessionsHelper(Error error, const std::string& description, bool idle_only) { WeakSessionList current_sessions = GetCurrentSessions(); for (base::WeakPtr& session : current_sessions) { if (!session) continue; if (idle_only && session->is_active()) continue; if (session->IsDraining()) continue; session->CloseSessionOnError(error, description); DCHECK(!IsSessionAvailable(session)); DCHECK(!session || session->IsDraining()); } } } // namespace net