| // 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 "chrome/browser/net/connection_tester.h" |
| |
| #include "base/bind.h" |
| #include "base/command_line.h" |
| #include "base/compiler_specific.h" |
| #include "base/logging.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/message_loop/message_loop.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/thread_task_runner_handle.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "net/base/io_buffer.h" |
| #include "net/base/net_errors.h" |
| #include "net/base/net_util.h" |
| #include "net/cert/cert_verifier.h" |
| #include "net/cookies/cookie_monster.h" |
| #include "net/dns/host_resolver.h" |
| #include "net/http/http_auth_handler_factory.h" |
| #include "net/http/http_cache.h" |
| #include "net/http/http_network_session.h" |
| #include "net/http/http_server_properties_impl.h" |
| #include "net/http/transport_security_state.h" |
| #include "net/proxy/dhcp_proxy_script_fetcher_factory.h" |
| #include "net/proxy/proxy_config_service_fixed.h" |
| #include "net/proxy/proxy_script_fetcher_impl.h" |
| #include "net/proxy/proxy_service.h" |
| #include "net/proxy/proxy_service_v8.h" |
| #include "net/ssl/ssl_config_service_defaults.h" |
| #include "net/url_request/url_request.h" |
| #include "net/url_request/url_request_context.h" |
| #include "net/url_request/url_request_context_storage.h" |
| |
| #if !defined(OS_ANDROID) && !defined(OS_IOS) |
| #include "chrome/browser/net/firefox_proxy_settings.h" |
| #endif |
| |
| namespace { |
| |
| // ExperimentURLRequestContext ------------------------------------------------ |
| |
| // An instance of ExperimentURLRequestContext is created for each experiment |
| // run by ConnectionTester. The class initializes network dependencies according |
| // to the specified "experiment". |
| class ExperimentURLRequestContext : public net::URLRequestContext { |
| public: |
| explicit ExperimentURLRequestContext( |
| net::URLRequestContext* proxy_request_context) : |
| #if !defined(OS_IOS) |
| proxy_request_context_(proxy_request_context), |
| #endif |
| storage_(this), |
| weak_factory_(this) {} |
| |
| virtual ~ExperimentURLRequestContext() {} |
| |
| // Creates a proxy config service for |experiment|. On success returns net::OK |
| // and fills |config_service| with a new pointer. Otherwise returns a network |
| // error code. |
| int CreateProxyConfigService( |
| ConnectionTester::ProxySettingsExperiment experiment, |
| scoped_ptr<net::ProxyConfigService>* config_service, |
| base::Callback<void(int)> callback) { |
| switch (experiment) { |
| case ConnectionTester::PROXY_EXPERIMENT_USE_SYSTEM_SETTINGS: |
| return CreateSystemProxyConfigService(config_service); |
| case ConnectionTester::PROXY_EXPERIMENT_USE_FIREFOX_SETTINGS: |
| return CreateFirefoxProxyConfigService(config_service, callback); |
| case ConnectionTester::PROXY_EXPERIMENT_USE_AUTO_DETECT: |
| config_service->reset(new net::ProxyConfigServiceFixed( |
| net::ProxyConfig::CreateAutoDetect())); |
| return net::OK; |
| case ConnectionTester::PROXY_EXPERIMENT_USE_DIRECT: |
| config_service->reset(new net::ProxyConfigServiceFixed( |
| net::ProxyConfig::CreateDirect())); |
| return net::OK; |
| default: |
| NOTREACHED(); |
| return net::ERR_UNEXPECTED; |
| } |
| } |
| |
| int Init(const ConnectionTester::Experiment& experiment, |
| scoped_ptr<net::ProxyConfigService>* proxy_config_service, |
| net::NetLog* net_log) { |
| int rv; |
| |
| // Create a custom HostResolver for this experiment. |
| scoped_ptr<net::HostResolver> host_resolver_tmp; |
| rv = CreateHostResolver(experiment.host_resolver_experiment, |
| &host_resolver_tmp); |
| if (rv != net::OK) |
| return rv; // Failure. |
| storage_.set_host_resolver(host_resolver_tmp.Pass()); |
| |
| // Create a custom ProxyService for this this experiment. |
| scoped_ptr<net::ProxyService> experiment_proxy_service; |
| rv = CreateProxyService(experiment.proxy_settings_experiment, |
| proxy_config_service, &experiment_proxy_service); |
| if (rv != net::OK) |
| return rv; // Failure. |
| storage_.set_proxy_service(experiment_proxy_service.release()); |
| |
| // The rest of the dependencies are standard, and don't depend on the |
| // experiment being run. |
| storage_.set_cert_verifier(net::CertVerifier::CreateDefault()); |
| storage_.set_transport_security_state(new net::TransportSecurityState); |
| storage_.set_ssl_config_service(new net::SSLConfigServiceDefaults); |
| storage_.set_http_auth_handler_factory( |
| net::HttpAuthHandlerFactory::CreateDefault(host_resolver())); |
| storage_.set_http_server_properties( |
| scoped_ptr<net::HttpServerProperties>( |
| new net::HttpServerPropertiesImpl())); |
| |
| net::HttpNetworkSession::Params session_params; |
| session_params.host_resolver = host_resolver(); |
| session_params.cert_verifier = cert_verifier(); |
| session_params.transport_security_state = transport_security_state(); |
| session_params.proxy_service = proxy_service(); |
| session_params.ssl_config_service = ssl_config_service(); |
| session_params.http_auth_handler_factory = http_auth_handler_factory(); |
| session_params.http_server_properties = http_server_properties(); |
| session_params.net_log = net_log; |
| scoped_refptr<net::HttpNetworkSession> network_session( |
| new net::HttpNetworkSession(session_params)); |
| storage_.set_http_transaction_factory(new net::HttpCache( |
| network_session.get(), net::HttpCache::DefaultBackend::InMemory(0))); |
| // In-memory cookie store. |
| storage_.set_cookie_store(new net::CookieMonster(NULL, NULL)); |
| |
| return net::OK; |
| } |
| |
| private: |
| // Creates a host resolver for |experiment|. On success returns net::OK and |
| // fills |host_resolver| with a new pointer. Otherwise returns a network |
| // error code. |
| int CreateHostResolver( |
| ConnectionTester::HostResolverExperiment experiment, |
| scoped_ptr<net::HostResolver>* host_resolver) { |
| // Create a vanilla HostResolver that disables caching. |
| const size_t kMaxJobs = 50u; |
| const size_t kMaxRetryAttempts = 4u; |
| net::HostResolver::Options options; |
| options.max_concurrent_resolves = kMaxJobs; |
| options.max_retry_attempts = kMaxRetryAttempts; |
| options.enable_caching = false; |
| scoped_ptr<net::HostResolver> resolver( |
| net::HostResolver::CreateSystemResolver(options, NULL /* NetLog */)); |
| |
| // Modify it slightly based on the experiment being run. |
| switch (experiment) { |
| case ConnectionTester::HOST_RESOLVER_EXPERIMENT_PLAIN: |
| break; |
| case ConnectionTester::HOST_RESOLVER_EXPERIMENT_DISABLE_IPV6: |
| resolver->SetDefaultAddressFamily(net::ADDRESS_FAMILY_IPV4); |
| break; |
| case ConnectionTester::HOST_RESOLVER_EXPERIMENT_IPV6_PROBE: { |
| // The system HostResolver will probe by default. |
| break; |
| } |
| default: |
| NOTREACHED(); |
| return net::ERR_UNEXPECTED; |
| } |
| host_resolver->swap(resolver); |
| return net::OK; |
| } |
| |
| // Creates a proxy service for |experiment|. On success returns net::OK |
| // and fills |experiment_proxy_service| with a new pointer. Otherwise returns |
| // a network error code. |
| int CreateProxyService( |
| ConnectionTester::ProxySettingsExperiment experiment, |
| scoped_ptr<net::ProxyConfigService>* proxy_config_service, |
| scoped_ptr<net::ProxyService>* experiment_proxy_service) { |
| if (CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kSingleProcess)) { |
| // We can't create a standard proxy resolver in single-process mode. |
| // Rather than falling-back to some other implementation, fail. |
| return net::ERR_NOT_IMPLEMENTED; |
| } |
| |
| net::DhcpProxyScriptFetcherFactory dhcp_factory; |
| if (CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kDisableDhcpWpad)) { |
| dhcp_factory.set_enabled(false); |
| } |
| |
| #if defined(OS_IOS) |
| experiment_proxy_service->reset( |
| net::ProxyService::CreateUsingSystemProxyResolver( |
| proxy_config_service->release(), 0u, NULL)); |
| #else |
| experiment_proxy_service->reset( |
| net::CreateProxyServiceUsingV8ProxyResolver( |
| proxy_config_service->release(), |
| new net::ProxyScriptFetcherImpl(proxy_request_context_), |
| dhcp_factory.Create(proxy_request_context_), |
| host_resolver(), |
| NULL, |
| NULL)); |
| #endif |
| |
| return net::OK; |
| } |
| |
| // Creates a proxy config service that pulls from the system proxy settings. |
| // On success returns net::OK and fills |config_service| with a new pointer. |
| // Otherwise returns a network error code. |
| int CreateSystemProxyConfigService( |
| scoped_ptr<net::ProxyConfigService>* config_service) { |
| #if defined(OS_LINUX) || defined(OS_OPENBSD) |
| // TODO(eroman): This is not supported on Linux yet, because of how |
| // construction needs ot happen on the UI thread. |
| return net::ERR_NOT_IMPLEMENTED; |
| #else |
| config_service->reset(net::ProxyService::CreateSystemProxyConfigService( |
| base::ThreadTaskRunnerHandle::Get().get(), NULL)); |
| return net::OK; |
| #endif |
| } |
| |
| #if !defined(OS_ANDROID) && !defined(OS_IOS) |
| static int FirefoxProxySettingsTask( |
| FirefoxProxySettings* firefox_settings) { |
| if (!FirefoxProxySettings::GetSettings(firefox_settings)) |
| return net::ERR_FILE_NOT_FOUND; |
| return net::OK; |
| } |
| |
| void FirefoxProxySettingsReply( |
| scoped_ptr<net::ProxyConfigService>* config_service, |
| FirefoxProxySettings* firefox_settings, |
| base::Callback<void(int)> callback, |
| int rv) { |
| if (rv == net::OK) { |
| if (FirefoxProxySettings::SYSTEM == firefox_settings->config_type()) { |
| rv = CreateSystemProxyConfigService(config_service); |
| } else { |
| net::ProxyConfig config; |
| if (firefox_settings->ToProxyConfig(&config)) |
| config_service->reset(new net::ProxyConfigServiceFixed(config)); |
| else |
| rv = net::ERR_FAILED; |
| } |
| } |
| callback.Run(rv); |
| } |
| #endif |
| |
| // Creates a fixed proxy config service that is initialized using Firefox's |
| // current proxy settings. On success returns net::OK and fills |
| // |config_service| with a new pointer. Otherwise returns a network error |
| // code. |
| int CreateFirefoxProxyConfigService( |
| scoped_ptr<net::ProxyConfigService>* config_service, |
| base::Callback<void(int)> callback) { |
| #if defined(OS_ANDROID) || defined(OS_IOS) |
| // Chrome on Android and iOS do not support Firefox settings. |
| return net::ERR_NOT_IMPLEMENTED; |
| #else |
| // Fetch Firefox's proxy settings (can fail if Firefox is not installed). |
| FirefoxProxySettings* ff_settings = new FirefoxProxySettings(); |
| base::Callback<int(void)> task = base::Bind( |
| &FirefoxProxySettingsTask, ff_settings); |
| base::Callback<void(int)> reply = base::Bind( |
| &ExperimentURLRequestContext::FirefoxProxySettingsReply, |
| weak_factory_.GetWeakPtr(), config_service, |
| base::Owned(ff_settings), callback); |
| if (!content::BrowserThread::PostTaskAndReplyWithResult<int>( |
| content::BrowserThread::FILE, FROM_HERE, task, reply)) |
| return net::ERR_FAILED; |
| return net::ERR_IO_PENDING; |
| #endif |
| } |
| |
| #if !defined(OS_IOS) |
| net::URLRequestContext* const proxy_request_context_; |
| #endif |
| net::URLRequestContextStorage storage_; |
| base::WeakPtrFactory<ExperimentURLRequestContext> weak_factory_; |
| }; |
| |
| } // namespace |
| |
| // ConnectionTester::TestRunner ---------------------------------------------- |
| |
| // TestRunner is a helper class for running an individual experiment. It can |
| // be deleted any time after it is started, and this will abort the request. |
| class ConnectionTester::TestRunner : public net::URLRequest::Delegate { |
| public: |
| // |tester| must remain alive throughout the TestRunner's lifetime. |
| // |tester| will be notified of completion. |
| TestRunner(ConnectionTester* tester, net::NetLog* net_log) |
| : tester_(tester), |
| net_log_(net_log), |
| weak_factory_(this) {} |
| |
| // Finish running |experiment| once a ProxyConfigService has been created. |
| // In the case of a FirefoxProxyConfigService, this will be called back |
| // after disk access has completed. |
| void ProxyConfigServiceCreated( |
| const Experiment& experiment, |
| scoped_ptr<net::ProxyConfigService>* proxy_config_service, int status); |
| |
| // Starts running |experiment|. Notifies tester->OnExperimentCompleted() when |
| // it is done. |
| void Run(const Experiment& experiment); |
| |
| // Overridden from net::URLRequest::Delegate: |
| virtual void OnResponseStarted(net::URLRequest* request) OVERRIDE; |
| virtual void OnReadCompleted(net::URLRequest* request, |
| int bytes_read) OVERRIDE; |
| // TODO(eroman): handle cases requiring authentication. |
| |
| private: |
| // The number of bytes to read each response body chunk. |
| static const int kReadBufferSize = 1024; |
| |
| // Starts reading the response's body (and keeps reading until an error or |
| // end of stream). |
| void ReadBody(net::URLRequest* request); |
| |
| // Called when the request has completed (for both success and failure). |
| void OnResponseCompleted(net::URLRequest* request); |
| void OnExperimentCompletedWithResult(int result); |
| |
| ConnectionTester* tester_; |
| scoped_ptr<ExperimentURLRequestContext> request_context_; |
| scoped_ptr<net::URLRequest> request_; |
| net::NetLog* net_log_; |
| |
| base::WeakPtrFactory<TestRunner> weak_factory_; |
| |
| DISALLOW_COPY_AND_ASSIGN(TestRunner); |
| }; |
| |
| void ConnectionTester::TestRunner::OnResponseStarted(net::URLRequest* request) { |
| if (!request->status().is_success()) { |
| OnResponseCompleted(request); |
| return; |
| } |
| |
| // Start reading the body. |
| ReadBody(request); |
| } |
| |
| void ConnectionTester::TestRunner::OnReadCompleted(net::URLRequest* request, |
| int bytes_read) { |
| if (bytes_read <= 0) { |
| OnResponseCompleted(request); |
| return; |
| } |
| |
| // Keep reading until the stream is closed. Throw the data read away. |
| ReadBody(request); |
| } |
| |
| void ConnectionTester::TestRunner::ReadBody(net::URLRequest* request) { |
| // Read the response body |kReadBufferSize| bytes at a time. |
| scoped_refptr<net::IOBuffer> unused_buffer( |
| new net::IOBuffer(kReadBufferSize)); |
| int num_bytes; |
| if (request->Read(unused_buffer.get(), kReadBufferSize, &num_bytes)) { |
| OnReadCompleted(request, num_bytes); |
| } else if (!request->status().is_io_pending()) { |
| // Read failed synchronously. |
| OnResponseCompleted(request); |
| } |
| } |
| |
| void ConnectionTester::TestRunner::OnResponseCompleted( |
| net::URLRequest* request) { |
| int result = net::OK; |
| if (!request->status().is_success()) { |
| DCHECK_NE(net::ERR_IO_PENDING, request->status().error()); |
| result = request->status().error(); |
| } |
| |
| // Post a task to notify the parent rather than handling it right away, |
| // to avoid re-entrancy problems with URLRequest. (Don't want the caller |
| // to end up deleting the URLRequest while in the middle of processing). |
| base::MessageLoop::current()->PostTask( |
| FROM_HERE, |
| base::Bind(&TestRunner::OnExperimentCompletedWithResult, |
| weak_factory_.GetWeakPtr(), result)); |
| } |
| |
| void ConnectionTester::TestRunner::OnExperimentCompletedWithResult(int result) { |
| tester_->OnExperimentCompleted(result); |
| } |
| |
| void ConnectionTester::TestRunner::ProxyConfigServiceCreated( |
| const Experiment& experiment, |
| scoped_ptr<net::ProxyConfigService>* proxy_config_service, |
| int status) { |
| if (status == net::OK) |
| status = request_context_->Init(experiment, |
| proxy_config_service, |
| net_log_); |
| if (status != net::OK) { |
| tester_->OnExperimentCompleted(status); |
| return; |
| } |
| // Fetch a request using the experimental context. |
| request_.reset(request_context_->CreateRequest(experiment.url, this)); |
| request_->Start(); |
| } |
| |
| void ConnectionTester::TestRunner::Run(const Experiment& experiment) { |
| // Try to create a net::URLRequestContext for this experiment. |
| request_context_.reset( |
| new ExperimentURLRequestContext(tester_->proxy_request_context_)); |
| scoped_ptr<net::ProxyConfigService>* proxy_config_service = |
| new scoped_ptr<net::ProxyConfigService>(); |
| base::Callback<void(int)> config_service_callback = |
| base::Bind( |
| &TestRunner::ProxyConfigServiceCreated, weak_factory_.GetWeakPtr(), |
| experiment, base::Owned(proxy_config_service)); |
| int rv = request_context_->CreateProxyConfigService( |
| experiment.proxy_settings_experiment, |
| proxy_config_service, config_service_callback); |
| if (rv != net::ERR_IO_PENDING) |
| ProxyConfigServiceCreated(experiment, proxy_config_service, rv); |
| } |
| |
| // ConnectionTester ---------------------------------------------------------- |
| |
| ConnectionTester::ConnectionTester( |
| Delegate* delegate, |
| net::URLRequestContext* proxy_request_context, |
| net::NetLog* net_log) |
| : delegate_(delegate), |
| proxy_request_context_(proxy_request_context), |
| net_log_(net_log) { |
| DCHECK(delegate); |
| DCHECK(proxy_request_context); |
| } |
| |
| ConnectionTester::~ConnectionTester() { |
| // Cancellation happens automatically by deleting test_runner_. |
| } |
| |
| void ConnectionTester::RunAllTests(const GURL& url) { |
| // Select all possible experiments to run. (In no particular order). |
| // It is possible that some of these experiments are actually duplicates. |
| GetAllPossibleExperimentCombinations(url, &remaining_experiments_); |
| |
| delegate_->OnStartConnectionTestSuite(); |
| StartNextExperiment(); |
| } |
| |
| // static |
| string16 ConnectionTester::ProxySettingsExperimentDescription( |
| ProxySettingsExperiment experiment) { |
| // TODO(eroman): Use proper string resources. |
| switch (experiment) { |
| case PROXY_EXPERIMENT_USE_DIRECT: |
| return ASCIIToUTF16("Don't use any proxy"); |
| case PROXY_EXPERIMENT_USE_SYSTEM_SETTINGS: |
| return ASCIIToUTF16("Use system proxy settings"); |
| case PROXY_EXPERIMENT_USE_FIREFOX_SETTINGS: |
| return ASCIIToUTF16("Use Firefox's proxy settings"); |
| case PROXY_EXPERIMENT_USE_AUTO_DETECT: |
| return ASCIIToUTF16("Auto-detect proxy settings"); |
| default: |
| NOTREACHED(); |
| return string16(); |
| } |
| } |
| |
| // static |
| string16 ConnectionTester::HostResolverExperimentDescription( |
| HostResolverExperiment experiment) { |
| // TODO(eroman): Use proper string resources. |
| switch (experiment) { |
| case HOST_RESOLVER_EXPERIMENT_PLAIN: |
| return string16(); |
| case HOST_RESOLVER_EXPERIMENT_DISABLE_IPV6: |
| return ASCIIToUTF16("Disable IPv6 host resolving"); |
| case HOST_RESOLVER_EXPERIMENT_IPV6_PROBE: |
| return ASCIIToUTF16("Probe for IPv6 host resolving"); |
| default: |
| NOTREACHED(); |
| return string16(); |
| } |
| } |
| |
| // static |
| void ConnectionTester::GetAllPossibleExperimentCombinations( |
| const GURL& url, |
| ConnectionTester::ExperimentList* list) { |
| list->clear(); |
| for (size_t resolver_experiment = 0; |
| resolver_experiment < HOST_RESOLVER_EXPERIMENT_COUNT; |
| ++resolver_experiment) { |
| for (size_t proxy_experiment = 0; |
| proxy_experiment < PROXY_EXPERIMENT_COUNT; |
| ++proxy_experiment) { |
| Experiment experiment( |
| url, |
| static_cast<ProxySettingsExperiment>(proxy_experiment), |
| static_cast<HostResolverExperiment>(resolver_experiment)); |
| list->push_back(experiment); |
| } |
| } |
| } |
| |
| void ConnectionTester::StartNextExperiment() { |
| DCHECK(!remaining_experiments_.empty()); |
| DCHECK(!current_test_runner_.get()); |
| |
| delegate_->OnStartConnectionTestExperiment(current_experiment()); |
| |
| current_test_runner_.reset(new TestRunner(this, net_log_)); |
| current_test_runner_->Run(current_experiment()); |
| } |
| |
| void ConnectionTester::OnExperimentCompleted(int result) { |
| Experiment current = current_experiment(); |
| |
| // Advance to the next experiment. |
| remaining_experiments_.erase(remaining_experiments_.begin()); |
| current_test_runner_.reset(); |
| |
| // Notify the delegate of completion. |
| delegate_->OnCompletedConnectionTestExperiment(current, result); |
| |
| if (remaining_experiments_.empty()) { |
| delegate_->OnCompletedConnectionTestSuite(); |
| } else { |
| StartNextExperiment(); |
| } |
| } |