1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "google_apis/gcm/engine/connection_factory_impl.h"
7 #include "base/message_loop/message_loop.h"
8 #include "base/metrics/histogram.h"
9 #include "base/metrics/sparse_histogram.h"
10 #include "google_apis/gcm/engine/connection_handler_impl.h"
11 #include "google_apis/gcm/protocol/mcs.pb.h"
12 #include "net/base/net_errors.h"
13 #include "net/http/http_network_session.h"
14 #include "net/http/http_request_headers.h"
15 #include "net/proxy/proxy_info.h"
16 #include "net/socket/client_socket_handle.h"
17 #include "net/socket/client_socket_pool_manager.h"
18 #include "net/ssl/ssl_config_service.h"
24 // The amount of time a Socket read should wait before timing out.
25 const int kReadTimeoutMs = 30000; // 30 seconds.
27 // If a connection is reset after succeeding within this window of time,
28 // the previous backoff entry is restored (and the connection success is treated
29 // as if it was transient).
30 const int kConnectionResetWindowSecs = 10; // 10 seconds.
32 // Decides whether the last login was within kConnectionResetWindowSecs of now
34 bool ShouldRestorePreviousBackoff(const base::TimeTicks& login_time,
35 const base::TimeTicks& now_ticks) {
36 return !login_time.is_null() &&
37 now_ticks - login_time <=
38 base::TimeDelta::FromSeconds(kConnectionResetWindowSecs);
43 ConnectionFactoryImpl::ConnectionFactoryImpl(
44 const std::vector<GURL>& mcs_endpoints,
45 const net::BackoffEntry::Policy& backoff_policy,
46 scoped_refptr<net::HttpNetworkSession> network_session,
48 : mcs_endpoints_(mcs_endpoints),
50 last_successful_endpoint_(0),
51 backoff_policy_(backoff_policy),
52 network_session_(network_session),
54 net::BoundNetLog::Make(net_log, net::NetLog::SOURCE_SOCKET)),
57 waiting_for_backoff_(false),
59 weak_ptr_factory_(this) {
60 DCHECK_GE(mcs_endpoints_.size(), 1U);
63 ConnectionFactoryImpl::~ConnectionFactoryImpl() {
65 network_session_->proxy_service()->CancelPacRequest(pac_request_);
70 void ConnectionFactoryImpl::Initialize(
71 const BuildLoginRequestCallback& request_builder,
72 const ConnectionHandler::ProtoReceivedCallback& read_callback,
73 const ConnectionHandler::ProtoSentCallback& write_callback) {
74 DCHECK(!connection_handler_);
76 previous_backoff_ = CreateBackoffEntry(&backoff_policy_);
77 backoff_entry_ = CreateBackoffEntry(&backoff_policy_);
78 request_builder_ = request_builder;
80 net::NetworkChangeNotifier::AddIPAddressObserver(this);
81 net::NetworkChangeNotifier::AddConnectionTypeObserver(this);
82 connection_handler_ = CreateConnectionHandler(
83 base::TimeDelta::FromMilliseconds(kReadTimeoutMs),
86 base::Bind(&ConnectionFactoryImpl::ConnectionHandlerCallback,
87 weak_ptr_factory_.GetWeakPtr())).Pass();
90 ConnectionHandler* ConnectionFactoryImpl::GetConnectionHandler() const {
91 return connection_handler_.get();
94 void ConnectionFactoryImpl::Connect() {
95 DCHECK(connection_handler_);
97 if (connecting_ || waiting_for_backoff_)
98 return; // Connection attempt already in progress or pending.
100 if (IsEndpointReachable())
101 return; // Already connected.
103 ConnectWithBackoff();
106 void ConnectionFactoryImpl::ConnectWithBackoff() {
107 // If a canary managed to connect while a backoff expiration was pending,
108 // just cleanup the internal state.
109 if (connecting_ || IsEndpointReachable()) {
110 waiting_for_backoff_ = false;
114 if (backoff_entry_->ShouldRejectRequest()) {
115 DVLOG(1) << "Delaying MCS endpoint connection for "
116 << backoff_entry_->GetTimeUntilRelease().InMilliseconds()
118 waiting_for_backoff_ = true;
119 base::MessageLoop::current()->PostDelayedTask(
121 base::Bind(&ConnectionFactoryImpl::ConnectWithBackoff,
122 weak_ptr_factory_.GetWeakPtr()),
123 backoff_entry_->GetTimeUntilRelease());
127 DVLOG(1) << "Attempting connection to MCS endpoint.";
128 waiting_for_backoff_ = false;
132 bool ConnectionFactoryImpl::IsEndpointReachable() const {
133 return connection_handler_ && connection_handler_->CanSendMessage();
136 void ConnectionFactoryImpl::SignalConnectionReset(
137 ConnectionResetReason reason) {
138 // A failure can trigger multiple resets, so no need to do anything if a
139 // connection is already in progress.
141 DVLOG(1) << "Connection in progress, ignoring reset.";
145 UMA_HISTOGRAM_ENUMERATION("GCM.ConnectionResetReason",
147 CONNECTION_RESET_COUNT);
148 if (!last_login_time_.is_null()) {
149 UMA_HISTOGRAM_CUSTOM_TIMES("GCM.ConnectionUpTime",
150 NowTicks() - last_login_time_,
151 base::TimeDelta::FromSeconds(1),
152 base::TimeDelta::FromHours(24),
154 // |last_login_time_| will be reset below, before attempting the new
159 DCHECK(!IsEndpointReachable());
161 // Network changes get special treatment as they can trigger a one-off canary
162 // request that bypasses backoff (but does nothing if a connection is in
163 // progress). Other connection reset events can be ignored as a connection
164 // is already awaiting backoff expiration.
165 if (waiting_for_backoff_ && reason != NETWORK_CHANGE) {
166 DVLOG(1) << "Backoff expiration pending, ignoring reset.";
171 // Failures prior to login completion just reuse the existing backoff entry.
173 backoff_entry_->InformOfRequest(false);
174 } else if (reason == LOGIN_FAILURE ||
175 ShouldRestorePreviousBackoff(last_login_time_, NowTicks())) {
176 // Failures due to login, or within the reset window of a login, restore
177 // the backoff entry that was saved off at login completion time.
178 backoff_entry_.swap(previous_backoff_);
179 backoff_entry_->InformOfRequest(false);
180 } else if (reason == NETWORK_CHANGE) {
181 ConnectImpl(); // Canary attempts bypass backoff without resetting it.
184 // We shouldn't be in backoff in thise case.
185 DCHECK_EQ(0, backoff_entry_->failure_count());
187 DCHECK(!connecting_);
188 DCHECK(!waiting_for_backoff_);
190 // At this point the last login time has been consumed or deemed irrelevant,
192 last_login_time_ = base::TimeTicks();
197 base::TimeTicks ConnectionFactoryImpl::NextRetryAttempt() const {
199 return base::TimeTicks();
200 return backoff_entry_->GetReleaseTime();
203 void ConnectionFactoryImpl::OnConnectionTypeChanged(
204 net::NetworkChangeNotifier::ConnectionType type) {
205 if (type == net::NetworkChangeNotifier::CONNECTION_NONE)
208 DVLOG(1) << "Connection type changed to " << type << ", reconnecting.";
210 // The connection may have been silently dropped, attempt to reconnect.
211 SignalConnectionReset(NETWORK_CHANGE);
214 void ConnectionFactoryImpl::OnIPAddressChanged() {
215 DVLOG(1) << "IP Address changed, reconnecting.";
217 // The connection may have been silently dropped, attempt to reconnect.
218 SignalConnectionReset(NETWORK_CHANGE);
221 GURL ConnectionFactoryImpl::GetCurrentEndpoint() const {
222 // Note that IsEndpointReachable() returns false anytime connecting_ is true,
223 // so while connecting this always uses |next_endpoint_|.
224 if (IsEndpointReachable())
225 return mcs_endpoints_[last_successful_endpoint_];
226 return mcs_endpoints_[next_endpoint_];
229 void ConnectionFactoryImpl::ConnectImpl() {
230 DCHECK(!IsEndpointReachable());
231 DCHECK(!socket_handle_.socket());
234 int status = network_session_->proxy_service()->ResolveProxy(
235 GetCurrentEndpoint(),
237 base::Bind(&ConnectionFactoryImpl::OnProxyResolveDone,
238 weak_ptr_factory_.GetWeakPtr()),
241 if (status != net::ERR_IO_PENDING)
242 OnProxyResolveDone(status);
245 void ConnectionFactoryImpl::InitHandler() {
246 // May be null in tests.
247 mcs_proto::LoginRequest login_request;
248 if (!request_builder_.is_null()) {
249 request_builder_.Run(&login_request);
250 DCHECK(login_request.IsInitialized());
253 connection_handler_->Init(login_request, socket_handle_.socket());
256 scoped_ptr<net::BackoffEntry> ConnectionFactoryImpl::CreateBackoffEntry(
257 const net::BackoffEntry::Policy* const policy) {
258 return scoped_ptr<net::BackoffEntry>(new net::BackoffEntry(policy));
261 scoped_ptr<ConnectionHandler> ConnectionFactoryImpl::CreateConnectionHandler(
262 base::TimeDelta read_timeout,
263 const ConnectionHandler::ProtoReceivedCallback& read_callback,
264 const ConnectionHandler::ProtoSentCallback& write_callback,
265 const ConnectionHandler::ConnectionChangedCallback& connection_callback) {
266 return make_scoped_ptr<ConnectionHandler>(
267 new ConnectionHandlerImpl(read_timeout,
270 connection_callback));
273 base::TimeTicks ConnectionFactoryImpl::NowTicks() {
274 return base::TimeTicks::Now();
277 void ConnectionFactoryImpl::OnConnectDone(int result) {
278 if (result != net::OK) {
279 // If the connection fails, try another proxy.
280 result = ReconsiderProxyAfterError(result);
281 // ReconsiderProxyAfterError either returns an error (in which case it is
282 // not reconsidering a proxy) or returns ERR_IO_PENDING if it is considering
284 DCHECK_NE(result, net::OK);
285 if (result == net::ERR_IO_PENDING)
286 return; // Proxy reconsideration pending. Return.
287 LOG(ERROR) << "Failed to connect to MCS endpoint with error " << result;
288 UMA_HISTOGRAM_BOOLEAN("GCM.ConnectionSuccessRate", false);
290 backoff_entry_->InformOfRequest(false);
291 UMA_HISTOGRAM_SPARSE_SLOWLY("GCM.ConnectionFailureErrorCode", result);
293 // If there are other endpoints available, use the next endpoint on the
296 if (next_endpoint_ >= mcs_endpoints_.size())
303 UMA_HISTOGRAM_BOOLEAN("GCM.ConnectionSuccessRate", true);
304 UMA_HISTOGRAM_COUNTS("GCM.ConnectionEndpoint", next_endpoint_);
305 UMA_HISTOGRAM_BOOLEAN("GCM.ConnectedViaProxy",
306 !(proxy_info_.is_empty() || proxy_info_.is_direct()));
307 ReportSuccessfulProxyConnection();
309 // Reset the endpoint back to the default.
310 // TODO(zea): consider prioritizing endpoints more intelligently based on
311 // which ones succeed most for this client? Although that will affect
312 // measuring the success rate of the default endpoint vs fallback.
313 last_successful_endpoint_ = next_endpoint_;
317 DVLOG(1) << "MCS endpoint socket connection success, starting login.";
321 void ConnectionFactoryImpl::ConnectionHandlerCallback(int result) {
322 DCHECK(!connecting_);
323 if (result != net::OK) {
324 // TODO(zea): Consider how to handle errors that may require some sort of
325 // user intervention (login page, etc.).
326 UMA_HISTOGRAM_SPARSE_SLOWLY("GCM.ConnectionDisconnectErrorCode", result);
327 SignalConnectionReset(SOCKET_FAILURE);
331 // Handshake complete, reset backoff. If the login failed with an error,
332 // the client should invoke SignalConnectionReset(LOGIN_FAILURE), which will
333 // restore the previous backoff.
334 DVLOG(1) << "Handshake complete.";
335 last_login_time_ = NowTicks();
336 previous_backoff_.swap(backoff_entry_);
337 backoff_entry_->Reset();
341 // This has largely been copied from
342 // HttpStreamFactoryImpl::Job::DoResolveProxyComplete. This should be
343 // refactored into some common place.
344 void ConnectionFactoryImpl::OnProxyResolveDone(int status) {
346 DVLOG(1) << "Proxy resolution status: " << status;
348 DCHECK_NE(status, net::ERR_IO_PENDING);
349 if (status == net::OK) {
350 // Remove unsupported proxies from the list.
351 proxy_info_.RemoveProxiesWithoutScheme(
352 net::ProxyServer::SCHEME_DIRECT |
353 net::ProxyServer::SCHEME_HTTP | net::ProxyServer::SCHEME_HTTPS |
354 net::ProxyServer::SCHEME_SOCKS4 | net::ProxyServer::SCHEME_SOCKS5);
356 if (proxy_info_.is_empty()) {
357 // No proxies/direct to choose from. This happens when we don't support
358 // any of the proxies in the returned list.
359 status = net::ERR_NO_SUPPORTED_PROXIES;
363 if (status != net::OK) {
364 // Failed to resolve proxy. Retry later.
365 OnConnectDone(status);
369 DVLOG(1) << "Resolved proxy with PAC:" << proxy_info_.ToPacString();
371 net::SSLConfig ssl_config;
372 network_session_->ssl_config_service()->GetSSLConfig(&ssl_config);
373 status = net::InitSocketHandleForTlsConnect(
374 net::HostPortPair::FromURL(GetCurrentEndpoint()),
375 network_session_.get(),
379 net::kPrivacyModeDisabled,
382 base::Bind(&ConnectionFactoryImpl::OnConnectDone,
383 weak_ptr_factory_.GetWeakPtr()));
384 if (status != net::ERR_IO_PENDING)
385 OnConnectDone(status);
388 // This has largely been copied from
389 // HttpStreamFactoryImpl::Job::ReconsiderProxyAfterError. This should be
390 // refactored into some common place.
391 // This method reconsiders the proxy on certain errors. If it does reconsider
392 // a proxy it always returns ERR_IO_PENDING and posts a call to
393 // OnProxyResolveDone with the result of the reconsideration.
394 int ConnectionFactoryImpl::ReconsiderProxyAfterError(int error) {
395 DCHECK(!pac_request_);
396 DCHECK_NE(error, net::OK);
397 DCHECK_NE(error, net::ERR_IO_PENDING);
398 // A failure to resolve the hostname or any error related to establishing a
399 // TCP connection could be grounds for trying a new proxy configuration.
401 // Why do this when a hostname cannot be resolved? Some URLs only make sense
402 // to proxy servers. The hostname in those URLs might fail to resolve if we
403 // are still using a non-proxy config. We need to check if a proxy config
404 // now exists that corresponds to a proxy server that could load the URL.
407 case net::ERR_PROXY_CONNECTION_FAILED:
408 case net::ERR_NAME_NOT_RESOLVED:
409 case net::ERR_INTERNET_DISCONNECTED:
410 case net::ERR_ADDRESS_UNREACHABLE:
411 case net::ERR_CONNECTION_CLOSED:
412 case net::ERR_CONNECTION_TIMED_OUT:
413 case net::ERR_CONNECTION_RESET:
414 case net::ERR_CONNECTION_REFUSED:
415 case net::ERR_CONNECTION_ABORTED:
416 case net::ERR_TIMED_OUT:
417 case net::ERR_TUNNEL_CONNECTION_FAILED:
418 case net::ERR_SOCKS_CONNECTION_FAILED:
419 // This can happen in the case of trying to talk to a proxy using SSL, and
420 // ending up talking to a captive portal that supports SSL instead.
421 case net::ERR_PROXY_CERTIFICATE_INVALID:
422 // This can happen when trying to talk SSL to a non-SSL server (Like a
424 case net::ERR_SSL_PROTOCOL_ERROR:
426 case net::ERR_SOCKS_CONNECTION_HOST_UNREACHABLE:
427 // Remap the SOCKS-specific "host unreachable" error to a more
428 // generic error code (this way consumers like the link doctor
429 // know to substitute their error page).
431 // Note that if the host resolving was done by the SOCKS5 proxy, we can't
432 // differentiate between a proxy-side "host not found" versus a proxy-side
433 // "address unreachable" error, and will report both of these failures as
434 // ERR_ADDRESS_UNREACHABLE.
435 return net::ERR_ADDRESS_UNREACHABLE;
440 net::SSLConfig ssl_config;
441 network_session_->ssl_config_service()->GetSSLConfig(&ssl_config);
442 if (proxy_info_.is_https() && ssl_config.send_client_cert) {
443 network_session_->ssl_client_auth_cache()->Remove(
444 proxy_info_.proxy_server().host_port_pair());
447 int status = network_session_->proxy_service()->ReconsiderProxyAfterError(
448 GetCurrentEndpoint(), &proxy_info_,
449 base::Bind(&ConnectionFactoryImpl::OnProxyResolveDone,
450 weak_ptr_factory_.GetWeakPtr()),
453 if (status == net::OK || status == net::ERR_IO_PENDING) {
456 // If ReconsiderProxyAfterError() failed synchronously, it means
457 // there was nothing left to fall-back to, so fail the transaction
458 // with the last connection error we got.
462 // If there is new proxy info, post OnProxyResolveDone to retry it. Otherwise,
463 // if there was an error falling back, fail synchronously.
464 if (status == net::OK) {
465 base::MessageLoop::current()->PostTask(
467 base::Bind(&ConnectionFactoryImpl::OnProxyResolveDone,
468 weak_ptr_factory_.GetWeakPtr(), status));
469 status = net::ERR_IO_PENDING;
474 void ConnectionFactoryImpl::ReportSuccessfulProxyConnection() {
475 if (network_session_ && network_session_->proxy_service())
476 network_session_->proxy_service()->ReportSuccess(proxy_info_);
479 void ConnectionFactoryImpl::CloseSocket() {
480 // The connection handler needs to be reset, else it'll attempt to keep using
481 // the destroyed socket.
482 if (connection_handler_)
483 connection_handler_->Reset();
485 if (socket_handle_.socket() && socket_handle_.socket()->IsConnected())
486 socket_handle_.socket()->Disconnect();
487 socket_handle_.Reset();