diff options
author | Zeno Albisser <zeno.albisser@digia.com> | 2013-08-15 21:46:11 +0200 |
---|---|---|
committer | Zeno Albisser <zeno.albisser@digia.com> | 2013-08-15 21:46:11 +0200 |
commit | 679147eead574d186ebf3069647b4c23e8ccace6 (patch) | |
tree | fc247a0ac8ff119f7c8550879ebb6d3dd8d1ff69 /chromium/net/proxy | |
download | qtwebengine-chromium-679147eead574d186ebf3069647b4c23e8ccace6.tar.gz |
Initial import.
Diffstat (limited to 'chromium/net/proxy')
90 files changed, 26294 insertions, 0 deletions
diff --git a/chromium/net/proxy/dhcp_proxy_script_adapter_fetcher_win.cc b/chromium/net/proxy/dhcp_proxy_script_adapter_fetcher_win.cc new file mode 100644 index 00000000000..56e47471bf0 --- /dev/null +++ b/chromium/net/proxy/dhcp_proxy_script_adapter_fetcher_win.cc @@ -0,0 +1,288 @@ +// 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/proxy/dhcp_proxy_script_adapter_fetcher_win.h" + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/logging.h" +#include "base/message_loop/message_loop_proxy.h" +#include "base/metrics/histogram.h" +#include "base/strings/sys_string_conversions.h" +#include "base/threading/worker_pool.h" +#include "base/time/time.h" +#include "net/base/net_errors.h" +#include "net/proxy/dhcpcsvc_init_win.h" +#include "net/proxy/proxy_script_fetcher_impl.h" +#include "net/url_request/url_request_context.h" + +#include <windows.h> +#include <winsock2.h> +#include <dhcpcsdk.h> +#pragma comment(lib, "dhcpcsvc.lib") + +namespace { + +// Maximum amount of time to wait for response from the Win32 DHCP API. +const int kTimeoutMs = 2000; + +} // namespace + +namespace net { + +DhcpProxyScriptAdapterFetcher::DhcpProxyScriptAdapterFetcher( + URLRequestContext* url_request_context) + : state_(STATE_START), + result_(ERR_IO_PENDING), + url_request_context_(url_request_context) { + DCHECK(url_request_context_); +} + +DhcpProxyScriptAdapterFetcher::~DhcpProxyScriptAdapterFetcher() { + Cancel(); +} + +void DhcpProxyScriptAdapterFetcher::Fetch( + const std::string& adapter_name, const CompletionCallback& callback) { + DCHECK(CalledOnValidThread()); + DCHECK_EQ(state_, STATE_START); + result_ = ERR_IO_PENDING; + pac_script_ = base::string16(); + state_ = STATE_WAIT_DHCP; + callback_ = callback; + + wait_timer_.Start(FROM_HERE, ImplGetTimeout(), + this, &DhcpProxyScriptAdapterFetcher::OnTimeout); + scoped_refptr<DhcpQuery> dhcp_query(ImplCreateDhcpQuery()); + base::WorkerPool::PostTaskAndReply( + FROM_HERE, + base::Bind( + &DhcpProxyScriptAdapterFetcher::DhcpQuery::GetPacURLForAdapter, + dhcp_query.get(), + adapter_name), + base::Bind( + &DhcpProxyScriptAdapterFetcher::OnDhcpQueryDone, + AsWeakPtr(), + dhcp_query), + true); +} + +void DhcpProxyScriptAdapterFetcher::Cancel() { + DCHECK(CalledOnValidThread()); + callback_.Reset(); + wait_timer_.Stop(); + script_fetcher_.reset(); + + switch (state_) { + case STATE_WAIT_DHCP: + // Nothing to do here, we let the worker thread run to completion, + // the task it posts back when it completes will check the state. + break; + case STATE_WAIT_URL: + break; + case STATE_START: + case STATE_FINISH: + case STATE_CANCEL: + break; + } + + if (state_ != STATE_FINISH) { + result_ = ERR_ABORTED; + state_ = STATE_CANCEL; + } +} + +bool DhcpProxyScriptAdapterFetcher::DidFinish() const { + DCHECK(CalledOnValidThread()); + return state_ == STATE_FINISH; +} + +int DhcpProxyScriptAdapterFetcher::GetResult() const { + DCHECK(CalledOnValidThread()); + return result_; +} + +base::string16 DhcpProxyScriptAdapterFetcher::GetPacScript() const { + DCHECK(CalledOnValidThread()); + return pac_script_; +} + +GURL DhcpProxyScriptAdapterFetcher::GetPacURL() const { + DCHECK(CalledOnValidThread()); + return pac_url_; +} + +DhcpProxyScriptAdapterFetcher::DhcpQuery::DhcpQuery() { +} + +DhcpProxyScriptAdapterFetcher::DhcpQuery::~DhcpQuery() { +} + +void DhcpProxyScriptAdapterFetcher::DhcpQuery::GetPacURLForAdapter( + const std::string& adapter_name) { + url_ = ImplGetPacURLFromDhcp(adapter_name); +} + +const std::string& DhcpProxyScriptAdapterFetcher::DhcpQuery::url() const { + return url_; +} + +std::string + DhcpProxyScriptAdapterFetcher::DhcpQuery::ImplGetPacURLFromDhcp( + const std::string& adapter_name) { + return DhcpProxyScriptAdapterFetcher::GetPacURLFromDhcp(adapter_name); +} + +void DhcpProxyScriptAdapterFetcher::OnDhcpQueryDone( + scoped_refptr<DhcpQuery> dhcp_query) { + DCHECK(CalledOnValidThread()); + // Because we can't cancel the call to the Win32 API, we can expect + // it to finish while we are in a few different states. The expected + // one is WAIT_DHCP, but it could be in CANCEL if Cancel() was called, + // or FINISH if timeout occurred. + DCHECK(state_ == STATE_WAIT_DHCP || state_ == STATE_CANCEL || + state_ == STATE_FINISH); + if (state_ != STATE_WAIT_DHCP) + return; + + wait_timer_.Stop(); + + pac_url_ = GURL(dhcp_query->url()); + if (pac_url_.is_empty() || !pac_url_.is_valid()) { + result_ = ERR_PAC_NOT_IN_DHCP; + TransitionToFinish(); + } else { + state_ = STATE_WAIT_URL; + script_fetcher_.reset(ImplCreateScriptFetcher()); + script_fetcher_->Fetch( + pac_url_, &pac_script_, + base::Bind(&DhcpProxyScriptAdapterFetcher::OnFetcherDone, + base::Unretained(this))); + } +} + +void DhcpProxyScriptAdapterFetcher::OnTimeout() { + DCHECK_EQ(state_, STATE_WAIT_DHCP); + result_ = ERR_TIMED_OUT; + TransitionToFinish(); +} + +void DhcpProxyScriptAdapterFetcher::OnFetcherDone(int result) { + DCHECK(CalledOnValidThread()); + DCHECK(state_ == STATE_WAIT_URL || state_ == STATE_CANCEL); + if (state_ == STATE_CANCEL) + return; + + // At this point, pac_script_ has already been written to. + script_fetcher_.reset(); + result_ = result; + TransitionToFinish(); +} + +void DhcpProxyScriptAdapterFetcher::TransitionToFinish() { + DCHECK(state_ == STATE_WAIT_DHCP || state_ == STATE_WAIT_URL); + state_ = STATE_FINISH; + CompletionCallback callback = callback_; + callback_.Reset(); + + // Be careful not to touch any member state after this, as the client + // may delete us during this callback. + callback.Run(result_); +} + +DhcpProxyScriptAdapterFetcher::State + DhcpProxyScriptAdapterFetcher::state() const { + return state_; +} + +ProxyScriptFetcher* DhcpProxyScriptAdapterFetcher::ImplCreateScriptFetcher() { + return new ProxyScriptFetcherImpl(url_request_context_); +} + +DhcpProxyScriptAdapterFetcher::DhcpQuery* + DhcpProxyScriptAdapterFetcher::ImplCreateDhcpQuery() { + return new DhcpQuery(); +} + +base::TimeDelta DhcpProxyScriptAdapterFetcher::ImplGetTimeout() const { + return base::TimeDelta::FromMilliseconds(kTimeoutMs); +} + +// static +std::string DhcpProxyScriptAdapterFetcher::GetPacURLFromDhcp( + const std::string& adapter_name) { + EnsureDhcpcsvcInit(); + + std::wstring adapter_name_wide = base::SysMultiByteToWide(adapter_name, + CP_ACP); + + DHCPCAPI_PARAMS_ARRAY send_params = { 0, NULL }; + + BYTE option_data[] = { 1, 252 }; + DHCPCAPI_PARAMS wpad_params = { 0 }; + wpad_params.OptionId = 252; + wpad_params.IsVendor = FALSE; // Surprising, but intentional. + + DHCPCAPI_PARAMS_ARRAY request_params = { 0 }; + request_params.nParams = 1; + request_params.Params = &wpad_params; + + // The maximum message size is typically 4096 bytes on Windows per + // http://support.microsoft.com/kb/321592 + DWORD result_buffer_size = 4096; + scoped_ptr_malloc<BYTE> result_buffer; + int retry_count = 0; + DWORD res = NO_ERROR; + do { + result_buffer.reset(reinterpret_cast<BYTE*>(malloc(result_buffer_size))); + + // Note that while the DHCPCAPI_REQUEST_SYNCHRONOUS flag seems to indicate + // there might be an asynchronous mode, there seems to be (at least in + // terms of well-documented use of this API) only a synchronous mode, with + // an optional "async notifications later if the option changes" mode. + // Even IE9, which we hope to emulate as IE is the most widely deployed + // previous implementation of the DHCP aspect of WPAD and the only one + // on Windows (Konqueror is the other, on Linux), uses this API with the + // synchronous flag. There seem to be several Microsoft Knowledge Base + // articles about calls to this function failing when other flags are used + // (e.g. http://support.microsoft.com/kb/885270) so we won't take any + // chances on non-standard, poorly documented usage. + res = ::DhcpRequestParams(DHCPCAPI_REQUEST_SYNCHRONOUS, + NULL, + const_cast<LPWSTR>(adapter_name_wide.c_str()), + NULL, + send_params, request_params, + result_buffer.get(), &result_buffer_size, + NULL); + ++retry_count; + } while (res == ERROR_MORE_DATA && retry_count <= 3); + + if (res != NO_ERROR) { + LOG(INFO) << "Error fetching PAC URL from DHCP: " << res; + UMA_HISTOGRAM_COUNTS("Net.DhcpWpadUnhandledDhcpError", 1); + } else if (wpad_params.nBytesData) { +#ifndef NDEBUG + // The result should be ASCII, not wide character. Some DHCP + // servers appear to count the trailing NULL in nBytesData, others + // do not. + size_t count_without_null = + strlen(reinterpret_cast<const char*>(wpad_params.Data)); + DCHECK(count_without_null == wpad_params.nBytesData || + count_without_null + 1 == wpad_params.nBytesData); +#endif + // Belt and suspenders: First, ensure we NULL-terminate after + // nBytesData; this is the inner constructor with nBytesData as a + // parameter. Then, return only up to the first null in case of + // embedded NULLs; this is the outer constructor that takes the + // result of c_str() on the inner. If the server is giving us + // back a buffer with embedded NULLs, something is broken anyway. + return std::string( + std::string(reinterpret_cast<const char *>(wpad_params.Data), + wpad_params.nBytesData).c_str()); + } + + return ""; +} + +} // namespace net diff --git a/chromium/net/proxy/dhcp_proxy_script_adapter_fetcher_win.h b/chromium/net/proxy/dhcp_proxy_script_adapter_fetcher_win.h new file mode 100644 index 00000000000..fadf2344656 --- /dev/null +++ b/chromium/net/proxy/dhcp_proxy_script_adapter_fetcher_win.h @@ -0,0 +1,176 @@ +// 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. + +#ifndef NET_PROXY_DHCP_PROXY_SCRIPT_ADAPTER_FETCHER_WIN_H_ +#define NET_PROXY_DHCP_PROXY_SCRIPT_ADAPTER_FETCHER_WIN_H_ + +#include <string> + +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/strings/string16.h" +#include "base/threading/non_thread_safe.h" +#include "base/timer/timer.h" +#include "net/base/completion_callback.h" +#include "net/base/net_export.h" +#include "url/gurl.h" + +namespace net { + +class ProxyScriptFetcher; +class URLRequestContext; + +// For a given adapter, this class takes care of first doing a DHCP lookup +// to get the PAC URL, then if there is one, trying to fetch it. +class NET_EXPORT_PRIVATE DhcpProxyScriptAdapterFetcher + : public base::SupportsWeakPtr<DhcpProxyScriptAdapterFetcher>, + NON_EXPORTED_BASE(public base::NonThreadSafe) { + public: + // |url_request_context| must outlive DhcpProxyScriptAdapterFetcher. + explicit DhcpProxyScriptAdapterFetcher( + URLRequestContext* url_request_context); + virtual ~DhcpProxyScriptAdapterFetcher(); + + // Starts a fetch. On completion (but not cancellation), |callback| + // will be invoked with the network error indicating success or failure + // of fetching a DHCP-configured PAC file on this adapter. + // + // On completion, results can be obtained via |GetPacScript()|, |GetPacURL()|. + // + // You may only call Fetch() once on a given instance of + // DhcpProxyScriptAdapterFetcher. + virtual void Fetch(const std::string& adapter_name, + const net::CompletionCallback& callback); + + // Cancels the fetch on this adapter. + virtual void Cancel(); + + // Returns true if in the FINISH state (not CANCEL). + virtual bool DidFinish() const; + + // Returns the network error indicating the result of the fetch. Will + // return IO_PENDING until the fetch is complete or cancelled. This is + // the same network error passed to the |callback| provided to |Fetch()|. + virtual int GetResult() const; + + // Returns the contents of the PAC file retrieved. Only valid if + // |IsComplete()| is true. Returns the empty string if |GetResult()| + // returns anything other than OK. + virtual base::string16 GetPacScript() const; + + // Returns the PAC URL retrieved from DHCP. Only guaranteed to be + // valid if |IsComplete()| is true. Returns an empty URL if no URL was + // configured in DHCP. May return a valid URL even if |result()| does + // not return OK (this would indicate that we found a URL configured in + // DHCP but failed to download it). + virtual GURL GetPacURL() const; + + // Returns the PAC URL configured in DHCP for the given |adapter_name|, or + // the empty string if none is configured. + // + // This function executes synchronously due to limitations of the Windows + // DHCP client API. + static std::string GetPacURLFromDhcp(const std::string& adapter_name); + + protected: + // This is the state machine for fetching from a given adapter. + // + // The state machine goes from START->WAIT_DHCP when it starts + // a worker thread to fetch the PAC URL from DHCP. + // + // In state WAIT_DHCP, if the DHCP query finishes and has no URL, it + // moves to state FINISH. If there is a URL, it starts a + // ProxyScriptFetcher to fetch it and moves to state WAIT_URL. + // + // It goes from WAIT_URL->FINISH when the ProxyScriptFetcher completes. + // + // In state FINISH, completion is indicated to the outer class, with + // the results of the fetch if a PAC script was successfully fetched. + // + // In state WAIT_DHCP, our timeout occurring can push us to FINISH. + // + // In any state except FINISH, a call to Cancel() will move to state + // CANCEL and cause all outstanding work to be cancelled or its + // results ignored when available. + enum State { + STATE_START, + STATE_WAIT_DHCP, + STATE_WAIT_URL, + STATE_FINISH, + STATE_CANCEL, + }; + + State state() const; + + // This inner class encapsulates work done on a worker pool thread. + // By using a separate object, we can keep the main object completely + // thread safe and let it be non-refcounted. + class NET_EXPORT_PRIVATE DhcpQuery + : public base::RefCountedThreadSafe<DhcpQuery> { + public: + DhcpQuery(); + virtual ~DhcpQuery(); + + // This method should run on a worker pool thread, via PostTaskAndReply. + // After it has run, the |url()| method on this object will return the + // URL retrieved. + void GetPacURLForAdapter(const std::string& adapter_name); + + // Returns the URL retrieved for the given adapter, once the task has run. + const std::string& url() const; + + protected: + // Virtual method introduced to allow unit testing. + virtual std::string ImplGetPacURLFromDhcp(const std::string& adapter_name); + + private: + // The URL retrieved for the given adapter. + std::string url_; + + DISALLOW_COPY_AND_ASSIGN(DhcpQuery); + }; + + // Virtual methods introduced to allow unit testing. + virtual ProxyScriptFetcher* ImplCreateScriptFetcher(); + virtual DhcpQuery* ImplCreateDhcpQuery(); + virtual base::TimeDelta ImplGetTimeout() const; + + private: + // Event/state transition handlers + void OnDhcpQueryDone(scoped_refptr<DhcpQuery> dhcp_query); + void OnTimeout(); + void OnFetcherDone(int result); + void TransitionToFinish(); + + // Current state of this state machine. + State state_; + + // A network error indicating result of operation. + int result_; + + // Empty string or the PAC script downloaded. + base::string16 pac_script_; + + // Empty URL or the PAC URL configured in DHCP. + GURL pac_url_; + + // Callback to let our client know we're done. Invalid in states + // START, FINISH and CANCEL. + net::CompletionCallback callback_; + + // Fetcher to retrieve PAC files once URL is known. + scoped_ptr<ProxyScriptFetcher> script_fetcher_; + + // Implements a timeout on the call to the Win32 DHCP API. + base::OneShotTimer<DhcpProxyScriptAdapterFetcher> wait_timer_; + + URLRequestContext* const url_request_context_; + + DISALLOW_IMPLICIT_CONSTRUCTORS(DhcpProxyScriptAdapterFetcher); +}; + +} // namespace net + +#endif // NET_PROXY_DHCP_PROXY_SCRIPT_ADAPTER_FETCHER_WIN_H_ diff --git a/chromium/net/proxy/dhcp_proxy_script_adapter_fetcher_win_unittest.cc b/chromium/net/proxy/dhcp_proxy_script_adapter_fetcher_win_unittest.cc new file mode 100644 index 00000000000..be177fa8ab8 --- /dev/null +++ b/chromium/net/proxy/dhcp_proxy_script_adapter_fetcher_win_unittest.cc @@ -0,0 +1,299 @@ +// 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/proxy/dhcp_proxy_script_adapter_fetcher_win.h" + +#include "base/perftimer.h" +#include "base/synchronization/waitable_event.h" +#include "base/test/test_timeouts.h" +#include "base/timer/timer.h" +#include "net/base/net_errors.h" +#include "net/base/test_completion_callback.h" +#include "net/proxy/mock_proxy_script_fetcher.h" +#include "net/proxy/proxy_script_fetcher_impl.h" +#include "net/test/spawned_test_server/spawned_test_server.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { + +namespace { + +const char* const kPacUrl = "http://pacserver/script.pac"; + +// In net/proxy/dhcp_proxy_script_fetcher_win_unittest.cc there are a few +// tests that exercise DhcpProxyScriptAdapterFetcher end-to-end along with +// DhcpProxyScriptFetcherWin, i.e. it tests the end-to-end usage of Win32 +// APIs and the network. In this file we test only by stubbing out +// functionality. + +// Version of DhcpProxyScriptAdapterFetcher that mocks out dependencies +// to allow unit testing. +class MockDhcpProxyScriptAdapterFetcher + : public DhcpProxyScriptAdapterFetcher { + public: + explicit MockDhcpProxyScriptAdapterFetcher(URLRequestContext* context) + : DhcpProxyScriptAdapterFetcher(context), + dhcp_delay_(base::TimeDelta::FromMilliseconds(1)), + timeout_(TestTimeouts::action_timeout()), + configured_url_(kPacUrl), + fetcher_delay_ms_(1), + fetcher_result_(OK), + pac_script_("bingo") { + } + + void Cancel() { + DhcpProxyScriptAdapterFetcher::Cancel(); + fetcher_ = NULL; + } + + virtual ProxyScriptFetcher* ImplCreateScriptFetcher() OVERRIDE { + // We don't maintain ownership of the fetcher, it is transferred to + // the caller. + fetcher_ = new MockProxyScriptFetcher(); + if (fetcher_delay_ms_ != -1) { + fetcher_timer_.Start(FROM_HERE, + base::TimeDelta::FromMilliseconds(fetcher_delay_ms_), + this, &MockDhcpProxyScriptAdapterFetcher::OnFetcherTimer); + } + return fetcher_; + } + + class DelayingDhcpQuery : public DhcpQuery { + public: + explicit DelayingDhcpQuery() + : DhcpQuery(), + test_finished_event_(true, false) { + } + + std::string ImplGetPacURLFromDhcp( + const std::string& adapter_name) OVERRIDE { + PerfTimer timer; + test_finished_event_.TimedWait(dhcp_delay_); + return configured_url_; + } + + base::WaitableEvent test_finished_event_; + base::TimeDelta dhcp_delay_; + std::string configured_url_; + }; + + virtual DhcpQuery* ImplCreateDhcpQuery() OVERRIDE { + dhcp_query_ = new DelayingDhcpQuery(); + dhcp_query_->dhcp_delay_ = dhcp_delay_; + dhcp_query_->configured_url_ = configured_url_; + return dhcp_query_; + } + + // Use a shorter timeout so tests can finish more quickly. + virtual base::TimeDelta ImplGetTimeout() const OVERRIDE { + return timeout_; + } + + void OnFetcherTimer() { + // Note that there is an assumption by this mock implementation that + // DhcpProxyScriptAdapterFetcher::Fetch will call ImplCreateScriptFetcher + // and call Fetch on the fetcher before the message loop is re-entered. + // This holds true today, but if you hit this DCHECK the problem can + // possibly be resolved by having a separate subclass of + // MockProxyScriptFetcher that adds the delay internally (instead of + // the simple approach currently used in ImplCreateScriptFetcher above). + DCHECK(fetcher_ && fetcher_->has_pending_request()); + fetcher_->NotifyFetchCompletion(fetcher_result_, pac_script_); + fetcher_ = NULL; + } + + bool IsWaitingForFetcher() const { + return state() == STATE_WAIT_URL; + } + + bool WasCancelled() const { + return state() == STATE_CANCEL; + } + + void FinishTest() { + DCHECK(dhcp_query_); + dhcp_query_->test_finished_event_.Signal(); + } + + base::TimeDelta dhcp_delay_; + base::TimeDelta timeout_; + std::string configured_url_; + int fetcher_delay_ms_; + int fetcher_result_; + std::string pac_script_; + MockProxyScriptFetcher* fetcher_; + base::OneShotTimer<MockDhcpProxyScriptAdapterFetcher> fetcher_timer_; + scoped_refptr<DelayingDhcpQuery> dhcp_query_; +}; + +class FetcherClient { + public: + FetcherClient() + : url_request_context_(new TestURLRequestContext()), + fetcher_( + new MockDhcpProxyScriptAdapterFetcher(url_request_context_.get())) { + } + + void WaitForResult(int expected_error) { + EXPECT_EQ(expected_error, callback_.WaitForResult()); + } + + void RunTest() { + fetcher_->Fetch("adapter name", callback_.callback()); + } + + void FinishTestAllowCleanup() { + fetcher_->FinishTest(); + base::MessageLoop::current()->RunUntilIdle(); + } + + TestCompletionCallback callback_; + scoped_ptr<URLRequestContext> url_request_context_; + scoped_ptr<MockDhcpProxyScriptAdapterFetcher> fetcher_; + base::string16 pac_text_; +}; + +TEST(DhcpProxyScriptAdapterFetcher, NormalCaseURLNotInDhcp) { + FetcherClient client; + client.fetcher_->configured_url_ = ""; + client.RunTest(); + client.WaitForResult(ERR_PAC_NOT_IN_DHCP); + ASSERT_TRUE(client.fetcher_->DidFinish()); + EXPECT_EQ(ERR_PAC_NOT_IN_DHCP, client.fetcher_->GetResult()); + EXPECT_EQ(base::string16(L""), client.fetcher_->GetPacScript()); +} + +TEST(DhcpProxyScriptAdapterFetcher, NormalCaseURLInDhcp) { + FetcherClient client; + client.RunTest(); + client.WaitForResult(OK); + ASSERT_TRUE(client.fetcher_->DidFinish()); + EXPECT_EQ(OK, client.fetcher_->GetResult()); + EXPECT_EQ(base::string16(L"bingo"), client.fetcher_->GetPacScript()); + EXPECT_EQ(GURL(kPacUrl), client.fetcher_->GetPacURL()); +} + +TEST(DhcpProxyScriptAdapterFetcher, TimeoutDuringDhcp) { + // Does a Fetch() with a long enough delay on accessing DHCP that the + // fetcher should time out. This is to test a case manual testing found, + // where under certain circumstances (e.g. adapter enabled for DHCP and + // needs to retrieve its configuration from DHCP, but no DHCP server + // present on the network) accessing DHCP can take on the order of tens + // of seconds. + FetcherClient client; + client.fetcher_->dhcp_delay_ = TestTimeouts::action_max_timeout(); + client.fetcher_->timeout_ = base::TimeDelta::FromMilliseconds(25); + + PerfTimer timer; + client.RunTest(); + // An error different from this would be received if the timeout didn't + // kick in. + client.WaitForResult(ERR_TIMED_OUT); + + ASSERT_TRUE(client.fetcher_->DidFinish()); + EXPECT_EQ(ERR_TIMED_OUT, client.fetcher_->GetResult()); + EXPECT_EQ(base::string16(L""), client.fetcher_->GetPacScript()); + EXPECT_EQ(GURL(), client.fetcher_->GetPacURL()); + client.FinishTestAllowCleanup(); +} + +TEST(DhcpProxyScriptAdapterFetcher, CancelWhileDhcp) { + FetcherClient client; + client.RunTest(); + client.fetcher_->Cancel(); + base::MessageLoop::current()->RunUntilIdle(); + ASSERT_FALSE(client.fetcher_->DidFinish()); + ASSERT_TRUE(client.fetcher_->WasCancelled()); + EXPECT_EQ(ERR_ABORTED, client.fetcher_->GetResult()); + EXPECT_EQ(base::string16(L""), client.fetcher_->GetPacScript()); + EXPECT_EQ(GURL(), client.fetcher_->GetPacURL()); + client.FinishTestAllowCleanup(); +} + +TEST(DhcpProxyScriptAdapterFetcher, CancelWhileFetcher) { + FetcherClient client; + // This causes the mock fetcher not to pretend the + // fetcher finishes after a timeout. + client.fetcher_->fetcher_delay_ms_ = -1; + client.RunTest(); + int max_loops = 4; + while (!client.fetcher_->IsWaitingForFetcher() && max_loops--) { + base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(10)); + base::MessageLoop::current()->RunUntilIdle(); + } + client.fetcher_->Cancel(); + base::MessageLoop::current()->RunUntilIdle(); + ASSERT_FALSE(client.fetcher_->DidFinish()); + ASSERT_TRUE(client.fetcher_->WasCancelled()); + EXPECT_EQ(ERR_ABORTED, client.fetcher_->GetResult()); + EXPECT_EQ(base::string16(L""), client.fetcher_->GetPacScript()); + // GetPacURL() still returns the URL fetched in this case. + EXPECT_EQ(GURL(kPacUrl), client.fetcher_->GetPacURL()); + client.FinishTestAllowCleanup(); +} + +TEST(DhcpProxyScriptAdapterFetcher, CancelAtCompletion) { + FetcherClient client; + client.RunTest(); + client.WaitForResult(OK); + client.fetcher_->Cancel(); + // Canceling after you're done should have no effect, so these + // are identical expectations to the NormalCaseURLInDhcp test. + ASSERT_TRUE(client.fetcher_->DidFinish()); + EXPECT_EQ(OK, client.fetcher_->GetResult()); + EXPECT_EQ(base::string16(L"bingo"), client.fetcher_->GetPacScript()); + EXPECT_EQ(GURL(kPacUrl), client.fetcher_->GetPacURL()); + client.FinishTestAllowCleanup(); +} + +// Does a real fetch on a mock DHCP configuration. +class MockDhcpRealFetchProxyScriptAdapterFetcher + : public MockDhcpProxyScriptAdapterFetcher { + public: + explicit MockDhcpRealFetchProxyScriptAdapterFetcher( + URLRequestContext* context) + : MockDhcpProxyScriptAdapterFetcher(context), + url_request_context_(context) { + } + + // Returns a real proxy script fetcher. + ProxyScriptFetcher* ImplCreateScriptFetcher() OVERRIDE { + ProxyScriptFetcher* fetcher = + new ProxyScriptFetcherImpl(url_request_context_); + return fetcher; + } + + URLRequestContext* url_request_context_; +}; + +TEST(DhcpProxyScriptAdapterFetcher, MockDhcpRealFetch) { + SpawnedTestServer test_server( + SpawnedTestServer::TYPE_HTTP, + SpawnedTestServer::kLocalhost, + base::FilePath( + FILE_PATH_LITERAL("net/data/proxy_script_fetcher_unittest"))); + ASSERT_TRUE(test_server.Start()); + + GURL configured_url = test_server.GetURL("files/downloadable.pac"); + + FetcherClient client; + TestURLRequestContext url_request_context; + client.fetcher_.reset( + new MockDhcpRealFetchProxyScriptAdapterFetcher( + &url_request_context)); + client.fetcher_->configured_url_ = configured_url.spec(); + client.RunTest(); + client.WaitForResult(OK); + ASSERT_TRUE(client.fetcher_->DidFinish()); + EXPECT_EQ(OK, client.fetcher_->GetResult()); + EXPECT_EQ(base::string16(L"-downloadable.pac-\n"), + client.fetcher_->GetPacScript()); + EXPECT_EQ(configured_url, + client.fetcher_->GetPacURL()); +} + +} // namespace + +} // namespace net diff --git a/chromium/net/proxy/dhcp_proxy_script_fetcher.cc b/chromium/net/proxy/dhcp_proxy_script_fetcher.cc new file mode 100644 index 00000000000..1771be05d64 --- /dev/null +++ b/chromium/net/proxy/dhcp_proxy_script_fetcher.cc @@ -0,0 +1,34 @@ +// 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/proxy/dhcp_proxy_script_fetcher.h" + +#include "net/base/net_errors.h" + +namespace net { + +std::string DhcpProxyScriptFetcher::GetFetcherName() const { + return std::string(); +} + +DhcpProxyScriptFetcher::DhcpProxyScriptFetcher() {} + +DhcpProxyScriptFetcher::~DhcpProxyScriptFetcher() {} + +DoNothingDhcpProxyScriptFetcher::DoNothingDhcpProxyScriptFetcher() {} + +DoNothingDhcpProxyScriptFetcher::~DoNothingDhcpProxyScriptFetcher() {} + +int DoNothingDhcpProxyScriptFetcher::Fetch( + base::string16* utf16_text, const CompletionCallback& callback) { + return ERR_NOT_IMPLEMENTED; +} + +void DoNothingDhcpProxyScriptFetcher::Cancel() {} + +const GURL& DoNothingDhcpProxyScriptFetcher::GetPacURL() const { + return gurl_; +} + +} // namespace net diff --git a/chromium/net/proxy/dhcp_proxy_script_fetcher.h b/chromium/net/proxy/dhcp_proxy_script_fetcher.h new file mode 100644 index 00000000000..0ec2ed4463c --- /dev/null +++ b/chromium/net/proxy/dhcp_proxy_script_fetcher.h @@ -0,0 +1,99 @@ +// 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. + +#ifndef NET_PROXY_DHCP_SCRIPT_FETCHER_H_ +#define NET_PROXY_DHCP_SCRIPT_FETCHER_H_ + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "base/strings/string16.h" +#include "net/base/completion_callback.h" +#include "net/base/net_export.h" +#include "net/proxy/proxy_script_fetcher.h" +#include "url/gurl.h" + +namespace net { + +// Interface for classes that can fetch a proxy script as configured via DHCP. +// +// The Fetch method on this interface tries to retrieve the most appropriate +// PAC script configured via DHCP. +// +// Normally there are zero or one DHCP scripts configured, but in the +// presence of multiple adapters with DHCP enabled, the fetcher resolves +// which PAC script to use if one or more are available. +class NET_EXPORT_PRIVATE DhcpProxyScriptFetcher { + public: + // Destruction should cancel any outstanding requests. + virtual ~DhcpProxyScriptFetcher(); + + // Attempts to retrieve the most appropriate PAC script configured via DHCP, + // and invokes |callback| on completion. + // + // Returns OK on success, otherwise the error code. If the return code is + // ERR_IO_PENDING, then the request completes asynchronously, and |callback| + // will be invoked later with the final error code. + // + // After synchronous or asynchronous completion with a result code of OK, + // |*utf16_text| is filled with the response. On failure, the result text is + // an empty string, and the result code is a network error. Some special + // network errors that may occur are: + // + // ERR_PAC_NOT_IN_DHCP -- no script configured in DHCP. + // + // The following all indicate there was one or more script configured + // in DHCP but all failed to download, and the error for the most + // preferred adapter that had a script configured was what the error + // code says: + // + // ERR_TIMED_OUT -- fetch took too long to complete. + // ERR_FILE_TOO_BIG -- response body was too large. + // ERR_PAC_STATUS_NOT_OK -- script failed to download. + // ERR_NOT_IMPLEMENTED -- script required authentication. + // + // If the request is cancelled (either using the "Cancel()" method or by + // deleting |this|), then no callback is invoked. + // + // Only one fetch is allowed to be outstanding at a time. + virtual int Fetch(base::string16* utf16_text, + const CompletionCallback& callback) = 0; + + // Aborts the in-progress fetch (if any). + virtual void Cancel() = 0; + + // After successful completion of |Fetch()|, this will return the URL + // retrieved from DHCP. It is reset if/when |Fetch()| is called again. + virtual const GURL& GetPacURL() const = 0; + + // Intended for unit tests only, so they can test that factories return + // the right types under given circumstances. + virtual std::string GetFetcherName() const; + + protected: + DhcpProxyScriptFetcher(); + + private: + DISALLOW_COPY_AND_ASSIGN(DhcpProxyScriptFetcher); +}; + +// A do-nothing retriever, always returns synchronously with +// ERR_NOT_IMPLEMENTED result and empty text. +class NET_EXPORT_PRIVATE DoNothingDhcpProxyScriptFetcher + : public DhcpProxyScriptFetcher { + public: + DoNothingDhcpProxyScriptFetcher(); + virtual ~DoNothingDhcpProxyScriptFetcher(); + + virtual int Fetch(base::string16* utf16_text, + const CompletionCallback& callback) OVERRIDE; + virtual void Cancel() OVERRIDE; + virtual const GURL& GetPacURL() const OVERRIDE; + private: + GURL gurl_; + DISALLOW_COPY_AND_ASSIGN(DoNothingDhcpProxyScriptFetcher); +}; + +} // namespace net + +#endif // NET_PROXY_DHCP_SCRIPT_FETCHER_H_ diff --git a/chromium/net/proxy/dhcp_proxy_script_fetcher_factory.cc b/chromium/net/proxy/dhcp_proxy_script_fetcher_factory.cc new file mode 100644 index 00000000000..01ede05b093 --- /dev/null +++ b/chromium/net/proxy/dhcp_proxy_script_fetcher_factory.cc @@ -0,0 +1,55 @@ +// 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/proxy/dhcp_proxy_script_fetcher_factory.h" + +#include "net/base/net_errors.h" +#include "net/proxy/dhcp_proxy_script_fetcher.h" + +#if defined(OS_WIN) +#include "net/proxy/dhcp_proxy_script_fetcher_win.h" +#endif + +namespace net { + +DhcpProxyScriptFetcherFactory::DhcpProxyScriptFetcherFactory() + : feature_enabled_(false) { + set_enabled(true); +} + +DhcpProxyScriptFetcher* DhcpProxyScriptFetcherFactory::Create( + URLRequestContext* context) { + if (!feature_enabled_) { + return new DoNothingDhcpProxyScriptFetcher(); + } else { + DCHECK(IsSupported()); + DhcpProxyScriptFetcher* ret = NULL; +#if defined(OS_WIN) + ret = new DhcpProxyScriptFetcherWin(context); +#endif + DCHECK(ret); + return ret; + } +} + +void DhcpProxyScriptFetcherFactory::set_enabled(bool enabled) { + if (IsSupported()) { + feature_enabled_ = enabled; + } +} + +bool DhcpProxyScriptFetcherFactory::enabled() const { + return feature_enabled_; +} + +// static +bool DhcpProxyScriptFetcherFactory::IsSupported() { +#if defined(OS_WIN) + return true; +#else + return false; +#endif +} + +} // namespace net diff --git a/chromium/net/proxy/dhcp_proxy_script_fetcher_factory.h b/chromium/net/proxy/dhcp_proxy_script_fetcher_factory.h new file mode 100644 index 00000000000..147435d6f0b --- /dev/null +++ b/chromium/net/proxy/dhcp_proxy_script_fetcher_factory.h @@ -0,0 +1,68 @@ +// 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. + +#ifndef NET_PROXY_DHCP_SCRIPT_FETCHER_FACTORY_H_ +#define NET_PROXY_DHCP_SCRIPT_FETCHER_FACTORY_H_ + +#include "base/basictypes.h" +#include "base/memory/singleton.h" +#include "net/base/completion_callback.h" +#include "net/base/net_export.h" + +namespace net { + +class DhcpProxyScriptFetcher; +class URLRequestContext; + +// Factory object for creating the appropriate concrete base class of +// DhcpProxyScriptFetcher for your operating system and settings. +// +// You might think we could just implement a DHCP client at the protocol +// level and have cross-platform support for retrieving PAC configuration +// from DHCP, but unfortunately the DHCP protocol assumes there is a single +// client per machine (specifically per network interface card), and there +// is an implicit state machine between the client and server, so adding a +// second client to the machine would not be advisable (see e.g. some +// discussion of what can happen at this URL: +// http://www.net.princeton.edu/multi-dhcp-one-interface-handling.html). +// +// Therefore, we have platform-specific implementations, and so we use +// this factory to select the right one. +class NET_EXPORT DhcpProxyScriptFetcherFactory { + public: + // Creates a new factory object with default settings. + DhcpProxyScriptFetcherFactory(); + + // Ownership is transferred to the caller. url_request_context must be valid + // and its lifetime must exceed that of the returned DhcpProxyScriptFetcher. + // + // Note that while a request is in progress, the fetcher may be holding a + // reference to |url_request_context|. Be careful not to create cycles + // between the fetcher and the context; you can break such cycles by calling + // Cancel(). + DhcpProxyScriptFetcher* Create(URLRequestContext* url_request_context); + + // Attempts to enable/disable the DHCP WPAD feature. Does nothing + // if |IsSupported()| returns false. + // + // The default is |enabled() == true|. + void set_enabled(bool enabled); + + // Returns true if the DHCP WPAD feature is enabled. Always returns + // false if |IsSupported()| is false. + bool enabled() const; + + // Returns true if the DHCP WPAD feature is supported on the current + // operating system. + static bool IsSupported(); + + private: + bool feature_enabled_; + + DISALLOW_COPY_AND_ASSIGN(DhcpProxyScriptFetcherFactory); +}; + +} // namespace net + +#endif // NET_PROXY_DHCP_SCRIPT_FETCHER_FACTORY_H_ diff --git a/chromium/net/proxy/dhcp_proxy_script_fetcher_factory_unittest.cc b/chromium/net/proxy/dhcp_proxy_script_fetcher_factory_unittest.cc new file mode 100644 index 00000000000..9eb7c67f1f1 --- /dev/null +++ b/chromium/net/proxy/dhcp_proxy_script_fetcher_factory_unittest.cc @@ -0,0 +1,59 @@ +// 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/proxy/dhcp_proxy_script_fetcher.h" +#include "net/proxy/dhcp_proxy_script_fetcher_factory.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { +namespace { + +TEST(DhcpProxyScriptFetcherFactoryTest, DoNothingWhenDisabled) { + DhcpProxyScriptFetcherFactory factory; + factory.set_enabled(false); + scoped_ptr<DhcpProxyScriptFetcher> fetcher(factory.Create(NULL)); + EXPECT_EQ("", fetcher->GetFetcherName()); +} + +#if defined(OS_WIN) +TEST(DhcpProxyScriptFetcherFactoryTest, WindowsFetcherOnWindows) { + DhcpProxyScriptFetcherFactory factory; + factory.set_enabled(true); + + scoped_ptr<TestURLRequestContext> context(new TestURLRequestContext()); + scoped_ptr<DhcpProxyScriptFetcher> fetcher(factory.Create(context.get())); + EXPECT_EQ("win", fetcher->GetFetcherName()); +} +#endif // defined(OS_WIN) + +TEST(DhcpProxyScriptFetcherFactoryTest, IsSupported) { +#if defined(OS_WIN) + ASSERT_TRUE(DhcpProxyScriptFetcherFactory::IsSupported()); +#else + ASSERT_FALSE(DhcpProxyScriptFetcherFactory::IsSupported()); +#endif // defined(OS_WIN) +} + +TEST(DhcpProxyScriptFetcherFactoryTest, SetEnabled) { + DhcpProxyScriptFetcherFactory factory; +#if defined(OS_WIN) + EXPECT_TRUE(factory.enabled()); +#else + EXPECT_FALSE(factory.enabled()); +#endif // defined(OS_WIN) + + factory.set_enabled(false); + EXPECT_FALSE(factory.enabled()); + + factory.set_enabled(true); +#if defined(OS_WIN) + EXPECT_TRUE(factory.enabled()); +#else + EXPECT_FALSE(factory.enabled()); +#endif // defined(OS_WIN) +} + +} // namespace +} // namespace net diff --git a/chromium/net/proxy/dhcp_proxy_script_fetcher_win.cc b/chromium/net/proxy/dhcp_proxy_script_fetcher_win.cc new file mode 100644 index 00000000000..9e34f5122e4 --- /dev/null +++ b/chromium/net/proxy/dhcp_proxy_script_fetcher_win.cc @@ -0,0 +1,370 @@ +// 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/proxy/dhcp_proxy_script_fetcher_win.h" + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/metrics/histogram.h" +#include "base/perftimer.h" +#include "base/threading/worker_pool.h" +#include "net/base/net_errors.h" +#include "net/proxy/dhcp_proxy_script_adapter_fetcher_win.h" + +#include <winsock2.h> +#include <iphlpapi.h> +#pragma comment(lib, "iphlpapi.lib") + +namespace { + +// How long to wait at maximum after we get results (a PAC file or +// knowledge that no PAC file is configured) from whichever network +// adapter finishes first. +const int kMaxWaitAfterFirstResultMs = 400; + +const int kGetAdaptersAddressesErrors[] = { + ERROR_ADDRESS_NOT_ASSOCIATED, + ERROR_BUFFER_OVERFLOW, + ERROR_INVALID_PARAMETER, + ERROR_NOT_ENOUGH_MEMORY, + ERROR_NO_DATA, +}; + +} // namespace + +namespace net { + +DhcpProxyScriptFetcherWin::DhcpProxyScriptFetcherWin( + URLRequestContext* url_request_context) + : state_(STATE_START), + num_pending_fetchers_(0), + destination_string_(NULL), + url_request_context_(url_request_context) { + DCHECK(url_request_context_); +} + +DhcpProxyScriptFetcherWin::~DhcpProxyScriptFetcherWin() { + // Count as user-initiated if we are not yet in STATE_DONE. + Cancel(); +} + +int DhcpProxyScriptFetcherWin::Fetch(base::string16* utf16_text, + const CompletionCallback& callback) { + DCHECK(CalledOnValidThread()); + if (state_ != STATE_START && state_ != STATE_DONE) { + NOTREACHED(); + return ERR_UNEXPECTED; + } + + fetch_start_time_ = base::TimeTicks::Now(); + + state_ = STATE_WAIT_ADAPTERS; + callback_ = callback; + destination_string_ = utf16_text; + + last_query_ = ImplCreateAdapterQuery(); + base::WorkerPool::PostTaskAndReply( + FROM_HERE, + base::Bind( + &DhcpProxyScriptFetcherWin::AdapterQuery::GetCandidateAdapterNames, + last_query_.get()), + base::Bind( + &DhcpProxyScriptFetcherWin::OnGetCandidateAdapterNamesDone, + AsWeakPtr(), + last_query_), + true); + + return ERR_IO_PENDING; +} + +void DhcpProxyScriptFetcherWin::Cancel() { + DCHECK(CalledOnValidThread()); + + if (state_ != STATE_DONE) { + // We only count this stat if the cancel was explicitly initiated by + // our client, and if we weren't already in STATE_DONE. + UMA_HISTOGRAM_TIMES("Net.DhcpWpadCancelTime", + base::TimeTicks::Now() - fetch_start_time_); + } + + CancelImpl(); +} + +void DhcpProxyScriptFetcherWin::CancelImpl() { + DCHECK(CalledOnValidThread()); + + if (state_ != STATE_DONE) { + callback_.Reset(); + wait_timer_.Stop(); + state_ = STATE_DONE; + + for (FetcherVector::iterator it = fetchers_.begin(); + it != fetchers_.end(); + ++it) { + (*it)->Cancel(); + } + + fetchers_.clear(); + } +} + +void DhcpProxyScriptFetcherWin::OnGetCandidateAdapterNamesDone( + scoped_refptr<AdapterQuery> query) { + DCHECK(CalledOnValidThread()); + + // This can happen if this object is reused for multiple queries, + // and a previous query was cancelled before it completed. + if (query.get() != last_query_.get()) + return; + last_query_ = NULL; + + // Enable unit tests to wait for this to happen; in production this function + // call is a no-op. + ImplOnGetCandidateAdapterNamesDone(); + + // We may have been cancelled. + if (state_ != STATE_WAIT_ADAPTERS) + return; + + state_ = STATE_NO_RESULTS; + + const std::set<std::string>& adapter_names = query->adapter_names(); + + if (adapter_names.empty()) { + TransitionToDone(); + return; + } + + for (std::set<std::string>::const_iterator it = adapter_names.begin(); + it != adapter_names.end(); + ++it) { + DhcpProxyScriptAdapterFetcher* fetcher(ImplCreateAdapterFetcher()); + fetcher->Fetch( + *it, base::Bind(&DhcpProxyScriptFetcherWin::OnFetcherDone, + base::Unretained(this))); + fetchers_.push_back(fetcher); + } + num_pending_fetchers_ = fetchers_.size(); +} + +std::string DhcpProxyScriptFetcherWin::GetFetcherName() const { + DCHECK(CalledOnValidThread()); + return "win"; +} + +const GURL& DhcpProxyScriptFetcherWin::GetPacURL() const { + DCHECK(CalledOnValidThread()); + DCHECK_EQ(state_, STATE_DONE); + + return pac_url_; +} + +void DhcpProxyScriptFetcherWin::OnFetcherDone(int result) { + DCHECK(state_ == STATE_NO_RESULTS || state_ == STATE_SOME_RESULTS); + + if (--num_pending_fetchers_ == 0) { + TransitionToDone(); + return; + } + + // If the only pending adapters are those less preferred than one + // with a valid PAC script, we do not need to wait any longer. + for (FetcherVector::iterator it = fetchers_.begin(); + it != fetchers_.end(); + ++it) { + bool did_finish = (*it)->DidFinish(); + int result = (*it)->GetResult(); + if (did_finish && result == OK) { + TransitionToDone(); + return; + } + if (!did_finish || result != ERR_PAC_NOT_IN_DHCP) { + break; + } + } + + // Once we have a single result, we set a maximum on how long to wait + // for the rest of the results. + if (state_ == STATE_NO_RESULTS) { + state_ = STATE_SOME_RESULTS; + wait_timer_.Start(FROM_HERE, + ImplGetMaxWait(), this, &DhcpProxyScriptFetcherWin::OnWaitTimer); + } +} + +void DhcpProxyScriptFetcherWin::OnWaitTimer() { + DCHECK_EQ(state_, STATE_SOME_RESULTS); + + // These are intended to help us understand whether our timeout may + // be too aggressive or not aggressive enough. + UMA_HISTOGRAM_COUNTS_100("Net.DhcpWpadNumAdaptersAtWaitTimer", + fetchers_.size()); + UMA_HISTOGRAM_COUNTS_100("Net.DhcpWpadNumPendingAdaptersAtWaitTimer", + num_pending_fetchers_); + + TransitionToDone(); +} + +void DhcpProxyScriptFetcherWin::TransitionToDone() { + DCHECK(state_ == STATE_NO_RESULTS || state_ == STATE_SOME_RESULTS); + + int result = ERR_PAC_NOT_IN_DHCP; // Default if no fetchers. + if (!fetchers_.empty()) { + // Scan twice for the result; once through the whole list for success, + // then if no success, return result for most preferred network adapter, + // preferring "real" network errors to the ERR_PAC_NOT_IN_DHCP error. + // Default to ERR_ABORTED if no fetcher completed. + result = ERR_ABORTED; + for (FetcherVector::iterator it = fetchers_.begin(); + it != fetchers_.end(); + ++it) { + if ((*it)->DidFinish() && (*it)->GetResult() == OK) { + result = OK; + *destination_string_ = (*it)->GetPacScript(); + pac_url_ = (*it)->GetPacURL(); + break; + } + } + if (result != OK) { + destination_string_->clear(); + for (FetcherVector::iterator it = fetchers_.begin(); + it != fetchers_.end(); + ++it) { + if ((*it)->DidFinish()) { + result = (*it)->GetResult(); + if (result != ERR_PAC_NOT_IN_DHCP) { + break; + } + } + } + } + } + + CompletionCallback callback = callback_; + CancelImpl(); + DCHECK_EQ(state_, STATE_DONE); + DCHECK(fetchers_.empty()); + DCHECK(callback_.is_null()); // Invariant of data. + + UMA_HISTOGRAM_TIMES("Net.DhcpWpadCompletionTime", + base::TimeTicks::Now() - fetch_start_time_); + + if (result != OK) { + UMA_HISTOGRAM_CUSTOM_ENUMERATION( + "Net.DhcpWpadFetchError", std::abs(result), GetAllErrorCodesForUma()); + } + + // We may be deleted re-entrantly within this outcall. + callback.Run(result); +} + +int DhcpProxyScriptFetcherWin::num_pending_fetchers() const { + return num_pending_fetchers_; +} + +URLRequestContext* DhcpProxyScriptFetcherWin::url_request_context() const { + return url_request_context_; +} + +DhcpProxyScriptAdapterFetcher* + DhcpProxyScriptFetcherWin::ImplCreateAdapterFetcher() { + return new DhcpProxyScriptAdapterFetcher(url_request_context_); +} + +DhcpProxyScriptFetcherWin::AdapterQuery* + DhcpProxyScriptFetcherWin::ImplCreateAdapterQuery() { + return new AdapterQuery(); +} + +base::TimeDelta DhcpProxyScriptFetcherWin::ImplGetMaxWait() { + return base::TimeDelta::FromMilliseconds(kMaxWaitAfterFirstResultMs); +} + +bool DhcpProxyScriptFetcherWin::GetCandidateAdapterNames( + std::set<std::string>* adapter_names) { + DCHECK(adapter_names); + adapter_names->clear(); + + // The GetAdaptersAddresses MSDN page recommends using a size of 15000 to + // avoid reallocation. + ULONG adapters_size = 15000; + scoped_ptr_malloc<IP_ADAPTER_ADDRESSES> adapters; + ULONG error = ERROR_SUCCESS; + int num_tries = 0; + + PerfTimer time_api_access; + do { + adapters.reset( + reinterpret_cast<IP_ADAPTER_ADDRESSES*>(malloc(adapters_size))); + // Return only unicast addresses, and skip information we do not need. + error = GetAdaptersAddresses(AF_UNSPEC, + GAA_FLAG_SKIP_ANYCAST | + GAA_FLAG_SKIP_MULTICAST | + GAA_FLAG_SKIP_DNS_SERVER | + GAA_FLAG_SKIP_FRIENDLY_NAME, + NULL, + adapters.get(), + &adapters_size); + ++num_tries; + } while (error == ERROR_BUFFER_OVERFLOW && num_tries <= 3); + + // This is primarily to validate our belief that the GetAdaptersAddresses API + // function is fast enough to call synchronously from the network thread. + UMA_HISTOGRAM_TIMES("Net.DhcpWpadGetAdaptersAddressesTime", + time_api_access.Elapsed()); + + if (error != ERROR_SUCCESS) { + UMA_HISTOGRAM_CUSTOM_ENUMERATION( + "Net.DhcpWpadGetAdaptersAddressesError", + error, + base::CustomHistogram::ArrayToCustomRanges( + kGetAdaptersAddressesErrors, + arraysize(kGetAdaptersAddressesErrors))); + } + + if (error == ERROR_NO_DATA) { + // There are no adapters that we care about. + return true; + } + + if (error != ERROR_SUCCESS) { + LOG(WARNING) << "Unexpected error retrieving WPAD configuration from DHCP."; + return false; + } + + IP_ADAPTER_ADDRESSES* adapter = NULL; + for (adapter = adapters.get(); adapter; adapter = adapter->Next) { + if (adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK) + continue; + if ((adapter->Flags & IP_ADAPTER_DHCP_ENABLED) == 0) + continue; + + DCHECK(adapter->AdapterName); + adapter_names->insert(adapter->AdapterName); + } + + return true; +} + +DhcpProxyScriptFetcherWin::AdapterQuery::AdapterQuery() { +} + +DhcpProxyScriptFetcherWin::AdapterQuery::~AdapterQuery() { +} + +void DhcpProxyScriptFetcherWin::AdapterQuery::GetCandidateAdapterNames() { + ImplGetCandidateAdapterNames(&adapter_names_); +} + +const std::set<std::string>& + DhcpProxyScriptFetcherWin::AdapterQuery::adapter_names() const { + return adapter_names_; +} + +bool DhcpProxyScriptFetcherWin::AdapterQuery::ImplGetCandidateAdapterNames( + std::set<std::string>* adapter_names) { + return DhcpProxyScriptFetcherWin::GetCandidateAdapterNames(adapter_names); +} + +} // namespace net diff --git a/chromium/net/proxy/dhcp_proxy_script_fetcher_win.h b/chromium/net/proxy/dhcp_proxy_script_fetcher_win.h new file mode 100644 index 00000000000..79fc4b348ef --- /dev/null +++ b/chromium/net/proxy/dhcp_proxy_script_fetcher_win.h @@ -0,0 +1,169 @@ +// 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. + +#ifndef NET_PROXY_DHCP_PROXY_SCRIPT_FETCHER_WIN_H_ +#define NET_PROXY_DHCP_PROXY_SCRIPT_FETCHER_WIN_H_ + +#include <set> +#include <string> + +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_vector.h" +#include "base/message_loop/message_loop_proxy.h" +#include "base/threading/non_thread_safe.h" +#include "base/time/time.h" +#include "base/timer/timer.h" +#include "net/proxy/dhcp_proxy_script_fetcher.h" + +namespace net { + +class DhcpProxyScriptAdapterFetcher; +class URLRequestContext; + +// Windows-specific implementation. +class NET_EXPORT_PRIVATE DhcpProxyScriptFetcherWin + : public DhcpProxyScriptFetcher, + public base::SupportsWeakPtr<DhcpProxyScriptFetcherWin>, + NON_EXPORTED_BASE(public base::NonThreadSafe) { + public: + // Creates a DhcpProxyScriptFetcherWin that issues requests through + // |url_request_context|. |url_request_context| must remain valid for + // the lifetime of DhcpProxyScriptFetcherWin. + explicit DhcpProxyScriptFetcherWin(URLRequestContext* url_request_context); + virtual ~DhcpProxyScriptFetcherWin(); + + // DhcpProxyScriptFetcher implementation. + int Fetch(base::string16* utf16_text, + const net::CompletionCallback& callback) OVERRIDE; + void Cancel() OVERRIDE; + const GURL& GetPacURL() const OVERRIDE; + std::string GetFetcherName() const OVERRIDE; + + // Sets |adapter_names| to contain the name of each network adapter on + // this machine that has DHCP enabled and is not a loop-back adapter. Returns + // false on error. + static bool GetCandidateAdapterNames(std::set<std::string>* adapter_names); + + protected: + int num_pending_fetchers() const; + + URLRequestContext* url_request_context() const; + + // This inner class encapsulate work done on a worker pool thread. + // The class calls GetCandidateAdapterNames, which can take a couple of + // hundred milliseconds. + class NET_EXPORT_PRIVATE AdapterQuery + : public base::RefCountedThreadSafe<AdapterQuery> { + public: + AdapterQuery(); + virtual ~AdapterQuery(); + + // This is the method that runs on the worker pool thread. + void GetCandidateAdapterNames(); + + // This set is valid after GetCandidateAdapterNames has + // been run. Its lifetime is scoped by this object. + const std::set<std::string>& adapter_names() const; + + protected: + // Virtual method introduced to allow unit testing. + virtual bool ImplGetCandidateAdapterNames( + std::set<std::string>* adapter_names); + + private: + // This is constructed on the originating thread, then used on the + // worker thread, then used again on the originating thread only when + // the task has completed on the worker thread. No locking required. + std::set<std::string> adapter_names_; + + DISALLOW_COPY_AND_ASSIGN(AdapterQuery); + }; + + // Virtual methods introduced to allow unit testing. + virtual DhcpProxyScriptAdapterFetcher* ImplCreateAdapterFetcher(); + virtual AdapterQuery* ImplCreateAdapterQuery(); + virtual base::TimeDelta ImplGetMaxWait(); + virtual void ImplOnGetCandidateAdapterNamesDone() {} + + private: + // Event/state transition handlers + void CancelImpl(); + void OnGetCandidateAdapterNamesDone(scoped_refptr<AdapterQuery> query); + void OnFetcherDone(int result); + void OnWaitTimer(); + void TransitionToDone(); + + // This is the outer state machine for fetching PAC configuration from + // DHCP. It relies for sub-states on the state machine of the + // DhcpProxyScriptAdapterFetcher class. + // + // The goal of the implementation is to the following work in parallel + // for all network adapters that are using DHCP: + // a) Try to get the PAC URL configured in DHCP; + // b) If one is configured, try to fetch the PAC URL. + // c) Once this is done for all adapters, or a timeout has passed after + // it has completed for the fastest adapter, return the PAC file + // available for the most preferred network adapter, if any. + // + // The state machine goes from START->WAIT_ADAPTERS when it starts a + // worker thread to get the list of adapters with DHCP enabled. + // It then goes from WAIT_ADAPTERS->NO_RESULTS when it creates + // and starts an DhcpProxyScriptAdapterFetcher for each adapter. It goes + // from NO_RESULTS->SOME_RESULTS when it gets the first result; at this + // point a wait timer is started. It goes from SOME_RESULTS->DONE in + // two cases: All results are known, or the wait timer expired. A call + // to Cancel() will also go straight to DONE from any state. Any + // way the DONE state is entered, we will at that point cancel any + // outstanding work and return the best known PAC script or the empty + // string. + // + // The state machine is reset for each Fetch(), a call to which is + // only valid in states START and DONE, as only one Fetch() is + // allowed to be outstanding at any given time. + enum State { + STATE_START, + STATE_WAIT_ADAPTERS, + STATE_NO_RESULTS, + STATE_SOME_RESULTS, + STATE_DONE, + }; + + // Current state of this state machine. + State state_; + + // Vector, in Windows' network adapter preference order, of + // DhcpProxyScriptAdapterFetcher objects that are or were attempting + // to fetch a PAC file based on DHCP configuration. + typedef ScopedVector<DhcpProxyScriptAdapterFetcher> FetcherVector; + FetcherVector fetchers_; + + // Number of fetchers we are waiting for. + int num_pending_fetchers_; + + // Lets our client know we're done. Not valid in states START or DONE. + net::CompletionCallback callback_; + + // Pointer to string we will write results to. Not valid in states + // START and DONE. + base::string16* destination_string_; + + // PAC URL retrieved from DHCP, if any. Valid only in state STATE_DONE. + GURL pac_url_; + + base::OneShotTimer<DhcpProxyScriptFetcherWin> wait_timer_; + + URLRequestContext* const url_request_context_; + + // NULL or the AdapterQuery currently in flight. + scoped_refptr<AdapterQuery> last_query_; + + // Time |Fetch()| was last called, 0 if never. + base::TimeTicks fetch_start_time_; + + DISALLOW_IMPLICIT_CONSTRUCTORS(DhcpProxyScriptFetcherWin); +}; + +} // namespace net + +#endif // NET_PROXY_DHCP_PROXY_SCRIPT_FETCHER_WIN_H_ diff --git a/chromium/net/proxy/dhcp_proxy_script_fetcher_win_unittest.cc b/chromium/net/proxy/dhcp_proxy_script_fetcher_win_unittest.cc new file mode 100644 index 00000000000..919787a435a --- /dev/null +++ b/chromium/net/proxy/dhcp_proxy_script_fetcher_win_unittest.cc @@ -0,0 +1,644 @@ +// 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/proxy/dhcp_proxy_script_fetcher_win.h" + +#include <vector> + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/message_loop/message_loop.h" +#include "base/perftimer.h" +#include "base/rand_util.h" +#include "base/test/test_timeouts.h" +#include "base/threading/platform_thread.h" +#include "net/base/completion_callback.h" +#include "net/proxy/dhcp_proxy_script_adapter_fetcher_win.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { + +namespace { + +TEST(DhcpProxyScriptFetcherWin, AdapterNamesAndPacURLFromDhcp) { + // This tests our core Win32 implementation without any of the wrappers + // we layer on top to achieve asynchronous and parallel operations. + // + // We don't make assumptions about the environment this unit test is + // running in, so it just exercises the code to make sure there + // is no crash and no error returned, but does not assert on the number + // of interfaces or the information returned via DHCP. + std::set<std::string> adapter_names; + DhcpProxyScriptFetcherWin::GetCandidateAdapterNames(&adapter_names); + for (std::set<std::string>::const_iterator it = adapter_names.begin(); + it != adapter_names.end(); + ++it) { + const std::string& adapter_name = *it; + std::string pac_url = + DhcpProxyScriptAdapterFetcher::GetPacURLFromDhcp(adapter_name); + printf("Adapter '%s' has PAC URL '%s' configured in DHCP.\n", + adapter_name.c_str(), + pac_url.c_str()); + } +} + +// Helper for RealFetch* tests below. +class RealFetchTester { + public: + RealFetchTester() + : context_(new TestURLRequestContext), + fetcher_(new DhcpProxyScriptFetcherWin(context_.get())), + finished_(false), + on_completion_is_error_(false) { + // Make sure the test ends. + timeout_.Start(FROM_HERE, + base::TimeDelta::FromSeconds(5), this, &RealFetchTester::OnTimeout); + } + + void RunTest() { + int result = fetcher_->Fetch( + &pac_text_, + base::Bind(&RealFetchTester::OnCompletion, base::Unretained(this))); + if (result != ERR_IO_PENDING) + finished_ = true; + } + + void RunTestWithCancel() { + RunTest(); + fetcher_->Cancel(); + } + + void RunTestWithDeferredCancel() { + // Put the cancellation into the queue before even running the + // test to avoid the chance of one of the adapter fetcher worker + // threads completing before cancellation. See http://crbug.com/86756. + cancel_timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds(0), + this, &RealFetchTester::OnCancelTimer); + RunTest(); + } + + void OnCompletion(int result) { + if (on_completion_is_error_) { + FAIL() << "Received completion for test in which this is error."; + } + finished_ = true; + printf("Result code %d PAC data length %d\n", result, pac_text_.size()); + } + + void OnTimeout() { + printf("Timeout!"); + OnCompletion(0); + } + + void OnCancelTimer() { + fetcher_->Cancel(); + finished_ = true; + } + + void WaitUntilDone() { + while (!finished_) { + base::MessageLoop::current()->RunUntilIdle(); + } + base::MessageLoop::current()->RunUntilIdle(); + } + + // Attempts to give worker threads time to finish. This is currently + // very simplistic as completion (via completion callback or cancellation) + // immediately "detaches" any worker threads, so the best we can do is give + // them a little time. If we start running into Valgrind leaks, we can + // do something a bit more clever to track worker threads even when the + // DhcpProxyScriptFetcherWin state machine has finished. + void FinishTestAllowCleanup() { + base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(30)); + } + + scoped_ptr<URLRequestContext> context_; + scoped_ptr<DhcpProxyScriptFetcherWin> fetcher_; + bool finished_; + base::string16 pac_text_; + base::OneShotTimer<RealFetchTester> timeout_; + base::OneShotTimer<RealFetchTester> cancel_timer_; + bool on_completion_is_error_; +}; + +TEST(DhcpProxyScriptFetcherWin, RealFetch) { + // This tests a call to Fetch() with no stubbing out of dependencies. + // + // We don't make assumptions about the environment this unit test is + // running in, so it just exercises the code to make sure there + // is no crash and no unexpected error returned, but does not assert on + // results beyond that. + RealFetchTester fetcher; + fetcher.RunTest(); + + fetcher.WaitUntilDone(); + printf("PAC URL was %s\n", + fetcher.fetcher_->GetPacURL().possibly_invalid_spec().c_str()); + + fetcher.FinishTestAllowCleanup(); +} + +TEST(DhcpProxyScriptFetcherWin, RealFetchWithCancel) { + // Does a Fetch() with an immediate cancel. As before, just + // exercises the code without stubbing out dependencies. + RealFetchTester fetcher; + fetcher.RunTestWithCancel(); + base::MessageLoop::current()->RunUntilIdle(); + + // Attempt to avoid Valgrind leak reports in case worker thread is + // still running. + fetcher.FinishTestAllowCleanup(); +} + +// For RealFetchWithDeferredCancel, below. +class DelayingDhcpProxyScriptAdapterFetcher + : public DhcpProxyScriptAdapterFetcher { + public: + explicit DelayingDhcpProxyScriptAdapterFetcher( + URLRequestContext* url_request_context) + : DhcpProxyScriptAdapterFetcher(url_request_context) { + } + + class DelayingDhcpQuery : public DhcpQuery { + public: + explicit DelayingDhcpQuery() + : DhcpQuery() { + } + + std::string ImplGetPacURLFromDhcp( + const std::string& adapter_name) OVERRIDE { + base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(20)); + return DhcpQuery::ImplGetPacURLFromDhcp(adapter_name); + } + }; + + DhcpQuery* ImplCreateDhcpQuery() OVERRIDE { + return new DelayingDhcpQuery(); + } +}; + +// For RealFetchWithDeferredCancel, below. +class DelayingDhcpProxyScriptFetcherWin + : public DhcpProxyScriptFetcherWin { + public: + explicit DelayingDhcpProxyScriptFetcherWin( + URLRequestContext* context) + : DhcpProxyScriptFetcherWin(context) { + } + + DhcpProxyScriptAdapterFetcher* ImplCreateAdapterFetcher() OVERRIDE { + return new DelayingDhcpProxyScriptAdapterFetcher(url_request_context()); + } +}; + +TEST(DhcpProxyScriptFetcherWin, RealFetchWithDeferredCancel) { + // Does a Fetch() with a slightly delayed cancel. As before, just + // exercises the code without stubbing out dependencies, but + // introduces a guaranteed 20 ms delay on the worker threads so that + // the cancel is called before they complete. + RealFetchTester fetcher; + fetcher.fetcher_.reset( + new DelayingDhcpProxyScriptFetcherWin(fetcher.context_.get())); + fetcher.on_completion_is_error_ = true; + fetcher.RunTestWithDeferredCancel(); + fetcher.WaitUntilDone(); +} + +// The remaining tests are to exercise our state machine in various +// situations, with actual network access fully stubbed out. + +class DummyDhcpProxyScriptAdapterFetcher + : public DhcpProxyScriptAdapterFetcher { + public: + explicit DummyDhcpProxyScriptAdapterFetcher(URLRequestContext* context) + : DhcpProxyScriptAdapterFetcher(context), + did_finish_(false), + result_(OK), + pac_script_(L"bingo"), + fetch_delay_ms_(1) { + } + + void Fetch(const std::string& adapter_name, + const CompletionCallback& callback) OVERRIDE { + callback_ = callback; + timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds(fetch_delay_ms_), + this, &DummyDhcpProxyScriptAdapterFetcher::OnTimer); + } + + void Cancel() OVERRIDE { + timer_.Stop(); + } + + bool DidFinish() const OVERRIDE { + return did_finish_; + } + + int GetResult() const OVERRIDE { + return result_; + } + + base::string16 GetPacScript() const OVERRIDE { + return pac_script_; + } + + void OnTimer() { + callback_.Run(result_); + } + + void Configure(bool did_finish, + int result, + base::string16 pac_script, + int fetch_delay_ms) { + did_finish_ = did_finish; + result_ = result; + pac_script_ = pac_script; + fetch_delay_ms_ = fetch_delay_ms; + } + + private: + bool did_finish_; + int result_; + base::string16 pac_script_; + int fetch_delay_ms_; + CompletionCallback callback_; + base::OneShotTimer<DummyDhcpProxyScriptAdapterFetcher> timer_; +}; + +class MockDhcpProxyScriptFetcherWin : public DhcpProxyScriptFetcherWin { + public: + class MockAdapterQuery : public AdapterQuery { + public: + MockAdapterQuery() { + } + + virtual ~MockAdapterQuery() { + } + + virtual bool ImplGetCandidateAdapterNames( + std::set<std::string>* adapter_names) OVERRIDE { + adapter_names->insert( + mock_adapter_names_.begin(), mock_adapter_names_.end()); + return true; + } + + std::vector<std::string> mock_adapter_names_; + }; + + MockDhcpProxyScriptFetcherWin(URLRequestContext* context) + : DhcpProxyScriptFetcherWin(context), + num_fetchers_created_(0), + worker_finished_event_(true, false) { + ResetTestState(); + } + + virtual ~MockDhcpProxyScriptFetcherWin() { + ResetTestState(); + } + + // Adds a fetcher object to the queue of fetchers used by + // |ImplCreateAdapterFetcher()|, and its name to the list of adapters + // returned by ImplGetCandidateAdapterNames. + void PushBackAdapter(const std::string& adapter_name, + DhcpProxyScriptAdapterFetcher* fetcher) { + adapter_query_->mock_adapter_names_.push_back(adapter_name); + adapter_fetchers_.push_back(fetcher); + } + + void ConfigureAndPushBackAdapter(const std::string& adapter_name, + bool did_finish, + int result, + base::string16 pac_script, + base::TimeDelta fetch_delay) { + scoped_ptr<DummyDhcpProxyScriptAdapterFetcher> adapter_fetcher( + new DummyDhcpProxyScriptAdapterFetcher(url_request_context())); + adapter_fetcher->Configure( + did_finish, result, pac_script, fetch_delay.InMilliseconds()); + PushBackAdapter(adapter_name, adapter_fetcher.release()); + } + + DhcpProxyScriptAdapterFetcher* ImplCreateAdapterFetcher() OVERRIDE { + ++num_fetchers_created_; + return adapter_fetchers_[next_adapter_fetcher_index_++]; + } + + virtual AdapterQuery* ImplCreateAdapterQuery() OVERRIDE { + DCHECK(adapter_query_); + return adapter_query_.get(); + } + + base::TimeDelta ImplGetMaxWait() OVERRIDE { + return max_wait_; + } + + void ImplOnGetCandidateAdapterNamesDone() OVERRIDE { + worker_finished_event_.Signal(); + } + + void ResetTestState() { + // Delete any adapter fetcher objects we didn't hand out. + std::vector<DhcpProxyScriptAdapterFetcher*>::const_iterator it + = adapter_fetchers_.begin(); + for (; it != adapter_fetchers_.end(); ++it) { + if (num_fetchers_created_-- <= 0) { + delete (*it); + } + } + + next_adapter_fetcher_index_ = 0; + num_fetchers_created_ = 0; + adapter_fetchers_.clear(); + adapter_query_ = new MockAdapterQuery(); + max_wait_ = TestTimeouts::tiny_timeout(); + } + + bool HasPendingFetchers() { + return num_pending_fetchers() > 0; + } + + int next_adapter_fetcher_index_; + + // Ownership gets transferred to the implementation class via + // ImplCreateAdapterFetcher, but any objects not handed out are + // deleted on destruction. + std::vector<DhcpProxyScriptAdapterFetcher*> adapter_fetchers_; + + scoped_refptr<MockAdapterQuery> adapter_query_; + + base::TimeDelta max_wait_; + int num_fetchers_created_; + base::WaitableEvent worker_finished_event_; +}; + +class FetcherClient { +public: + FetcherClient() + : context_(new TestURLRequestContext), + fetcher_(context_.get()), + finished_(false), + result_(ERR_UNEXPECTED) { + } + + void RunTest() { + int result = fetcher_.Fetch( + &pac_text_, + base::Bind(&FetcherClient::OnCompletion, base::Unretained(this))); + ASSERT_EQ(ERR_IO_PENDING, result); + } + + void RunMessageLoopUntilComplete() { + while (!finished_) { + base::MessageLoop::current()->RunUntilIdle(); + } + base::MessageLoop::current()->RunUntilIdle(); + } + + void RunMessageLoopUntilWorkerDone() { + DCHECK(fetcher_.adapter_query_.get()); + while (!fetcher_.worker_finished_event_.TimedWait( + base::TimeDelta::FromMilliseconds(10))) { + base::MessageLoop::current()->RunUntilIdle(); + } + } + + void OnCompletion(int result) { + finished_ = true; + result_ = result; + } + + void ResetTestState() { + finished_ = false; + result_ = ERR_UNEXPECTED; + pac_text_ = L""; + fetcher_.ResetTestState(); + } + + scoped_ptr<URLRequestContext> context_; + MockDhcpProxyScriptFetcherWin fetcher_; + bool finished_; + int result_; + base::string16 pac_text_; +}; + +// We separate out each test's logic so that we can easily implement +// the ReuseFetcher test at the bottom. +void TestNormalCaseURLConfiguredOneAdapter(FetcherClient* client) { + TestURLRequestContext context; + scoped_ptr<DummyDhcpProxyScriptAdapterFetcher> adapter_fetcher( + new DummyDhcpProxyScriptAdapterFetcher(&context)); + adapter_fetcher->Configure(true, OK, L"bingo", 1); + client->fetcher_.PushBackAdapter("a", adapter_fetcher.release()); + client->RunTest(); + client->RunMessageLoopUntilComplete(); + ASSERT_EQ(OK, client->result_); + ASSERT_EQ(L"bingo", client->pac_text_); +} + +TEST(DhcpProxyScriptFetcherWin, NormalCaseURLConfiguredOneAdapter) { + FetcherClient client; + TestNormalCaseURLConfiguredOneAdapter(&client); +} + +void TestNormalCaseURLConfiguredMultipleAdapters(FetcherClient* client) { + client->fetcher_.ConfigureAndPushBackAdapter( + "most_preferred", true, ERR_PAC_NOT_IN_DHCP, L"", + base::TimeDelta::FromMilliseconds(1)); + client->fetcher_.ConfigureAndPushBackAdapter( + "second", true, OK, L"bingo", base::TimeDelta::FromMilliseconds(50)); + client->fetcher_.ConfigureAndPushBackAdapter( + "third", true, OK, L"rocko", base::TimeDelta::FromMilliseconds(1)); + client->RunTest(); + client->RunMessageLoopUntilComplete(); + ASSERT_EQ(OK, client->result_); + ASSERT_EQ(L"bingo", client->pac_text_); +} + +TEST(DhcpProxyScriptFetcherWin, NormalCaseURLConfiguredMultipleAdapters) { + FetcherClient client; + TestNormalCaseURLConfiguredMultipleAdapters(&client); +} + +void TestNormalCaseURLConfiguredMultipleAdaptersWithTimeout( + FetcherClient* client) { + client->fetcher_.ConfigureAndPushBackAdapter( + "most_preferred", true, ERR_PAC_NOT_IN_DHCP, L"", + base::TimeDelta::FromMilliseconds(1)); + // This will time out. + client->fetcher_.ConfigureAndPushBackAdapter( + "second", false, ERR_IO_PENDING, L"bingo", + TestTimeouts::action_timeout()); + client->fetcher_.ConfigureAndPushBackAdapter( + "third", true, OK, L"rocko", base::TimeDelta::FromMilliseconds(1)); + client->RunTest(); + client->RunMessageLoopUntilComplete(); + ASSERT_EQ(OK, client->result_); + ASSERT_EQ(L"rocko", client->pac_text_); +} + +TEST(DhcpProxyScriptFetcherWin, + NormalCaseURLConfiguredMultipleAdaptersWithTimeout) { + FetcherClient client; + TestNormalCaseURLConfiguredMultipleAdaptersWithTimeout(&client); +} + +void TestFailureCaseURLConfiguredMultipleAdaptersWithTimeout( + FetcherClient* client) { + client->fetcher_.ConfigureAndPushBackAdapter( + "most_preferred", true, ERR_PAC_NOT_IN_DHCP, L"", + base::TimeDelta::FromMilliseconds(1)); + // This will time out. + client->fetcher_.ConfigureAndPushBackAdapter( + "second", false, ERR_IO_PENDING, L"bingo", + TestTimeouts::action_timeout()); + // This is the first non-ERR_PAC_NOT_IN_DHCP error and as such + // should be chosen. + client->fetcher_.ConfigureAndPushBackAdapter( + "third", true, ERR_PAC_STATUS_NOT_OK, L"", + base::TimeDelta::FromMilliseconds(1)); + client->fetcher_.ConfigureAndPushBackAdapter( + "fourth", true, ERR_NOT_IMPLEMENTED, L"", + base::TimeDelta::FromMilliseconds(1)); + client->RunTest(); + client->RunMessageLoopUntilComplete(); + ASSERT_EQ(ERR_PAC_STATUS_NOT_OK, client->result_); + ASSERT_EQ(L"", client->pac_text_); +} + +TEST(DhcpProxyScriptFetcherWin, + FailureCaseURLConfiguredMultipleAdaptersWithTimeout) { + FetcherClient client; + TestFailureCaseURLConfiguredMultipleAdaptersWithTimeout(&client); +} + +void TestFailureCaseNoURLConfigured(FetcherClient* client) { + client->fetcher_.ConfigureAndPushBackAdapter( + "most_preferred", true, ERR_PAC_NOT_IN_DHCP, L"", + base::TimeDelta::FromMilliseconds(1)); + // This will time out. + client->fetcher_.ConfigureAndPushBackAdapter( + "second", false, ERR_IO_PENDING, L"bingo", + TestTimeouts::action_timeout()); + // This is the first non-ERR_PAC_NOT_IN_DHCP error and as such + // should be chosen. + client->fetcher_.ConfigureAndPushBackAdapter( + "third", true, ERR_PAC_NOT_IN_DHCP, L"", + base::TimeDelta::FromMilliseconds(1)); + client->RunTest(); + client->RunMessageLoopUntilComplete(); + ASSERT_EQ(ERR_PAC_NOT_IN_DHCP, client->result_); + ASSERT_EQ(L"", client->pac_text_); +} + +TEST(DhcpProxyScriptFetcherWin, FailureCaseNoURLConfigured) { + FetcherClient client; + TestFailureCaseNoURLConfigured(&client); +} + +void TestFailureCaseNoDhcpAdapters(FetcherClient* client) { + client->RunTest(); + client->RunMessageLoopUntilComplete(); + ASSERT_EQ(ERR_PAC_NOT_IN_DHCP, client->result_); + ASSERT_EQ(L"", client->pac_text_); + ASSERT_EQ(0, client->fetcher_.num_fetchers_created_); +} + +TEST(DhcpProxyScriptFetcherWin, FailureCaseNoDhcpAdapters) { + FetcherClient client; + TestFailureCaseNoDhcpAdapters(&client); +} + +void TestShortCircuitLessPreferredAdapters(FetcherClient* client) { + // Here we have a bunch of adapters; the first reports no PAC in DHCP, + // the second responds quickly with a PAC file, the rest take a long + // time. Verify that we complete quickly and do not wait for the slow + // adapters, i.e. we finish before timeout. + client->fetcher_.ConfigureAndPushBackAdapter( + "1", true, ERR_PAC_NOT_IN_DHCP, L"", + base::TimeDelta::FromMilliseconds(1)); + client->fetcher_.ConfigureAndPushBackAdapter( + "2", true, OK, L"bingo", + base::TimeDelta::FromMilliseconds(1)); + client->fetcher_.ConfigureAndPushBackAdapter( + "3", true, OK, L"wrongo", TestTimeouts::action_max_timeout()); + + // Increase the timeout to ensure the short circuit mechanism has + // time to kick in before the timeout waiting for more adapters kicks in. + client->fetcher_.max_wait_ = TestTimeouts::action_timeout(); + + PerfTimer timer; + client->RunTest(); + client->RunMessageLoopUntilComplete(); + ASSERT_TRUE(client->fetcher_.HasPendingFetchers()); + // Assert that the time passed is definitely less than the wait timer + // timeout, to get a second signal that it was the shortcut mechanism + // (in OnFetcherDone) that kicked in, and not the timeout waiting for + // more adapters. + ASSERT_GT(client->fetcher_.max_wait_ - (client->fetcher_.max_wait_ / 10), + timer.Elapsed()); +} + +TEST(DhcpProxyScriptFetcherWin, ShortCircuitLessPreferredAdapters) { + FetcherClient client; + TestShortCircuitLessPreferredAdapters(&client); +} + +void TestImmediateCancel(FetcherClient* client) { + TestURLRequestContext context; + scoped_ptr<DummyDhcpProxyScriptAdapterFetcher> adapter_fetcher( + new DummyDhcpProxyScriptAdapterFetcher(&context)); + adapter_fetcher->Configure(true, OK, L"bingo", 1); + client->fetcher_.PushBackAdapter("a", adapter_fetcher.release()); + client->RunTest(); + client->fetcher_.Cancel(); + client->RunMessageLoopUntilWorkerDone(); + ASSERT_EQ(0, client->fetcher_.num_fetchers_created_); +} + +// Regression test to check that when we cancel immediately, no +// adapter fetchers get created. +TEST(DhcpProxyScriptFetcherWin, ImmediateCancel) { + FetcherClient client; + TestImmediateCancel(&client); +} + +TEST(DhcpProxyScriptFetcherWin, ReuseFetcher) { + FetcherClient client; + + // The ProxyScriptFetcher interface stipulates that only a single + // |Fetch()| may be in flight at once, but allows reuse, so test + // that the state transitions correctly from done to start in all + // cases we're testing. + + typedef void (*FetcherClientTestFunction)(FetcherClient*); + typedef std::vector<FetcherClientTestFunction> TestVector; + TestVector test_functions; + test_functions.push_back(TestNormalCaseURLConfiguredOneAdapter); + test_functions.push_back(TestNormalCaseURLConfiguredMultipleAdapters); + test_functions.push_back( + TestNormalCaseURLConfiguredMultipleAdaptersWithTimeout); + test_functions.push_back( + TestFailureCaseURLConfiguredMultipleAdaptersWithTimeout); + test_functions.push_back(TestFailureCaseNoURLConfigured); + test_functions.push_back(TestFailureCaseNoDhcpAdapters); + test_functions.push_back(TestShortCircuitLessPreferredAdapters); + test_functions.push_back(TestImmediateCancel); + + std::random_shuffle(test_functions.begin(), + test_functions.end(), + base::RandGenerator); + for (TestVector::const_iterator it = test_functions.begin(); + it != test_functions.end(); + ++it) { + (*it)(&client); + client.ResetTestState(); + } + + // Re-do the first test to make sure the last test that was run did + // not leave things in a bad state. + (*test_functions.begin())(&client); +} + +} // namespace + +} // namespace net diff --git a/chromium/net/proxy/dhcpcsvc_init_win.cc b/chromium/net/proxy/dhcpcsvc_init_win.cc new file mode 100644 index 00000000000..7e32aeae9cd --- /dev/null +++ b/chromium/net/proxy/dhcpcsvc_init_win.cc @@ -0,0 +1,40 @@ +// 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/proxy/dhcpcsvc_init_win.h" + +#include "base/lazy_instance.h" +#include "base/logging.h" + +#include <dhcpcsdk.h> +#include <dhcpv6csdk.h> + +namespace { + +class DhcpcsvcInitSingleton { + public: + DhcpcsvcInitSingleton() { + DWORD version = 0; + DWORD err = DhcpCApiInitialize(&version); + DCHECK(err == ERROR_SUCCESS); // DCHECK_EQ complains of unsigned mismatch. + } + + ~DhcpcsvcInitSingleton() { + // Worker pool threads that use the DHCP API may still be running, so skip + // cleanup. + } +}; + +static base::LazyInstance<DhcpcsvcInitSingleton> g_dhcpcsvc_init_singleton = + LAZY_INSTANCE_INITIALIZER; + +} // namespace + +namespace net { + +void EnsureDhcpcsvcInit() { + g_dhcpcsvc_init_singleton.Get(); +} + +} // namespace net diff --git a/chromium/net/proxy/dhcpcsvc_init_win.h b/chromium/net/proxy/dhcpcsvc_init_win.h new file mode 100644 index 00000000000..39bdf24c10c --- /dev/null +++ b/chromium/net/proxy/dhcpcsvc_init_win.h @@ -0,0 +1,19 @@ +// 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. +#ifndef NET_PROXY_DHCPCSVC_INIT_WIN_H +#define NET_PROXY_DHCPCSVC_INIT_WIN_H + +namespace net { + +// Initialization of the Dhcpcsvc library must happen before any of its +// calls are made. This function will make sure that the appropriate +// initialization has been done, and that uninitialization is also +// performed at static uninitialization time. +// +// Note: This initializes only for DHCP, not DHCPv6. +void EnsureDhcpcsvcInit(); + +} // namespace net + +#endif // NET_PROXY_DHCPCSVC_INIT_WIN_H diff --git a/chromium/net/proxy/mock_proxy_resolver.cc b/chromium/net/proxy/mock_proxy_resolver.cc new file mode 100644 index 00000000000..fb4a914ccbd --- /dev/null +++ b/chromium/net/proxy/mock_proxy_resolver.cc @@ -0,0 +1,115 @@ +// 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/proxy/mock_proxy_resolver.h" + +#include "base/logging.h" +#include "base/message_loop/message_loop.h" + +namespace net { + +MockAsyncProxyResolverBase::Request::Request( + MockAsyncProxyResolverBase* resolver, + const GURL& url, + ProxyInfo* results, + const CompletionCallback& callback) + : resolver_(resolver), + url_(url), + results_(results), + callback_(callback), + origin_loop_(base::MessageLoop::current()) {} + + void MockAsyncProxyResolverBase::Request::CompleteNow(int rv) { + CompletionCallback callback = callback_; + + // May delete |this|. + resolver_->RemovePendingRequest(this); + + callback.Run(rv); + } + +MockAsyncProxyResolverBase::Request::~Request() {} + +MockAsyncProxyResolverBase::SetPacScriptRequest::SetPacScriptRequest( + MockAsyncProxyResolverBase* resolver, + const scoped_refptr<ProxyResolverScriptData>& script_data, + const CompletionCallback& callback) + : resolver_(resolver), + script_data_(script_data), + callback_(callback), + origin_loop_(base::MessageLoop::current()) {} + +MockAsyncProxyResolverBase::SetPacScriptRequest::~SetPacScriptRequest() {} + + void MockAsyncProxyResolverBase::SetPacScriptRequest::CompleteNow(int rv) { + CompletionCallback callback = callback_; + + // Will delete |this|. + resolver_->RemovePendingSetPacScriptRequest(this); + + callback.Run(rv); + } + +MockAsyncProxyResolverBase::~MockAsyncProxyResolverBase() {} + +int MockAsyncProxyResolverBase::GetProxyForURL( + const GURL& url, ProxyInfo* results, const CompletionCallback& callback, + RequestHandle* request_handle, const BoundNetLog& /*net_log*/) { + scoped_refptr<Request> request = new Request(this, url, results, callback); + pending_requests_.push_back(request); + + if (request_handle) + *request_handle = reinterpret_cast<RequestHandle>(request.get()); + + // Test code completes the request by calling request->CompleteNow(). + return ERR_IO_PENDING; +} + +void MockAsyncProxyResolverBase::CancelRequest(RequestHandle request_handle) { + scoped_refptr<Request> request = reinterpret_cast<Request*>(request_handle); + cancelled_requests_.push_back(request); + RemovePendingRequest(request.get()); +} + +LoadState MockAsyncProxyResolverBase::GetLoadState( + RequestHandle request_handle) const { + return LOAD_STATE_RESOLVING_PROXY_FOR_URL; +} + +int MockAsyncProxyResolverBase::SetPacScript( + const scoped_refptr<ProxyResolverScriptData>& script_data, + const CompletionCallback& callback) { + DCHECK(!pending_set_pac_script_request_.get()); + pending_set_pac_script_request_.reset( + new SetPacScriptRequest(this, script_data, callback)); + // Finished when user calls SetPacScriptRequest::CompleteNow(). + return ERR_IO_PENDING; +} + +void MockAsyncProxyResolverBase::CancelSetPacScript() { + // Do nothing (caller was responsible for completing the request). +} + +MockAsyncProxyResolverBase::SetPacScriptRequest* +MockAsyncProxyResolverBase::pending_set_pac_script_request() const { + return pending_set_pac_script_request_.get(); +} + +void MockAsyncProxyResolverBase::RemovePendingRequest(Request* request) { + RequestsList::iterator it = std::find( + pending_requests_.begin(), pending_requests_.end(), request); + DCHECK(it != pending_requests_.end()); + pending_requests_.erase(it); +} + +void MockAsyncProxyResolverBase::RemovePendingSetPacScriptRequest( + SetPacScriptRequest* request) { + DCHECK_EQ(request, pending_set_pac_script_request()); + pending_set_pac_script_request_.reset(); +} + +MockAsyncProxyResolverBase::MockAsyncProxyResolverBase(bool expects_pac_bytes) + : ProxyResolver(expects_pac_bytes) {} + +} // namespace net diff --git a/chromium/net/proxy/mock_proxy_resolver.h b/chromium/net/proxy/mock_proxy_resolver.h new file mode 100644 index 00000000000..3864d530de9 --- /dev/null +++ b/chromium/net/proxy/mock_proxy_resolver.h @@ -0,0 +1,129 @@ +// 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. + +#ifndef NET_PROXY_MOCK_PROXY_RESOLVER_H_ +#define NET_PROXY_MOCK_PROXY_RESOLVER_H_ + +#include <vector> + +#include "base/memory/scoped_ptr.h" +#include "net/base/net_errors.h" +#include "net/proxy/proxy_resolver.h" +#include "url/gurl.h" + +namespace base { +class MessageLoop; +} + +namespace net { + +// Asynchronous mock proxy resolver. All requests complete asynchronously, +// user must call Request::CompleteNow() on a pending request to signal it. +class MockAsyncProxyResolverBase : public ProxyResolver { + public: + class Request : public base::RefCounted<Request> { + public: + Request(MockAsyncProxyResolverBase* resolver, + const GURL& url, + ProxyInfo* results, + const net::CompletionCallback& callback); + + const GURL& url() const { return url_; } + ProxyInfo* results() const { return results_; } + const net::CompletionCallback& callback() const { return callback_; } + + void CompleteNow(int rv); + + private: + friend class base::RefCounted<Request>; + + virtual ~Request(); + + MockAsyncProxyResolverBase* resolver_; + const GURL url_; + ProxyInfo* results_; + net::CompletionCallback callback_; + base::MessageLoop* origin_loop_; + }; + + class SetPacScriptRequest { + public: + SetPacScriptRequest( + MockAsyncProxyResolverBase* resolver, + const scoped_refptr<ProxyResolverScriptData>& script_data, + const net::CompletionCallback& callback); + ~SetPacScriptRequest(); + + const ProxyResolverScriptData* script_data() const { + return script_data_.get(); + } + + void CompleteNow(int rv); + + private: + MockAsyncProxyResolverBase* resolver_; + const scoped_refptr<ProxyResolverScriptData> script_data_; + net::CompletionCallback callback_; + base::MessageLoop* origin_loop_; + }; + + typedef std::vector<scoped_refptr<Request> > RequestsList; + + virtual ~MockAsyncProxyResolverBase(); + + // ProxyResolver implementation. + virtual int GetProxyForURL(const GURL& url, + ProxyInfo* results, + const net::CompletionCallback& callback, + RequestHandle* request_handle, + const BoundNetLog& /*net_log*/) OVERRIDE; + virtual void CancelRequest(RequestHandle request_handle) OVERRIDE; + virtual LoadState GetLoadState(RequestHandle request_handle) const OVERRIDE; + virtual int SetPacScript( + const scoped_refptr<ProxyResolverScriptData>& script_data, + const net::CompletionCallback& callback) OVERRIDE; + virtual void CancelSetPacScript() OVERRIDE; + + const RequestsList& pending_requests() const { + return pending_requests_; + } + + const RequestsList& cancelled_requests() const { + return cancelled_requests_; + } + + SetPacScriptRequest* pending_set_pac_script_request() const; + + bool has_pending_set_pac_script_request() const { + return pending_set_pac_script_request_.get() != NULL; + } + + void RemovePendingRequest(Request* request); + + void RemovePendingSetPacScriptRequest(SetPacScriptRequest* request); + + protected: + explicit MockAsyncProxyResolverBase(bool expects_pac_bytes); + + private: + RequestsList pending_requests_; + RequestsList cancelled_requests_; + scoped_ptr<SetPacScriptRequest> pending_set_pac_script_request_; +}; + +class MockAsyncProxyResolver : public MockAsyncProxyResolverBase { + public: + MockAsyncProxyResolver() + : MockAsyncProxyResolverBase(false /*expects_pac_bytes*/) {} +}; + +class MockAsyncProxyResolverExpectsBytes : public MockAsyncProxyResolverBase { + public: + MockAsyncProxyResolverExpectsBytes() + : MockAsyncProxyResolverBase(true /*expects_pac_bytes*/) {} +}; + +} // namespace net + +#endif // NET_PROXY_MOCK_PROXY_RESOLVER_H_ diff --git a/chromium/net/proxy/mock_proxy_script_fetcher.cc b/chromium/net/proxy/mock_proxy_script_fetcher.cc new file mode 100644 index 00000000000..5d66e6c6147 --- /dev/null +++ b/chromium/net/proxy/mock_proxy_script_fetcher.cc @@ -0,0 +1,69 @@ +// 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/proxy/mock_proxy_script_fetcher.h" + +#include "base/logging.h" +#include "base/message_loop/message_loop.h" +#include "base/strings/string16.h" +#include "base/strings/utf_string_conversions.h" +#include "net/base/net_errors.h" + +namespace net { + +MockProxyScriptFetcher::MockProxyScriptFetcher() + : pending_request_text_(NULL), + waiting_for_fetch_(false) { +} + +MockProxyScriptFetcher::~MockProxyScriptFetcher() {} + +// ProxyScriptFetcher implementation. +int MockProxyScriptFetcher::Fetch(const GURL& url, base::string16* text, + const CompletionCallback& callback) { + DCHECK(!has_pending_request()); + + // Save the caller's information, and have them wait. + pending_request_url_ = url; + pending_request_callback_ = callback; + pending_request_text_ = text; + + if (waiting_for_fetch_) + base::MessageLoop::current()->Quit(); + + return ERR_IO_PENDING; +} + +void MockProxyScriptFetcher::NotifyFetchCompletion( + int result, const std::string& ascii_text) { + DCHECK(has_pending_request()); + *pending_request_text_ = ASCIIToUTF16(ascii_text); + CompletionCallback callback = pending_request_callback_; + pending_request_callback_.Reset(); + callback.Run(result); +} + +void MockProxyScriptFetcher::Cancel() { +} + +URLRequestContext* MockProxyScriptFetcher::GetRequestContext() const { + return NULL; +} + +const GURL& MockProxyScriptFetcher::pending_request_url() const { + return pending_request_url_; +} + +bool MockProxyScriptFetcher::has_pending_request() const { + return !pending_request_callback_.is_null(); +} + +void MockProxyScriptFetcher::WaitUntilFetch() { + DCHECK(!has_pending_request()); + waiting_for_fetch_ = true; + base::MessageLoop::current()->Run(); + waiting_for_fetch_ = false; +} + +} // namespace net diff --git a/chromium/net/proxy/mock_proxy_script_fetcher.h b/chromium/net/proxy/mock_proxy_script_fetcher.h new file mode 100644 index 00000000000..81e221f0a9b --- /dev/null +++ b/chromium/net/proxy/mock_proxy_script_fetcher.h @@ -0,0 +1,48 @@ +// 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. + +#ifndef NET_PROXY_MOCK_PROXY_SCRIPT_FETCHER_H_ +#define NET_PROXY_MOCK_PROXY_SCRIPT_FETCHER_H_ + +#include "base/compiler_specific.h" +#include "net/proxy/proxy_script_fetcher.h" +#include "url/gurl.h" + +#include <string> + +namespace net { + +class URLRequestContext; + +// A mock ProxyScriptFetcher. No result will be returned to the fetch client +// until we call NotifyFetchCompletion() to set the results. +class MockProxyScriptFetcher : public ProxyScriptFetcher { + public: + MockProxyScriptFetcher(); + virtual ~MockProxyScriptFetcher(); + + // ProxyScriptFetcher implementation. + virtual int Fetch(const GURL& url, + base::string16* text, + const CompletionCallback& callback) OVERRIDE; + virtual void Cancel() OVERRIDE; + virtual URLRequestContext* GetRequestContext() const OVERRIDE; + + void NotifyFetchCompletion(int result, const std::string& ascii_text); + const GURL& pending_request_url() const; + bool has_pending_request() const; + + // Spins the message loop until this->Fetch() is invoked. + void WaitUntilFetch(); + + private: + GURL pending_request_url_; + CompletionCallback pending_request_callback_; + base::string16* pending_request_text_; + bool waiting_for_fetch_; +}; + +} // namespace net + +#endif // NET_PROXY_MOCK_PROXY_SCRIPT_FETCHER_H_ diff --git a/chromium/net/proxy/multi_threaded_proxy_resolver.cc b/chromium/net/proxy/multi_threaded_proxy_resolver.cc new file mode 100644 index 00000000000..b54cf1fbac7 --- /dev/null +++ b/chromium/net/proxy/multi_threaded_proxy_resolver.cc @@ -0,0 +1,583 @@ +// 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/proxy/multi_threaded_proxy_resolver.h" + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/message_loop/message_loop_proxy.h" +#include "base/metrics/histogram.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/threading/thread.h" +#include "base/threading/thread_restrictions.h" +#include "net/base/net_errors.h" +#include "net/base/net_log.h" +#include "net/proxy/proxy_info.h" + +// TODO(eroman): Have the MultiThreadedProxyResolver clear its PAC script +// data when SetPacScript fails. That will reclaim memory when +// testing bogus scripts. + +namespace net { + +// An "executor" is a job-runner for PAC requests. It encapsulates a worker +// thread and a synchronous ProxyResolver (which will be operated on said +// thread.) +class MultiThreadedProxyResolver::Executor + : public base::RefCountedThreadSafe<MultiThreadedProxyResolver::Executor > { + public: + // |coordinator| must remain valid throughout our lifetime. It is used to + // signal when the executor is ready to receive work by calling + // |coordinator->OnExecutorReady()|. + // The constructor takes ownership of |resolver|. + // |thread_number| is an identifier used when naming the worker thread. + Executor(MultiThreadedProxyResolver* coordinator, + ProxyResolver* resolver, + int thread_number); + + // Submit a job to this executor. + void StartJob(Job* job); + + // Callback for when a job has completed running on the executor's thread. + void OnJobCompleted(Job* job); + + // Cleanup the executor. Cancels all outstanding work, and frees the thread + // and resolver. + void Destroy(); + + void PurgeMemory(); + + // Returns the outstanding job, or NULL. + Job* outstanding_job() const { return outstanding_job_.get(); } + + ProxyResolver* resolver() { return resolver_.get(); } + + int thread_number() const { return thread_number_; } + + private: + friend class base::RefCountedThreadSafe<Executor>; + ~Executor(); + + MultiThreadedProxyResolver* coordinator_; + const int thread_number_; + + // The currently active job for this executor (either a SetPacScript or + // GetProxyForURL task). + scoped_refptr<Job> outstanding_job_; + + // The synchronous resolver implementation. + scoped_ptr<ProxyResolver> resolver_; + + // The thread where |resolver_| is run on. + // Note that declaration ordering is important here. |thread_| needs to be + // destroyed *before* |resolver_|, in case |resolver_| is currently + // executing on |thread_|. + scoped_ptr<base::Thread> thread_; +}; + +// MultiThreadedProxyResolver::Job --------------------------------------------- + +class MultiThreadedProxyResolver::Job + : public base::RefCountedThreadSafe<MultiThreadedProxyResolver::Job> { + public: + // Identifies the subclass of Job (only being used for debugging purposes). + enum Type { + TYPE_GET_PROXY_FOR_URL, + TYPE_SET_PAC_SCRIPT, + TYPE_SET_PAC_SCRIPT_INTERNAL, + }; + + Job(Type type, const CompletionCallback& callback) + : type_(type), + callback_(callback), + executor_(NULL), + was_cancelled_(false) { + } + + void set_executor(Executor* executor) { + executor_ = executor; + } + + // The "executor" is the job runner that is scheduling this job. If + // this job has not been submitted to an executor yet, this will be + // NULL (and we know it hasn't started yet). + Executor* executor() { + return executor_; + } + + // Mark the job as having been cancelled. + void Cancel() { + was_cancelled_ = true; + } + + // Returns true if Cancel() has been called. + bool was_cancelled() const { return was_cancelled_; } + + Type type() const { return type_; } + + // Returns true if this job still has a user callback. Some jobs + // do not have a user callback, because they were helper jobs + // scheduled internally (for example TYPE_SET_PAC_SCRIPT_INTERNAL). + // + // Otherwise jobs that correspond with user-initiated work will + // have a non-null callback up until the callback is run. + bool has_user_callback() const { return !callback_.is_null(); } + + // This method is called when the job is inserted into a wait queue + // because no executors were ready to accept it. + virtual void WaitingForThread() {} + + // This method is called just before the job is posted to the work thread. + virtual void FinishedWaitingForThread() {} + + // This method is called on the worker thread to do the job's work. On + // completion, implementors are expected to call OnJobCompleted() on + // |origin_loop|. + virtual void Run(scoped_refptr<base::MessageLoopProxy> origin_loop) = 0; + + protected: + void OnJobCompleted() { + // |executor_| will be NULL if the executor has already been deleted. + if (executor_) + executor_->OnJobCompleted(this); + } + + void RunUserCallback(int result) { + DCHECK(has_user_callback()); + CompletionCallback callback = callback_; + // Reset the callback so has_user_callback() will now return false. + callback_.Reset(); + callback.Run(result); + } + + friend class base::RefCountedThreadSafe<MultiThreadedProxyResolver::Job>; + + virtual ~Job() {} + + private: + const Type type_; + CompletionCallback callback_; + Executor* executor_; + bool was_cancelled_; +}; + +// MultiThreadedProxyResolver::SetPacScriptJob --------------------------------- + +// Runs on the worker thread to call ProxyResolver::SetPacScript. +class MultiThreadedProxyResolver::SetPacScriptJob + : public MultiThreadedProxyResolver::Job { + public: + SetPacScriptJob(const scoped_refptr<ProxyResolverScriptData>& script_data, + const CompletionCallback& callback) + : Job(!callback.is_null() ? TYPE_SET_PAC_SCRIPT : + TYPE_SET_PAC_SCRIPT_INTERNAL, + callback), + script_data_(script_data) { + } + + // Runs on the worker thread. + virtual void Run(scoped_refptr<base::MessageLoopProxy> origin_loop) OVERRIDE { + ProxyResolver* resolver = executor()->resolver(); + int rv = resolver->SetPacScript(script_data_, CompletionCallback()); + + DCHECK_NE(rv, ERR_IO_PENDING); + origin_loop->PostTask( + FROM_HERE, + base::Bind(&SetPacScriptJob::RequestComplete, this, rv)); + } + + protected: + virtual ~SetPacScriptJob() {} + + private: + // Runs the completion callback on the origin thread. + void RequestComplete(int result_code) { + // The task may have been cancelled after it was started. + if (!was_cancelled() && has_user_callback()) { + RunUserCallback(result_code); + } + OnJobCompleted(); + } + + const scoped_refptr<ProxyResolverScriptData> script_data_; +}; + +// MultiThreadedProxyResolver::GetProxyForURLJob ------------------------------ + +class MultiThreadedProxyResolver::GetProxyForURLJob + : public MultiThreadedProxyResolver::Job { + public: + // |url| -- the URL of the query. + // |results| -- the structure to fill with proxy resolve results. + GetProxyForURLJob(const GURL& url, + ProxyInfo* results, + const CompletionCallback& callback, + const BoundNetLog& net_log) + : Job(TYPE_GET_PROXY_FOR_URL, callback), + results_(results), + net_log_(net_log), + url_(url), + was_waiting_for_thread_(false) { + DCHECK(!callback.is_null()); + start_time_ = base::TimeTicks::Now(); + } + + BoundNetLog* net_log() { return &net_log_; } + + virtual void WaitingForThread() OVERRIDE { + was_waiting_for_thread_ = true; + net_log_.BeginEvent(NetLog::TYPE_WAITING_FOR_PROXY_RESOLVER_THREAD); + } + + virtual void FinishedWaitingForThread() OVERRIDE { + DCHECK(executor()); + + submitted_to_thread_time_ = base::TimeTicks::Now(); + + if (was_waiting_for_thread_) { + net_log_.EndEvent(NetLog::TYPE_WAITING_FOR_PROXY_RESOLVER_THREAD); + } + + net_log_.AddEvent( + NetLog::TYPE_SUBMITTED_TO_RESOLVER_THREAD, + NetLog::IntegerCallback("thread_number", executor()->thread_number())); + } + + // Runs on the worker thread. + virtual void Run(scoped_refptr<base::MessageLoopProxy> origin_loop) OVERRIDE { + ProxyResolver* resolver = executor()->resolver(); + int rv = resolver->GetProxyForURL( + url_, &results_buf_, CompletionCallback(), NULL, net_log_); + DCHECK_NE(rv, ERR_IO_PENDING); + + origin_loop->PostTask( + FROM_HERE, + base::Bind(&GetProxyForURLJob::QueryComplete, this, rv)); + } + + protected: + virtual ~GetProxyForURLJob() {} + + private: + // Runs the completion callback on the origin thread. + void QueryComplete(int result_code) { + // The Job may have been cancelled after it was started. + if (!was_cancelled()) { + RecordPerformanceMetrics(); + if (result_code >= OK) { // Note: unit-tests use values > 0. + results_->Use(results_buf_); + } + RunUserCallback(result_code); + } + OnJobCompleted(); + } + + void RecordPerformanceMetrics() { + DCHECK(!was_cancelled()); + + base::TimeTicks now = base::TimeTicks::Now(); + + // Log the total time the request took to complete. + UMA_HISTOGRAM_MEDIUM_TIMES("Net.MTPR_GetProxyForUrl_Time", + now - start_time_); + + // Log the time the request was stalled waiting for a thread to free up. + UMA_HISTOGRAM_MEDIUM_TIMES("Net.MTPR_GetProxyForUrl_Thread_Wait_Time", + submitted_to_thread_time_ - start_time_); + } + + // Must only be used on the "origin" thread. + ProxyInfo* results_; + + // Can be used on either "origin" or worker thread. + BoundNetLog net_log_; + const GURL url_; + + // Usable from within DoQuery on the worker thread. + ProxyInfo results_buf_; + + base::TimeTicks start_time_; + base::TimeTicks submitted_to_thread_time_; + + bool was_waiting_for_thread_; +}; + +// MultiThreadedProxyResolver::Executor ---------------------------------------- + +MultiThreadedProxyResolver::Executor::Executor( + MultiThreadedProxyResolver* coordinator, + ProxyResolver* resolver, + int thread_number) + : coordinator_(coordinator), + thread_number_(thread_number), + resolver_(resolver) { + DCHECK(coordinator); + DCHECK(resolver); + // Start up the thread. + // Note that it is safe to pass a temporary C-String to Thread(), as it will + // make a copy. + std::string thread_name = + base::StringPrintf("PAC thread #%d", thread_number); + thread_.reset(new base::Thread(thread_name.c_str())); + CHECK(thread_->Start()); +} + +void MultiThreadedProxyResolver::Executor::StartJob(Job* job) { + DCHECK(!outstanding_job_.get()); + outstanding_job_ = job; + + // Run the job. Once it has completed (regardless of whether it was + // cancelled), it will invoke OnJobCompleted() on this thread. + job->set_executor(this); + job->FinishedWaitingForThread(); + thread_->message_loop()->PostTask( + FROM_HERE, + base::Bind(&Job::Run, job, base::MessageLoopProxy::current())); +} + +void MultiThreadedProxyResolver::Executor::OnJobCompleted(Job* job) { + DCHECK_EQ(job, outstanding_job_.get()); + outstanding_job_ = NULL; + coordinator_->OnExecutorReady(this); +} + +void MultiThreadedProxyResolver::Executor::Destroy() { + DCHECK(coordinator_); + + { + // See http://crbug.com/69710. + base::ThreadRestrictions::ScopedAllowIO allow_io; + + // Join the worker thread. + thread_.reset(); + } + + // Cancel any outstanding job. + if (outstanding_job_.get()) { + outstanding_job_->Cancel(); + // Orphan the job (since this executor may be deleted soon). + outstanding_job_->set_executor(NULL); + } + + // It is now safe to free the ProxyResolver, since all the tasks that + // were using it on the resolver thread have completed. + resolver_.reset(); + + // Null some stuff as a precaution. + coordinator_ = NULL; + outstanding_job_ = NULL; +} + +void MultiThreadedProxyResolver::Executor::PurgeMemory() { + thread_->message_loop()->PostTask( + FROM_HERE, + base::Bind(&ProxyResolver::PurgeMemory, + base::Unretained(resolver_.get()))); +} + +MultiThreadedProxyResolver::Executor::~Executor() { + // The important cleanup happens as part of Destroy(), which should always be + // called first. + DCHECK(!coordinator_) << "Destroy() was not called"; + DCHECK(!thread_.get()); + DCHECK(!resolver_.get()); + DCHECK(!outstanding_job_.get()); +} + +// MultiThreadedProxyResolver -------------------------------------------------- + +MultiThreadedProxyResolver::MultiThreadedProxyResolver( + ProxyResolverFactory* resolver_factory, + size_t max_num_threads) + : ProxyResolver(resolver_factory->resolvers_expect_pac_bytes()), + resolver_factory_(resolver_factory), + max_num_threads_(max_num_threads) { + DCHECK_GE(max_num_threads, 1u); +} + +MultiThreadedProxyResolver::~MultiThreadedProxyResolver() { + // We will cancel all outstanding requests. + pending_jobs_.clear(); + ReleaseAllExecutors(); +} + +int MultiThreadedProxyResolver::GetProxyForURL( + const GURL& url, ProxyInfo* results, const CompletionCallback& callback, + RequestHandle* request, const BoundNetLog& net_log) { + DCHECK(CalledOnValidThread()); + DCHECK(!callback.is_null()); + DCHECK(current_script_data_.get()) + << "Resolver is un-initialized. Must call SetPacScript() first!"; + + scoped_refptr<GetProxyForURLJob> job( + new GetProxyForURLJob(url, results, callback, net_log)); + + // Completion will be notified through |callback|, unless the caller cancels + // the request using |request|. + if (request) + *request = reinterpret_cast<RequestHandle>(job.get()); + + // If there is an executor that is ready to run this request, submit it! + Executor* executor = FindIdleExecutor(); + if (executor) { + DCHECK_EQ(0u, pending_jobs_.size()); + executor->StartJob(job.get()); + return ERR_IO_PENDING; + } + + // Otherwise queue this request. (We will schedule it to a thread once one + // becomes available). + job->WaitingForThread(); + pending_jobs_.push_back(job); + + // If we haven't already reached the thread limit, provision a new thread to + // drain the requests more quickly. + if (executors_.size() < max_num_threads_) { + executor = AddNewExecutor(); + executor->StartJob( + new SetPacScriptJob(current_script_data_, CompletionCallback())); + } + + return ERR_IO_PENDING; +} + +void MultiThreadedProxyResolver::CancelRequest(RequestHandle req) { + DCHECK(CalledOnValidThread()); + DCHECK(req); + + Job* job = reinterpret_cast<Job*>(req); + DCHECK_EQ(Job::TYPE_GET_PROXY_FOR_URL, job->type()); + + if (job->executor()) { + // If the job was already submitted to the executor, just mark it + // as cancelled so the user callback isn't run on completion. + job->Cancel(); + } else { + // Otherwise the job is just sitting in a queue. + PendingJobsQueue::iterator it = + std::find(pending_jobs_.begin(), pending_jobs_.end(), job); + DCHECK(it != pending_jobs_.end()); + pending_jobs_.erase(it); + } +} + +LoadState MultiThreadedProxyResolver::GetLoadState(RequestHandle req) const { + DCHECK(CalledOnValidThread()); + DCHECK(req); + return LOAD_STATE_RESOLVING_PROXY_FOR_URL; +} + +void MultiThreadedProxyResolver::CancelSetPacScript() { + DCHECK(CalledOnValidThread()); + DCHECK_EQ(0u, pending_jobs_.size()); + DCHECK_EQ(1u, executors_.size()); + DCHECK_EQ(Job::TYPE_SET_PAC_SCRIPT, + executors_[0]->outstanding_job()->type()); + + // Defensively clear some data which shouldn't be getting used + // anymore. + current_script_data_ = NULL; + + ReleaseAllExecutors(); +} + +void MultiThreadedProxyResolver::PurgeMemory() { + DCHECK(CalledOnValidThread()); + for (ExecutorList::iterator it = executors_.begin(); + it != executors_.end(); ++it) { + Executor* executor = it->get(); + executor->PurgeMemory(); + } +} + +int MultiThreadedProxyResolver::SetPacScript( + const scoped_refptr<ProxyResolverScriptData>& script_data, + const CompletionCallback&callback) { + DCHECK(CalledOnValidThread()); + DCHECK(!callback.is_null()); + + // Save the script details, so we can provision new executors later. + current_script_data_ = script_data; + + // The user should not have any outstanding requests when they call + // SetPacScript(). + CheckNoOutstandingUserRequests(); + + // Destroy all of the current threads and their proxy resolvers. + ReleaseAllExecutors(); + + // Provision a new executor, and run the SetPacScript request. On completion + // notification will be sent through |callback|. + Executor* executor = AddNewExecutor(); + executor->StartJob(new SetPacScriptJob(script_data, callback)); + return ERR_IO_PENDING; +} + +void MultiThreadedProxyResolver::CheckNoOutstandingUserRequests() const { + DCHECK(CalledOnValidThread()); + CHECK_EQ(0u, pending_jobs_.size()); + + for (ExecutorList::const_iterator it = executors_.begin(); + it != executors_.end(); ++it) { + const Executor* executor = it->get(); + Job* job = executor->outstanding_job(); + // The "has_user_callback()" is to exclude jobs for which the callback + // has already been invoked, or was not user-initiated (as in the case of + // lazy thread provisions). User-initiated jobs may !has_user_callback() + // when the callback has already been run. (Since we only clear the + // outstanding job AFTER the callback has been invoked, it is possible + // for a new request to be started from within the callback). + CHECK(!job || job->was_cancelled() || !job->has_user_callback()); + } +} + +void MultiThreadedProxyResolver::ReleaseAllExecutors() { + DCHECK(CalledOnValidThread()); + for (ExecutorList::iterator it = executors_.begin(); + it != executors_.end(); ++it) { + Executor* executor = it->get(); + executor->Destroy(); + } + executors_.clear(); +} + +MultiThreadedProxyResolver::Executor* +MultiThreadedProxyResolver::FindIdleExecutor() { + DCHECK(CalledOnValidThread()); + for (ExecutorList::iterator it = executors_.begin(); + it != executors_.end(); ++it) { + Executor* executor = it->get(); + if (!executor->outstanding_job()) + return executor; + } + return NULL; +} + +MultiThreadedProxyResolver::Executor* +MultiThreadedProxyResolver::AddNewExecutor() { + DCHECK(CalledOnValidThread()); + DCHECK_LT(executors_.size(), max_num_threads_); + // The "thread number" is used to give the thread a unique name. + int thread_number = executors_.size(); + ProxyResolver* resolver = resolver_factory_->CreateProxyResolver(); + Executor* executor = new Executor( + this, resolver, thread_number); + executors_.push_back(make_scoped_refptr(executor)); + return executor; +} + +void MultiThreadedProxyResolver::OnExecutorReady(Executor* executor) { + DCHECK(CalledOnValidThread()); + if (pending_jobs_.empty()) + return; + + // Get the next job to process (FIFO). Transfer it from the pending queue + // to the executor. + scoped_refptr<Job> job = pending_jobs_.front(); + pending_jobs_.pop_front(); + executor->StartJob(job.get()); +} + +} // namespace net diff --git a/chromium/net/proxy/multi_threaded_proxy_resolver.h b/chromium/net/proxy/multi_threaded_proxy_resolver.h new file mode 100644 index 00000000000..3076c36a55f --- /dev/null +++ b/chromium/net/proxy/multi_threaded_proxy_resolver.h @@ -0,0 +1,143 @@ +// 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. + +#ifndef NET_PROXY_MULTI_THREADED_PROXY_RESOLVER_H_ +#define NET_PROXY_MULTI_THREADED_PROXY_RESOLVER_H_ + +#include <deque> +#include <vector> + +#include "base/basictypes.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/threading/non_thread_safe.h" +#include "net/base/net_export.h" +#include "net/proxy/proxy_resolver.h" + +namespace base { +class Thread; +} // namespace base + +namespace net { + +// ProxyResolverFactory is an interface for creating ProxyResolver instances. +class ProxyResolverFactory { + public: + explicit ProxyResolverFactory(bool resolvers_expect_pac_bytes) + : resolvers_expect_pac_bytes_(resolvers_expect_pac_bytes) {} + + virtual ~ProxyResolverFactory() {} + + // Creates a new ProxyResolver. The caller is responsible for freeing this + // object. + virtual ProxyResolver* CreateProxyResolver() = 0; + + bool resolvers_expect_pac_bytes() const { + return resolvers_expect_pac_bytes_; + } + + private: + bool resolvers_expect_pac_bytes_; + DISALLOW_COPY_AND_ASSIGN(ProxyResolverFactory); +}; + +// MultiThreadedProxyResolver is a ProxyResolver implementation that runs +// synchronous ProxyResolver implementations on worker threads. +// +// Threads are created lazily on demand, up to a maximum total. The advantage +// of having a pool of threads, is faster performance. In particular, being +// able to keep servicing PAC requests even if one blocks its execution. +// +// During initialization (SetPacScript), a single thread is spun up to test +// the script. If this succeeds, we cache the input script, and will re-use +// this to lazily provision any new threads as needed. +// +// For each new thread that we spawn, a corresponding new ProxyResolver is +// created using ProxyResolverFactory. +// +// Because we are creating multiple ProxyResolver instances, this means we +// are duplicating script contexts for what is ordinarily seen as being a +// single script. This can affect compatibility on some classes of PAC +// script: +// +// (a) Scripts whose initialization has external dependencies on network or +// time may end up successfully initializing on some threads, but not +// others. So depending on what thread services the request, the result +// may jump between several possibilities. +// +// (b) Scripts whose FindProxyForURL() depends on side-effects may now +// work differently. For example, a PAC script which was incrementing +// a global counter and using that to make a decision. In the +// multi-threaded model, each thread may have a different value for this +// counter, so it won't globally be seen as monotonically increasing! +class NET_EXPORT_PRIVATE MultiThreadedProxyResolver + : public ProxyResolver, + NON_EXPORTED_BASE(public base::NonThreadSafe) { + public: + // Creates an asynchronous ProxyResolver that runs requests on up to + // |max_num_threads|. + // + // For each thread that is created, an accompanying synchronous ProxyResolver + // will be provisioned using |resolver_factory|. All methods on these + // ProxyResolvers will be called on the one thread, with the exception of + // ProxyResolver::Shutdown() which will be called from the origin thread + // prior to destruction. + // + // The constructor takes ownership of |resolver_factory|. + MultiThreadedProxyResolver(ProxyResolverFactory* resolver_factory, + size_t max_num_threads); + + virtual ~MultiThreadedProxyResolver(); + + // ProxyResolver implementation: + virtual int GetProxyForURL(const GURL& url, + ProxyInfo* results, + const CompletionCallback& callback, + RequestHandle* request, + const BoundNetLog& net_log) OVERRIDE; + virtual void CancelRequest(RequestHandle request) OVERRIDE; + virtual LoadState GetLoadState(RequestHandle request) const OVERRIDE; + virtual void CancelSetPacScript() OVERRIDE; + virtual void PurgeMemory() OVERRIDE; + virtual int SetPacScript( + const scoped_refptr<ProxyResolverScriptData>& script_data, + const CompletionCallback& callback) OVERRIDE; + + private: + class Executor; + class Job; + class SetPacScriptJob; + class GetProxyForURLJob; + // FIFO queue of pending jobs waiting to be started. + // TODO(eroman): Make this priority queue. + typedef std::deque<scoped_refptr<Job> > PendingJobsQueue; + typedef std::vector<scoped_refptr<Executor> > ExecutorList; + + // Asserts that there are no outstanding user-initiated jobs on any of the + // worker threads. + void CheckNoOutstandingUserRequests() const; + + // Stops and deletes all of the worker threads. + void ReleaseAllExecutors(); + + // Returns an idle worker thread which is ready to receive GetProxyForURL() + // requests. If all threads are occupied, returns NULL. + Executor* FindIdleExecutor(); + + // Creates a new worker thread, and appends it to |executors_|. + Executor* AddNewExecutor(); + + // Starts the next job from |pending_jobs_| if possible. + void OnExecutorReady(Executor* executor); + + const scoped_ptr<ProxyResolverFactory> resolver_factory_; + const size_t max_num_threads_; + PendingJobsQueue pending_jobs_; + ExecutorList executors_; + scoped_refptr<ProxyResolverScriptData> current_script_data_; +}; + +} // namespace net + +#endif // NET_PROXY_MULTI_THREADED_PROXY_RESOLVER_H_ diff --git a/chromium/net/proxy/multi_threaded_proxy_resolver_unittest.cc b/chromium/net/proxy/multi_threaded_proxy_resolver_unittest.cc new file mode 100644 index 00000000000..79c0acc1ded --- /dev/null +++ b/chromium/net/proxy/multi_threaded_proxy_resolver_unittest.cc @@ -0,0 +1,786 @@ +// 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/proxy/multi_threaded_proxy_resolver.h" + +#include "base/message_loop/message_loop.h" +#include "base/stl_util.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "base/synchronization/waitable_event.h" +#include "base/threading/platform_thread.h" +#include "net/base/net_errors.h" +#include "net/base/net_log.h" +#include "net/base/net_log_unittest.h" +#include "net/base/test_completion_callback.h" +#include "net/proxy/proxy_info.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace net { + +namespace { + +// A synchronous mock ProxyResolver implementation, which can be used in +// conjunction with MultiThreadedProxyResolver. +// - returns a single-item proxy list with the query's host. +class MockProxyResolver : public ProxyResolver { + public: + MockProxyResolver() + : ProxyResolver(true /*expects_pac_bytes*/), + wrong_loop_(base::MessageLoop::current()), + request_count_(0), + purge_count_(0) {} + + // ProxyResolver implementation. + virtual int GetProxyForURL(const GURL& query_url, + ProxyInfo* results, + const CompletionCallback& callback, + RequestHandle* request, + const BoundNetLog& net_log) OVERRIDE { + if (resolve_latency_ != base::TimeDelta()) + base::PlatformThread::Sleep(resolve_latency_); + + CheckIsOnWorkerThread(); + + EXPECT_TRUE(callback.is_null()); + EXPECT_TRUE(request == NULL); + + // Write something into |net_log| (doesn't really have any meaning.) + net_log.BeginEvent(NetLog::TYPE_PAC_JAVASCRIPT_ALERT); + + results->UseNamedProxy(query_url.host()); + + // Return a success code which represents the request's order. + return request_count_++; + } + + virtual void CancelRequest(RequestHandle request) OVERRIDE { + NOTREACHED(); + } + + virtual LoadState GetLoadState(RequestHandle request) const OVERRIDE { + NOTREACHED(); + return LOAD_STATE_IDLE; + } + + virtual void CancelSetPacScript() OVERRIDE { + NOTREACHED(); + } + + virtual int SetPacScript( + const scoped_refptr<ProxyResolverScriptData>& script_data, + const CompletionCallback& callback) OVERRIDE { + CheckIsOnWorkerThread(); + last_script_data_ = script_data; + return OK; + } + + virtual void PurgeMemory() OVERRIDE { + CheckIsOnWorkerThread(); + ++purge_count_; + } + + int purge_count() const { return purge_count_; } + int request_count() const { return request_count_; } + + const ProxyResolverScriptData* last_script_data() const { + return last_script_data_.get(); + } + + void SetResolveLatency(base::TimeDelta latency) { + resolve_latency_ = latency; + } + + private: + void CheckIsOnWorkerThread() { + // We should be running on the worker thread -- while we don't know the + // message loop of MultiThreadedProxyResolver's worker thread, we do + // know that it is going to be distinct from the loop running the + // test, so at least make sure it isn't the main loop. + EXPECT_NE(base::MessageLoop::current(), wrong_loop_); + } + + base::MessageLoop* wrong_loop_; + int request_count_; + int purge_count_; + scoped_refptr<ProxyResolverScriptData> last_script_data_; + base::TimeDelta resolve_latency_; +}; + + +// A mock synchronous ProxyResolver which can be set to block upon reaching +// GetProxyForURL(). +// TODO(eroman): WaitUntilBlocked() *must* be called before calling Unblock(), +// otherwise there will be a race on |should_block_| since it is +// read without any synchronization. +class BlockableProxyResolver : public MockProxyResolver { + public: + BlockableProxyResolver() + : should_block_(false), + unblocked_(true, true), + blocked_(true, false) { + } + + void Block() { + should_block_ = true; + unblocked_.Reset(); + } + + void Unblock() { + should_block_ = false; + blocked_.Reset(); + unblocked_.Signal(); + } + + void WaitUntilBlocked() { + blocked_.Wait(); + } + + virtual int GetProxyForURL(const GURL& query_url, + ProxyInfo* results, + const CompletionCallback& callback, + RequestHandle* request, + const BoundNetLog& net_log) OVERRIDE { + if (should_block_) { + blocked_.Signal(); + unblocked_.Wait(); + } + + return MockProxyResolver::GetProxyForURL( + query_url, results, callback, request, net_log); + } + + private: + bool should_block_; + base::WaitableEvent unblocked_; + base::WaitableEvent blocked_; +}; + +// ForwardingProxyResolver forwards all requests to |impl|. +class ForwardingProxyResolver : public ProxyResolver { + public: + explicit ForwardingProxyResolver(ProxyResolver* impl) + : ProxyResolver(impl->expects_pac_bytes()), + impl_(impl) {} + + virtual int GetProxyForURL(const GURL& query_url, + ProxyInfo* results, + const CompletionCallback& callback, + RequestHandle* request, + const BoundNetLog& net_log) OVERRIDE { + return impl_->GetProxyForURL( + query_url, results, callback, request, net_log); + } + + virtual void CancelRequest(RequestHandle request) OVERRIDE { + impl_->CancelRequest(request); + } + + virtual LoadState GetLoadState(RequestHandle request) const OVERRIDE { + NOTREACHED(); + return LOAD_STATE_IDLE; + } + + virtual void CancelSetPacScript() OVERRIDE { + impl_->CancelSetPacScript(); + } + + virtual int SetPacScript( + const scoped_refptr<ProxyResolverScriptData>& script_data, + const CompletionCallback& callback) OVERRIDE { + return impl_->SetPacScript(script_data, callback); + } + + virtual void PurgeMemory() OVERRIDE { + impl_->PurgeMemory(); + } + + private: + ProxyResolver* impl_; +}; + +// This factory returns ProxyResolvers that forward all requests to +// |resolver|. +class ForwardingProxyResolverFactory : public ProxyResolverFactory { + public: + explicit ForwardingProxyResolverFactory(ProxyResolver* resolver) + : ProxyResolverFactory(resolver->expects_pac_bytes()), + resolver_(resolver) {} + + virtual ProxyResolver* CreateProxyResolver() OVERRIDE { + return new ForwardingProxyResolver(resolver_); + } + + private: + ProxyResolver* resolver_; +}; + +// This factory returns new instances of BlockableProxyResolver. +class BlockableProxyResolverFactory : public ProxyResolverFactory { + public: + BlockableProxyResolverFactory() : ProxyResolverFactory(true) {} + + virtual ~BlockableProxyResolverFactory() { + STLDeleteElements(&resolvers_); + } + + virtual ProxyResolver* CreateProxyResolver() OVERRIDE { + BlockableProxyResolver* resolver = new BlockableProxyResolver; + resolvers_.push_back(resolver); + return new ForwardingProxyResolver(resolver); + } + + std::vector<BlockableProxyResolver*> resolvers() { + return resolvers_; + } + + private: + std::vector<BlockableProxyResolver*> resolvers_; +}; + +TEST(MultiThreadedProxyResolverTest, SingleThread_Basic) { + const size_t kNumThreads = 1u; + scoped_ptr<MockProxyResolver> mock(new MockProxyResolver); + MultiThreadedProxyResolver resolver( + new ForwardingProxyResolverFactory(mock.get()), kNumThreads); + + int rv; + + EXPECT_TRUE(resolver.expects_pac_bytes()); + + // Call SetPacScriptByData() -- verify that it reaches the synchronous + // resolver. + TestCompletionCallback set_script_callback; + rv = resolver.SetPacScript( + ProxyResolverScriptData::FromUTF8("pac script bytes"), + set_script_callback.callback()); + EXPECT_EQ(ERR_IO_PENDING, rv); + EXPECT_EQ(OK, set_script_callback.WaitForResult()); + EXPECT_EQ(ASCIIToUTF16("pac script bytes"), + mock->last_script_data()->utf16()); + + // Start request 0. + TestCompletionCallback callback0; + CapturingBoundNetLog log0; + ProxyInfo results0; + rv = resolver.GetProxyForURL(GURL("http://request0"), &results0, + callback0.callback(), NULL, log0.bound()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Wait for request 0 to finish. + rv = callback0.WaitForResult(); + EXPECT_EQ(0, rv); + EXPECT_EQ("PROXY request0:80", results0.ToPacString()); + + // The mock proxy resolver should have written 1 log entry. And + // on completion, this should have been copied into |log0|. + // We also have 1 log entry that was emitted by the + // MultiThreadedProxyResolver. + CapturingNetLog::CapturedEntryList entries0; + log0.GetEntries(&entries0); + + ASSERT_EQ(2u, entries0.size()); + EXPECT_EQ(NetLog::TYPE_SUBMITTED_TO_RESOLVER_THREAD, entries0[0].type); + + // Start 3 more requests (request1 to request3). + + TestCompletionCallback callback1; + ProxyInfo results1; + rv = resolver.GetProxyForURL(GURL("http://request1"), &results1, + callback1.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + TestCompletionCallback callback2; + ProxyInfo results2; + rv = resolver.GetProxyForURL(GURL("http://request2"), &results2, + callback2.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + TestCompletionCallback callback3; + ProxyInfo results3; + rv = resolver.GetProxyForURL(GURL("http://request3"), &results3, + callback3.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Wait for the requests to finish (they must finish in the order they were + // started, which is what we check for from their magic return value) + + rv = callback1.WaitForResult(); + EXPECT_EQ(1, rv); + EXPECT_EQ("PROXY request1:80", results1.ToPacString()); + + rv = callback2.WaitForResult(); + EXPECT_EQ(2, rv); + EXPECT_EQ("PROXY request2:80", results2.ToPacString()); + + rv = callback3.WaitForResult(); + EXPECT_EQ(3, rv); + EXPECT_EQ("PROXY request3:80", results3.ToPacString()); + + // Ensure that PurgeMemory() reaches the wrapped resolver and happens on the + // right thread. + EXPECT_EQ(0, mock->purge_count()); + resolver.PurgeMemory(); + // There is no way to get a callback directly when PurgeMemory() completes, so + // we queue up a dummy request after the PurgeMemory() call and wait until it + // finishes to ensure PurgeMemory() has had a chance to run. + TestCompletionCallback dummy_callback; + rv = resolver.SetPacScript(ProxyResolverScriptData::FromUTF8("dummy"), + dummy_callback.callback()); + EXPECT_EQ(OK, dummy_callback.WaitForResult()); + EXPECT_EQ(1, mock->purge_count()); +} + +// Tests that the NetLog is updated to include the time the request was waiting +// to be scheduled to a thread. +TEST(MultiThreadedProxyResolverTest, + SingleThread_UpdatesNetLogWithThreadWait) { + const size_t kNumThreads = 1u; + scoped_ptr<BlockableProxyResolver> mock(new BlockableProxyResolver); + MultiThreadedProxyResolver resolver( + new ForwardingProxyResolverFactory(mock.get()), kNumThreads); + + int rv; + + // Initialize the resolver. + TestCompletionCallback init_callback; + rv = resolver.SetPacScript(ProxyResolverScriptData::FromUTF8("foo"), + init_callback.callback()); + EXPECT_EQ(OK, init_callback.WaitForResult()); + + // Block the proxy resolver, so no request can complete. + mock->Block(); + + // Start request 0. + ProxyResolver::RequestHandle request0; + TestCompletionCallback callback0; + ProxyInfo results0; + CapturingBoundNetLog log0; + rv = resolver.GetProxyForURL(GURL("http://request0"), &results0, + callback0.callback(), &request0, log0.bound()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Start 2 more requests (request1 and request2). + + TestCompletionCallback callback1; + ProxyInfo results1; + CapturingBoundNetLog log1; + rv = resolver.GetProxyForURL(GURL("http://request1"), &results1, + callback1.callback(), NULL, log1.bound()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + ProxyResolver::RequestHandle request2; + TestCompletionCallback callback2; + ProxyInfo results2; + CapturingBoundNetLog log2; + rv = resolver.GetProxyForURL(GURL("http://request2"), &results2, + callback2.callback(), &request2, log2.bound()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Unblock the worker thread so the requests can continue running. + mock->WaitUntilBlocked(); + mock->Unblock(); + + // Check that request 0 completed as expected. + // The NetLog has 1 entry that came from the MultiThreadedProxyResolver, and + // 1 entry from the mock proxy resolver. + EXPECT_EQ(0, callback0.WaitForResult()); + EXPECT_EQ("PROXY request0:80", results0.ToPacString()); + + CapturingNetLog::CapturedEntryList entries0; + log0.GetEntries(&entries0); + + ASSERT_EQ(2u, entries0.size()); + EXPECT_EQ(NetLog::TYPE_SUBMITTED_TO_RESOLVER_THREAD, + entries0[0].type); + + // Check that request 1 completed as expected. + EXPECT_EQ(1, callback1.WaitForResult()); + EXPECT_EQ("PROXY request1:80", results1.ToPacString()); + + CapturingNetLog::CapturedEntryList entries1; + log1.GetEntries(&entries1); + + ASSERT_EQ(4u, entries1.size()); + EXPECT_TRUE(LogContainsBeginEvent( + entries1, 0, + NetLog::TYPE_WAITING_FOR_PROXY_RESOLVER_THREAD)); + EXPECT_TRUE(LogContainsEndEvent( + entries1, 1, + NetLog::TYPE_WAITING_FOR_PROXY_RESOLVER_THREAD)); + + // Check that request 2 completed as expected. + EXPECT_EQ(2, callback2.WaitForResult()); + EXPECT_EQ("PROXY request2:80", results2.ToPacString()); + + CapturingNetLog::CapturedEntryList entries2; + log2.GetEntries(&entries2); + + ASSERT_EQ(4u, entries2.size()); + EXPECT_TRUE(LogContainsBeginEvent( + entries2, 0, + NetLog::TYPE_WAITING_FOR_PROXY_RESOLVER_THREAD)); + EXPECT_TRUE(LogContainsEndEvent( + entries2, 1, + NetLog::TYPE_WAITING_FOR_PROXY_RESOLVER_THREAD)); +} + +// Cancel a request which is in progress, and then cancel a request which +// is pending. +TEST(MultiThreadedProxyResolverTest, SingleThread_CancelRequest) { + const size_t kNumThreads = 1u; + scoped_ptr<BlockableProxyResolver> mock(new BlockableProxyResolver); + MultiThreadedProxyResolver resolver( + new ForwardingProxyResolverFactory(mock.get()), + kNumThreads); + + int rv; + + // Initialize the resolver. + TestCompletionCallback init_callback; + rv = resolver.SetPacScript(ProxyResolverScriptData::FromUTF8("foo"), + init_callback.callback()); + EXPECT_EQ(OK, init_callback.WaitForResult()); + + // Block the proxy resolver, so no request can complete. + mock->Block(); + + // Start request 0. + ProxyResolver::RequestHandle request0; + TestCompletionCallback callback0; + ProxyInfo results0; + rv = resolver.GetProxyForURL(GURL("http://request0"), &results0, + callback0.callback(), &request0, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Wait until requests 0 reaches the worker thread. + mock->WaitUntilBlocked(); + + // Start 3 more requests (request1 : request3). + + TestCompletionCallback callback1; + ProxyInfo results1; + rv = resolver.GetProxyForURL(GURL("http://request1"), &results1, + callback1.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + ProxyResolver::RequestHandle request2; + TestCompletionCallback callback2; + ProxyInfo results2; + rv = resolver.GetProxyForURL(GURL("http://request2"), &results2, + callback2.callback(), &request2, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + TestCompletionCallback callback3; + ProxyInfo results3; + rv = resolver.GetProxyForURL(GURL("http://request3"), &results3, + callback3.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Cancel request0 (inprogress) and request2 (pending). + resolver.CancelRequest(request0); + resolver.CancelRequest(request2); + + // Unblock the worker thread so the requests can continue running. + mock->Unblock(); + + // Wait for requests 1 and 3 to finish. + + rv = callback1.WaitForResult(); + EXPECT_EQ(1, rv); + EXPECT_EQ("PROXY request1:80", results1.ToPacString()); + + rv = callback3.WaitForResult(); + // Note that since request2 was cancelled before reaching the resolver, + // the request count is 2 and not 3 here. + EXPECT_EQ(2, rv); + EXPECT_EQ("PROXY request3:80", results3.ToPacString()); + + // Requests 0 and 2 which were cancelled, hence their completion callbacks + // were never summoned. + EXPECT_FALSE(callback0.have_result()); + EXPECT_FALSE(callback2.have_result()); +} + +// Test that deleting MultiThreadedProxyResolver while requests are +// outstanding cancels them (and doesn't leak anything). +TEST(MultiThreadedProxyResolverTest, SingleThread_CancelRequestByDeleting) { + const size_t kNumThreads = 1u; + scoped_ptr<BlockableProxyResolver> mock(new BlockableProxyResolver); + scoped_ptr<MultiThreadedProxyResolver> resolver( + new MultiThreadedProxyResolver( + new ForwardingProxyResolverFactory(mock.get()), kNumThreads)); + + int rv; + + // Initialize the resolver. + TestCompletionCallback init_callback; + rv = resolver->SetPacScript(ProxyResolverScriptData::FromUTF8("foo"), + init_callback.callback()); + EXPECT_EQ(OK, init_callback.WaitForResult()); + + // Block the proxy resolver, so no request can complete. + mock->Block(); + + // Start 3 requests. + + TestCompletionCallback callback0; + ProxyInfo results0; + rv = resolver->GetProxyForURL(GURL("http://request0"), &results0, + callback0.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + TestCompletionCallback callback1; + ProxyInfo results1; + rv = resolver->GetProxyForURL(GURL("http://request1"), &results1, + callback1.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + TestCompletionCallback callback2; + ProxyInfo results2; + rv = resolver->GetProxyForURL(GURL("http://request2"), &results2, + callback2.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Wait until request 0 reaches the worker thread. + mock->WaitUntilBlocked(); + + // Add some latency, to improve the chance that when + // MultiThreadedProxyResolver is deleted below we are still running inside + // of the worker thread. The test will pass regardless, so this race doesn't + // cause flakiness. However the destruction during execution is a more + // interesting case to test. + mock->SetResolveLatency(base::TimeDelta::FromMilliseconds(100)); + + // Unblock the worker thread and delete the underlying + // MultiThreadedProxyResolver immediately. + mock->Unblock(); + resolver.reset(); + + // Give any posted tasks a chance to run (in case there is badness). + base::MessageLoop::current()->RunUntilIdle(); + + // Check that none of the outstanding requests were completed. + EXPECT_FALSE(callback0.have_result()); + EXPECT_FALSE(callback1.have_result()); + EXPECT_FALSE(callback2.have_result()); +} + +// Cancel an outstanding call to SetPacScriptByData(). +TEST(MultiThreadedProxyResolverTest, SingleThread_CancelSetPacScript) { + const size_t kNumThreads = 1u; + scoped_ptr<BlockableProxyResolver> mock(new BlockableProxyResolver); + MultiThreadedProxyResolver resolver( + new ForwardingProxyResolverFactory(mock.get()), kNumThreads); + + int rv; + + TestCompletionCallback set_pac_script_callback; + rv = resolver.SetPacScript(ProxyResolverScriptData::FromUTF8("data"), + set_pac_script_callback.callback()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Cancel the SetPacScriptByData request. + resolver.CancelSetPacScript(); + + // Start another SetPacScript request + TestCompletionCallback set_pac_script_callback2; + rv = resolver.SetPacScript(ProxyResolverScriptData::FromUTF8("data2"), + set_pac_script_callback2.callback()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Wait for the initialization to complete. + + rv = set_pac_script_callback2.WaitForResult(); + EXPECT_EQ(0, rv); + EXPECT_EQ(ASCIIToUTF16("data2"), mock->last_script_data()->utf16()); + + // The first SetPacScript callback should never have been completed. + EXPECT_FALSE(set_pac_script_callback.have_result()); +} + +// Tests setting the PAC script once, lazily creating new threads, and +// cancelling requests. +TEST(MultiThreadedProxyResolverTest, ThreeThreads_Basic) { + const size_t kNumThreads = 3u; + BlockableProxyResolverFactory* factory = new BlockableProxyResolverFactory; + MultiThreadedProxyResolver resolver(factory, kNumThreads); + + int rv; + + EXPECT_TRUE(resolver.expects_pac_bytes()); + + // Call SetPacScriptByData() -- verify that it reaches the synchronous + // resolver. + TestCompletionCallback set_script_callback; + rv = resolver.SetPacScript( + ProxyResolverScriptData::FromUTF8("pac script bytes"), + set_script_callback.callback()); + EXPECT_EQ(ERR_IO_PENDING, rv); + EXPECT_EQ(OK, set_script_callback.WaitForResult()); + // One thread has been provisioned (i.e. one ProxyResolver was created). + ASSERT_EQ(1u, factory->resolvers().size()); + EXPECT_EQ(ASCIIToUTF16("pac script bytes"), + factory->resolvers()[0]->last_script_data()->utf16()); + + const int kNumRequests = 9; + TestCompletionCallback callback[kNumRequests]; + ProxyInfo results[kNumRequests]; + ProxyResolver::RequestHandle request[kNumRequests]; + + // Start request 0 -- this should run on thread 0 as there is nothing else + // going on right now. + rv = resolver.GetProxyForURL( + GURL("http://request0"), &results[0], callback[0].callback(), &request[0], + BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Wait for request 0 to finish. + rv = callback[0].WaitForResult(); + EXPECT_EQ(0, rv); + EXPECT_EQ("PROXY request0:80", results[0].ToPacString()); + ASSERT_EQ(1u, factory->resolvers().size()); + EXPECT_EQ(1, factory->resolvers()[0]->request_count()); + + base::MessageLoop::current()->RunUntilIdle(); + + // We now start 8 requests in parallel -- this will cause the maximum of + // three threads to be provisioned (an additional two from what we already + // have). + + for (int i = 1; i < kNumRequests; ++i) { + rv = resolver.GetProxyForURL( + GURL(base::StringPrintf("http://request%d", i)), &results[i], + callback[i].callback(), &request[i], BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + } + + // We should now have a total of 3 threads, each with its own ProxyResolver + // that will get initialized with the same data. (We check this later since + // the assignment happens on the worker threads and may not have occurred + // yet.) + ASSERT_EQ(3u, factory->resolvers().size()); + + // Cancel 3 of the 8 oustanding requests. + resolver.CancelRequest(request[1]); + resolver.CancelRequest(request[3]); + resolver.CancelRequest(request[6]); + + // Wait for the remaining requests to complete. + int kNonCancelledRequests[] = {2, 4, 5, 7, 8}; + for (size_t i = 0; i < arraysize(kNonCancelledRequests); ++i) { + int request_index = kNonCancelledRequests[i]; + EXPECT_GE(callback[request_index].WaitForResult(), 0); + } + + // Check that the cancelled requests never invoked their callback. + EXPECT_FALSE(callback[1].have_result()); + EXPECT_FALSE(callback[3].have_result()); + EXPECT_FALSE(callback[6].have_result()); + + // We call SetPacScript again, solely to stop the current worker threads. + // (That way we can test to see the values observed by the synchronous + // resolvers in a non-racy manner). + TestCompletionCallback set_script_callback2; + rv = resolver.SetPacScript(ProxyResolverScriptData::FromUTF8("xyz"), + set_script_callback2.callback()); + EXPECT_EQ(ERR_IO_PENDING, rv); + EXPECT_EQ(OK, set_script_callback2.WaitForResult()); + ASSERT_EQ(4u, factory->resolvers().size()); + + for (int i = 0; i < 3; ++i) { + EXPECT_EQ( + ASCIIToUTF16("pac script bytes"), + factory->resolvers()[i]->last_script_data()->utf16()) << "i=" << i; + } + + EXPECT_EQ(ASCIIToUTF16("xyz"), + factory->resolvers()[3]->last_script_data()->utf16()); + + // We don't know the exact ordering that requests ran on threads with, + // but we do know the total count that should have reached the threads. + // 8 total were submitted, and three were cancelled. Of the three that + // were cancelled, one of them (request 1) was cancelled after it had + // already been posted to the worker thread. So the resolvers will + // have seen 6 total (and 1 from the run prior). + ASSERT_EQ(4u, factory->resolvers().size()); + int total_count = 0; + for (int i = 0; i < 3; ++i) { + total_count += factory->resolvers()[i]->request_count(); + } + EXPECT_EQ(7, total_count); +} + +// Tests using two threads. The first request hangs the first thread. Checks +// that other requests are able to complete while this first request remains +// stalled. +TEST(MultiThreadedProxyResolverTest, OneThreadBlocked) { + const size_t kNumThreads = 2u; + BlockableProxyResolverFactory* factory = new BlockableProxyResolverFactory; + MultiThreadedProxyResolver resolver(factory, kNumThreads); + + int rv; + + EXPECT_TRUE(resolver.expects_pac_bytes()); + + // Initialize the resolver. + TestCompletionCallback set_script_callback; + rv = resolver.SetPacScript( + ProxyResolverScriptData::FromUTF8("pac script bytes"), + set_script_callback.callback()); + EXPECT_EQ(ERR_IO_PENDING, rv); + EXPECT_EQ(OK, set_script_callback.WaitForResult()); + // One thread has been provisioned (i.e. one ProxyResolver was created). + ASSERT_EQ(1u, factory->resolvers().size()); + EXPECT_EQ(ASCIIToUTF16("pac script bytes"), + factory->resolvers()[0]->last_script_data()->utf16()); + + const int kNumRequests = 4; + TestCompletionCallback callback[kNumRequests]; + ProxyInfo results[kNumRequests]; + ProxyResolver::RequestHandle request[kNumRequests]; + + // Start a request that will block the first thread. + + factory->resolvers()[0]->Block(); + + rv = resolver.GetProxyForURL( + GURL("http://request0"), &results[0], callback[0].callback(), &request[0], + BoundNetLog()); + + EXPECT_EQ(ERR_IO_PENDING, rv); + factory->resolvers()[0]->WaitUntilBlocked(); + + // Start 3 more requests -- they should all be serviced by thread #2 + // since thread #1 is blocked. + + for (int i = 1; i < kNumRequests; ++i) { + rv = resolver.GetProxyForURL( + GURL(base::StringPrintf("http://request%d", i)), + &results[i], callback[i].callback(), &request[i], BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + } + + // Wait for the three requests to complete (they should complete in FIFO + // order). + for (int i = 1; i < kNumRequests; ++i) { + EXPECT_EQ(i - 1, callback[i].WaitForResult()); + } + + // Unblock the first thread. + factory->resolvers()[0]->Unblock(); + EXPECT_EQ(0, callback[0].WaitForResult()); + + // All in all, the first thread should have seen just 1 request. And the + // second thread 3 requests. + ASSERT_EQ(2u, factory->resolvers().size()); + EXPECT_EQ(1, factory->resolvers()[0]->request_count()); + EXPECT_EQ(3, factory->resolvers()[1]->request_count()); +} + +} // namespace + +} // namespace net diff --git a/chromium/net/proxy/network_delegate_error_observer.cc b/chromium/net/proxy/network_delegate_error_observer.cc new file mode 100644 index 00000000000..8029141a0a4 --- /dev/null +++ b/chromium/net/proxy/network_delegate_error_observer.cc @@ -0,0 +1,82 @@ +// 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/proxy/network_delegate_error_observer.h" + +#include "base/bind.h" +#include "base/location.h" +#include "base/message_loop/message_loop_proxy.h" +#include "net/base/net_errors.h" +#include "net/base/network_delegate.h" + +namespace net { + +// NetworkDelegateErrorObserver::Core ----------------------------------------- + +class NetworkDelegateErrorObserver::Core + : public base::RefCountedThreadSafe<NetworkDelegateErrorObserver::Core> { + public: + Core(NetworkDelegate* network_delegate, base::MessageLoopProxy* origin_loop); + + void NotifyPACScriptError(int line_number, const base::string16& error); + + void Shutdown(); + + private: + friend class base::RefCountedThreadSafe<NetworkDelegateErrorObserver::Core>; + + virtual ~Core(); + + NetworkDelegate* network_delegate_; + scoped_refptr<base::MessageLoopProxy> origin_loop_; + + DISALLOW_COPY_AND_ASSIGN(Core); +}; + +NetworkDelegateErrorObserver::Core::Core(NetworkDelegate* network_delegate, + base::MessageLoopProxy* origin_loop) + : network_delegate_(network_delegate), + origin_loop_(origin_loop) { + DCHECK(origin_loop); +} + +NetworkDelegateErrorObserver::Core::~Core() {} + + +void NetworkDelegateErrorObserver::Core::NotifyPACScriptError( + int line_number, + const base::string16& error) { + if (!origin_loop_->BelongsToCurrentThread()) { + origin_loop_->PostTask( + FROM_HERE, + base::Bind(&Core::NotifyPACScriptError, this, line_number, error)); + return; + } + if (network_delegate_) + network_delegate_->NotifyPACScriptError(line_number, error); +} + +void NetworkDelegateErrorObserver::Core::Shutdown() { + CHECK(origin_loop_->BelongsToCurrentThread()); + network_delegate_ = NULL; +} + +// NetworkDelegateErrorObserver ----------------------------------------------- + +NetworkDelegateErrorObserver::NetworkDelegateErrorObserver( + NetworkDelegate* network_delegate, + base::MessageLoopProxy* origin_loop) + : core_(new Core(network_delegate, origin_loop)) {} + +NetworkDelegateErrorObserver::~NetworkDelegateErrorObserver() { + core_->Shutdown(); +} + +void NetworkDelegateErrorObserver::OnPACScriptError( + int line_number, + const base::string16& error) { + core_->NotifyPACScriptError(line_number, error); +} + +} // namespace net diff --git a/chromium/net/proxy/network_delegate_error_observer.h b/chromium/net/proxy/network_delegate_error_observer.h new file mode 100644 index 00000000000..e4b03aa4437 --- /dev/null +++ b/chromium/net/proxy/network_delegate_error_observer.h @@ -0,0 +1,43 @@ +// 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. + +#ifndef NET_PROXY_NETWORK_DELEGATE_ERROR_OBSERVER_H_ +#define NET_PROXY_NETWORK_DELEGATE_ERROR_OBSERVER_H_ + +#include "base/compiler_specific.h" +#include "base/memory/ref_counted.h" +#include "net/proxy/proxy_resolver_error_observer.h" + +namespace base { +class MessageLoopProxy; +} + +namespace net { + +class NetworkDelegate; + +// An implementation of ProxyResolverErrorObserver that forwards PAC script +// errors to a NetworkDelegate object on the thread it lives on. +class NET_EXPORT_PRIVATE NetworkDelegateErrorObserver + : public ProxyResolverErrorObserver { + public: + NetworkDelegateErrorObserver(NetworkDelegate* network_delegate, + base::MessageLoopProxy* origin_loop); + virtual ~NetworkDelegateErrorObserver(); + + // ProxyResolverErrorObserver implementation. + virtual void OnPACScriptError(int line_number, const base::string16& error) + OVERRIDE; + + private: + class Core; + + scoped_refptr<Core> core_; + + DISALLOW_COPY_AND_ASSIGN(NetworkDelegateErrorObserver); +}; + +} // namespace net + +#endif // NET_PROXY_NETWORK_DELEGATE_ERROR_OBSERVER_H_ diff --git a/chromium/net/proxy/network_delegate_error_observer_unittest.cc b/chromium/net/proxy/network_delegate_error_observer_unittest.cc new file mode 100644 index 00000000000..cb32760cafd --- /dev/null +++ b/chromium/net/proxy/network_delegate_error_observer_unittest.cc @@ -0,0 +1,132 @@ +// 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/proxy/network_delegate_error_observer.h" + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/message_loop/message_loop_proxy.h" +#include "base/threading/thread.h" +#include "net/base/net_errors.h" +#include "net/base/network_delegate.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { + +namespace { + +class TestNetworkDelegate : public net::NetworkDelegate { + public: + TestNetworkDelegate() : got_pac_error_(false) {} + virtual ~TestNetworkDelegate() {} + + bool got_pac_error() const { return got_pac_error_; } + + private: + // net::NetworkDelegate implementation. + virtual int OnBeforeURLRequest(URLRequest* request, + const CompletionCallback& callback, + GURL* new_url) OVERRIDE { + return OK; + } + virtual int OnBeforeSendHeaders(URLRequest* request, + const CompletionCallback& callback, + HttpRequestHeaders* headers) OVERRIDE { + return OK; + } + virtual void OnSendHeaders(URLRequest* request, + const HttpRequestHeaders& headers) OVERRIDE {} + virtual int OnHeadersReceived( + URLRequest* request, + const CompletionCallback& callback, + const HttpResponseHeaders* original_response_headers, + scoped_refptr<HttpResponseHeaders>* override_response_headers) OVERRIDE { + return net::OK; + } + virtual void OnBeforeRedirect(URLRequest* request, + const GURL& new_location) OVERRIDE {} + virtual void OnResponseStarted(URLRequest* request) OVERRIDE {} + virtual void OnRawBytesRead(const URLRequest& request, + int bytes_read) OVERRIDE {} + virtual void OnCompleted(URLRequest* request, bool started) OVERRIDE {} + virtual void OnURLRequestDestroyed(URLRequest* request) OVERRIDE {} + + virtual void OnPACScriptError(int line_number, + const base::string16& error) OVERRIDE { + got_pac_error_ = true; + } + virtual AuthRequiredResponse OnAuthRequired( + URLRequest* request, + const AuthChallengeInfo& auth_info, + const AuthCallback& callback, + AuthCredentials* credentials) OVERRIDE { + return AUTH_REQUIRED_RESPONSE_NO_ACTION; + } + virtual bool OnCanGetCookies(const URLRequest& request, + const CookieList& cookie_list) OVERRIDE { + return true; + } + virtual bool OnCanSetCookie(const URLRequest& request, + const std::string& cookie_line, + CookieOptions* options) OVERRIDE { + return true; + } + virtual bool OnCanAccessFile(const net::URLRequest& request, + const base::FilePath& path) const OVERRIDE { + return true; + } + virtual bool OnCanThrottleRequest(const URLRequest& request) const OVERRIDE { + return false; + } + virtual int OnBeforeSocketStreamConnect( + SocketStream* stream, + const CompletionCallback& callback) OVERRIDE { + return OK; + } + virtual void OnRequestWaitStateChange(const net::URLRequest& request, + RequestWaitState state) OVERRIDE { + } + + bool got_pac_error_; +}; + +} // namespace + +// Check that the OnPACScriptError method can be called from an arbitrary +// thread. +TEST(NetworkDelegateErrorObserverTest, CallOnThread) { + base::Thread thread("test_thread"); + thread.Start(); + TestNetworkDelegate network_delegate; + NetworkDelegateErrorObserver observer( + &network_delegate, base::MessageLoopProxy::current().get()); + thread.message_loop() + ->PostTask(FROM_HERE, + base::Bind(&NetworkDelegateErrorObserver::OnPACScriptError, + base::Unretained(&observer), + 42, + base::string16())); + thread.Stop(); + base::MessageLoop::current()->RunUntilIdle(); + ASSERT_TRUE(network_delegate.got_pac_error()); +} + +// Check that passing a NULL network delegate works. +TEST(NetworkDelegateErrorObserverTest, NoDelegate) { + base::Thread thread("test_thread"); + thread.Start(); + NetworkDelegateErrorObserver observer( + NULL, base::MessageLoopProxy::current().get()); + thread.message_loop() + ->PostTask(FROM_HERE, + base::Bind(&NetworkDelegateErrorObserver::OnPACScriptError, + base::Unretained(&observer), + 42, + base::string16())); + thread.Stop(); + base::MessageLoop::current()->RunUntilIdle(); + // Shouldn't have crashed until here... +} + +} // namespace net diff --git a/chromium/net/proxy/polling_proxy_config_service.cc b/chromium/net/proxy/polling_proxy_config_service.cc new file mode 100644 index 00000000000..611ea5975c0 --- /dev/null +++ b/chromium/net/proxy/polling_proxy_config_service.cc @@ -0,0 +1,194 @@ +// 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/proxy/polling_proxy_config_service.h" + +#include "base/bind.h" +#include "base/location.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop/message_loop_proxy.h" +#include "base/observer_list.h" +#include "base/synchronization/lock.h" +#include "base/threading/worker_pool.h" +#include "net/proxy/proxy_config.h" + +namespace net { + +// Reference-counted wrapper that does all the work (needs to be +// reference-counted since we post tasks between threads; may outlive +// the parent PollingProxyConfigService). +class PollingProxyConfigService::Core + : public base::RefCountedThreadSafe<PollingProxyConfigService::Core> { + public: + Core(base::TimeDelta poll_interval, + GetConfigFunction get_config_func) + : get_config_func_(get_config_func), + poll_interval_(poll_interval), + have_initialized_origin_loop_(false), + has_config_(false), + poll_task_outstanding_(false), + poll_task_queued_(false) { + } + + // Called when the parent PollingProxyConfigService is destroyed + // (observers should not be called past this point). + void Orphan() { + base::AutoLock l(lock_); + origin_loop_proxy_ = NULL; + } + + bool GetLatestProxyConfig(ProxyConfig* config) { + LazyInitializeOriginLoop(); + DCHECK(origin_loop_proxy_->BelongsToCurrentThread()); + + OnLazyPoll(); + + // If we have already retrieved the proxy settings (on worker thread) + // then return what we last saw. + if (has_config_) { + *config = last_config_; + return true; + } + return false; + } + + void AddObserver(Observer* observer) { + LazyInitializeOriginLoop(); + DCHECK(origin_loop_proxy_->BelongsToCurrentThread()); + observers_.AddObserver(observer); + } + + void RemoveObserver(Observer* observer) { + DCHECK(origin_loop_proxy_->BelongsToCurrentThread()); + observers_.RemoveObserver(observer); + } + + // Check for a new configuration if enough time has elapsed. + void OnLazyPoll() { + LazyInitializeOriginLoop(); + DCHECK(origin_loop_proxy_->BelongsToCurrentThread()); + + if (last_poll_time_.is_null() || + (base::TimeTicks::Now() - last_poll_time_) > poll_interval_) { + CheckForChangesNow(); + } + } + + void CheckForChangesNow() { + LazyInitializeOriginLoop(); + DCHECK(origin_loop_proxy_->BelongsToCurrentThread()); + + if (poll_task_outstanding_) { + // Only allow one task to be outstanding at a time. If we get a poll + // request while we are busy, we will defer it until the current poll + // completes. + poll_task_queued_ = true; + return; + } + + last_poll_time_ = base::TimeTicks::Now(); + poll_task_outstanding_ = true; + poll_task_queued_ = false; + base::WorkerPool::PostTask( + FROM_HERE, + base::Bind(&Core::PollOnWorkerThread, this, get_config_func_), + true); + } + + private: + friend class base::RefCountedThreadSafe<Core>; + ~Core() {} + + void PollOnWorkerThread(GetConfigFunction func) { + ProxyConfig config; + func(&config); + + base::AutoLock l(lock_); + if (origin_loop_proxy_.get()) { + origin_loop_proxy_->PostTask( + FROM_HERE, base::Bind(&Core::GetConfigCompleted, this, config)); + } + } + + // Called after the worker thread has finished retrieving a configuration. + void GetConfigCompleted(const ProxyConfig& config) { + DCHECK(poll_task_outstanding_); + poll_task_outstanding_ = false; + + if (!origin_loop_proxy_.get()) + return; // Was orphaned (parent has already been destroyed). + + DCHECK(origin_loop_proxy_->BelongsToCurrentThread()); + + if (!has_config_ || !last_config_.Equals(config)) { + // If the configuration has changed, notify the observers. + has_config_ = true; + last_config_ = config; + FOR_EACH_OBSERVER(Observer, observers_, + OnProxyConfigChanged(config, + ProxyConfigService::CONFIG_VALID)); + } + + if (poll_task_queued_) + CheckForChangesNow(); + } + + void LazyInitializeOriginLoop() { + // TODO(eroman): Really this should be done in the constructor, but right + // now chrome is constructing the ProxyConfigService on the + // UI thread so we can't cache the IO thread for the purpose + // of DCHECKs until the first call is made. + if (!have_initialized_origin_loop_) { + origin_loop_proxy_ = base::MessageLoopProxy::current(); + have_initialized_origin_loop_ = true; + } + } + + GetConfigFunction get_config_func_; + ObserverList<Observer> observers_; + ProxyConfig last_config_; + base::TimeTicks last_poll_time_; + base::TimeDelta poll_interval_; + + base::Lock lock_; + scoped_refptr<base::MessageLoopProxy> origin_loop_proxy_; + + bool have_initialized_origin_loop_; + bool has_config_; + bool poll_task_outstanding_; + bool poll_task_queued_; +}; + +void PollingProxyConfigService::AddObserver(Observer* observer) { + core_->AddObserver(observer); +} + +void PollingProxyConfigService::RemoveObserver(Observer* observer) { + core_->RemoveObserver(observer); +} + +ProxyConfigService::ConfigAvailability + PollingProxyConfigService::GetLatestProxyConfig(ProxyConfig* config) { + return core_->GetLatestProxyConfig(config) ? CONFIG_VALID : CONFIG_PENDING; +} + +void PollingProxyConfigService::OnLazyPoll() { + core_->OnLazyPoll(); +} + +PollingProxyConfigService::PollingProxyConfigService( + base::TimeDelta poll_interval, + GetConfigFunction get_config_func) + : core_(new Core(poll_interval, get_config_func)) { +} + +PollingProxyConfigService::~PollingProxyConfigService() { + core_->Orphan(); +} + +void PollingProxyConfigService::CheckForChangesNow() { + core_->CheckForChangesNow(); +} + +} // namespace net diff --git a/chromium/net/proxy/polling_proxy_config_service.h b/chromium/net/proxy/polling_proxy_config_service.h new file mode 100644 index 00000000000..f5f5117238a --- /dev/null +++ b/chromium/net/proxy/polling_proxy_config_service.h @@ -0,0 +1,55 @@ +// 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. + +#ifndef NET_PROXY_POLLING_PROXY_CONFIG_SERVICE_H_ +#define NET_PROXY_POLLING_PROXY_CONFIG_SERVICE_H_ + +#include "base/compiler_specific.h" +#include "base/memory/ref_counted.h" +#include "base/time/time.h" +#include "net/proxy/proxy_config_service.h" + +namespace net { + +// PollingProxyConfigService is a base class for creating ProxyConfigService +// implementations that use polling to notice when settings have change. +// +// It runs code to get the current proxy settings on a background worker +// thread, and notifies registered observers when the value changes. +class NET_EXPORT_PRIVATE PollingProxyConfigService : public ProxyConfigService { + public: + // ProxyConfigService implementation: + virtual void AddObserver(Observer* observer) OVERRIDE; + virtual void RemoveObserver(Observer* observer) OVERRIDE; + virtual ConfigAvailability GetLatestProxyConfig(ProxyConfig* config) OVERRIDE; + virtual void OnLazyPoll() OVERRIDE; + + protected: + // Function for retrieving the current proxy configuration. + // Implementors must be threadsafe as the function will be invoked from + // worker threads. + typedef void (*GetConfigFunction)(ProxyConfig*); + + // Creates a polling-based ProxyConfigService which will test for new + // settings at most every |poll_interval| time by calling |get_config_func| + // on a worker thread. + PollingProxyConfigService( + base::TimeDelta poll_interval, + GetConfigFunction get_config_func); + + virtual ~PollingProxyConfigService(); + + // Polls for changes by posting a task to the worker pool. + void CheckForChangesNow(); + + private: + class Core; + scoped_refptr<Core> core_; + + DISALLOW_COPY_AND_ASSIGN(PollingProxyConfigService); +}; + +} // namespace net + +#endif // NET_PROXY_POLLING_PROXY_CONFIG_SERVICE_H_ diff --git a/chromium/net/proxy/proxy_bypass_rules.cc b/chromium/net/proxy/proxy_bypass_rules.cc new file mode 100644 index 00000000000..a60adc352ff --- /dev/null +++ b/chromium/net/proxy/proxy_bypass_rules.cc @@ -0,0 +1,347 @@ +// 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/proxy/proxy_bypass_rules.h" + +#include "base/stl_util.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/strings/string_piece.h" +#include "base/strings/string_tokenizer.h" +#include "net/base/net_util.h" + +namespace net { + +namespace { + +class HostnamePatternRule : public ProxyBypassRules::Rule { + public: + HostnamePatternRule(const std::string& optional_scheme, + const std::string& hostname_pattern, + int optional_port) + : optional_scheme_(StringToLowerASCII(optional_scheme)), + hostname_pattern_(StringToLowerASCII(hostname_pattern)), + optional_port_(optional_port) { + } + + virtual bool Matches(const GURL& url) const OVERRIDE { + if (optional_port_ != -1 && url.EffectiveIntPort() != optional_port_) + return false; // Didn't match port expectation. + + if (!optional_scheme_.empty() && url.scheme() != optional_scheme_) + return false; // Didn't match scheme expectation. + + // Note it is necessary to lower-case the host, since GURL uses capital + // letters for percent-escaped characters. + return MatchPattern(StringToLowerASCII(url.host()), hostname_pattern_); + } + + virtual std::string ToString() const OVERRIDE { + std::string str; + if (!optional_scheme_.empty()) + base::StringAppendF(&str, "%s://", optional_scheme_.c_str()); + str += hostname_pattern_; + if (optional_port_ != -1) + base::StringAppendF(&str, ":%d", optional_port_); + return str; + } + + virtual Rule* Clone() const OVERRIDE { + return new HostnamePatternRule(optional_scheme_, + hostname_pattern_, + optional_port_); + } + + private: + const std::string optional_scheme_; + const std::string hostname_pattern_; + const int optional_port_; +}; + +class BypassLocalRule : public ProxyBypassRules::Rule { + public: + virtual bool Matches(const GURL& url) const OVERRIDE { + const std::string& host = url.host(); + if (host == "127.0.0.1" || host == "[::1]") + return true; + return host.find('.') == std::string::npos; + } + + virtual std::string ToString() const OVERRIDE { + return "<local>"; + } + + virtual Rule* Clone() const OVERRIDE { + return new BypassLocalRule(); + } +}; + +// Rule for matching a URL that is an IP address, if that IP address falls +// within a certain numeric range. For example, you could use this rule to +// match all the IPs in the CIDR block 10.10.3.4/24. +class BypassIPBlockRule : public ProxyBypassRules::Rule { + public: + // |ip_prefix| + |prefix_length| define the IP block to match. + BypassIPBlockRule(const std::string& description, + const std::string& optional_scheme, + const IPAddressNumber& ip_prefix, + size_t prefix_length_in_bits) + : description_(description), + optional_scheme_(optional_scheme), + ip_prefix_(ip_prefix), + prefix_length_in_bits_(prefix_length_in_bits) { + } + + virtual bool Matches(const GURL& url) const OVERRIDE { + if (!url.HostIsIPAddress()) + return false; + + if (!optional_scheme_.empty() && url.scheme() != optional_scheme_) + return false; // Didn't match scheme expectation. + + // Parse the input IP literal to a number. + IPAddressNumber ip_number; + if (!ParseIPLiteralToNumber(url.HostNoBrackets(), &ip_number)) + return false; + + // Test if it has the expected prefix. + return IPNumberMatchesPrefix(ip_number, ip_prefix_, + prefix_length_in_bits_); + } + + virtual std::string ToString() const OVERRIDE { + return description_; + } + + virtual Rule* Clone() const OVERRIDE { + return new BypassIPBlockRule(description_, + optional_scheme_, + ip_prefix_, + prefix_length_in_bits_); + } + + private: + const std::string description_; + const std::string optional_scheme_; + const IPAddressNumber ip_prefix_; + const size_t prefix_length_in_bits_; +}; + +// Returns true if the given string represents an IP address. +bool IsIPAddress(const std::string& domain) { + // From GURL::HostIsIPAddress() + url_canon::RawCanonOutputT<char, 128> ignored_output; + url_canon::CanonHostInfo host_info; + url_parse::Component domain_comp(0, domain.size()); + url_canon::CanonicalizeIPAddress(domain.c_str(), domain_comp, + &ignored_output, &host_info); + return host_info.IsIPAddress(); +} + +} // namespace + +ProxyBypassRules::Rule::Rule() { +} + +ProxyBypassRules::Rule::~Rule() { +} + +bool ProxyBypassRules::Rule::Equals(const Rule& rule) const { + return ToString() == rule.ToString(); +} + +ProxyBypassRules::ProxyBypassRules() { +} + +ProxyBypassRules::ProxyBypassRules(const ProxyBypassRules& rhs) { + AssignFrom(rhs); +} + +ProxyBypassRules::~ProxyBypassRules() { + Clear(); +} + +ProxyBypassRules& ProxyBypassRules::operator=(const ProxyBypassRules& rhs) { + AssignFrom(rhs); + return *this; +} + +bool ProxyBypassRules::Matches(const GURL& url) const { + for (RuleList::const_iterator it = rules_.begin(); it != rules_.end(); ++it) { + if ((*it)->Matches(url)) + return true; + } + return false; +} + +bool ProxyBypassRules::Equals(const ProxyBypassRules& other) const { + if (rules_.size() != other.rules_.size()) + return false; + + for (size_t i = 0; i < rules_.size(); ++i) { + if (!rules_[i]->Equals(*other.rules_[i])) + return false; + } + return true; +} + +void ProxyBypassRules::ParseFromString(const std::string& raw) { + ParseFromStringInternal(raw, false); +} + +void ProxyBypassRules::ParseFromStringUsingSuffixMatching( + const std::string& raw) { + ParseFromStringInternal(raw, true); +} + +bool ProxyBypassRules::AddRuleForHostname(const std::string& optional_scheme, + const std::string& hostname_pattern, + int optional_port) { + if (hostname_pattern.empty()) + return false; + + rules_.push_back(new HostnamePatternRule(optional_scheme, + hostname_pattern, + optional_port)); + return true; +} + +void ProxyBypassRules::AddRuleToBypassLocal() { + rules_.push_back(new BypassLocalRule); +} + +bool ProxyBypassRules::AddRuleFromString(const std::string& raw) { + return AddRuleFromStringInternalWithLogging(raw, false); +} + +bool ProxyBypassRules::AddRuleFromStringUsingSuffixMatching( + const std::string& raw) { + return AddRuleFromStringInternalWithLogging(raw, true); +} + +std::string ProxyBypassRules::ToString() const { + std::string result; + for (RuleList::const_iterator rule(rules_.begin()); + rule != rules_.end(); + ++rule) { + result += (*rule)->ToString(); + result += ";"; + } + return result; +} + +void ProxyBypassRules::Clear() { + STLDeleteElements(&rules_); +} + +void ProxyBypassRules::AssignFrom(const ProxyBypassRules& other) { + Clear(); + + // Make a copy of the rules list. + for (RuleList::const_iterator it = other.rules_.begin(); + it != other.rules_.end(); ++it) { + rules_.push_back((*it)->Clone()); + } +} + +void ProxyBypassRules::ParseFromStringInternal( + const std::string& raw, + bool use_hostname_suffix_matching) { + Clear(); + + base::StringTokenizer entries(raw, ",;"); + while (entries.GetNext()) { + AddRuleFromStringInternalWithLogging(entries.token(), + use_hostname_suffix_matching); + } +} + +bool ProxyBypassRules::AddRuleFromStringInternal( + const std::string& raw_untrimmed, + bool use_hostname_suffix_matching) { + std::string raw; + TrimWhitespaceASCII(raw_untrimmed, TRIM_ALL, &raw); + + // This is the special syntax used by WinInet's bypass list -- we allow it + // on all platforms and interpret it the same way. + if (LowerCaseEqualsASCII(raw, "<local>")) { + AddRuleToBypassLocal(); + return true; + } + + // Extract any scheme-restriction. + std::string::size_type scheme_pos = raw.find("://"); + std::string scheme; + if (scheme_pos != std::string::npos) { + scheme = raw.substr(0, scheme_pos); + raw = raw.substr(scheme_pos + 3); + if (scheme.empty()) + return false; + } + + if (raw.empty()) + return false; + + // If there is a forward slash in the input, it is probably a CIDR style + // mask. + if (raw.find('/') != std::string::npos) { + IPAddressNumber ip_prefix; + size_t prefix_length_in_bits; + + if (!ParseCIDRBlock(raw, &ip_prefix, &prefix_length_in_bits)) + return false; + + rules_.push_back( + new BypassIPBlockRule(raw, scheme, ip_prefix, prefix_length_in_bits)); + + return true; + } + + // Check if we have an <ip-address>[:port] input. We need to treat this + // separately since the IP literal may not be in a canonical form. + std::string host; + int port; + if (ParseHostAndPort(raw, &host, &port)) { + if (IsIPAddress(host)) { + // Canonicalize the IP literal before adding it as a string pattern. + GURL tmp_url("http://" + host); + return AddRuleForHostname(scheme, tmp_url.host(), port); + } + } + + // Otherwise assume we have <hostname-pattern>[:port]. + std::string::size_type pos_colon = raw.rfind(':'); + host = raw; + port = -1; + if (pos_colon != std::string::npos) { + if (!base::StringToInt(base::StringPiece(raw.begin() + pos_colon + 1, + raw.end()), + &port) || + (port < 0 || port > 0xFFFF)) { + return false; // Port was invalid. + } + raw = raw.substr(0, pos_colon); + } + + // Special-case hostnames that begin with a period. + // For example, we remap ".google.com" --> "*.google.com". + if (StartsWithASCII(raw, ".", false)) + raw = "*" + raw; + + // If suffix matching was asked for, make sure the pattern starts with a + // wildcard. + if (use_hostname_suffix_matching && !StartsWithASCII(raw, "*", false)) + raw = "*" + raw; + + return AddRuleForHostname(scheme, raw, port); +} + +bool ProxyBypassRules::AddRuleFromStringInternalWithLogging( + const std::string& raw, + bool use_hostname_suffix_matching) { + return AddRuleFromStringInternal(raw, use_hostname_suffix_matching); +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_bypass_rules.h b/chromium/net/proxy/proxy_bypass_rules.h new file mode 100644 index 00000000000..6afeeee88c1 --- /dev/null +++ b/chromium/net/proxy/proxy_bypass_rules.h @@ -0,0 +1,182 @@ +// 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. + +#ifndef NET_PROXY_PROXY_BYPASS_RULES_H_ +#define NET_PROXY_PROXY_BYPASS_RULES_H_ + +#include <string> +#include <vector> + +#include "net/base/net_export.h" +#include "url/gurl.h" + +namespace net { + +// ProxyBypassRules describes the set of URLs that should bypass the proxy +// settings, as a list of rules. A URL is said to match the bypass rules +// if it matches any one of these rules. +class NET_EXPORT ProxyBypassRules { + public: + // Interface for an individual proxy bypass rule. + class NET_EXPORT Rule { + public: + Rule(); + virtual ~Rule(); + + // Returns true if |url| matches the rule. + virtual bool Matches(const GURL& url) const = 0; + + // Returns a string representation of this rule. This is used both for + // visualizing the rules, and also to test equality of a rules list. + virtual std::string ToString() const = 0; + + // Creates a copy of this rule. (Caller is responsible for deleting it) + virtual Rule* Clone() const = 0; + + bool Equals(const Rule& rule) const; + + private: + DISALLOW_COPY_AND_ASSIGN(Rule); + }; + + typedef std::vector<const Rule*> RuleList; + + // Note: This class supports copy constructor and assignment. + ProxyBypassRules(); + ProxyBypassRules(const ProxyBypassRules& rhs); + ~ProxyBypassRules(); + ProxyBypassRules& operator=(const ProxyBypassRules& rhs); + + // Returns the current list of rules. The rules list contains pointers + // which are owned by this class, callers should NOT keep references + // or delete them. + const RuleList& rules() const { return rules_; } + + // Returns true if |url| matches any of the proxy bypass rules. + bool Matches(const GURL& url) const; + + // Returns true if |*this| is equal to |other|; in other words, whether they + // describe the same set of rules. + bool Equals(const ProxyBypassRules& other) const; + + // Initializes the list of rules by parsing the string |raw|. |raw| is a + // comma separated list of rules. See AddRuleFromString() to see the list + // of supported formats. + void ParseFromString(const std::string& raw); + + // This is a variant of ParseFromString, which interprets hostname patterns + // as suffix tests rather than hostname tests (so "google.com" would actually + // match "*google.com"). This is only currently used for the linux no_proxy + // evironment variable. It is less flexible, since with the suffix matching + // format you can't match an individual host. + // NOTE: Use ParseFromString() unless you truly need this behavior. + void ParseFromStringUsingSuffixMatching(const std::string& raw); + + // Adds a rule that matches a URL when all of the following are true: + // (a) The URL's scheme matches |optional_scheme|, if + // |!optional_scheme.empty()| + // (b) The URL's hostname matches |hostname_pattern|. + // (c) The URL's (effective) port number matches |optional_port| if + // |optional_port != -1| + // Returns true if the rule was successfully added. + bool AddRuleForHostname(const std::string& optional_scheme, + const std::string& hostname_pattern, + int optional_port); + + // Adds a rule that bypasses all "local" hostnames. + // This matches IE's interpretation of the + // "Bypass proxy server for local addresses" settings checkbox. Fully + // qualified domain names or IP addresses are considered non-local, + // regardless of what they map to (except for the loopback addresses). + void AddRuleToBypassLocal(); + + // Adds a rule given by the string |raw|. The format of |raw| can be any of + // the following: + // + // (1) [ URL_SCHEME "://" ] HOSTNAME_PATTERN [ ":" <port> ] + // + // Match all hostnames that match the pattern HOSTNAME_PATTERN. + // + // Examples: + // "foobar.com", "*foobar.com", "*.foobar.com", "*foobar.com:99", + // "https://x.*.y.com:99" + // + // (2) "." HOSTNAME_SUFFIX_PATTERN [ ":" PORT ] + // + // Match a particular domain suffix. + // + // Examples: + // ".google.com", ".com", "http://.google.com" + // + // (3) [ SCHEME "://" ] IP_LITERAL [ ":" PORT ] + // + // Match URLs which are IP address literals. + // + // Conceptually this is the similar to (1), but with special cases + // to handle IP literal canonicalization. For example matching + // on "[0:0:0::1]" would be the same as matching on "[::1]" since + // the IPv6 canonicalization is done internally. + // + // Examples: + // "127.0.1", "[0:0::1]", "[::1]", "http://[::1]:99" + // + // (4) IP_LITERAL "/" PREFIX_LENGHT_IN_BITS + // + // Match any URL that is to an IP literal that falls between the + // given range. IP range is specified using CIDR notation. + // + // Examples: + // "192.168.1.1/16", "fefe:13::abc/33". + // + // (5) "<local>" + // + // Match local addresses. The meaning of "<local>" is whether the + // host matches one of: "127.0.0.1", "::1", "localhost". + // + // See the unit-tests for more examples. + // + // Returns true if the rule was successfully added. + // + // TODO(eroman): support IPv6 literals without brackets. + // + bool AddRuleFromString(const std::string& raw); + + // This is a variant of AddFromString, which interprets hostname patterns as + // suffix tests rather than hostname tests (so "google.com" would actually + // match "*google.com"). This is used for KDE which interprets every rule as + // a suffix test. It is less flexible, since with the suffix matching format + // you can't match an individual host. + // + // Returns true if the rule was successfully added. + // + // NOTE: Use AddRuleFromString() unless you truly need this behavior. + bool AddRuleFromStringUsingSuffixMatching(const std::string& raw); + + // Converts the rules to string representation. Inverse operation to + // ParseFromString(). + std::string ToString() const; + + // Removes all the rules. + void Clear(); + + // Sets |*this| to |other|. + void AssignFrom(const ProxyBypassRules& other); + + private: + // The following are variants of ParseFromString() and AddRuleFromString(), + // which additionally prefix hostname patterns with a wildcard if + // |use_hostname_suffix_matching| was true. + void ParseFromStringInternal(const std::string& raw, + bool use_hostname_suffix_matching); + bool AddRuleFromStringInternal(const std::string& raw, + bool use_hostname_suffix_matching); + bool AddRuleFromStringInternalWithLogging(const std::string& raw, + bool use_hostname_suffix_matching); + + RuleList rules_; +}; + +} // namespace net + +#endif // NET_PROXY_PROXY_BYPASS_RULES_H_ diff --git a/chromium/net/proxy/proxy_bypass_rules_unittest.cc b/chromium/net/proxy/proxy_bypass_rules_unittest.cc new file mode 100644 index 00000000000..40758fd4ad7 --- /dev/null +++ b/chromium/net/proxy/proxy_bypass_rules_unittest.cc @@ -0,0 +1,315 @@ +// Copyright (c) 2010 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/proxy/proxy_bypass_rules.h" + +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "net/proxy/proxy_config_service_common_unittest.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { + +namespace { + +TEST(ProxyBypassRulesTest, ParseAndMatchBasicHost) { + ProxyBypassRules rules; + rules.ParseFromString("wWw.gOogle.com"); + ASSERT_EQ(1u, rules.rules().size()); + EXPECT_EQ("www.google.com", rules.rules()[0]->ToString()); + + // All of these match; port, scheme, and non-hostname components don't + // matter. + EXPECT_TRUE(rules.Matches(GURL("http://www.google.com"))); + EXPECT_TRUE(rules.Matches(GURL("ftp://www.google.com:99"))); + EXPECT_TRUE(rules.Matches(GURL("https://www.google.com:81"))); + + // Must be a strict host match to work. + EXPECT_FALSE(rules.Matches(GURL("http://foo.www.google.com"))); + EXPECT_FALSE(rules.Matches(GURL("http://xxx.google.com"))); + EXPECT_FALSE(rules.Matches(GURL("http://google.com"))); + EXPECT_FALSE(rules.Matches(GURL("http://www.google.com.baz.org"))); +} + +TEST(ProxyBypassRulesTest, ParseAndMatchBasicDomain) { + ProxyBypassRules rules; + rules.ParseFromString(".gOOgle.com"); + ASSERT_EQ(1u, rules.rules().size()); + // Note that we inferred this was an "ends with" test. + EXPECT_EQ("*.google.com", rules.rules()[0]->ToString()); + + // All of these match; port, scheme, and non-hostname components don't + // matter. + EXPECT_TRUE(rules.Matches(GURL("http://www.google.com"))); + EXPECT_TRUE(rules.Matches(GURL("ftp://www.google.com:99"))); + EXPECT_TRUE(rules.Matches(GURL("https://a.google.com:81"))); + EXPECT_TRUE(rules.Matches(GURL("http://foo.google.com/x/y?q"))); + EXPECT_TRUE(rules.Matches(GURL("http://foo:bar@baz.google.com#x"))); + + // Must be a strict "ends with" to work. + EXPECT_FALSE(rules.Matches(GURL("http://google.com"))); + EXPECT_FALSE(rules.Matches(GURL("http://foo.google.com.baz.org"))); +} + +TEST(ProxyBypassRulesTest, ParseAndMatchBasicDomainWithPort) { + ProxyBypassRules rules; + rules.ParseFromString("*.GOOGLE.com:80"); + ASSERT_EQ(1u, rules.rules().size()); + EXPECT_EQ("*.google.com:80", rules.rules()[0]->ToString()); + + // All of these match; scheme, and non-hostname components don't matter. + EXPECT_TRUE(rules.Matches(GURL("http://www.google.com"))); + EXPECT_TRUE(rules.Matches(GURL("ftp://www.google.com:80"))); + EXPECT_TRUE(rules.Matches(GURL("https://a.google.com:80?x"))); + + // Must be a strict "ends with" to work. + EXPECT_FALSE(rules.Matches(GURL("http://google.com"))); + EXPECT_FALSE(rules.Matches(GURL("http://foo.google.com.baz.org"))); + + // The ports must match. + EXPECT_FALSE(rules.Matches(GURL("http://www.google.com:90"))); + EXPECT_FALSE(rules.Matches(GURL("https://www.google.com"))); +} + +TEST(ProxyBypassRulesTest, MatchAll) { + ProxyBypassRules rules; + rules.ParseFromString("*"); + ASSERT_EQ(1u, rules.rules().size()); + EXPECT_EQ("*", rules.rules()[0]->ToString()); + + EXPECT_TRUE(rules.Matches(GURL("http://www.google.com"))); + EXPECT_TRUE(rules.Matches(GURL("ftp://www.foobar.com:99"))); + EXPECT_TRUE(rules.Matches(GURL("https://a.google.com:80?x"))); +} + +TEST(ProxyBypassRulesTest, WildcardAtStart) { + ProxyBypassRules rules; + rules.ParseFromString("*.org:443"); + ASSERT_EQ(1u, rules.rules().size()); + EXPECT_EQ("*.org:443", rules.rules()[0]->ToString()); + + EXPECT_TRUE(rules.Matches(GURL("http://www.google.org:443"))); + EXPECT_TRUE(rules.Matches(GURL("https://www.google.org"))); + + EXPECT_FALSE(rules.Matches(GURL("http://www.google.org"))); + EXPECT_FALSE(rules.Matches(GURL("https://www.google.com"))); + EXPECT_FALSE(rules.Matches(GURL("https://www.google.org.com"))); +} + +TEST(ProxyBypassRulesTest, IPV4Address) { + ProxyBypassRules rules; + rules.ParseFromString("192.168.1.1"); + ASSERT_EQ(1u, rules.rules().size()); + EXPECT_EQ("192.168.1.1", rules.rules()[0]->ToString()); + + EXPECT_TRUE(rules.Matches(GURL("http://192.168.1.1"))); + EXPECT_TRUE(rules.Matches(GURL("https://192.168.1.1:90"))); + + EXPECT_FALSE(rules.Matches(GURL("http://www.google.com"))); + EXPECT_FALSE(rules.Matches(GURL("http://sup.192.168.1.1"))); +} + +TEST(ProxyBypassRulesTest, IPV4AddressWithPort) { + ProxyBypassRules rules; + rules.ParseFromString("192.168.1.1:33"); + ASSERT_EQ(1u, rules.rules().size()); + EXPECT_EQ("192.168.1.1:33", rules.rules()[0]->ToString()); + + EXPECT_TRUE(rules.Matches(GURL("http://192.168.1.1:33"))); + + EXPECT_FALSE(rules.Matches(GURL("http://www.google.com"))); + EXPECT_FALSE(rules.Matches(GURL("http://192.168.1.1"))); + EXPECT_FALSE(rules.Matches(GURL("http://sup.192.168.1.1:33"))); +} + +TEST(ProxyBypassRulesTest, IPV6Address) { + ProxyBypassRules rules; + rules.ParseFromString("[3ffe:2a00:100:7031:0:0::1]"); + ASSERT_EQ(1u, rules.rules().size()); + // Note that we canonicalized the IP address. + EXPECT_EQ("[3ffe:2a00:100:7031::1]", rules.rules()[0]->ToString()); + + EXPECT_TRUE(rules.Matches(GURL("http://[3ffe:2a00:100:7031::1]"))); + EXPECT_TRUE(rules.Matches(GURL("http://[3ffe:2a00:100:7031::1]:33"))); + + EXPECT_FALSE(rules.Matches(GURL("http://www.google.com"))); + EXPECT_FALSE(rules.Matches(GURL("http://sup.192.168.1.1:33"))); +} + +TEST(ProxyBypassRulesTest, IPV6AddressWithPort) { + ProxyBypassRules rules; + rules.ParseFromString("[3ffe:2a00:100:7031::1]:33"); + ASSERT_EQ(1u, rules.rules().size()); + EXPECT_EQ("[3ffe:2a00:100:7031::1]:33", rules.rules()[0]->ToString()); + + EXPECT_TRUE(rules.Matches(GURL("http://[3ffe:2a00:100:7031::1]:33"))); + + EXPECT_FALSE(rules.Matches(GURL("http://[3ffe:2a00:100:7031::1]"))); + EXPECT_FALSE(rules.Matches(GURL("http://www.google.com"))); +} + +TEST(ProxyBypassRulesTest, HTTPOnly) { + ProxyBypassRules rules; + rules.ParseFromString("http://www.google.com"); + ASSERT_EQ(1u, rules.rules().size()); + EXPECT_EQ("http://www.google.com", rules.rules()[0]->ToString()); + + EXPECT_TRUE(rules.Matches(GURL("http://www.google.com/foo"))); + EXPECT_TRUE(rules.Matches(GURL("http://www.google.com:99"))); + + EXPECT_FALSE(rules.Matches(GURL("https://www.google.com"))); + EXPECT_FALSE(rules.Matches(GURL("ftp://www.google.com"))); + EXPECT_FALSE(rules.Matches(GURL("http://foo.www.google.com"))); + EXPECT_FALSE(rules.Matches(GURL("http://www.google.com.org"))); + EXPECT_FALSE(rules.Matches(GURL("https://www.google.com"))); +} + +TEST(ProxyBypassRulesTest, HTTPOnlyWithWildcard) { + ProxyBypassRules rules; + rules.ParseFromString("http://*www.google.com"); + ASSERT_EQ(1u, rules.rules().size()); + EXPECT_EQ("http://*www.google.com", rules.rules()[0]->ToString()); + + EXPECT_TRUE(rules.Matches(GURL("http://www.google.com/foo"))); + EXPECT_TRUE(rules.Matches(GURL("http://www.google.com:99"))); + EXPECT_TRUE(rules.Matches(GURL("http://foo.www.google.com"))); + + EXPECT_FALSE(rules.Matches(GURL("https://www.google.com"))); + EXPECT_FALSE(rules.Matches(GURL("ftp://www.google.com"))); + EXPECT_FALSE(rules.Matches(GURL("http://www.google.com.org"))); + EXPECT_FALSE(rules.Matches(GURL("https://www.google.com"))); +} + +TEST(ProxyBypassRulesTest, UseSuffixMatching) { + ProxyBypassRules rules; + rules.ParseFromStringUsingSuffixMatching( + "foo1.com, .foo2.com, 192.168.1.1, " + "*foobar.com:80, *.foo, http://baz, <local>"); + ASSERT_EQ(7u, rules.rules().size()); + EXPECT_EQ("*foo1.com", rules.rules()[0]->ToString()); + EXPECT_EQ("*.foo2.com", rules.rules()[1]->ToString()); + EXPECT_EQ("192.168.1.1", rules.rules()[2]->ToString()); + EXPECT_EQ("*foobar.com:80", rules.rules()[3]->ToString()); + EXPECT_EQ("*.foo", rules.rules()[4]->ToString()); + EXPECT_EQ("http://*baz", rules.rules()[5]->ToString()); + EXPECT_EQ("<local>", rules.rules()[6]->ToString()); + + EXPECT_TRUE(rules.Matches(GURL("http://foo1.com"))); + EXPECT_TRUE(rules.Matches(GURL("http://aaafoo1.com"))); + EXPECT_FALSE(rules.Matches(GURL("http://aaafoo1.com.net"))); +} + +TEST(ProxyBypassRulesTest, MultipleRules) { + ProxyBypassRules rules; + rules.ParseFromString(".google.com , .foobar.com:30"); + ASSERT_EQ(2u, rules.rules().size()); + + EXPECT_TRUE(rules.Matches(GURL("http://baz.google.com:40"))); + EXPECT_FALSE(rules.Matches(GURL("http://google.com:40"))); + EXPECT_TRUE(rules.Matches(GURL("http://bar.foobar.com:30"))); + EXPECT_FALSE(rules.Matches(GURL("http://bar.foobar.com"))); + EXPECT_FALSE(rules.Matches(GURL("http://bar.foobar.com:33"))); +} + +TEST(ProxyBypassRulesTest, BadInputs) { + ProxyBypassRules rules; + EXPECT_FALSE(rules.AddRuleFromString("://")); + EXPECT_FALSE(rules.AddRuleFromString(" ")); + EXPECT_FALSE(rules.AddRuleFromString("http://")); + EXPECT_FALSE(rules.AddRuleFromString("*.foo.com:-34")); + EXPECT_EQ(0u, rules.rules().size()); +} + +TEST(ProxyBypassRulesTest, Equals) { + ProxyBypassRules rules1; + ProxyBypassRules rules2; + + rules1.ParseFromString("foo1.com, .foo2.com"); + rules2.ParseFromString("foo1.com,.FOo2.com"); + + EXPECT_TRUE(rules1.Equals(rules2)); + EXPECT_TRUE(rules2.Equals(rules1)); + + rules1.ParseFromString(".foo2.com"); + rules2.ParseFromString("foo1.com,.FOo2.com"); + + EXPECT_FALSE(rules1.Equals(rules2)); + EXPECT_FALSE(rules2.Equals(rules1)); +} + +TEST(ProxyBypassRulesTest, BypassLocalNames) { + const struct { + const char* url; + bool expected_is_local; + } tests[] = { + // Single-component hostnames are considered local. + {"http://localhost/x", true}, + {"http://www", true}, + + // IPv4 loopback interface. + {"http://127.0.0.1/x", true}, + {"http://127.0.0.1:80/x", true}, + + // IPv6 loopback interface. + {"http://[::1]:80/x", true}, + {"http://[0:0::1]:6233/x", true}, + {"http://[0:0:0:0:0:0:0:1]/x", true}, + + // Non-local URLs. + {"http://foo.com/", false}, + {"http://localhost.i/", false}, + {"http://www.google.com/", false}, + {"http://192.168.0.1/", false}, + + // Try with different protocols. + {"ftp://127.0.0.1/x", true}, + {"ftp://foobar.com/x", false}, + + // This is a bit of a gray-area, but GURL does not strip trailing dots + // in host-names, so the following are considered non-local. + {"http://www./x", false}, + {"http://localhost./x", false}, + }; + + ProxyBypassRules rules; + rules.ParseFromString("<local>"); + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { + SCOPED_TRACE(base::StringPrintf( + "Test[%d]: %s", static_cast<int>(i), tests[i].url)); + EXPECT_EQ(tests[i].expected_is_local, rules.Matches(GURL(tests[i].url))); + } +} + +TEST(ProxyBypassRulesTest, ParseAndMatchCIDR_IPv4) { + ProxyBypassRules rules; + rules.ParseFromString("192.168.1.1/16"); + ASSERT_EQ(1u, rules.rules().size()); + EXPECT_EQ("192.168.1.1/16", rules.rules()[0]->ToString()); + + EXPECT_TRUE(rules.Matches(GURL("http://192.168.1.1"))); + EXPECT_TRUE(rules.Matches(GURL("ftp://192.168.4.4"))); + EXPECT_TRUE(rules.Matches(GURL("https://192.168.0.0:81"))); + EXPECT_TRUE(rules.Matches(GURL("http://[::ffff:192.168.11.11]"))); + + EXPECT_FALSE(rules.Matches(GURL("http://foobar.com"))); + EXPECT_FALSE(rules.Matches(GURL("http://192.169.1.1"))); + EXPECT_FALSE(rules.Matches(GURL("http://xxx.192.168.1.1"))); + EXPECT_FALSE(rules.Matches(GURL("http://192.168.1.1.xx"))); +} + +TEST(ProxyBypassRulesTest, ParseAndMatchCIDR_IPv6) { + ProxyBypassRules rules; + rules.ParseFromString("a:b:c:d::/48"); + ASSERT_EQ(1u, rules.rules().size()); + EXPECT_EQ("a:b:c:d::/48", rules.rules()[0]->ToString()); + + EXPECT_TRUE(rules.Matches(GURL("http://[A:b:C:9::]"))); + EXPECT_FALSE(rules.Matches(GURL("http://foobar.com"))); + EXPECT_FALSE(rules.Matches(GURL("http://192.169.1.1"))); +} + +} // namespace + +} // namespace net diff --git a/chromium/net/proxy/proxy_config.cc b/chromium/net/proxy/proxy_config.cc new file mode 100644 index 00000000000..c1fe0f5ceb4 --- /dev/null +++ b/chromium/net/proxy/proxy_config.cc @@ -0,0 +1,277 @@ +// 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/proxy/proxy_config.h" + +#include "base/logging.h" +#include "base/strings/string_util.h" +#include "base/strings/string_tokenizer.h" +#include "base/values.h" +#include "net/proxy/proxy_info.h" + +namespace net { + +namespace { + +// If |proxies| is non-empty, sets it in |dict| under the key |name|. +void AddProxyListToValue(const char* name, + const ProxyList& proxies, + base::DictionaryValue* dict) { + if (!proxies.IsEmpty()) + dict->Set(name, proxies.ToValue()); +} + +// Split the |uri_list| on commas and add each entry to |proxy_list| in turn. +void AddProxyURIListToProxyList(std::string uri_list, + ProxyList* proxy_list, + ProxyServer::Scheme default_scheme) { + base::StringTokenizer proxy_uri_list(uri_list, ","); + while (proxy_uri_list.GetNext()) { + proxy_list->AddProxyServer( + ProxyServer::FromURI(proxy_uri_list.token(), default_scheme)); + } +} + +} // namespace + +ProxyConfig::ProxyRules::ProxyRules() + : reverse_bypass(false), + type(TYPE_NO_RULES) { +} + +ProxyConfig::ProxyRules::~ProxyRules() { +} + +void ProxyConfig::ProxyRules::Apply(const GURL& url, ProxyInfo* result) const { + if (empty()) { + result->UseDirect(); + return; + } + + bool bypass_proxy = bypass_rules.Matches(url); + if (reverse_bypass) + bypass_proxy = !bypass_proxy; + if (bypass_proxy) { + result->UseDirectWithBypassedProxy(); + return; + } + + switch (type) { + case ProxyRules::TYPE_SINGLE_PROXY: { + result->UseProxyList(single_proxies); + return; + } + case ProxyRules::TYPE_PROXY_PER_SCHEME: { + const ProxyList* entry = MapUrlSchemeToProxyList(url.scheme()); + if (entry) { + result->UseProxyList(*entry); + } else { + // We failed to find a matching proxy server for the current URL + // scheme. Default to direct. + result->UseDirect(); + } + return; + } + default: { + result->UseDirect(); + NOTREACHED(); + return; + } + } +} + +void ProxyConfig::ProxyRules::ParseFromString(const std::string& proxy_rules) { + // Reset. + type = TYPE_NO_RULES; + single_proxies = ProxyList(); + proxies_for_http = ProxyList(); + proxies_for_https = ProxyList(); + proxies_for_ftp = ProxyList(); + fallback_proxies = ProxyList(); + + base::StringTokenizer proxy_server_list(proxy_rules, ";"); + while (proxy_server_list.GetNext()) { + base::StringTokenizer proxy_server_for_scheme( + proxy_server_list.token_begin(), proxy_server_list.token_end(), "="); + + while (proxy_server_for_scheme.GetNext()) { + std::string url_scheme = proxy_server_for_scheme.token(); + + // If we fail to get the proxy server here, it means that + // this is a regular proxy server configuration, i.e. proxies + // are not configured per protocol. + if (!proxy_server_for_scheme.GetNext()) { + if (type == TYPE_PROXY_PER_SCHEME) + continue; // Unexpected. + AddProxyURIListToProxyList(url_scheme, + &single_proxies, + ProxyServer::SCHEME_HTTP); + type = TYPE_SINGLE_PROXY; + return; + } + + // Trim whitespace off the url scheme. + TrimWhitespaceASCII(url_scheme, TRIM_ALL, &url_scheme); + + // Add it to the per-scheme mappings (if supported scheme). + type = TYPE_PROXY_PER_SCHEME; + ProxyList* entry = MapUrlSchemeToProxyListNoFallback(url_scheme); + ProxyServer::Scheme default_scheme = ProxyServer::SCHEME_HTTP; + + // socks=XXX is inconsistent with the other formats, since "socks" + // is not a URL scheme. Rather this means "for everything else, send + // it to the SOCKS proxy server XXX". + if (url_scheme == "socks") { + DCHECK(!entry); + entry = &fallback_proxies; + // Note that here 'socks' is understood to be SOCKS4, even though + // 'socks' maps to SOCKS5 in ProxyServer::GetSchemeFromURIInternal. + default_scheme = ProxyServer::SCHEME_SOCKS4; + } + + if (entry) { + AddProxyURIListToProxyList(proxy_server_for_scheme.token(), + entry, + default_scheme); + } + } + } +} + +const ProxyList* ProxyConfig::ProxyRules::MapUrlSchemeToProxyList( + const std::string& url_scheme) const { + const ProxyList* proxy_server_list = const_cast<ProxyRules*>(this)-> + MapUrlSchemeToProxyListNoFallback(url_scheme); + if (proxy_server_list && !proxy_server_list->IsEmpty()) + return proxy_server_list; + if (!fallback_proxies.IsEmpty()) + return &fallback_proxies; + return NULL; // No mapping for this scheme. Use direct. +} + +bool ProxyConfig::ProxyRules::Equals(const ProxyRules& other) const { + return type == other.type && + single_proxies.Equals(other.single_proxies) && + proxies_for_http.Equals(other.proxies_for_http) && + proxies_for_https.Equals(other.proxies_for_https) && + proxies_for_ftp.Equals(other.proxies_for_ftp) && + fallback_proxies.Equals(other.fallback_proxies) && + bypass_rules.Equals(other.bypass_rules) && + reverse_bypass == other.reverse_bypass; +} + +ProxyList* ProxyConfig::ProxyRules::MapUrlSchemeToProxyListNoFallback( + const std::string& scheme) { + DCHECK_EQ(TYPE_PROXY_PER_SCHEME, type); + if (scheme == "http") + return &proxies_for_http; + if (scheme == "https") + return &proxies_for_https; + if (scheme == "ftp") + return &proxies_for_ftp; + return NULL; // No mapping for this scheme. +} + +ProxyConfig::ProxyConfig() + : auto_detect_(false), pac_mandatory_(false), + source_(PROXY_CONFIG_SOURCE_UNKNOWN), id_(kInvalidConfigID) { +} + +ProxyConfig::ProxyConfig(const ProxyConfig& config) + : auto_detect_(config.auto_detect_), + pac_url_(config.pac_url_), + pac_mandatory_(config.pac_mandatory_), + proxy_rules_(config.proxy_rules_), + source_(config.source_), + id_(config.id_) { +} + +ProxyConfig::~ProxyConfig() { +} + +ProxyConfig& ProxyConfig::operator=(const ProxyConfig& config) { + auto_detect_ = config.auto_detect_; + pac_url_ = config.pac_url_; + pac_mandatory_ = config.pac_mandatory_; + proxy_rules_ = config.proxy_rules_; + source_ = config.source_; + id_ = config.id_; + return *this; +} + +bool ProxyConfig::Equals(const ProxyConfig& other) const { + // The two configs can have different IDs and sources. We are just interested + // in if they have the same settings. + return auto_detect_ == other.auto_detect_ && + pac_url_ == other.pac_url_ && + pac_mandatory_ == other.pac_mandatory_ && + proxy_rules_.Equals(other.proxy_rules()); +} + +bool ProxyConfig::HasAutomaticSettings() const { + return auto_detect_ || has_pac_url(); +} + +void ProxyConfig::ClearAutomaticSettings() { + auto_detect_ = false; + pac_url_ = GURL(); +} + +base::DictionaryValue* ProxyConfig::ToValue() const { + base::DictionaryValue* dict = new base::DictionaryValue(); + + // Output the automatic settings. + if (auto_detect_) + dict->SetBoolean("auto_detect", auto_detect_); + if (has_pac_url()) { + dict->SetString("pac_url", pac_url_.possibly_invalid_spec()); + if (pac_mandatory_) + dict->SetBoolean("pac_mandatory", pac_mandatory_); + } + + // Output the manual settings. + if (proxy_rules_.type != ProxyRules::TYPE_NO_RULES) { + switch (proxy_rules_.type) { + case ProxyRules::TYPE_SINGLE_PROXY: + AddProxyListToValue("single_proxy", + proxy_rules_.single_proxies, dict); + break; + case ProxyRules::TYPE_PROXY_PER_SCHEME: { + base::DictionaryValue* dict2 = new base::DictionaryValue(); + AddProxyListToValue("http", proxy_rules_.proxies_for_http, dict2); + AddProxyListToValue("https", proxy_rules_.proxies_for_https, dict2); + AddProxyListToValue("ftp", proxy_rules_.proxies_for_ftp, dict2); + AddProxyListToValue("fallback", proxy_rules_.fallback_proxies, dict2); + dict->Set("proxy_per_scheme", dict2); + break; + } + default: + NOTREACHED(); + } + + // Output the bypass rules. + const ProxyBypassRules& bypass = proxy_rules_.bypass_rules; + if (!bypass.rules().empty()) { + if (proxy_rules_.reverse_bypass) + dict->SetBoolean("reverse_bypass", true); + + base::ListValue* list = new base::ListValue(); + + for (ProxyBypassRules::RuleList::const_iterator it = + bypass.rules().begin(); + it != bypass.rules().end(); ++it) { + list->Append(new base::StringValue((*it)->ToString())); + } + + dict->Set("bypass_list", list); + } + } + + // Output the source. + dict->SetString("source", ProxyConfigSourceToString(source_)); + + return dict; +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_config.h b/chromium/net/proxy/proxy_config.h new file mode 100644 index 00000000000..e558174a465 --- /dev/null +++ b/chromium/net/proxy/proxy_config.h @@ -0,0 +1,263 @@ +// 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. + +#ifndef NET_PROXY_PROXY_CONFIG_H_ +#define NET_PROXY_PROXY_CONFIG_H_ + +#include <string> + +#include "net/base/net_export.h" +#include "net/proxy/proxy_bypass_rules.h" +#include "net/proxy/proxy_config_source.h" +#include "net/proxy/proxy_list.h" +#include "net/proxy/proxy_server.h" +#include "url/gurl.h" + +namespace base { +class Value; +} + +namespace net { + +class ProxyInfo; + +// ProxyConfig describes a user's proxy settings. +// +// There are two categories of proxy settings: +// (1) Automatic (indicates the methods to obtain a PAC script) +// (2) Manual (simple set of proxy servers per scheme, and bypass patterns) +// +// When both automatic and manual settings are specified, the Automatic ones +// take precedence over the manual ones. +// +// For more details see: +// http://www.chromium.org/developers/design-documents/proxy-settings-fallback +class NET_EXPORT ProxyConfig { + public: + // ProxyRules describes the "manual" proxy settings. + // TODO(eroman): Turn this into a class. + // TODO(marq): Update the enum names; "TYPE_SINGLE_PROXY" really means + // the same set of proxies are used for all requests. + struct NET_EXPORT ProxyRules { + enum Type { + TYPE_NO_RULES, + TYPE_SINGLE_PROXY, + TYPE_PROXY_PER_SCHEME, + }; + + // Note that the default of TYPE_NO_RULES results in direct connections + // being made when using this ProxyConfig. + ProxyRules(); + ~ProxyRules(); + + bool empty() const { + return type == TYPE_NO_RULES; + } + + // Sets |result| with the proxies to use for |url| based on the current + // rules. + void Apply(const GURL& url, ProxyInfo* result) const; + + // Parses the rules from a string, indicating which proxies to use. + // + // proxy-uri = [<proxy-scheme>"://"]<proxy-host>[":"<proxy-port>] + // + // proxy-uri-list = <proxy-uri>[","<proxy-uri-list>] + // + // url-scheme = "http" | "https" | "ftp" | "socks" + // + // scheme-proxies = [<url-scheme>"="]<proxy-uri-list> + // + // proxy-rules = scheme-proxies[";"<scheme-proxies>] + // + // Thus, the proxy-rules string should be a semicolon-separated list of + // ordered proxies that apply to a particular URL scheme. Unless specified, + // the proxy scheme for proxy-uris is assumed to be http. + // + // Some special cases: + // * If the scheme is omitted from the first proxy list, that list applies + // to all URL schemes and subsequent lists are ignored. + // * If a scheme is omitted from any proxy list after a list where a scheme + // has been provided, the list without a scheme is ignored. + // * If the url-scheme is set to 'socks', that sets a fallback list that + // to all otherwise unspecified url-schemes, however the default proxy- + // scheme for proxy urls in the 'socks' list is understood to be + // socks4:// if unspecified. + // + // For example: + // "http=foopy:80;ftp=foopy2" -- use HTTP proxy "foopy:80" for http:// + // URLs, and HTTP proxy "foopy2:80" for + // ftp:// URLs. + // "foopy:80" -- use HTTP proxy "foopy:80" for all URLs. + // "foopy:80,bar,direct://" -- use HTTP proxy "foopy:80" for all URLs, + // failing over to "bar" if "foopy:80" is + // unavailable, and after that using no + // proxy. + // "socks4://foopy" -- use SOCKS v4 proxy "foopy:1080" for all + // URLs. + // "http=foop,socks5://bar.com -- use HTTP proxy "foopy" for http URLs, + // and fail over to the SOCKS5 proxy + // "bar.com" if "foop" is unavailable. + // "http=foopy,direct:// -- use HTTP proxy "foopy" for http URLs, + // and use no proxy if "foopy" is + // unavailable. + // "http=foopy;socks=foopy2 -- use HTTP proxy "foopy" for http URLs, + // and use socks4://foopy2 for all other + // URLs. + void ParseFromString(const std::string& proxy_rules); + + // Returns one of {&proxies_for_http, &proxies_for_https, &proxies_for_ftp, + // &fallback_proxies}, or NULL if there is no proxy to use. + // Should only call this if the type is TYPE_PROXY_PER_SCHEME. + const ProxyList* MapUrlSchemeToProxyList( + const std::string& url_scheme) const; + + // Returns true if |*this| describes the same configuration as |other|. + bool Equals(const ProxyRules& other) const; + + // Exceptions for when not to use a proxy. + ProxyBypassRules bypass_rules; + + // Reverse the meaning of |bypass_rules|. + bool reverse_bypass; + + Type type; + + // Set if |type| is TYPE_SINGLE_PROXY. + ProxyList single_proxies; + + // Set if |type| is TYPE_PROXY_PER_SCHEME. + ProxyList proxies_for_http; + ProxyList proxies_for_https; + ProxyList proxies_for_ftp; + + // Used when a fallback has been defined and the url to be proxied doesn't + // match any of the standard schemes. + ProxyList fallback_proxies; + + private: + // Returns one of {&proxies_for_http, &proxies_for_https, &proxies_for_ftp} + // or NULL if it is a scheme that we don't have a mapping + // for. Should only call this if the type is TYPE_PROXY_PER_SCHEME. + ProxyList* MapUrlSchemeToProxyListNoFallback(const std::string& scheme); + }; + + typedef int ID; + + // Indicates an invalid proxy config. + static const ID kInvalidConfigID = 0; + + ProxyConfig(); + ProxyConfig(const ProxyConfig& config); + ~ProxyConfig(); + ProxyConfig& operator=(const ProxyConfig& config); + + // Used to numerically identify this configuration. + ID id() const { return id_; } + void set_id(ID id) { id_ = id; } + bool is_valid() const { return id_ != kInvalidConfigID; } + + // Returns true if the given config is equivalent to this config. The + // comparison ignores differences in |id()| and |source()|. + bool Equals(const ProxyConfig& other) const; + + // Returns true if this config contains any "automatic" settings. See the + // class description for what that means. + bool HasAutomaticSettings() const; + + void ClearAutomaticSettings(); + + // Creates a Value dump of this configuration. The caller is responsible for + // deleting the returned value. + base::DictionaryValue* ToValue() const; + + ProxyRules& proxy_rules() { + return proxy_rules_; + } + + const ProxyRules& proxy_rules() const { + return proxy_rules_; + } + + void set_pac_url(const GURL& url) { + pac_url_ = url; + } + + const GURL& pac_url() const { + return pac_url_; + } + + void set_pac_mandatory(bool enable_pac_mandatory) { + pac_mandatory_ = enable_pac_mandatory; + } + + bool pac_mandatory() const { + return pac_mandatory_; + } + + bool has_pac_url() const { + return pac_url_.is_valid(); + } + + void set_auto_detect(bool enable_auto_detect) { + auto_detect_ = enable_auto_detect; + } + + bool auto_detect() const { + return auto_detect_; + } + + void set_source(ProxyConfigSource source) { + source_ = source; + } + + ProxyConfigSource source() const { + return source_; + } + + // Helpers to construct some common proxy configurations. + + static ProxyConfig CreateDirect() { + return ProxyConfig(); + } + + static ProxyConfig CreateAutoDetect() { + ProxyConfig config; + config.set_auto_detect(true); + return config; + } + + static ProxyConfig CreateFromCustomPacURL(const GURL& pac_url) { + ProxyConfig config; + config.set_pac_url(pac_url); + // By default fall back to direct connection in case PAC script fails. + config.set_pac_mandatory(false); + return config; + } + + private: + // True if the proxy configuration should be auto-detected. + bool auto_detect_; + + // If non-empty, indicates the URL of the proxy auto-config file to use. + GURL pac_url_; + + // If true, blocks all traffic in case fetching the pac script from |pac_url_| + // fails. Only valid if |pac_url_| is non-empty. + bool pac_mandatory_; + + // Manual proxy settings. + ProxyRules proxy_rules_; + + // Source of proxy settings. + ProxyConfigSource source_; + + ID id_; +}; + +} // namespace net + + + +#endif // NET_PROXY_PROXY_CONFIG_H_ diff --git a/chromium/net/proxy/proxy_config_service.h b/chromium/net/proxy/proxy_config_service.h new file mode 100644 index 00000000000..5e14995d093 --- /dev/null +++ b/chromium/net/proxy/proxy_config_service.h @@ -0,0 +1,68 @@ +// 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. + +#ifndef NET_PROXY_PROXY_CONFIG_SERVICE_H_ +#define NET_PROXY_PROXY_CONFIG_SERVICE_H_ + +#include "net/base/net_export.h" + +namespace net { + +class ProxyConfig; + +// Service for watching when the proxy settings have changed. +class NET_EXPORT ProxyConfigService { + public: + // Indicates whether proxy configuration is valid, and if not, why. + enum ConfigAvailability { + // Configuration is pending, observers will be notified later. + CONFIG_PENDING, + // Configuration is present and valid. + CONFIG_VALID, + // No configuration is set. + CONFIG_UNSET + }; + + // Observer for being notified when the proxy settings have changed. + class NET_EXPORT Observer { + public: + virtual ~Observer() {} + // Notification callback that should be invoked by ProxyConfigService + // implementors whenever the configuration changes. |availability| indicates + // the new availability status and can be CONFIG_UNSET or CONFIG_VALID (in + // which case |config| contains the configuration). Implementors must not + // pass CONFIG_PENDING. + virtual void OnProxyConfigChanged(const ProxyConfig& config, + ConfigAvailability availability) = 0; + }; + + virtual ~ProxyConfigService() {} + + // Adds/Removes an observer that will be called whenever the proxy + // configuration has changed. + virtual void AddObserver(Observer* observer) = 0; + virtual void RemoveObserver(Observer* observer) = 0; + + // Gets the most recent availability status. If a configuration is present, + // the proxy configuration is written to |config| and CONFIG_VALID is + // returned. Returns CONFIG_PENDING if it is not available yet. In this case, + // it is guaranteed that subscribed observers will be notified of a change at + // some point in the future once the configuration is available. + // Note that to avoid re-entrancy problems, implementations should not + // dispatch any change notifications from within this function. + virtual ConfigAvailability GetLatestProxyConfig(ProxyConfig* config) = 0; + + // ProxyService will call this periodically during periods of activity. + // It can be used as a signal for polling-based implementations. + // + // Note that this is purely used as an optimization -- polling + // implementations could simply set a global timer that goes off every + // X seconds at which point they check for changes. However that has + // the disadvantage of doing continuous work even during idle periods. + virtual void OnLazyPoll() {} +}; + +} // namespace net + +#endif // NET_PROXY_PROXY_CONFIG_SERVICE_H_ diff --git a/chromium/net/proxy/proxy_config_service_android.cc b/chromium/net/proxy/proxy_config_service_android.cc new file mode 100644 index 00000000000..41bef8a4cd8 --- /dev/null +++ b/chromium/net/proxy/proxy_config_service_android.cc @@ -0,0 +1,339 @@ +// 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/proxy/proxy_config_service_android.h" + +#include <sys/system_properties.h> + +#include "base/android/jni_string.h" +#include "base/basictypes.h" +#include "base/bind.h" +#include "base/callback.h" +#include "base/compiler_specific.h" +#include "base/location.h" +#include "base/logging.h" +#include "base/memory/ref_counted.h" +#include "base/observer_list.h" +#include "base/sequenced_task_runner.h" +#include "base/strings/string_tokenizer.h" +#include "base/strings/string_util.h" +#include "jni/ProxyChangeListener_jni.h" +#include "net/base/host_port_pair.h" +#include "net/proxy/proxy_config.h" +#include "url/url_parse.h" + +using base::android::AttachCurrentThread; +using base::android::ConvertUTF8ToJavaString; +using base::android::ConvertJavaStringToUTF8; +using base::android::CheckException; +using base::android::ClearException; +using base::android::ScopedJavaGlobalRef; + +namespace net { + +namespace { + +typedef ProxyConfigServiceAndroid::GetPropertyCallback GetPropertyCallback; + +// Returns whether the provided string was successfully converted to a port. +bool ConvertStringToPort(const std::string& port, int* output) { + url_parse::Component component(0, port.size()); + int result = url_parse::ParsePort(port.c_str(), component); + if (result == url_parse::PORT_INVALID || + result == url_parse::PORT_UNSPECIFIED) + return false; + *output = result; + return true; +} + +ProxyServer ConstructProxyServer(ProxyServer::Scheme scheme, + const std::string& proxy_host, + const std::string& proxy_port) { + DCHECK(!proxy_host.empty()); + int port_as_int = 0; + if (proxy_port.empty()) + port_as_int = ProxyServer::GetDefaultPortForScheme(scheme); + else if (!ConvertStringToPort(proxy_port, &port_as_int)) + return ProxyServer(); + DCHECK(port_as_int > 0); + return ProxyServer( + scheme, + HostPortPair(proxy_host, static_cast<uint16>(port_as_int))); +} + +ProxyServer LookupProxy(const std::string& prefix, + const GetPropertyCallback& get_property, + ProxyServer::Scheme scheme) { + DCHECK(!prefix.empty()); + std::string proxy_host = get_property.Run(prefix + ".proxyHost"); + if (!proxy_host.empty()) { + std::string proxy_port = get_property.Run(prefix + ".proxyPort"); + return ConstructProxyServer(scheme, proxy_host, proxy_port); + } + // Fall back to default proxy, if any. + proxy_host = get_property.Run("proxyHost"); + if (!proxy_host.empty()) { + std::string proxy_port = get_property.Run("proxyPort"); + return ConstructProxyServer(scheme, proxy_host, proxy_port); + } + return ProxyServer(); +} + +ProxyServer LookupSocksProxy(const GetPropertyCallback& get_property) { + std::string proxy_host = get_property.Run("socksProxyHost"); + if (!proxy_host.empty()) { + std::string proxy_port = get_property.Run("socksProxyPort"); + return ConstructProxyServer(ProxyServer::SCHEME_SOCKS5, proxy_host, + proxy_port); + } + return ProxyServer(); +} + +void AddBypassRules(const std::string& scheme, + const GetPropertyCallback& get_property, + ProxyBypassRules* bypass_rules) { + // The format of a hostname pattern is a list of hostnames that are separated + // by | and that use * as a wildcard. For example, setting the + // http.nonProxyHosts property to *.android.com|*.kernel.org will cause + // requests to http://developer.android.com to be made without a proxy. + std::string non_proxy_hosts = + get_property.Run(scheme + ".nonProxyHosts"); + if (non_proxy_hosts.empty()) + return; + base::StringTokenizer tokenizer(non_proxy_hosts, "|"); + while (tokenizer.GetNext()) { + std::string token = tokenizer.token(); + std::string pattern; + TrimWhitespaceASCII(token, TRIM_ALL, &pattern); + if (pattern.empty()) + continue; + // '?' is not one of the specified pattern characters above. + DCHECK_EQ(std::string::npos, pattern.find('?')); + bypass_rules->AddRuleForHostname(scheme, pattern, -1); + } +} + +// Returns true if a valid proxy was found. +bool GetProxyRules(const GetPropertyCallback& get_property, + ProxyConfig::ProxyRules* rules) { + // See libcore/luni/src/main/java/java/net/ProxySelectorImpl.java for the + // mostly equivalent Android implementation. There is one intentional + // difference: by default Chromium uses the HTTP port (80) for HTTPS + // connections via proxy. This default is identical on other platforms. + // On the opposite, Java spec suggests to use HTTPS port (443) by default (the + // default value of https.proxyPort). + rules->type = ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME; + rules->proxies_for_http.SetSingleProxyServer( + LookupProxy("http", get_property, ProxyServer::SCHEME_HTTP)); + rules->proxies_for_https.SetSingleProxyServer( + LookupProxy("https", get_property, ProxyServer::SCHEME_HTTP)); + rules->proxies_for_ftp.SetSingleProxyServer( + LookupProxy("ftp", get_property, ProxyServer::SCHEME_HTTP)); + rules->fallback_proxies.SetSingleProxyServer(LookupSocksProxy(get_property)); + rules->bypass_rules.Clear(); + AddBypassRules("ftp", get_property, &rules->bypass_rules); + AddBypassRules("http", get_property, &rules->bypass_rules); + AddBypassRules("https", get_property, &rules->bypass_rules); + // We know a proxy was found if not all of the proxy lists are empty. + return !(rules->proxies_for_http.IsEmpty() && + rules->proxies_for_https.IsEmpty() && + rules->proxies_for_ftp.IsEmpty() && + rules->fallback_proxies.IsEmpty()); +}; + +void GetLatestProxyConfigInternal(const GetPropertyCallback& get_property, + ProxyConfig* config) { + if (!GetProxyRules(get_property, &config->proxy_rules())) + *config = ProxyConfig::CreateDirect(); +} + +std::string GetJavaProperty(const std::string& property) { + // Use Java System.getProperty to get configuration information. + // TODO(pliard): Conversion to/from UTF8 ok here? + JNIEnv* env = AttachCurrentThread(); + ScopedJavaLocalRef<jstring> str = ConvertUTF8ToJavaString(env, property); + ScopedJavaLocalRef<jstring> result = + Java_ProxyChangeListener_getProperty(env, str.obj()); + return result.is_null() ? + std::string() : ConvertJavaStringToUTF8(env, result.obj()); +} + +} // namespace + +class ProxyConfigServiceAndroid::Delegate + : public base::RefCountedThreadSafe<Delegate> { + public: + Delegate(base::SequencedTaskRunner* network_task_runner, + base::SequencedTaskRunner* jni_task_runner, + const GetPropertyCallback& get_property_callback) + : jni_delegate_(this), + network_task_runner_(network_task_runner), + jni_task_runner_(jni_task_runner), + get_property_callback_(get_property_callback) { + } + + void SetupJNI() { + DCHECK(OnJNIThread()); + JNIEnv* env = AttachCurrentThread(); + if (java_proxy_change_listener_.is_null()) { + java_proxy_change_listener_.Reset( + Java_ProxyChangeListener_create( + env, base::android::GetApplicationContext())); + CHECK(!java_proxy_change_listener_.is_null()); + } + Java_ProxyChangeListener_start( + env, + java_proxy_change_listener_.obj(), + reinterpret_cast<jint>(&jni_delegate_)); + } + + void FetchInitialConfig() { + DCHECK(OnJNIThread()); + ProxyConfig proxy_config; + GetLatestProxyConfigInternal(get_property_callback_, &proxy_config); + network_task_runner_->PostTask( + FROM_HERE, + base::Bind(&Delegate::SetNewConfigOnNetworkThread, this, proxy_config)); + } + + void Shutdown() { + if (OnJNIThread()) { + ShutdownOnJNIThread(); + } else { + jni_task_runner_->PostTask( + FROM_HERE, + base::Bind(&Delegate::ShutdownOnJNIThread, this)); + } + } + + // Called only on the network thread. + void AddObserver(Observer* observer) { + DCHECK(OnNetworkThread()); + observers_.AddObserver(observer); + } + + void RemoveObserver(Observer* observer) { + DCHECK(OnNetworkThread()); + observers_.RemoveObserver(observer); + } + + ConfigAvailability GetLatestProxyConfig(ProxyConfig* config) { + DCHECK(OnNetworkThread()); + if (!config) + return ProxyConfigService::CONFIG_UNSET; + *config = proxy_config_; + return ProxyConfigService::CONFIG_VALID; + } + + // Called on the JNI thread. + void ProxySettingsChanged() { + DCHECK(OnJNIThread()); + ProxyConfig proxy_config; + GetLatestProxyConfigInternal(get_property_callback_, &proxy_config); + network_task_runner_->PostTask( + FROM_HERE, + base::Bind( + &Delegate::SetNewConfigOnNetworkThread, this, proxy_config)); + } + + private: + friend class base::RefCountedThreadSafe<Delegate>; + + class JNIDelegateImpl : public ProxyConfigServiceAndroid::JNIDelegate { + public: + explicit JNIDelegateImpl(Delegate* delegate) : delegate_(delegate) {} + + // ProxyConfigServiceAndroid::JNIDelegate overrides. + virtual void ProxySettingsChanged(JNIEnv*, jobject) OVERRIDE { + delegate_->ProxySettingsChanged(); + } + + private: + Delegate* const delegate_; + }; + + virtual ~Delegate() {} + + void ShutdownOnJNIThread() { + if (java_proxy_change_listener_.is_null()) + return; + JNIEnv* env = AttachCurrentThread(); + Java_ProxyChangeListener_stop(env, java_proxy_change_listener_.obj()); + } + + // Called on the network thread. + void SetNewConfigOnNetworkThread(const ProxyConfig& proxy_config) { + DCHECK(OnNetworkThread()); + proxy_config_ = proxy_config; + FOR_EACH_OBSERVER(Observer, observers_, + OnProxyConfigChanged(proxy_config, + ProxyConfigService::CONFIG_VALID)); + } + + bool OnJNIThread() const { + return jni_task_runner_->RunsTasksOnCurrentThread(); + } + + bool OnNetworkThread() const { + return network_task_runner_->RunsTasksOnCurrentThread(); + } + + ScopedJavaGlobalRef<jobject> java_proxy_change_listener_; + + JNIDelegateImpl jni_delegate_; + ObserverList<Observer> observers_; + scoped_refptr<base::SequencedTaskRunner> network_task_runner_; + scoped_refptr<base::SequencedTaskRunner> jni_task_runner_; + GetPropertyCallback get_property_callback_; + ProxyConfig proxy_config_; + + DISALLOW_COPY_AND_ASSIGN(Delegate); +}; + +ProxyConfigServiceAndroid::ProxyConfigServiceAndroid( + base::SequencedTaskRunner* network_task_runner, + base::SequencedTaskRunner* jni_task_runner) + : delegate_(new Delegate( + network_task_runner, jni_task_runner, base::Bind(&GetJavaProperty))) { + delegate_->SetupJNI(); + delegate_->FetchInitialConfig(); +} + +ProxyConfigServiceAndroid::~ProxyConfigServiceAndroid() { + delegate_->Shutdown(); +} + +// static +bool ProxyConfigServiceAndroid::Register(JNIEnv* env) { + return RegisterNativesImpl(env); +} + +void ProxyConfigServiceAndroid::AddObserver(Observer* observer) { + delegate_->AddObserver(observer); +} + +void ProxyConfigServiceAndroid::RemoveObserver(Observer* observer) { + delegate_->RemoveObserver(observer); +} + +ProxyConfigService::ConfigAvailability +ProxyConfigServiceAndroid::GetLatestProxyConfig(ProxyConfig* config) { + return delegate_->GetLatestProxyConfig(config); +} + +ProxyConfigServiceAndroid::ProxyConfigServiceAndroid( + base::SequencedTaskRunner* network_task_runner, + base::SequencedTaskRunner* jni_task_runner, + GetPropertyCallback get_property_callback) + : delegate_(new Delegate( + network_task_runner, jni_task_runner, get_property_callback)) { + delegate_->SetupJNI(); + delegate_->FetchInitialConfig(); +} + +void ProxyConfigServiceAndroid::ProxySettingsChanged() { + delegate_->ProxySettingsChanged(); +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_config_service_android.h b/chromium/net/proxy/proxy_config_service_android.h new file mode 100644 index 00000000000..3ddbaebe78e --- /dev/null +++ b/chromium/net/proxy/proxy_config_service_android.h @@ -0,0 +1,79 @@ +// 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. + +#ifndef NET_PROXY_PROXY_CONFIG_SERVICE_ANDROID_H_ +#define NET_PROXY_PROXY_CONFIG_SERVICE_ANDROID_H_ + +#include <string> + +#include "base/android/jni_android.h" +#include "base/basictypes.h" +#include "base/callback_forward.h" +#include "base/compiler_specific.h" +#include "base/memory/ref_counted.h" +#include "net/base/net_export.h" +#include "net/proxy/proxy_config_service.h" + +namespace base { +class SequencedTaskRunner; +} + +namespace net { + +class ProxyConfig; + +class NET_EXPORT ProxyConfigServiceAndroid : public ProxyConfigService { + public: + // Callback that returns the value of the property identified by the provided + // key. If it was not found, an empty string is returned. Note that this + // interface does not let you distinguish an empty property from a + // non-existing property. This callback is invoked on the JNI thread. + typedef base::Callback<std::string (const std::string& property)> + GetPropertyCallback; + + // Separate class whose instance is owned by the Delegate class implemented in + // the .cc file. + class JNIDelegate { + public: + virtual ~JNIDelegate() {} + + // Called from Java (on JNI thread) to signal that the proxy settings have + // changed. + virtual void ProxySettingsChanged(JNIEnv*, jobject) = 0; + }; + + ProxyConfigServiceAndroid(base::SequencedTaskRunner* network_task_runner, + base::SequencedTaskRunner* jni_task_runner); + + virtual ~ProxyConfigServiceAndroid(); + + // Register JNI bindings. + static bool Register(JNIEnv* env); + + // ProxyConfigService: + // Called only on the network thread. + virtual void AddObserver(Observer* observer) OVERRIDE; + virtual void RemoveObserver(Observer* observer) OVERRIDE; + virtual ConfigAvailability GetLatestProxyConfig(ProxyConfig* config) OVERRIDE; + + private: + friend class ProxyConfigServiceAndroidTestBase; + class Delegate; + + // For tests. + ProxyConfigServiceAndroid(base::SequencedTaskRunner* network_task_runner, + base::SequencedTaskRunner* jni_task_runner, + GetPropertyCallback get_property_callback); + + // For tests. + void ProxySettingsChanged(); + + scoped_refptr<Delegate> delegate_; + + DISALLOW_COPY_AND_ASSIGN(ProxyConfigServiceAndroid); +}; + +} // namespace net + +#endif // NET_PROXY_PROXY_CONFIG_SERVICE_ANDROID_H_ diff --git a/chromium/net/proxy/proxy_config_service_android_unittest.cc b/chromium/net/proxy/proxy_config_service_android_unittest.cc new file mode 100644 index 00000000000..03324b9ed77 --- /dev/null +++ b/chromium/net/proxy/proxy_config_service_android_unittest.cc @@ -0,0 +1,352 @@ +// 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 <map> +#include <string> + +#include "base/bind.h" +#include "base/compiler_specific.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop/message_loop.h" +#include "net/proxy/proxy_config.h" +#include "net/proxy/proxy_config_service_android.h" +#include "net/proxy/proxy_info.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { + +namespace { + +class TestObserver : public ProxyConfigService::Observer { + public: + TestObserver() : availability_(ProxyConfigService::CONFIG_UNSET) {} + + // ProxyConfigService::Observer: + virtual void OnProxyConfigChanged( + const ProxyConfig& config, + ProxyConfigService::ConfigAvailability availability) OVERRIDE { + config_ = config; + availability_ = availability; + } + + ProxyConfigService::ConfigAvailability availability() const { + return availability_; + } + + const ProxyConfig& config() const { + return config_; + } + + private: + ProxyConfig config_; + ProxyConfigService::ConfigAvailability availability_; +}; + +} // namespace + +typedef std::map<std::string, std::string> StringMap; + +class ProxyConfigServiceAndroidTestBase : public testing::Test { + protected: + // Note that the current thread's message loop is initialized by the test + // suite (see net/test/net_test_suite.cc). + ProxyConfigServiceAndroidTestBase(const StringMap& initial_configuration) + : configuration_(initial_configuration), + message_loop_(base::MessageLoop::current()), + service_(message_loop_->message_loop_proxy(), + message_loop_->message_loop_proxy(), + base::Bind(&ProxyConfigServiceAndroidTestBase::GetProperty, + base::Unretained(this))) {} + + virtual ~ProxyConfigServiceAndroidTestBase() {} + + // testing::Test: + virtual void SetUp() OVERRIDE { + message_loop_->RunUntilIdle(); + service_.AddObserver(&observer_); + } + + virtual void TearDown() OVERRIDE { + service_.RemoveObserver(&observer_); + } + + void ClearConfiguration() { + configuration_.clear(); + } + + void AddProperty(const std::string& key, const std::string& value) { + configuration_[key] = value; + } + + std::string GetProperty(const std::string& key) { + StringMap::const_iterator it = configuration_.find(key); + if (it == configuration_.end()) + return std::string(); + return it->second; + } + + void ProxySettingsChanged() { + service_.ProxySettingsChanged(); + message_loop_->RunUntilIdle(); + } + + void TestMapping(const std::string& url, const std::string& expected) { + ProxyConfigService::ConfigAvailability availability; + ProxyConfig proxy_config; + availability = service_.GetLatestProxyConfig(&proxy_config); + EXPECT_EQ(ProxyConfigService::CONFIG_VALID, availability); + ProxyInfo proxy_info; + proxy_config.proxy_rules().Apply(GURL(url), &proxy_info); + EXPECT_EQ(expected, proxy_info.ToPacString()); + } + + StringMap configuration_; + TestObserver observer_; + base::MessageLoop* const message_loop_; + ProxyConfigServiceAndroid service_; +}; + +class ProxyConfigServiceAndroidTest : public ProxyConfigServiceAndroidTestBase { + public: + ProxyConfigServiceAndroidTest() + : ProxyConfigServiceAndroidTestBase(StringMap()) {} +}; + +class ProxyConfigServiceAndroidWithInitialConfigTest + : public ProxyConfigServiceAndroidTestBase { + public: + ProxyConfigServiceAndroidWithInitialConfigTest() + : ProxyConfigServiceAndroidTestBase(MakeInitialConfiguration()) {} + + private: + StringMap MakeInitialConfiguration() { + StringMap initial_configuration; + initial_configuration["http.proxyHost"] = "httpproxy.com"; + initial_configuration["http.proxyPort"] = "8080"; + return initial_configuration; + } +}; + +TEST_F(ProxyConfigServiceAndroidTest, TestChangePropertiesNotification) { + // Set up a non-empty configuration + AddProperty("http.proxyHost", "localhost"); + ProxySettingsChanged(); + EXPECT_EQ(ProxyConfigService::CONFIG_VALID, observer_.availability()); + EXPECT_FALSE(observer_.config().proxy_rules().empty()); + + // Set up an empty configuration + ClearConfiguration(); + ProxySettingsChanged(); + EXPECT_EQ(ProxyConfigService::CONFIG_VALID, observer_.availability()); + EXPECT_TRUE(observer_.config().proxy_rules().empty()); +} + +TEST_F(ProxyConfigServiceAndroidWithInitialConfigTest, TestInitialConfig) { + // Make sure that the initial config is set. + TestMapping("ftp://example.com/", "DIRECT"); + TestMapping("http://example.com/", "PROXY httpproxy.com:8080"); + + // Override the initial configuration. + ClearConfiguration(); + AddProperty("http.proxyHost", "httpproxy.com"); + ProxySettingsChanged(); + TestMapping("http://example.com/", "PROXY httpproxy.com:80"); +} + +// !! The following test cases are automatically generated from +// !! net/android/tools/proxy_test_cases.py. +// !! Please edit that file instead of editing the test cases below and +// !! update also the corresponding Java unit tests in +// !! AndroidProxySelectorTest.java + +TEST_F(ProxyConfigServiceAndroidTest, NoProxy) { + // Test direct mapping when no proxy defined. + ProxySettingsChanged(); + TestMapping("ftp://example.com/", "DIRECT"); + TestMapping("http://example.com/", "DIRECT"); + TestMapping("https://example.com/", "DIRECT"); +} + +TEST_F(ProxyConfigServiceAndroidTest, HttpProxyHostAndPort) { + // Test http.proxyHost and http.proxyPort works. + AddProperty("http.proxyHost", "httpproxy.com"); + AddProperty("http.proxyPort", "8080"); + ProxySettingsChanged(); + TestMapping("ftp://example.com/", "DIRECT"); + TestMapping("http://example.com/", "PROXY httpproxy.com:8080"); + TestMapping("https://example.com/", "DIRECT"); +} + +TEST_F(ProxyConfigServiceAndroidTest, HttpProxyHostOnly) { + // We should get the default port (80) for proxied hosts. + AddProperty("http.proxyHost", "httpproxy.com"); + ProxySettingsChanged(); + TestMapping("ftp://example.com/", "DIRECT"); + TestMapping("http://example.com/", "PROXY httpproxy.com:80"); + TestMapping("https://example.com/", "DIRECT"); +} + +TEST_F(ProxyConfigServiceAndroidTest, HttpProxyPortOnly) { + // http.proxyPort only should not result in any hosts being proxied. + AddProperty("http.proxyPort", "8080"); + ProxySettingsChanged(); + TestMapping("ftp://example.com/", "DIRECT"); + TestMapping("http://example.com/", "DIRECT"); + TestMapping("https://example.com/", "DIRECT"); +} + +TEST_F(ProxyConfigServiceAndroidTest, HttpNonProxyHosts1) { + // Test that HTTP non proxy hosts are mapped correctly + AddProperty("http.nonProxyHosts", "slashdot.org"); + AddProperty("http.proxyHost", "httpproxy.com"); + AddProperty("http.proxyPort", "8080"); + ProxySettingsChanged(); + TestMapping("http://example.com/", "PROXY httpproxy.com:8080"); + TestMapping("http://slashdot.org/", "DIRECT"); +} + +TEST_F(ProxyConfigServiceAndroidTest, HttpNonProxyHosts2) { + // Test that | pattern works. + AddProperty("http.nonProxyHosts", "slashdot.org|freecode.net"); + AddProperty("http.proxyHost", "httpproxy.com"); + AddProperty("http.proxyPort", "8080"); + ProxySettingsChanged(); + TestMapping("http://example.com/", "PROXY httpproxy.com:8080"); + TestMapping("http://freecode.net/", "DIRECT"); + TestMapping("http://slashdot.org/", "DIRECT"); +} + +TEST_F(ProxyConfigServiceAndroidTest, HttpNonProxyHosts3) { + // Test that * pattern works. + AddProperty("http.nonProxyHosts", "*example.com"); + AddProperty("http.proxyHost", "httpproxy.com"); + AddProperty("http.proxyPort", "8080"); + ProxySettingsChanged(); + TestMapping("http://example.com/", "DIRECT"); + TestMapping("http://slashdot.org/", "PROXY httpproxy.com:8080"); + TestMapping("http://www.example.com/", "DIRECT"); +} + +TEST_F(ProxyConfigServiceAndroidTest, FtpNonProxyHosts) { + // Test that FTP non proxy hosts are mapped correctly + AddProperty("ftp.nonProxyHosts", "slashdot.org"); + AddProperty("ftp.proxyHost", "httpproxy.com"); + AddProperty("ftp.proxyPort", "8080"); + ProxySettingsChanged(); + TestMapping("ftp://example.com/", "PROXY httpproxy.com:8080"); + TestMapping("http://example.com/", "DIRECT"); +} + +TEST_F(ProxyConfigServiceAndroidTest, FtpProxyHostAndPort) { + // Test ftp.proxyHost and ftp.proxyPort works. + AddProperty("ftp.proxyHost", "httpproxy.com"); + AddProperty("ftp.proxyPort", "8080"); + ProxySettingsChanged(); + TestMapping("ftp://example.com/", "PROXY httpproxy.com:8080"); + TestMapping("http://example.com/", "DIRECT"); + TestMapping("https://example.com/", "DIRECT"); +} + +TEST_F(ProxyConfigServiceAndroidTest, FtpProxyHostOnly) { + // Test ftp.proxyHost and default port. + AddProperty("ftp.proxyHost", "httpproxy.com"); + ProxySettingsChanged(); + TestMapping("ftp://example.com/", "PROXY httpproxy.com:80"); + TestMapping("http://example.com/", "DIRECT"); + TestMapping("https://example.com/", "DIRECT"); +} + +TEST_F(ProxyConfigServiceAndroidTest, HttpsProxyHostAndPort) { + // Test https.proxyHost and https.proxyPort works. + AddProperty("https.proxyHost", "httpproxy.com"); + AddProperty("https.proxyPort", "8080"); + ProxySettingsChanged(); + TestMapping("ftp://example.com/", "DIRECT"); + TestMapping("http://example.com/", "DIRECT"); + TestMapping("https://example.com/", "PROXY httpproxy.com:8080"); +} + +TEST_F(ProxyConfigServiceAndroidTest, HttpsProxyHostOnly) { + // Test https.proxyHost and default port. + AddProperty("https.proxyHost", "httpproxy.com"); + ProxySettingsChanged(); + TestMapping("ftp://example.com/", "DIRECT"); + TestMapping("http://example.com/", "DIRECT"); + TestMapping("https://example.com/", "PROXY httpproxy.com:80"); +} + +TEST_F(ProxyConfigServiceAndroidTest, HttpProxyHostIPv6) { + // Test IPv6 https.proxyHost and default port. + AddProperty("http.proxyHost", "a:b:c::d:1"); + ProxySettingsChanged(); + TestMapping("ftp://example.com/", "DIRECT"); + TestMapping("http://example.com/", "PROXY [a:b:c::d:1]:80"); +} + +TEST_F(ProxyConfigServiceAndroidTest, HttpProxyHostAndPortIPv6) { + // Test IPv6 http.proxyHost and http.proxyPort works. + AddProperty("http.proxyHost", "a:b:c::d:1"); + AddProperty("http.proxyPort", "8080"); + ProxySettingsChanged(); + TestMapping("ftp://example.com/", "DIRECT"); + TestMapping("http://example.com/", "PROXY [a:b:c::d:1]:8080"); +} + +TEST_F(ProxyConfigServiceAndroidTest, HttpProxyHostAndInvalidPort) { + // Test invalid http.proxyPort does not crash. + AddProperty("http.proxyHost", "a:b:c::d:1"); + AddProperty("http.proxyPort", "65536"); + ProxySettingsChanged(); + TestMapping("ftp://example.com/", "DIRECT"); + TestMapping("http://example.com/", "DIRECT"); +} + +TEST_F(ProxyConfigServiceAndroidTest, DefaultProxyExplictPort) { + // Default http proxy is used if a scheme-specific one is not found. + AddProperty("ftp.proxyHost", "httpproxy.com"); + AddProperty("ftp.proxyPort", "8080"); + AddProperty("proxyHost", "defaultproxy.com"); + AddProperty("proxyPort", "8080"); + ProxySettingsChanged(); + TestMapping("ftp://example.com/", "PROXY httpproxy.com:8080"); + TestMapping("http://example.com/", "PROXY defaultproxy.com:8080"); + TestMapping("https://example.com/", "PROXY defaultproxy.com:8080"); +} + +TEST_F(ProxyConfigServiceAndroidTest, DefaultProxyDefaultPort) { + // Check that the default proxy port is as expected. + AddProperty("proxyHost", "defaultproxy.com"); + ProxySettingsChanged(); + TestMapping("http://example.com/", "PROXY defaultproxy.com:80"); + TestMapping("https://example.com/", "PROXY defaultproxy.com:80"); +} + +TEST_F(ProxyConfigServiceAndroidTest, FallbackToSocks) { + // SOCKS proxy is used if scheme-specific one is not found. + AddProperty("http.proxyHost", "defaultproxy.com"); + AddProperty("socksProxyHost", "socksproxy.com"); + ProxySettingsChanged(); + TestMapping("ftp://example.com", "SOCKS5 socksproxy.com:1080"); + TestMapping("http://example.com/", "PROXY defaultproxy.com:80"); + TestMapping("https://example.com/", "SOCKS5 socksproxy.com:1080"); +} + +TEST_F(ProxyConfigServiceAndroidTest, SocksExplicitPort) { + // SOCKS proxy port is used if specified + AddProperty("socksProxyHost", "socksproxy.com"); + AddProperty("socksProxyPort", "9000"); + ProxySettingsChanged(); + TestMapping("http://example.com/", "SOCKS5 socksproxy.com:9000"); +} + +TEST_F(ProxyConfigServiceAndroidTest, HttpProxySupercedesSocks) { + // SOCKS proxy is ignored if default HTTP proxy defined. + AddProperty("proxyHost", "defaultproxy.com"); + AddProperty("socksProxyHost", "socksproxy.com"); + AddProperty("socksProxyPort", "9000"); + ProxySettingsChanged(); + TestMapping("http://example.com/", "PROXY defaultproxy.com:80"); +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_config_service_common_unittest.cc b/chromium/net/proxy/proxy_config_service_common_unittest.cc new file mode 100644 index 00000000000..3835bb1ad20 --- /dev/null +++ b/chromium/net/proxy/proxy_config_service_common_unittest.cc @@ -0,0 +1,191 @@ +// Copyright (c) 2009 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/proxy/proxy_config_service_common_unittest.h" + +#include <string> +#include <vector> + +#include "net/proxy/proxy_config.h" + +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { + +namespace { + +// Helper to verify that |expected_proxy| matches the first proxy conatined in +// |actual_proxies|, and that |actual_proxies| contains exactly one proxy. If +// either condition is untrue, then |*did_fail| is set to true, and +// |*failure_details| is filled with a description of the failure. +void MatchesProxyServerHelper(const char* failure_message, + const char* expected_proxy, + const ProxyList& actual_proxies, + ::testing::AssertionResult* failure_details, + bool* did_fail) { + // If |expected_proxy| is empty, then we expect |actual_proxies| to be so as + // well. + if (strlen(expected_proxy) == 0) { + if (!actual_proxies.IsEmpty()) { + *did_fail = true; + *failure_details + << failure_message << ". Was expecting no proxies but got " + << actual_proxies.size() << "."; + } + return; + } + + // Otherwise we check that |actual_proxies| holds a single matching proxy. + if (actual_proxies.size() != 1) { + *did_fail = true; + *failure_details + << failure_message << ". Was expecting exactly one proxy but got " + << actual_proxies.size() << "."; + return; + } + + ProxyServer actual_proxy = actual_proxies.Get(); + std::string actual_proxy_string; + if (actual_proxy.is_valid()) + actual_proxy_string = actual_proxy.ToURI(); + + if (std::string(expected_proxy) != actual_proxy_string) { + *failure_details + << failure_message << ". Was expecting: \"" << expected_proxy + << "\" but got: \"" << actual_proxy_string << "\""; + *did_fail = true; + } +} + +std::string FlattenProxyBypass(const ProxyBypassRules& bypass_rules) { + std::string flattened_proxy_bypass; + for (ProxyBypassRules::RuleList::const_iterator it = + bypass_rules.rules().begin(); + it != bypass_rules.rules().end(); ++it) { + if (!flattened_proxy_bypass.empty()) + flattened_proxy_bypass += ","; + flattened_proxy_bypass += (*it)->ToString(); + } + return flattened_proxy_bypass; +} + +} // namespace + +ProxyRulesExpectation::ProxyRulesExpectation( + ProxyConfig::ProxyRules::Type type, + const char* single_proxy, + const char* proxy_for_http, + const char* proxy_for_https, + const char* proxy_for_ftp, + const char* fallback_proxy, + const char* flattened_bypass_rules, + bool reverse_bypass) + : type(type), + single_proxy(single_proxy), + proxy_for_http(proxy_for_http), + proxy_for_https(proxy_for_https), + proxy_for_ftp(proxy_for_ftp), + fallback_proxy(fallback_proxy), + flattened_bypass_rules(flattened_bypass_rules), + reverse_bypass(reverse_bypass) { +} + + +::testing::AssertionResult ProxyRulesExpectation::Matches( + const ProxyConfig::ProxyRules& rules) const { + ::testing::AssertionResult failure_details = ::testing::AssertionFailure(); + bool failed = false; + + if (rules.type != type) { + failure_details << "Type mismatch. Expected: " + << type << " but was: " << rules.type; + failed = true; + } + + MatchesProxyServerHelper("Bad single_proxy", single_proxy, + rules.single_proxies, &failure_details, &failed); + MatchesProxyServerHelper("Bad proxy_for_http", proxy_for_http, + rules.proxies_for_http, &failure_details, + &failed); + MatchesProxyServerHelper("Bad proxy_for_https", proxy_for_https, + rules.proxies_for_https, &failure_details, + &failed); + MatchesProxyServerHelper("Bad fallback_proxy", fallback_proxy, + rules.fallback_proxies, &failure_details, &failed); + + std::string actual_flattened_bypass = FlattenProxyBypass(rules.bypass_rules); + if (std::string(flattened_bypass_rules) != actual_flattened_bypass) { + failure_details + << "Bad bypass rules. Expected: \"" << flattened_bypass_rules + << "\" but got: \"" << actual_flattened_bypass << "\""; + failed = true; + } + + if (rules.reverse_bypass != reverse_bypass) { + failure_details << "Bad reverse_bypass. Expected: " << reverse_bypass + << " but got: " << rules.reverse_bypass; + failed = true; + } + + return failed ? failure_details : ::testing::AssertionSuccess(); +} + +// static +ProxyRulesExpectation ProxyRulesExpectation::Empty() { + return ProxyRulesExpectation(ProxyConfig::ProxyRules::TYPE_NO_RULES, + "", "", "", "", "", "", false); +} + +// static +ProxyRulesExpectation ProxyRulesExpectation::EmptyWithBypass( + const char* flattened_bypass_rules) { + return ProxyRulesExpectation(ProxyConfig::ProxyRules::TYPE_NO_RULES, + "", "", "", "", "", flattened_bypass_rules, + false); +} + +// static +ProxyRulesExpectation ProxyRulesExpectation::Single( + const char* single_proxy, + const char* flattened_bypass_rules) { + return ProxyRulesExpectation(ProxyConfig::ProxyRules::TYPE_SINGLE_PROXY, + single_proxy, "", "", "", "", + flattened_bypass_rules, false); +} + +// static +ProxyRulesExpectation ProxyRulesExpectation::PerScheme( + const char* proxy_http, + const char* proxy_https, + const char* proxy_ftp, + const char* flattened_bypass_rules) { + return ProxyRulesExpectation(ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME, + "", proxy_http, proxy_https, proxy_ftp, "", + flattened_bypass_rules, false); +} + +// static +ProxyRulesExpectation ProxyRulesExpectation::PerSchemeWithSocks( + const char* proxy_http, + const char* proxy_https, + const char* proxy_ftp, + const char* socks_proxy, + const char* flattened_bypass_rules) { + return ProxyRulesExpectation(ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME, + "", proxy_http, proxy_https, proxy_ftp, + socks_proxy, flattened_bypass_rules, false); +} + +// static +ProxyRulesExpectation ProxyRulesExpectation::PerSchemeWithBypassReversed( + const char* proxy_http, + const char* proxy_https, + const char* proxy_ftp, + const char* flattened_bypass_rules) { + return ProxyRulesExpectation(ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME, + "", proxy_http, proxy_https, proxy_ftp, "", + flattened_bypass_rules, true); +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_config_service_common_unittest.h b/chromium/net/proxy/proxy_config_service_common_unittest.h new file mode 100644 index 00000000000..dbacbab79b8 --- /dev/null +++ b/chromium/net/proxy/proxy_config_service_common_unittest.h @@ -0,0 +1,80 @@ +// Copyright (c) 2010 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. + +#ifndef NET_PROXY_PROXY_CONFIG_SERVICE_COMMON_UNITTEST_H_ +#define NET_PROXY_PROXY_CONFIG_SERVICE_COMMON_UNITTEST_H_ + +#include "net/proxy/proxy_config.h" +#include "testing/gtest/include/gtest/gtest.h" + +// Helper functions to describe the expected value of a +// ProxyConfig::ProxyRules, and to check for a match. + +namespace net { + +// This structure contains our expectations on what values the ProxyRules +// should have. +struct ProxyRulesExpectation { + ProxyRulesExpectation(ProxyConfig::ProxyRules::Type type, + const char* single_proxy, + const char* proxy_for_http, + const char* proxy_for_https, + const char* proxy_for_ftp, + const char* fallback_proxy, + const char* flattened_bypass_rules, + bool reverse_bypass); + + // Call this within an EXPECT_TRUE(), to assert that |rules| matches + // our expected values |*this|. + ::testing::AssertionResult Matches( + const ProxyConfig::ProxyRules& rules) const; + + // Creates an expectation that the ProxyRules has no rules. + static ProxyRulesExpectation Empty(); + + // Creates an expectation that the ProxyRules has nothing other than + // the specified bypass rules. + static ProxyRulesExpectation EmptyWithBypass( + const char* flattened_bypass_rules); + + // Creates an expectation that the ProxyRules is for a single proxy + // server for all schemes. + static ProxyRulesExpectation Single(const char* single_proxy, + const char* flattened_bypass_rules); + + // Creates an expectation that the ProxyRules specifies a different + // proxy server for each URL scheme. + static ProxyRulesExpectation PerScheme(const char* proxy_http, + const char* proxy_https, + const char* proxy_ftp, + const char* flattened_bypass_rules); + + // Same as above, but additionally with a SOCKS fallback. + static ProxyRulesExpectation PerSchemeWithSocks( + const char* proxy_http, + const char* proxy_https, + const char* proxy_ftp, + const char* fallback_proxy, + const char* flattened_bypass_rules); + + // Same as PerScheme, but with the bypass rules reversed + static ProxyRulesExpectation PerSchemeWithBypassReversed( + const char* proxy_http, + const char* proxy_https, + const char* proxy_ftp, + const char* flattened_bypass_rules); + + ProxyConfig::ProxyRules::Type type; + const char* single_proxy; + const char* proxy_for_http; + const char* proxy_for_https; + const char* proxy_for_ftp; + const char* fallback_proxy; + const char* flattened_bypass_rules; + bool reverse_bypass; +}; + +} // namespace net + +#endif // NET_PROXY_PROXY_CONFIG_SERVICE_COMMON_UNITTEST_H_ diff --git a/chromium/net/proxy/proxy_config_service_fixed.cc b/chromium/net/proxy/proxy_config_service_fixed.cc new file mode 100644 index 00000000000..3081ea5afa1 --- /dev/null +++ b/chromium/net/proxy/proxy_config_service_fixed.cc @@ -0,0 +1,21 @@ +// 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/proxy/proxy_config_service_fixed.h" + +namespace net { + +ProxyConfigServiceFixed::ProxyConfigServiceFixed(const ProxyConfig& pc) + : pc_(pc) { +} + +ProxyConfigServiceFixed::~ProxyConfigServiceFixed() {} + +ProxyConfigService::ConfigAvailability + ProxyConfigServiceFixed::GetLatestProxyConfig(ProxyConfig* config) { + *config = pc_; + return CONFIG_VALID; +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_config_service_fixed.h b/chromium/net/proxy/proxy_config_service_fixed.h new file mode 100644 index 00000000000..b5f30e3654b --- /dev/null +++ b/chromium/net/proxy/proxy_config_service_fixed.h @@ -0,0 +1,32 @@ +// 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. + +#ifndef NET_PROXY_PROXY_CONFIG_SERVICE_FIXED_H_ +#define NET_PROXY_PROXY_CONFIG_SERVICE_FIXED_H_ + +#include "base/compiler_specific.h" +#include "net/base/net_errors.h" +#include "net/proxy/proxy_config.h" +#include "net/proxy/proxy_config_service.h" + +namespace net { + +// Implementation of ProxyConfigService that returns a fixed result. +class NET_EXPORT ProxyConfigServiceFixed : public ProxyConfigService { + public: + explicit ProxyConfigServiceFixed(const ProxyConfig& pc); + virtual ~ProxyConfigServiceFixed(); + + // ProxyConfigService methods: + virtual void AddObserver(Observer* observer) OVERRIDE {} + virtual void RemoveObserver(Observer* observer) OVERRIDE {} + virtual ConfigAvailability GetLatestProxyConfig(ProxyConfig* config) OVERRIDE; + + private: + ProxyConfig pc_; +}; + +} // namespace net + +#endif // NET_PROXY_PROXY_CONFIG_SERVICE_FIXED_H_ diff --git a/chromium/net/proxy/proxy_config_service_ios.cc b/chromium/net/proxy/proxy_config_service_ios.cc new file mode 100644 index 00000000000..84a5adcedf5 --- /dev/null +++ b/chromium/net/proxy/proxy_config_service_ios.cc @@ -0,0 +1,109 @@ +// 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/proxy/proxy_config_service_ios.h" + +#include <CoreFoundation/CoreFoundation.h> +#include <CFNetwork/CFProxySupport.h> + +#include "base/mac/foundation_util.h" +#include "base/mac/scoped_cftyperef.h" +#include "base/message_loop/message_loop.h" +#include "base/strings/sys_string_conversions.h" +#include "net/proxy/proxy_config.h" + +namespace net { + +namespace { + +const int kPollIntervalSec = 10; + +// Utility function to pull out a boolean value from a dictionary and return it, +// returning a default value if the key is not present. +bool GetBoolFromDictionary(CFDictionaryRef dict, + CFStringRef key, + bool default_value) { + CFNumberRef number = + base::mac::GetValueFromDictionary<CFNumberRef>(dict, key); + if (!number) + return default_value; + + int int_value; + if (CFNumberGetValue(number, kCFNumberIntType, &int_value)) + return int_value; + else + return default_value; +} + +void GetCurrentProxyConfig(ProxyConfig* config) { + base::ScopedCFTypeRef<CFDictionaryRef> config_dict( + CFNetworkCopySystemProxySettings()); + DCHECK(config_dict); + + // Auto-detect is not supported. + // The kCFNetworkProxiesProxyAutoDiscoveryEnable key is not available on iOS. + + // PAC file + + if (GetBoolFromDictionary(config_dict.get(), + kCFNetworkProxiesProxyAutoConfigEnable, + false)) { + CFStringRef pac_url_ref = base::mac::GetValueFromDictionary<CFStringRef>( + config_dict.get(), kCFNetworkProxiesProxyAutoConfigURLString); + if (pac_url_ref) + config->set_pac_url(GURL(base::SysCFStringRefToUTF8(pac_url_ref))); + } + + // Proxies (for now http). + + // The following keys are not available on iOS: + // kCFNetworkProxiesFTPEnable + // kCFNetworkProxiesFTPProxy + // kCFNetworkProxiesFTPPort + // kCFNetworkProxiesHTTPSEnable + // kCFNetworkProxiesHTTPSProxy + // kCFNetworkProxiesHTTPSPort + // kCFNetworkProxiesSOCKSEnable + // kCFNetworkProxiesSOCKSProxy + // kCFNetworkProxiesSOCKSPort + if (GetBoolFromDictionary(config_dict.get(), + kCFNetworkProxiesHTTPEnable, + false)) { + ProxyServer proxy_server = + ProxyServer::FromDictionary(ProxyServer::SCHEME_HTTP, + config_dict.get(), + kCFNetworkProxiesHTTPProxy, + kCFNetworkProxiesHTTPPort); + if (proxy_server.is_valid()) { + config->proxy_rules().type = + ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME; + config->proxy_rules().proxies_for_http.SetSingleProxyServer(proxy_server); + // Desktop Safari applies the HTTP proxy to http:// URLs only, but + // Mobile Safari applies the HTTP proxy to https:// URLs as well. + config->proxy_rules().proxies_for_https.SetSingleProxyServer( + proxy_server); + } + } + + // Proxy bypass list is not supported. + // The kCFNetworkProxiesExceptionsList key is not available on iOS. + + // Proxy bypass boolean is not supported. + // The kCFNetworkProxiesExcludeSimpleHostnames key is not available on iOS. + + // Source + config->set_source(PROXY_CONFIG_SOURCE_SYSTEM); +} + +} // namespace + +ProxyConfigServiceIOS::ProxyConfigServiceIOS() + : PollingProxyConfigService(base::TimeDelta::FromSeconds(kPollIntervalSec), + GetCurrentProxyConfig) { +} + +ProxyConfigServiceIOS::~ProxyConfigServiceIOS() { +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_config_service_ios.h b/chromium/net/proxy/proxy_config_service_ios.h new file mode 100644 index 00000000000..bf8f76b6b67 --- /dev/null +++ b/chromium/net/proxy/proxy_config_service_ios.h @@ -0,0 +1,24 @@ +// 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. + +#ifndef NET_PROXY_PROXY_CONFIG_SERVICE_IOS_H_ +#define NET_PROXY_PROXY_CONFIG_SERVICE_IOS_H_ + +#include "net/proxy/polling_proxy_config_service.h" + +namespace net { + +class ProxyConfigServiceIOS : public PollingProxyConfigService { + public: + // Constructs a ProxyConfigService that watches the iOS system proxy settings. + explicit ProxyConfigServiceIOS(); + virtual ~ProxyConfigServiceIOS(); + + private: + DISALLOW_COPY_AND_ASSIGN(ProxyConfigServiceIOS); +}; + +} // namespace net + +#endif // NET_PROXY_PROXY_CONFIG_SERVICE_IOS_H_ diff --git a/chromium/net/proxy/proxy_config_service_linux.cc b/chromium/net/proxy/proxy_config_service_linux.cc new file mode 100644 index 00000000000..2c21283ea75 --- /dev/null +++ b/chromium/net/proxy/proxy_config_service_linux.cc @@ -0,0 +1,1766 @@ +// 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/proxy/proxy_config_service_linux.h" + +#include <errno.h> +#include <fcntl.h> +#if defined(USE_GCONF) +#include <gconf/gconf-client.h> +#endif // defined(USE_GCONF) +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/inotify.h> +#include <unistd.h> + +#include <map> + +#include "base/bind.h" +#include "base/compiler_specific.h" +#include "base/environment.h" +#include "base/file_util.h" +#include "base/files/file_path.h" +#include "base/logging.h" +#include "base/message_loop/message_loop.h" +#include "base/nix/xdg_util.h" +#include "base/single_thread_task_runner.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_tokenizer.h" +#include "base/strings/string_util.h" +#include "base/threading/thread_restrictions.h" +#include "base/timer/timer.h" +#include "net/base/net_errors.h" +#include "net/http/http_util.h" +#include "net/proxy/proxy_config.h" +#include "net/proxy/proxy_server.h" +#include "url/url_canon.h" + +#if defined(USE_GIO) +#include "library_loaders/libgio.h" +#endif // defined(USE_GIO) + +namespace net { + +namespace { + +// Given a proxy hostname from a setting, returns that hostname with +// an appropriate proxy server scheme prefix. +// scheme indicates the desired proxy scheme: usually http, with +// socks 4 or 5 as special cases. +// TODO(arindam): Remove URI string manipulation by using MapUrlSchemeToProxy. +std::string FixupProxyHostScheme(ProxyServer::Scheme scheme, + std::string host) { + if (scheme == ProxyServer::SCHEME_SOCKS5 && + StartsWithASCII(host, "socks4://", false)) { + // We default to socks 5, but if the user specifically set it to + // socks4://, then use that. + scheme = ProxyServer::SCHEME_SOCKS4; + } + // Strip the scheme if any. + std::string::size_type colon = host.find("://"); + if (colon != std::string::npos) + host = host.substr(colon + 3); + // If a username and perhaps password are specified, give a warning. + std::string::size_type at_sign = host.find("@"); + // Should this be supported? + if (at_sign != std::string::npos) { + // ProxyConfig does not support authentication parameters, but Chrome + // will prompt for the password later. Disregard the + // authentication parameters and continue with this hostname. + LOG(WARNING) << "Proxy authentication parameters ignored, see bug 16709"; + host = host.substr(at_sign + 1); + } + // If this is a socks proxy, prepend a scheme so as to tell + // ProxyServer. This also allows ProxyServer to choose the right + // default port. + if (scheme == ProxyServer::SCHEME_SOCKS4) + host = "socks4://" + host; + else if (scheme == ProxyServer::SCHEME_SOCKS5) + host = "socks5://" + host; + // If there is a trailing slash, remove it so |host| will parse correctly + // even if it includes a port number (since the slash is not numeric). + if (host.length() && host[host.length() - 1] == '/') + host.resize(host.length() - 1); + return host; +} + +} // namespace + +ProxyConfigServiceLinux::Delegate::~Delegate() { +} + +bool ProxyConfigServiceLinux::Delegate::GetProxyFromEnvVarForScheme( + const char* variable, ProxyServer::Scheme scheme, + ProxyServer* result_server) { + std::string env_value; + if (env_var_getter_->GetVar(variable, &env_value)) { + if (!env_value.empty()) { + env_value = FixupProxyHostScheme(scheme, env_value); + ProxyServer proxy_server = + ProxyServer::FromURI(env_value, ProxyServer::SCHEME_HTTP); + if (proxy_server.is_valid() && !proxy_server.is_direct()) { + *result_server = proxy_server; + return true; + } else { + LOG(ERROR) << "Failed to parse environment variable " << variable; + } + } + } + return false; +} + +bool ProxyConfigServiceLinux::Delegate::GetProxyFromEnvVar( + const char* variable, ProxyServer* result_server) { + return GetProxyFromEnvVarForScheme(variable, ProxyServer::SCHEME_HTTP, + result_server); +} + +bool ProxyConfigServiceLinux::Delegate::GetConfigFromEnv(ProxyConfig* config) { + // Check for automatic configuration first, in + // "auto_proxy". Possibly only the "environment_proxy" firefox + // extension has ever used this, but it still sounds like a good + // idea. + std::string auto_proxy; + if (env_var_getter_->GetVar("auto_proxy", &auto_proxy)) { + if (auto_proxy.empty()) { + // Defined and empty => autodetect + config->set_auto_detect(true); + } else { + // specified autoconfig URL + config->set_pac_url(GURL(auto_proxy)); + } + return true; + } + // "all_proxy" is a shortcut to avoid defining {http,https,ftp}_proxy. + ProxyServer proxy_server; + if (GetProxyFromEnvVar("all_proxy", &proxy_server)) { + config->proxy_rules().type = ProxyConfig::ProxyRules::TYPE_SINGLE_PROXY; + config->proxy_rules().single_proxies.SetSingleProxyServer(proxy_server); + } else { + bool have_http = GetProxyFromEnvVar("http_proxy", &proxy_server); + if (have_http) + config->proxy_rules().proxies_for_http.SetSingleProxyServer(proxy_server); + // It would be tempting to let http_proxy apply for all protocols + // if https_proxy and ftp_proxy are not defined. Googling turns up + // several documents that mention only http_proxy. But then the + // user really might not want to proxy https. And it doesn't seem + // like other apps do this. So we will refrain. + bool have_https = GetProxyFromEnvVar("https_proxy", &proxy_server); + if (have_https) + config->proxy_rules().proxies_for_https. + SetSingleProxyServer(proxy_server); + bool have_ftp = GetProxyFromEnvVar("ftp_proxy", &proxy_server); + if (have_ftp) + config->proxy_rules().proxies_for_ftp.SetSingleProxyServer(proxy_server); + if (have_http || have_https || have_ftp) { + // mustn't change type unless some rules are actually set. + config->proxy_rules().type = + ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME; + } + } + if (config->proxy_rules().empty()) { + // If the above were not defined, try for socks. + // For environment variables, we default to version 5, per the gnome + // documentation: http://library.gnome.org/devel/gnet/stable/gnet-socks.html + ProxyServer::Scheme scheme = ProxyServer::SCHEME_SOCKS5; + std::string env_version; + if (env_var_getter_->GetVar("SOCKS_VERSION", &env_version) + && env_version == "4") + scheme = ProxyServer::SCHEME_SOCKS4; + if (GetProxyFromEnvVarForScheme("SOCKS_SERVER", scheme, &proxy_server)) { + config->proxy_rules().type = ProxyConfig::ProxyRules::TYPE_SINGLE_PROXY; + config->proxy_rules().single_proxies.SetSingleProxyServer(proxy_server); + } + } + // Look for the proxy bypass list. + std::string no_proxy; + env_var_getter_->GetVar("no_proxy", &no_proxy); + if (config->proxy_rules().empty()) { + // Having only "no_proxy" set, presumably to "*", makes it + // explicit that env vars do specify a configuration: having no + // rules specified only means the user explicitly asks for direct + // connections. + return !no_proxy.empty(); + } + // Note that this uses "suffix" matching. So a bypass of "google.com" + // is understood to mean a bypass of "*google.com". + config->proxy_rules().bypass_rules.ParseFromStringUsingSuffixMatching( + no_proxy); + return true; +} + +namespace { + +const int kDebounceTimeoutMilliseconds = 250; + +#if defined(USE_GCONF) +// This setting getter uses gconf, as used in GNOME 2 and some GNOME 3 desktops. +class SettingGetterImplGConf : public ProxyConfigServiceLinux::SettingGetter { + public: + SettingGetterImplGConf() + : client_(NULL), system_proxy_id_(0), system_http_proxy_id_(0), + notify_delegate_(NULL) { + } + + virtual ~SettingGetterImplGConf() { + // client_ should have been released before now, from + // Delegate::OnDestroy(), while running on the UI thread. However + // on exiting the process, it may happen that Delegate::OnDestroy() + // task is left pending on the glib loop after the loop was quit, + // and pending tasks may then be deleted without being run. + if (client_) { + // gconf client was not cleaned up. + if (task_runner_->BelongsToCurrentThread()) { + // We are on the UI thread so we can clean it safely. This is + // the case at least for ui_tests running under Valgrind in + // bug 16076. + VLOG(1) << "~SettingGetterImplGConf: releasing gconf client"; + ShutDown(); + } else { + // This is very bad! We are deleting the setting getter but we're not on + // the UI thread. This is not supposed to happen: the setting getter is + // owned by the proxy config service's delegate, which is supposed to be + // destroyed on the UI thread only. We will get change notifications to + // a deleted object if we continue here, so fail now. + LOG(FATAL) << "~SettingGetterImplGConf: deleting on wrong thread!"; + } + } + DCHECK(!client_); + } + + virtual bool Init(base::SingleThreadTaskRunner* glib_thread_task_runner, + base::MessageLoopForIO* file_loop) OVERRIDE { + DCHECK(glib_thread_task_runner->BelongsToCurrentThread()); + DCHECK(!client_); + DCHECK(!task_runner_.get()); + task_runner_ = glib_thread_task_runner; + client_ = gconf_client_get_default(); + if (!client_) { + // It's not clear whether/when this can return NULL. + LOG(ERROR) << "Unable to create a gconf client"; + task_runner_ = NULL; + return false; + } + GError* error = NULL; + bool added_system_proxy = false; + // We need to add the directories for which we'll be asking + // for notifications, and we might as well ask to preload them. + // These need to be removed again in ShutDown(); we are careful + // here to only leave client_ non-NULL if both have been added. + gconf_client_add_dir(client_, "/system/proxy", + GCONF_CLIENT_PRELOAD_ONELEVEL, &error); + if (error == NULL) { + added_system_proxy = true; + gconf_client_add_dir(client_, "/system/http_proxy", + GCONF_CLIENT_PRELOAD_ONELEVEL, &error); + } + if (error != NULL) { + LOG(ERROR) << "Error requesting gconf directory: " << error->message; + g_error_free(error); + if (added_system_proxy) + gconf_client_remove_dir(client_, "/system/proxy", NULL); + g_object_unref(client_); + client_ = NULL; + task_runner_ = NULL; + return false; + } + return true; + } + + virtual void ShutDown() OVERRIDE { + if (client_) { + DCHECK(task_runner_->BelongsToCurrentThread()); + // We must explicitly disable gconf notifications here, because the gconf + // client will be shared between all setting getters, and they do not all + // have the same lifetimes. (For instance, incognito sessions get their + // own, which is destroyed when the session ends.) + gconf_client_notify_remove(client_, system_http_proxy_id_); + gconf_client_notify_remove(client_, system_proxy_id_); + gconf_client_remove_dir(client_, "/system/http_proxy", NULL); + gconf_client_remove_dir(client_, "/system/proxy", NULL); + g_object_unref(client_); + client_ = NULL; + task_runner_ = NULL; + } + } + + virtual bool SetUpNotifications( + ProxyConfigServiceLinux::Delegate* delegate) OVERRIDE { + DCHECK(client_); + DCHECK(task_runner_->BelongsToCurrentThread()); + GError* error = NULL; + notify_delegate_ = delegate; + // We have to keep track of the IDs returned by gconf_client_notify_add() so + // that we can remove them in ShutDown(). (Otherwise, notifications will be + // delivered to this object after it is deleted, which is bad, m'kay?) + system_proxy_id_ = gconf_client_notify_add( + client_, "/system/proxy", + OnGConfChangeNotification, this, + NULL, &error); + if (error == NULL) { + system_http_proxy_id_ = gconf_client_notify_add( + client_, "/system/http_proxy", + OnGConfChangeNotification, this, + NULL, &error); + } + if (error != NULL) { + LOG(ERROR) << "Error requesting gconf notifications: " << error->message; + g_error_free(error); + ShutDown(); + return false; + } + // Simulate a change to avoid possibly losing updates before this point. + OnChangeNotification(); + return true; + } + + virtual base::SingleThreadTaskRunner* GetNotificationTaskRunner() OVERRIDE { + return task_runner_.get(); + } + + virtual ProxyConfigSource GetConfigSource() OVERRIDE { + return PROXY_CONFIG_SOURCE_GCONF; + } + + virtual bool GetString(StringSetting key, std::string* result) OVERRIDE { + switch (key) { + case PROXY_MODE: + return GetStringByPath("/system/proxy/mode", result); + case PROXY_AUTOCONF_URL: + return GetStringByPath("/system/proxy/autoconfig_url", result); + case PROXY_HTTP_HOST: + return GetStringByPath("/system/http_proxy/host", result); + case PROXY_HTTPS_HOST: + return GetStringByPath("/system/proxy/secure_host", result); + case PROXY_FTP_HOST: + return GetStringByPath("/system/proxy/ftp_host", result); + case PROXY_SOCKS_HOST: + return GetStringByPath("/system/proxy/socks_host", result); + } + return false; // Placate compiler. + } + virtual bool GetBool(BoolSetting key, bool* result) OVERRIDE { + switch (key) { + case PROXY_USE_HTTP_PROXY: + return GetBoolByPath("/system/http_proxy/use_http_proxy", result); + case PROXY_USE_SAME_PROXY: + return GetBoolByPath("/system/http_proxy/use_same_proxy", result); + case PROXY_USE_AUTHENTICATION: + return GetBoolByPath("/system/http_proxy/use_authentication", result); + } + return false; // Placate compiler. + } + virtual bool GetInt(IntSetting key, int* result) OVERRIDE { + switch (key) { + case PROXY_HTTP_PORT: + return GetIntByPath("/system/http_proxy/port", result); + case PROXY_HTTPS_PORT: + return GetIntByPath("/system/proxy/secure_port", result); + case PROXY_FTP_PORT: + return GetIntByPath("/system/proxy/ftp_port", result); + case PROXY_SOCKS_PORT: + return GetIntByPath("/system/proxy/socks_port", result); + } + return false; // Placate compiler. + } + virtual bool GetStringList(StringListSetting key, + std::vector<std::string>* result) OVERRIDE { + switch (key) { + case PROXY_IGNORE_HOSTS: + return GetStringListByPath("/system/http_proxy/ignore_hosts", result); + } + return false; // Placate compiler. + } + + virtual bool BypassListIsReversed() OVERRIDE { + // This is a KDE-specific setting. + return false; + } + + virtual bool MatchHostsUsingSuffixMatching() OVERRIDE { + return false; + } + + private: + bool GetStringByPath(const char* key, std::string* result) { + DCHECK(client_); + DCHECK(task_runner_->BelongsToCurrentThread()); + GError* error = NULL; + gchar* value = gconf_client_get_string(client_, key, &error); + if (HandleGError(error, key)) + return false; + if (!value) + return false; + *result = value; + g_free(value); + return true; + } + bool GetBoolByPath(const char* key, bool* result) { + DCHECK(client_); + DCHECK(task_runner_->BelongsToCurrentThread()); + GError* error = NULL; + // We want to distinguish unset values from values defaulting to + // false. For that we need to use the type-generic + // gconf_client_get() rather than gconf_client_get_bool(). + GConfValue* gconf_value = gconf_client_get(client_, key, &error); + if (HandleGError(error, key)) + return false; + if (!gconf_value) { + // Unset. + return false; + } + if (gconf_value->type != GCONF_VALUE_BOOL) { + gconf_value_free(gconf_value); + return false; + } + gboolean bool_value = gconf_value_get_bool(gconf_value); + *result = static_cast<bool>(bool_value); + gconf_value_free(gconf_value); + return true; + } + bool GetIntByPath(const char* key, int* result) { + DCHECK(client_); + DCHECK(task_runner_->BelongsToCurrentThread()); + GError* error = NULL; + int value = gconf_client_get_int(client_, key, &error); + if (HandleGError(error, key)) + return false; + // We don't bother to distinguish an unset value because callers + // don't care. 0 is returned if unset. + *result = value; + return true; + } + bool GetStringListByPath(const char* key, std::vector<std::string>* result) { + DCHECK(client_); + DCHECK(task_runner_->BelongsToCurrentThread()); + GError* error = NULL; + GSList* list = gconf_client_get_list(client_, key, + GCONF_VALUE_STRING, &error); + if (HandleGError(error, key)) + return false; + if (!list) + return false; + for (GSList *it = list; it; it = it->next) { + result->push_back(static_cast<char*>(it->data)); + g_free(it->data); + } + g_slist_free(list); + return true; + } + + // Logs and frees a glib error. Returns false if there was no error + // (error is NULL). + bool HandleGError(GError* error, const char* key) { + if (error != NULL) { + LOG(ERROR) << "Error getting gconf value for " << key + << ": " << error->message; + g_error_free(error); + return true; + } + return false; + } + + // This is the callback from the debounce timer. + void OnDebouncedNotification() { + DCHECK(task_runner_->BelongsToCurrentThread()); + CHECK(notify_delegate_); + // Forward to a method on the proxy config service delegate object. + notify_delegate_->OnCheckProxyConfigSettings(); + } + + void OnChangeNotification() { + // We don't use Reset() because the timer may not yet be running. + // (In that case Stop() is a no-op.) + debounce_timer_.Stop(); + debounce_timer_.Start(FROM_HERE, + base::TimeDelta::FromMilliseconds(kDebounceTimeoutMilliseconds), + this, &SettingGetterImplGConf::OnDebouncedNotification); + } + + // gconf notification callback, dispatched on the default glib main loop. + static void OnGConfChangeNotification(GConfClient* client, guint cnxn_id, + GConfEntry* entry, gpointer user_data) { + VLOG(1) << "gconf change notification for key " + << gconf_entry_get_key(entry); + // We don't track which key has changed, just that something did change. + SettingGetterImplGConf* setting_getter = + reinterpret_cast<SettingGetterImplGConf*>(user_data); + setting_getter->OnChangeNotification(); + } + + GConfClient* client_; + // These ids are the values returned from gconf_client_notify_add(), which we + // will need in order to later call gconf_client_notify_remove(). + guint system_proxy_id_; + guint system_http_proxy_id_; + + ProxyConfigServiceLinux::Delegate* notify_delegate_; + base::OneShotTimer<SettingGetterImplGConf> debounce_timer_; + + // Task runner for the thread that we make gconf calls on. It should + // be the UI thread and all our methods should be called on this + // thread. Only for assertions. + scoped_refptr<base::SingleThreadTaskRunner> task_runner_; + + DISALLOW_COPY_AND_ASSIGN(SettingGetterImplGConf); +}; +#endif // defined(USE_GCONF) + +#if defined(USE_GIO) +// This setting getter uses gsettings, as used in most GNOME 3 desktops. +class SettingGetterImplGSettings + : public ProxyConfigServiceLinux::SettingGetter { + public: + SettingGetterImplGSettings() : + client_(NULL), + http_client_(NULL), + https_client_(NULL), + ftp_client_(NULL), + socks_client_(NULL), + notify_delegate_(NULL) { + } + + virtual ~SettingGetterImplGSettings() { + // client_ should have been released before now, from + // Delegate::OnDestroy(), while running on the UI thread. However + // on exiting the process, it may happen that + // Delegate::OnDestroy() task is left pending on the glib loop + // after the loop was quit, and pending tasks may then be deleted + // without being run. + if (client_) { + // gconf client was not cleaned up. + if (task_runner_->BelongsToCurrentThread()) { + // We are on the UI thread so we can clean it safely. This is + // the case at least for ui_tests running under Valgrind in + // bug 16076. + VLOG(1) << "~SettingGetterImplGSettings: releasing gsettings client"; + ShutDown(); + } else { + LOG(WARNING) << "~SettingGetterImplGSettings: leaking gsettings client"; + client_ = NULL; + } + } + DCHECK(!client_); + } + + bool SchemaExists(const char* schema_name) { + const gchar* const* schemas = libgio_loader_.g_settings_list_schemas(); + while (*schemas) { + if (strcmp(schema_name, static_cast<const char*>(*schemas)) == 0) + return true; + schemas++; + } + return false; + } + + // LoadAndCheckVersion() must be called *before* Init()! + bool LoadAndCheckVersion(base::Environment* env); + + virtual bool Init(base::SingleThreadTaskRunner* glib_thread_task_runner, + base::MessageLoopForIO* file_loop) OVERRIDE { + DCHECK(glib_thread_task_runner->BelongsToCurrentThread()); + DCHECK(!client_); + DCHECK(!task_runner_.get()); + + if (!SchemaExists("org.gnome.system.proxy") || + !(client_ = libgio_loader_.g_settings_new("org.gnome.system.proxy"))) { + // It's not clear whether/when this can return NULL. + LOG(ERROR) << "Unable to create a gsettings client"; + return false; + } + task_runner_ = glib_thread_task_runner; + // We assume these all work if the above call worked. + http_client_ = libgio_loader_.g_settings_get_child(client_, "http"); + https_client_ = libgio_loader_.g_settings_get_child(client_, "https"); + ftp_client_ = libgio_loader_.g_settings_get_child(client_, "ftp"); + socks_client_ = libgio_loader_.g_settings_get_child(client_, "socks"); + DCHECK(http_client_ && https_client_ && ftp_client_ && socks_client_); + return true; + } + + virtual void ShutDown() OVERRIDE { + if (client_) { + DCHECK(task_runner_->BelongsToCurrentThread()); + // This also disables gsettings notifications. + g_object_unref(socks_client_); + g_object_unref(ftp_client_); + g_object_unref(https_client_); + g_object_unref(http_client_); + g_object_unref(client_); + // We only need to null client_ because it's the only one that we check. + client_ = NULL; + task_runner_ = NULL; + } + } + + virtual bool SetUpNotifications( + ProxyConfigServiceLinux::Delegate* delegate) OVERRIDE { + DCHECK(client_); + DCHECK(task_runner_->BelongsToCurrentThread()); + notify_delegate_ = delegate; + // We could watch for the change-event signal instead of changed, but + // since we have to watch more than one object, we'd still have to + // debounce change notifications. This is conceptually simpler. + g_signal_connect(G_OBJECT(client_), "changed", + G_CALLBACK(OnGSettingsChangeNotification), this); + g_signal_connect(G_OBJECT(http_client_), "changed", + G_CALLBACK(OnGSettingsChangeNotification), this); + g_signal_connect(G_OBJECT(https_client_), "changed", + G_CALLBACK(OnGSettingsChangeNotification), this); + g_signal_connect(G_OBJECT(ftp_client_), "changed", + G_CALLBACK(OnGSettingsChangeNotification), this); + g_signal_connect(G_OBJECT(socks_client_), "changed", + G_CALLBACK(OnGSettingsChangeNotification), this); + // Simulate a change to avoid possibly losing updates before this point. + OnChangeNotification(); + return true; + } + + virtual base::SingleThreadTaskRunner* GetNotificationTaskRunner() OVERRIDE { + return task_runner_.get(); + } + + virtual ProxyConfigSource GetConfigSource() OVERRIDE { + return PROXY_CONFIG_SOURCE_GSETTINGS; + } + + virtual bool GetString(StringSetting key, std::string* result) OVERRIDE { + DCHECK(client_); + switch (key) { + case PROXY_MODE: + return GetStringByPath(client_, "mode", result); + case PROXY_AUTOCONF_URL: + return GetStringByPath(client_, "autoconfig-url", result); + case PROXY_HTTP_HOST: + return GetStringByPath(http_client_, "host", result); + case PROXY_HTTPS_HOST: + return GetStringByPath(https_client_, "host", result); + case PROXY_FTP_HOST: + return GetStringByPath(ftp_client_, "host", result); + case PROXY_SOCKS_HOST: + return GetStringByPath(socks_client_, "host", result); + } + return false; // Placate compiler. + } + virtual bool GetBool(BoolSetting key, bool* result) OVERRIDE { + DCHECK(client_); + switch (key) { + case PROXY_USE_HTTP_PROXY: + // Although there is an "enabled" boolean in http_client_, it is not set + // to true by the proxy config utility. We ignore it and return false. + return false; + case PROXY_USE_SAME_PROXY: + // Similarly, although there is a "use-same-proxy" boolean in client_, + // it is never set to false by the proxy config utility. We ignore it. + return false; + case PROXY_USE_AUTHENTICATION: + // There is also no way to set this in the proxy config utility, but it + // doesn't hurt us to get the actual setting (unlike the two above). + return GetBoolByPath(http_client_, "use-authentication", result); + } + return false; // Placate compiler. + } + virtual bool GetInt(IntSetting key, int* result) OVERRIDE { + DCHECK(client_); + switch (key) { + case PROXY_HTTP_PORT: + return GetIntByPath(http_client_, "port", result); + case PROXY_HTTPS_PORT: + return GetIntByPath(https_client_, "port", result); + case PROXY_FTP_PORT: + return GetIntByPath(ftp_client_, "port", result); + case PROXY_SOCKS_PORT: + return GetIntByPath(socks_client_, "port", result); + } + return false; // Placate compiler. + } + virtual bool GetStringList(StringListSetting key, + std::vector<std::string>* result) OVERRIDE { + DCHECK(client_); + switch (key) { + case PROXY_IGNORE_HOSTS: + return GetStringListByPath(client_, "ignore-hosts", result); + } + return false; // Placate compiler. + } + + virtual bool BypassListIsReversed() OVERRIDE { + // This is a KDE-specific setting. + return false; + } + + virtual bool MatchHostsUsingSuffixMatching() OVERRIDE { + return false; + } + + private: + bool GetStringByPath(GSettings* client, const char* key, + std::string* result) { + DCHECK(task_runner_->BelongsToCurrentThread()); + gchar* value = libgio_loader_.g_settings_get_string(client, key); + if (!value) + return false; + *result = value; + g_free(value); + return true; + } + bool GetBoolByPath(GSettings* client, const char* key, bool* result) { + DCHECK(task_runner_->BelongsToCurrentThread()); + *result = static_cast<bool>( + libgio_loader_.g_settings_get_boolean(client, key)); + return true; + } + bool GetIntByPath(GSettings* client, const char* key, int* result) { + DCHECK(task_runner_->BelongsToCurrentThread()); + *result = libgio_loader_.g_settings_get_int(client, key); + return true; + } + bool GetStringListByPath(GSettings* client, const char* key, + std::vector<std::string>* result) { + DCHECK(task_runner_->BelongsToCurrentThread()); + gchar** list = libgio_loader_.g_settings_get_strv(client, key); + if (!list) + return false; + for (size_t i = 0; list[i]; ++i) { + result->push_back(static_cast<char*>(list[i])); + g_free(list[i]); + } + g_free(list); + return true; + } + + // This is the callback from the debounce timer. + void OnDebouncedNotification() { + DCHECK(task_runner_->BelongsToCurrentThread()); + CHECK(notify_delegate_); + // Forward to a method on the proxy config service delegate object. + notify_delegate_->OnCheckProxyConfigSettings(); + } + + void OnChangeNotification() { + // We don't use Reset() because the timer may not yet be running. + // (In that case Stop() is a no-op.) + debounce_timer_.Stop(); + debounce_timer_.Start(FROM_HERE, + base::TimeDelta::FromMilliseconds(kDebounceTimeoutMilliseconds), + this, &SettingGetterImplGSettings::OnDebouncedNotification); + } + + // gsettings notification callback, dispatched on the default glib main loop. + static void OnGSettingsChangeNotification(GSettings* client, gchar* key, + gpointer user_data) { + VLOG(1) << "gsettings change notification for key " << key; + // We don't track which key has changed, just that something did change. + SettingGetterImplGSettings* setting_getter = + reinterpret_cast<SettingGetterImplGSettings*>(user_data); + setting_getter->OnChangeNotification(); + } + + GSettings* client_; + GSettings* http_client_; + GSettings* https_client_; + GSettings* ftp_client_; + GSettings* socks_client_; + ProxyConfigServiceLinux::Delegate* notify_delegate_; + base::OneShotTimer<SettingGetterImplGSettings> debounce_timer_; + + // Task runner for the thread that we make gsettings calls on. It should + // be the UI thread and all our methods should be called on this + // thread. Only for assertions. + scoped_refptr<base::SingleThreadTaskRunner> task_runner_; + + LibGioLoader libgio_loader_; + + DISALLOW_COPY_AND_ASSIGN(SettingGetterImplGSettings); +}; + +bool SettingGetterImplGSettings::LoadAndCheckVersion( + base::Environment* env) { + // LoadAndCheckVersion() must be called *before* Init()! + DCHECK(!client_); + + // The APIs to query gsettings were introduced after the minimum glib + // version we target, so we can't link directly against them. We load them + // dynamically at runtime, and if they don't exist, return false here. (We + // support linking directly via gyp flags though.) Additionally, even when + // they are present, we do two additional checks to make sure we should use + // them and not gconf. First, we attempt to load the schema for proxy + // settings. Second, we check for the program that was used in older + // versions of GNOME to configure proxy settings, and return false if it + // exists. Some distributions (e.g. Ubuntu 11.04) have the API and schema + // but don't use gsettings for proxy settings, but they do have the old + // binary, so we detect these systems that way. + + { + // TODO(phajdan.jr): Redesign the code to load library on different thread. + base::ThreadRestrictions::ScopedAllowIO allow_io; + + // Try also without .0 at the end; on some systems this may be required. + if (!libgio_loader_.Load("libgio-2.0.so.0") && + !libgio_loader_.Load("libgio-2.0.so")) { + VLOG(1) << "Cannot load gio library. Will fall back to gconf."; + return false; + } + } + + GSettings* client; + if (!SchemaExists("org.gnome.system.proxy") || + !(client = libgio_loader_.g_settings_new("org.gnome.system.proxy"))) { + VLOG(1) << "Cannot create gsettings client. Will fall back to gconf."; + return false; + } + g_object_unref(client); + + std::string path; + if (!env->GetVar("PATH", &path)) { + LOG(ERROR) << "No $PATH variable. Assuming no gnome-network-properties."; + } else { + // Yes, we're on the UI thread. Yes, we're accessing the file system. + // Sadly, we don't have much choice. We need the proxy settings and we + // need them now, and to figure out where to get them, we have to check + // for this binary. See http://crbug.com/69057 for additional details. + base::ThreadRestrictions::ScopedAllowIO allow_io; + std::vector<std::string> paths; + Tokenize(path, ":", &paths); + for (size_t i = 0; i < paths.size(); ++i) { + base::FilePath file(paths[i]); + if (base::PathExists(file.Append("gnome-network-properties"))) { + VLOG(1) << "Found gnome-network-properties. Will fall back to gconf."; + return false; + } + } + } + + VLOG(1) << "All gsettings tests OK. Will get proxy config from gsettings."; + return true; +} +#endif // defined(USE_GIO) + +// This is the KDE version that reads kioslaverc and simulates gconf. +// Doing this allows the main Delegate code, as well as the unit tests +// for it, to stay the same - and the settings map fairly well besides. +class SettingGetterImplKDE : public ProxyConfigServiceLinux::SettingGetter, + public base::MessagePumpLibevent::Watcher { + public: + explicit SettingGetterImplKDE(base::Environment* env_var_getter) + : inotify_fd_(-1), notify_delegate_(NULL), indirect_manual_(false), + auto_no_pac_(false), reversed_bypass_list_(false), + env_var_getter_(env_var_getter), file_loop_(NULL) { + // This has to be called on the UI thread (http://crbug.com/69057). + base::ThreadRestrictions::ScopedAllowIO allow_io; + + // Derive the location of the kde config dir from the environment. + std::string home; + if (env_var_getter->GetVar("KDEHOME", &home) && !home.empty()) { + // $KDEHOME is set. Use it unconditionally. + kde_config_dir_ = KDEHomeToConfigPath(base::FilePath(home)); + } else { + // $KDEHOME is unset. Try to figure out what to use. This seems to be + // the common case on most distributions. + if (!env_var_getter->GetVar(base::env_vars::kHome, &home)) + // User has no $HOME? Give up. Later we'll report the failure. + return; + if (base::nix::GetDesktopEnvironment(env_var_getter) == + base::nix::DESKTOP_ENVIRONMENT_KDE3) { + // KDE3 always uses .kde for its configuration. + base::FilePath kde_path = base::FilePath(home).Append(".kde"); + kde_config_dir_ = KDEHomeToConfigPath(kde_path); + } else { + // Some distributions patch KDE4 to use .kde4 instead of .kde, so that + // both can be installed side-by-side. Sadly they don't all do this, and + // they don't always do this: some distributions have started switching + // back as well. So if there is a .kde4 directory, check the timestamps + // of the config directories within and use the newest one. + // Note that we should currently be running in the UI thread, because in + // the gconf version, that is the only thread that can access the proxy + // settings (a gconf restriction). As noted below, the initial read of + // the proxy settings will be done in this thread anyway, so we check + // for .kde4 here in this thread as well. + base::FilePath kde3_path = base::FilePath(home).Append(".kde"); + base::FilePath kde3_config = KDEHomeToConfigPath(kde3_path); + base::FilePath kde4_path = base::FilePath(home).Append(".kde4"); + base::FilePath kde4_config = KDEHomeToConfigPath(kde4_path); + bool use_kde4 = false; + if (base::DirectoryExists(kde4_path)) { + base::PlatformFileInfo kde3_info; + base::PlatformFileInfo kde4_info; + if (file_util::GetFileInfo(kde4_config, &kde4_info)) { + if (file_util::GetFileInfo(kde3_config, &kde3_info)) { + use_kde4 = kde4_info.last_modified >= kde3_info.last_modified; + } else { + use_kde4 = true; + } + } + } + if (use_kde4) { + kde_config_dir_ = KDEHomeToConfigPath(kde4_path); + } else { + kde_config_dir_ = KDEHomeToConfigPath(kde3_path); + } + } + } + } + + virtual ~SettingGetterImplKDE() { + // inotify_fd_ should have been closed before now, from + // Delegate::OnDestroy(), while running on the file thread. However + // on exiting the process, it may happen that Delegate::OnDestroy() + // task is left pending on the file loop after the loop was quit, + // and pending tasks may then be deleted without being run. + // Here in the KDE version, we can safely close the file descriptor + // anyway. (Not that it really matters; the process is exiting.) + if (inotify_fd_ >= 0) + ShutDown(); + DCHECK(inotify_fd_ < 0); + } + + virtual bool Init(base::SingleThreadTaskRunner* glib_thread_task_runner, + base::MessageLoopForIO* file_loop) OVERRIDE { + // This has to be called on the UI thread (http://crbug.com/69057). + base::ThreadRestrictions::ScopedAllowIO allow_io; + DCHECK(inotify_fd_ < 0); + inotify_fd_ = inotify_init(); + if (inotify_fd_ < 0) { + PLOG(ERROR) << "inotify_init failed"; + return false; + } + int flags = fcntl(inotify_fd_, F_GETFL); + if (fcntl(inotify_fd_, F_SETFL, flags | O_NONBLOCK) < 0) { + PLOG(ERROR) << "fcntl failed"; + close(inotify_fd_); + inotify_fd_ = -1; + return false; + } + file_loop_ = file_loop; + // The initial read is done on the current thread, not |file_loop_|, + // since we will need to have it for SetUpAndFetchInitialConfig(). + UpdateCachedSettings(); + return true; + } + + virtual void ShutDown() OVERRIDE { + if (inotify_fd_ >= 0) { + ResetCachedSettings(); + inotify_watcher_.StopWatchingFileDescriptor(); + close(inotify_fd_); + inotify_fd_ = -1; + } + } + + virtual bool SetUpNotifications( + ProxyConfigServiceLinux::Delegate* delegate) OVERRIDE { + DCHECK(inotify_fd_ >= 0); + DCHECK(base::MessageLoop::current() == file_loop_); + // We can't just watch the kioslaverc file directly, since KDE will write + // a new copy of it and then rename it whenever settings are changed and + // inotify watches inodes (so we'll be watching the old deleted file after + // the first change, and it will never change again). So, we watch the + // directory instead. We then act only on changes to the kioslaverc entry. + if (inotify_add_watch(inotify_fd_, kde_config_dir_.value().c_str(), + IN_MODIFY | IN_MOVED_TO) < 0) + return false; + notify_delegate_ = delegate; + if (!file_loop_->WatchFileDescriptor(inotify_fd_, + true, + base::MessageLoopForIO::WATCH_READ, + &inotify_watcher_, + this)) + return false; + // Simulate a change to avoid possibly losing updates before this point. + OnChangeNotification(); + return true; + } + + virtual base::SingleThreadTaskRunner* GetNotificationTaskRunner() OVERRIDE { + return file_loop_ ? file_loop_->message_loop_proxy().get() : NULL; + } + + // Implement base::MessagePumpLibevent::Watcher. + virtual void OnFileCanReadWithoutBlocking(int fd) OVERRIDE { + DCHECK_EQ(fd, inotify_fd_); + DCHECK(base::MessageLoop::current() == file_loop_); + OnChangeNotification(); + } + virtual void OnFileCanWriteWithoutBlocking(int fd) OVERRIDE { + NOTREACHED(); + } + + virtual ProxyConfigSource GetConfigSource() OVERRIDE { + return PROXY_CONFIG_SOURCE_KDE; + } + + virtual bool GetString(StringSetting key, std::string* result) OVERRIDE { + string_map_type::iterator it = string_table_.find(key); + if (it == string_table_.end()) + return false; + *result = it->second; + return true; + } + virtual bool GetBool(BoolSetting key, bool* result) OVERRIDE { + // We don't ever have any booleans. + return false; + } + virtual bool GetInt(IntSetting key, int* result) OVERRIDE { + // We don't ever have any integers. (See AddProxy() below about ports.) + return false; + } + virtual bool GetStringList(StringListSetting key, + std::vector<std::string>* result) OVERRIDE { + strings_map_type::iterator it = strings_table_.find(key); + if (it == strings_table_.end()) + return false; + *result = it->second; + return true; + } + + virtual bool BypassListIsReversed() OVERRIDE { + return reversed_bypass_list_; + } + + virtual bool MatchHostsUsingSuffixMatching() OVERRIDE { + return true; + } + + private: + void ResetCachedSettings() { + string_table_.clear(); + strings_table_.clear(); + indirect_manual_ = false; + auto_no_pac_ = false; + reversed_bypass_list_ = false; + } + + base::FilePath KDEHomeToConfigPath(const base::FilePath& kde_home) { + return kde_home.Append("share").Append("config"); + } + + void AddProxy(StringSetting host_key, const std::string& value) { + if (value.empty() || value.substr(0, 3) == "//:") + // No proxy. + return; + size_t space = value.find(' '); + if (space != std::string::npos) { + // Newer versions of KDE use a space rather than a colon to separate the + // port number from the hostname. If we find this, we need to convert it. + std::string fixed = value; + fixed[space] = ':'; + string_table_[host_key] = fixed; + } else { + // We don't need to parse the port number out; GetProxyFromSettings() + // would only append it right back again. So we just leave the port + // number right in the host string. + string_table_[host_key] = value; + } + } + + void AddHostList(StringListSetting key, const std::string& value) { + std::vector<std::string> tokens; + base::StringTokenizer tk(value, ", "); + while (tk.GetNext()) { + std::string token = tk.token(); + if (!token.empty()) + tokens.push_back(token); + } + strings_table_[key] = tokens; + } + + void AddKDESetting(const std::string& key, const std::string& value) { + if (key == "ProxyType") { + const char* mode = "none"; + indirect_manual_ = false; + auto_no_pac_ = false; + int int_value; + base::StringToInt(value, &int_value); + switch (int_value) { + case 0: // No proxy, or maybe kioslaverc syntax error. + break; + case 1: // Manual configuration. + mode = "manual"; + break; + case 2: // PAC URL. + mode = "auto"; + break; + case 3: // WPAD. + mode = "auto"; + auto_no_pac_ = true; + break; + case 4: // Indirect manual via environment variables. + mode = "manual"; + indirect_manual_ = true; + break; + } + string_table_[PROXY_MODE] = mode; + } else if (key == "Proxy Config Script") { + string_table_[PROXY_AUTOCONF_URL] = value; + } else if (key == "httpProxy") { + AddProxy(PROXY_HTTP_HOST, value); + } else if (key == "httpsProxy") { + AddProxy(PROXY_HTTPS_HOST, value); + } else if (key == "ftpProxy") { + AddProxy(PROXY_FTP_HOST, value); + } else if (key == "socksProxy") { + // Older versions of KDE configure SOCKS in a weird way involving + // LD_PRELOAD and a library that intercepts network calls to SOCKSify + // them. We don't support it. KDE 4.8 added a proper SOCKS setting. + AddProxy(PROXY_SOCKS_HOST, value); + } else if (key == "ReversedException") { + // We count "true" or any nonzero number as true, otherwise false. + // Note that if the value is not actually numeric StringToInt() + // will return 0, which we count as false. + int int_value; + base::StringToInt(value, &int_value); + reversed_bypass_list_ = (value == "true" || int_value); + } else if (key == "NoProxyFor") { + AddHostList(PROXY_IGNORE_HOSTS, value); + } else if (key == "AuthMode") { + // Check for authentication, just so we can warn. + int mode; + base::StringToInt(value, &mode); + if (mode) { + // ProxyConfig does not support authentication parameters, but + // Chrome will prompt for the password later. So we ignore this. + LOG(WARNING) << + "Proxy authentication parameters ignored, see bug 16709"; + } + } + } + + void ResolveIndirect(StringSetting key) { + string_map_type::iterator it = string_table_.find(key); + if (it != string_table_.end()) { + std::string value; + if (env_var_getter_->GetVar(it->second.c_str(), &value)) + it->second = value; + else + string_table_.erase(it); + } + } + + void ResolveIndirectList(StringListSetting key) { + strings_map_type::iterator it = strings_table_.find(key); + if (it != strings_table_.end()) { + std::string value; + if (!it->second.empty() && + env_var_getter_->GetVar(it->second[0].c_str(), &value)) + AddHostList(key, value); + else + strings_table_.erase(it); + } + } + + // The settings in kioslaverc could occur in any order, but some affect + // others. Rather than read the whole file in and then query them in an + // order that allows us to handle that, we read the settings in whatever + // order they occur and do any necessary tweaking after we finish. + void ResolveModeEffects() { + if (indirect_manual_) { + ResolveIndirect(PROXY_HTTP_HOST); + ResolveIndirect(PROXY_HTTPS_HOST); + ResolveIndirect(PROXY_FTP_HOST); + ResolveIndirectList(PROXY_IGNORE_HOSTS); + } + if (auto_no_pac_) { + // Remove the PAC URL; we're not supposed to use it. + string_table_.erase(PROXY_AUTOCONF_URL); + } + } + + // Reads kioslaverc one line at a time and calls AddKDESetting() to add + // each relevant name-value pair to the appropriate value table. + void UpdateCachedSettings() { + base::FilePath kioslaverc = kde_config_dir_.Append("kioslaverc"); + file_util::ScopedFILE input(file_util::OpenFile(kioslaverc, "r")); + if (!input.get()) + return; + ResetCachedSettings(); + bool in_proxy_settings = false; + bool line_too_long = false; + char line[BUFFER_SIZE]; + // fgets() will return NULL on EOF or error. + while (fgets(line, sizeof(line), input.get())) { + // fgets() guarantees the line will be properly terminated. + size_t length = strlen(line); + if (!length) + continue; + // This should be true even with CRLF endings. + if (line[length - 1] != '\n') { + line_too_long = true; + continue; + } + if (line_too_long) { + // The previous line had no line ending, but this done does. This is + // the end of the line that was too long, so warn here and skip it. + LOG(WARNING) << "skipped very long line in " << kioslaverc.value(); + line_too_long = false; + continue; + } + // Remove the LF at the end, and the CR if there is one. + line[--length] = '\0'; + if (length && line[length - 1] == '\r') + line[--length] = '\0'; + // Now parse the line. + if (line[0] == '[') { + // Switching sections. All we care about is whether this is + // the (a?) proxy settings section, for both KDE3 and KDE4. + in_proxy_settings = !strncmp(line, "[Proxy Settings]", 16); + } else if (in_proxy_settings) { + // A regular line, in the (a?) proxy settings section. + char* split = strchr(line, '='); + // Skip this line if it does not contain an = sign. + if (!split) + continue; + // Split the line on the = and advance |split|. + *(split++) = 0; + std::string key = line; + std::string value = split; + TrimWhitespaceASCII(key, TRIM_ALL, &key); + TrimWhitespaceASCII(value, TRIM_ALL, &value); + // Skip this line if the key name is empty. + if (key.empty()) + continue; + // Is the value name localized? + if (key[key.length() - 1] == ']') { + // Find the matching bracket. + length = key.rfind('['); + // Skip this line if the localization indicator is malformed. + if (length == std::string::npos) + continue; + // Trim the localization indicator off. + key.resize(length); + // Remove any resulting trailing whitespace. + TrimWhitespaceASCII(key, TRIM_TRAILING, &key); + // Skip this line if the key name is now empty. + if (key.empty()) + continue; + } + // Now fill in the tables. + AddKDESetting(key, value); + } + } + if (ferror(input.get())) + LOG(ERROR) << "error reading " << kioslaverc.value(); + ResolveModeEffects(); + } + + // This is the callback from the debounce timer. + void OnDebouncedNotification() { + DCHECK(base::MessageLoop::current() == file_loop_); + VLOG(1) << "inotify change notification for kioslaverc"; + UpdateCachedSettings(); + CHECK(notify_delegate_); + // Forward to a method on the proxy config service delegate object. + notify_delegate_->OnCheckProxyConfigSettings(); + } + + // Called by OnFileCanReadWithoutBlocking() on the file thread. Reads + // from the inotify file descriptor and starts up a debounce timer if + // an event for kioslaverc is seen. + void OnChangeNotification() { + DCHECK_GE(inotify_fd_, 0); + DCHECK(base::MessageLoop::current() == file_loop_); + char event_buf[(sizeof(inotify_event) + NAME_MAX + 1) * 4]; + bool kioslaverc_touched = false; + ssize_t r; + while ((r = read(inotify_fd_, event_buf, sizeof(event_buf))) > 0) { + // inotify returns variable-length structures, which is why we have + // this strange-looking loop instead of iterating through an array. + char* event_ptr = event_buf; + while (event_ptr < event_buf + r) { + inotify_event* event = reinterpret_cast<inotify_event*>(event_ptr); + // The kernel always feeds us whole events. + CHECK_LE(event_ptr + sizeof(inotify_event), event_buf + r); + CHECK_LE(event->name + event->len, event_buf + r); + if (!strcmp(event->name, "kioslaverc")) + kioslaverc_touched = true; + // Advance the pointer just past the end of the filename. + event_ptr = event->name + event->len; + } + // We keep reading even if |kioslaverc_touched| is true to drain the + // inotify event queue. + } + if (!r) + // Instead of returning -1 and setting errno to EINVAL if there is not + // enough buffer space, older kernels (< 2.6.21) return 0. Simulate the + // new behavior (EINVAL) so we can reuse the code below. + errno = EINVAL; + if (errno != EAGAIN) { + PLOG(WARNING) << "error reading inotify file descriptor"; + if (errno == EINVAL) { + // Our buffer is not large enough to read the next event. This should + // not happen (because its size is calculated to always be sufficiently + // large), but if it does we'd warn continuously since |inotify_fd_| + // would be forever ready to read. Close it and stop watching instead. + LOG(ERROR) << "inotify failure; no longer watching kioslaverc!"; + inotify_watcher_.StopWatchingFileDescriptor(); + close(inotify_fd_); + inotify_fd_ = -1; + } + } + if (kioslaverc_touched) { + // We don't use Reset() because the timer may not yet be running. + // (In that case Stop() is a no-op.) + debounce_timer_.Stop(); + debounce_timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds( + kDebounceTimeoutMilliseconds), this, + &SettingGetterImplKDE::OnDebouncedNotification); + } + } + + typedef std::map<StringSetting, std::string> string_map_type; + typedef std::map<StringListSetting, + std::vector<std::string> > strings_map_type; + + int inotify_fd_; + base::MessagePumpLibevent::FileDescriptorWatcher inotify_watcher_; + ProxyConfigServiceLinux::Delegate* notify_delegate_; + base::OneShotTimer<SettingGetterImplKDE> debounce_timer_; + base::FilePath kde_config_dir_; + bool indirect_manual_; + bool auto_no_pac_; + bool reversed_bypass_list_; + // We don't own |env_var_getter_|. It's safe to hold a pointer to it, since + // both it and us are owned by ProxyConfigServiceLinux::Delegate, and have the + // same lifetime. + base::Environment* env_var_getter_; + + // We cache these settings whenever we re-read the kioslaverc file. + string_map_type string_table_; + strings_map_type strings_table_; + + // Message loop of the file thread, for reading kioslaverc. If NULL, + // just read it directly (for testing). We also handle inotify events + // on this thread. + base::MessageLoopForIO* file_loop_; + + DISALLOW_COPY_AND_ASSIGN(SettingGetterImplKDE); +}; + +} // namespace + +bool ProxyConfigServiceLinux::Delegate::GetProxyFromSettings( + SettingGetter::StringSetting host_key, + ProxyServer* result_server) { + std::string host; + if (!setting_getter_->GetString(host_key, &host) || host.empty()) { + // Unset or empty. + return false; + } + // Check for an optional port. + int port = 0; + SettingGetter::IntSetting port_key = + SettingGetter::HostSettingToPortSetting(host_key); + setting_getter_->GetInt(port_key, &port); + if (port != 0) { + // If a port is set and non-zero: + host += ":" + base::IntToString(port); + } + + // gconf settings do not appear to distinguish between SOCKS version. We + // default to version 5. For more information on this policy decision, see: + // http://code.google.com/p/chromium/issues/detail?id=55912#c2 + ProxyServer::Scheme scheme = (host_key == SettingGetter::PROXY_SOCKS_HOST) ? + ProxyServer::SCHEME_SOCKS5 : ProxyServer::SCHEME_HTTP; + host = FixupProxyHostScheme(scheme, host); + ProxyServer proxy_server = ProxyServer::FromURI(host, + ProxyServer::SCHEME_HTTP); + if (proxy_server.is_valid()) { + *result_server = proxy_server; + return true; + } + return false; +} + +bool ProxyConfigServiceLinux::Delegate::GetConfigFromSettings( + ProxyConfig* config) { + std::string mode; + if (!setting_getter_->GetString(SettingGetter::PROXY_MODE, &mode)) { + // We expect this to always be set, so if we don't see it then we + // probably have a gconf/gsettings problem, and so we don't have a valid + // proxy config. + return false; + } + if (mode == "none") { + // Specifically specifies no proxy. + return true; + } + + if (mode == "auto") { + // Automatic proxy config. + std::string pac_url_str; + if (setting_getter_->GetString(SettingGetter::PROXY_AUTOCONF_URL, + &pac_url_str)) { + if (!pac_url_str.empty()) { + // If the PAC URL is actually a file path, then put file:// in front. + if (pac_url_str[0] == '/') + pac_url_str = "file://" + pac_url_str; + GURL pac_url(pac_url_str); + if (!pac_url.is_valid()) + return false; + config->set_pac_url(pac_url); + return true; + } + } + config->set_auto_detect(true); + return true; + } + + if (mode != "manual") { + // Mode is unrecognized. + return false; + } + bool use_http_proxy; + if (setting_getter_->GetBool(SettingGetter::PROXY_USE_HTTP_PROXY, + &use_http_proxy) + && !use_http_proxy) { + // Another master switch for some reason. If set to false, then no + // proxy. But we don't panic if the key doesn't exist. + return true; + } + + bool same_proxy = false; + // Indicates to use the http proxy for all protocols. This one may + // not exist (presumably on older versions); we assume false in that + // case. + setting_getter_->GetBool(SettingGetter::PROXY_USE_SAME_PROXY, + &same_proxy); + + ProxyServer proxy_for_http; + ProxyServer proxy_for_https; + ProxyServer proxy_for_ftp; + ProxyServer socks_proxy; // (socks) + + // This counts how many of the above ProxyServers were defined and valid. + size_t num_proxies_specified = 0; + + // Extract the per-scheme proxies. If we failed to parse it, or no proxy was + // specified for the scheme, then the resulting ProxyServer will be invalid. + if (GetProxyFromSettings(SettingGetter::PROXY_HTTP_HOST, &proxy_for_http)) + num_proxies_specified++; + if (GetProxyFromSettings(SettingGetter::PROXY_HTTPS_HOST, &proxy_for_https)) + num_proxies_specified++; + if (GetProxyFromSettings(SettingGetter::PROXY_FTP_HOST, &proxy_for_ftp)) + num_proxies_specified++; + if (GetProxyFromSettings(SettingGetter::PROXY_SOCKS_HOST, &socks_proxy)) + num_proxies_specified++; + + if (same_proxy) { + if (proxy_for_http.is_valid()) { + // Use the http proxy for all schemes. + config->proxy_rules().type = ProxyConfig::ProxyRules::TYPE_SINGLE_PROXY; + config->proxy_rules().single_proxies.SetSingleProxyServer(proxy_for_http); + } + } else if (num_proxies_specified > 0) { + if (socks_proxy.is_valid() && num_proxies_specified == 1) { + // If the only proxy specified was for SOCKS, use it for all schemes. + config->proxy_rules().type = ProxyConfig::ProxyRules::TYPE_SINGLE_PROXY; + config->proxy_rules().single_proxies.SetSingleProxyServer(socks_proxy); + } else { + // Otherwise use the indicated proxies per-scheme. + config->proxy_rules().type = + ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME; + config->proxy_rules().proxies_for_http. + SetSingleProxyServer(proxy_for_http); + config->proxy_rules().proxies_for_https. + SetSingleProxyServer(proxy_for_https); + config->proxy_rules().proxies_for_ftp.SetSingleProxyServer(proxy_for_ftp); + config->proxy_rules().fallback_proxies.SetSingleProxyServer(socks_proxy); + } + } + + if (config->proxy_rules().empty()) { + // Manual mode but we couldn't parse any rules. + return false; + } + + // Check for authentication, just so we can warn. + bool use_auth = false; + setting_getter_->GetBool(SettingGetter::PROXY_USE_AUTHENTICATION, + &use_auth); + if (use_auth) { + // ProxyConfig does not support authentication parameters, but + // Chrome will prompt for the password later. So we ignore + // /system/http_proxy/*auth* settings. + LOG(WARNING) << "Proxy authentication parameters ignored, see bug 16709"; + } + + // Now the bypass list. + std::vector<std::string> ignore_hosts_list; + config->proxy_rules().bypass_rules.Clear(); + if (setting_getter_->GetStringList(SettingGetter::PROXY_IGNORE_HOSTS, + &ignore_hosts_list)) { + std::vector<std::string>::const_iterator it(ignore_hosts_list.begin()); + for (; it != ignore_hosts_list.end(); ++it) { + if (setting_getter_->MatchHostsUsingSuffixMatching()) { + config->proxy_rules().bypass_rules. + AddRuleFromStringUsingSuffixMatching(*it); + } else { + config->proxy_rules().bypass_rules.AddRuleFromString(*it); + } + } + } + // Note that there are no settings with semantics corresponding to + // bypass of local names in GNOME. In KDE, "<local>" is supported + // as a hostname rule. + + // KDE allows one to reverse the bypass rules. + config->proxy_rules().reverse_bypass = + setting_getter_->BypassListIsReversed(); + + return true; +} + +ProxyConfigServiceLinux::Delegate::Delegate(base::Environment* env_var_getter) + : env_var_getter_(env_var_getter) { + // Figure out which SettingGetterImpl to use, if any. + switch (base::nix::GetDesktopEnvironment(env_var_getter)) { + case base::nix::DESKTOP_ENVIRONMENT_GNOME: + case base::nix::DESKTOP_ENVIRONMENT_UNITY: +#if defined(USE_GIO) + { + scoped_ptr<SettingGetterImplGSettings> gs_getter( + new SettingGetterImplGSettings()); + // We have to load symbols and check the GNOME version in use to decide + // if we should use the gsettings getter. See LoadAndCheckVersion(). + if (gs_getter->LoadAndCheckVersion(env_var_getter)) + setting_getter_.reset(gs_getter.release()); + } +#endif +#if defined(USE_GCONF) + // Fall back on gconf if gsettings is unavailable or incorrect. + if (!setting_getter_.get()) + setting_getter_.reset(new SettingGetterImplGConf()); +#endif + break; + case base::nix::DESKTOP_ENVIRONMENT_KDE3: + case base::nix::DESKTOP_ENVIRONMENT_KDE4: + setting_getter_.reset(new SettingGetterImplKDE(env_var_getter)); + break; + case base::nix::DESKTOP_ENVIRONMENT_XFCE: + case base::nix::DESKTOP_ENVIRONMENT_OTHER: + break; + } +} + +ProxyConfigServiceLinux::Delegate::Delegate( + base::Environment* env_var_getter, SettingGetter* setting_getter) + : env_var_getter_(env_var_getter), setting_getter_(setting_getter) { +} + +void ProxyConfigServiceLinux::Delegate::SetUpAndFetchInitialConfig( + base::SingleThreadTaskRunner* glib_thread_task_runner, + base::SingleThreadTaskRunner* io_thread_task_runner, + base::MessageLoopForIO* file_loop) { + // We should be running on the default glib main loop thread right + // now. gconf can only be accessed from this thread. + DCHECK(glib_thread_task_runner->BelongsToCurrentThread()); + glib_thread_task_runner_ = glib_thread_task_runner; + io_thread_task_runner_ = io_thread_task_runner; + + // If we are passed a NULL |io_thread_task_runner| or |file_loop|, + // then we don't set up proxy setting change notifications. This + // should not be the usual case but is intended to simplify test + // setups. + if (!io_thread_task_runner_.get() || !file_loop) + VLOG(1) << "Monitoring of proxy setting changes is disabled"; + + // Fetch and cache the current proxy config. The config is left in + // cached_config_, where GetLatestProxyConfig() running on the IO thread + // will expect to find it. This is safe to do because we return + // before this ProxyConfigServiceLinux is passed on to + // the ProxyService. + + // Note: It would be nice to prioritize environment variables + // and only fall back to gconf if env vars were unset. But + // gnome-terminal "helpfully" sets http_proxy and no_proxy, and it + // does so even if the proxy mode is set to auto, which would + // mislead us. + + bool got_config = false; + if (setting_getter_.get() && + setting_getter_->Init(glib_thread_task_runner, file_loop) && + GetConfigFromSettings(&cached_config_)) { + cached_config_.set_id(1); // Mark it as valid. + cached_config_.set_source(setting_getter_->GetConfigSource()); + VLOG(1) << "Obtained proxy settings from " + << ProxyConfigSourceToString(cached_config_.source()); + + // If gconf proxy mode is "none", meaning direct, then we take + // that to be a valid config and will not check environment + // variables. The alternative would have been to look for a proxy + // whereever we can find one. + got_config = true; + + // Keep a copy of the config for use from this thread for + // comparison with updated settings when we get notifications. + reference_config_ = cached_config_; + reference_config_.set_id(1); // Mark it as valid. + + // We only set up notifications if we have IO and file loops available. + // We do this after getting the initial configuration so that we don't have + // to worry about cancelling it if the initial fetch above fails. Note that + // setting up notifications has the side effect of simulating a change, so + // that we won't lose any updates that may have happened after the initial + // fetch and before setting up notifications. We'll detect the common case + // of no changes in OnCheckProxyConfigSettings() (or sooner) and ignore it. + if (io_thread_task_runner && file_loop) { + scoped_refptr<base::SingleThreadTaskRunner> required_loop = + setting_getter_->GetNotificationTaskRunner(); + if (!required_loop.get() || required_loop->BelongsToCurrentThread()) { + // In this case we are already on an acceptable thread. + SetUpNotifications(); + } else { + // Post a task to set up notifications. We don't wait for success. + required_loop->PostTask(FROM_HERE, base::Bind( + &ProxyConfigServiceLinux::Delegate::SetUpNotifications, this)); + } + } + } + + if (!got_config) { + // We fall back on environment variables. + // + // Consulting environment variables doesn't need to be done from the + // default glib main loop, but it's a tiny enough amount of work. + if (GetConfigFromEnv(&cached_config_)) { + cached_config_.set_source(PROXY_CONFIG_SOURCE_ENV); + cached_config_.set_id(1); // Mark it as valid. + VLOG(1) << "Obtained proxy settings from environment variables"; + } + } +} + +// Depending on the SettingGetter in use, this method will be called +// on either the UI thread (GConf) or the file thread (KDE). +void ProxyConfigServiceLinux::Delegate::SetUpNotifications() { + scoped_refptr<base::SingleThreadTaskRunner> required_loop = + setting_getter_->GetNotificationTaskRunner(); + DCHECK(!required_loop.get() || required_loop->BelongsToCurrentThread()); + if (!setting_getter_->SetUpNotifications(this)) + LOG(ERROR) << "Unable to set up proxy configuration change notifications"; +} + +void ProxyConfigServiceLinux::Delegate::AddObserver(Observer* observer) { + observers_.AddObserver(observer); +} + +void ProxyConfigServiceLinux::Delegate::RemoveObserver(Observer* observer) { + observers_.RemoveObserver(observer); +} + +ProxyConfigService::ConfigAvailability + ProxyConfigServiceLinux::Delegate::GetLatestProxyConfig( + ProxyConfig* config) { + // This is called from the IO thread. + DCHECK(!io_thread_task_runner_.get() || + io_thread_task_runner_->BelongsToCurrentThread()); + + // Simply return the last proxy configuration that glib_default_loop + // notified us of. + if (cached_config_.is_valid()) { + *config = cached_config_; + } else { + *config = ProxyConfig::CreateDirect(); + config->set_source(PROXY_CONFIG_SOURCE_SYSTEM_FAILED); + } + + // We return CONFIG_VALID to indicate that *config was filled in. It is always + // going to be available since we initialized eagerly on the UI thread. + // TODO(eroman): do lazy initialization instead, so we no longer need + // to construct ProxyConfigServiceLinux on the UI thread. + // In which case, we may return false here. + return CONFIG_VALID; +} + +// Depending on the SettingGetter in use, this method will be called +// on either the UI thread (GConf) or the file thread (KDE). +void ProxyConfigServiceLinux::Delegate::OnCheckProxyConfigSettings() { + scoped_refptr<base::SingleThreadTaskRunner> required_loop = + setting_getter_->GetNotificationTaskRunner(); + DCHECK(!required_loop.get() || required_loop->BelongsToCurrentThread()); + ProxyConfig new_config; + bool valid = GetConfigFromSettings(&new_config); + if (valid) + new_config.set_id(1); // mark it as valid + + // See if it is different from what we had before. + if (new_config.is_valid() != reference_config_.is_valid() || + !new_config.Equals(reference_config_)) { + // Post a task to the IO thread with the new configuration, so it can + // update |cached_config_|. + io_thread_task_runner_->PostTask(FROM_HERE, base::Bind( + &ProxyConfigServiceLinux::Delegate::SetNewProxyConfig, + this, new_config)); + // Update the thread-private copy in |reference_config_| as well. + reference_config_ = new_config; + } else { + VLOG(1) << "Detected no-op change to proxy settings. Doing nothing."; + } +} + +void ProxyConfigServiceLinux::Delegate::SetNewProxyConfig( + const ProxyConfig& new_config) { + DCHECK(io_thread_task_runner_->BelongsToCurrentThread()); + VLOG(1) << "Proxy configuration changed"; + cached_config_ = new_config; + FOR_EACH_OBSERVER( + Observer, observers_, + OnProxyConfigChanged(new_config, ProxyConfigService::CONFIG_VALID)); +} + +void ProxyConfigServiceLinux::Delegate::PostDestroyTask() { + if (!setting_getter_.get()) + return; + scoped_refptr<base::SingleThreadTaskRunner> shutdown_loop = + setting_getter_->GetNotificationTaskRunner(); + if (!shutdown_loop.get() || shutdown_loop->BelongsToCurrentThread()) { + // Already on the right thread, call directly. + // This is the case for the unittests. + OnDestroy(); + } else { + // Post to shutdown thread. Note that on browser shutdown, we may quit + // this MessageLoop and exit the program before ever running this. + shutdown_loop->PostTask(FROM_HERE, base::Bind( + &ProxyConfigServiceLinux::Delegate::OnDestroy, this)); + } +} +void ProxyConfigServiceLinux::Delegate::OnDestroy() { + scoped_refptr<base::SingleThreadTaskRunner> shutdown_loop = + setting_getter_->GetNotificationTaskRunner(); + DCHECK(!shutdown_loop.get() || shutdown_loop->BelongsToCurrentThread()); + setting_getter_->ShutDown(); +} + +ProxyConfigServiceLinux::ProxyConfigServiceLinux() + : delegate_(new Delegate(base::Environment::Create())) { +} + +ProxyConfigServiceLinux::~ProxyConfigServiceLinux() { + delegate_->PostDestroyTask(); +} + +ProxyConfigServiceLinux::ProxyConfigServiceLinux( + base::Environment* env_var_getter) + : delegate_(new Delegate(env_var_getter)) { +} + +ProxyConfigServiceLinux::ProxyConfigServiceLinux( + base::Environment* env_var_getter, SettingGetter* setting_getter) + : delegate_(new Delegate(env_var_getter, setting_getter)) { +} + +void ProxyConfigServiceLinux::AddObserver(Observer* observer) { + delegate_->AddObserver(observer); +} + +void ProxyConfigServiceLinux::RemoveObserver(Observer* observer) { + delegate_->RemoveObserver(observer); +} + +ProxyConfigService::ConfigAvailability + ProxyConfigServiceLinux::GetLatestProxyConfig(ProxyConfig* config) { + return delegate_->GetLatestProxyConfig(config); +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_config_service_linux.h b/chromium/net/proxy/proxy_config_service_linux.h new file mode 100644 index 00000000000..5dccd9deb77 --- /dev/null +++ b/chromium/net/proxy/proxy_config_service_linux.h @@ -0,0 +1,309 @@ +// 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. + +#ifndef NET_PROXY_PROXY_CONFIG_SERVICE_LINUX_H_ +#define NET_PROXY_PROXY_CONFIG_SERVICE_LINUX_H_ + +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "base/environment.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/observer_list.h" +#include "net/base/net_export.h" +#include "net/proxy/proxy_config.h" +#include "net/proxy/proxy_config_service.h" +#include "net/proxy/proxy_server.h" + +namespace base { +class MessageLoopForIO; +class SingleThreadTaskRunner; +} // namespace base + +namespace net { + +// Implementation of ProxyConfigService that retrieves the system proxy +// settings from environment variables, gconf, gsettings, or kioslaverc (KDE). +class NET_EXPORT_PRIVATE ProxyConfigServiceLinux : public ProxyConfigService { + public: + + // Forward declaration of Delegate. + class Delegate; + + class SettingGetter { + public: + // Buffer size used in some implementations of this class when reading + // files. Defined here so unit tests can construct worst-case inputs. + static const size_t BUFFER_SIZE = 512; + + SettingGetter() {} + virtual ~SettingGetter() {} + + // Initializes the class: obtains a gconf/gsettings client, or simulates + // one, in the concrete implementations. Returns true on success. Must be + // called before using other methods, and should be called on the thread + // running the glib main loop. + // One of |glib_thread_task_runner| and |file_loop| will be used for + // gconf/gsettings calls or reading necessary files, depending on the + // implementation. + virtual bool Init(base::SingleThreadTaskRunner* glib_thread_task_runner, + base::MessageLoopForIO* file_loop) = 0; + + // Releases the gconf/gsettings client, which clears cached directories and + // stops notifications. + virtual void ShutDown() = 0; + + // Requests notification of gconf/gsettings changes for proxy + // settings. Returns true on success. + virtual bool SetUpNotifications(Delegate* delegate) = 0; + + // Returns the message loop for the thread on which this object + // handles notifications, and also on which it must be destroyed. + // Returns NULL if it does not matter. + virtual base::SingleThreadTaskRunner* GetNotificationTaskRunner() = 0; + + // Returns the source of proxy settings. + virtual ProxyConfigSource GetConfigSource() = 0; + + // These are all the values that can be fetched. We used to just use the + // corresponding paths in gconf for these, but gconf is now obsolete and + // in the future we'll be using mostly gsettings/kioslaverc so we + // enumerate them instead to avoid unnecessary string operations. + enum StringSetting { + PROXY_MODE, + PROXY_AUTOCONF_URL, + PROXY_HTTP_HOST, + PROXY_HTTPS_HOST, + PROXY_FTP_HOST, + PROXY_SOCKS_HOST, + }; + enum BoolSetting { + PROXY_USE_HTTP_PROXY, + PROXY_USE_SAME_PROXY, + PROXY_USE_AUTHENTICATION, + }; + enum IntSetting { + PROXY_HTTP_PORT, + PROXY_HTTPS_PORT, + PROXY_FTP_PORT, + PROXY_SOCKS_PORT, + }; + enum StringListSetting { + PROXY_IGNORE_HOSTS, + }; + + // Given a PROXY_*_HOST value, return the corresponding PROXY_*_PORT value. + static IntSetting HostSettingToPortSetting(StringSetting host) { + switch (host) { + case PROXY_HTTP_HOST: + return PROXY_HTTP_PORT; + case PROXY_HTTPS_HOST: + return PROXY_HTTPS_PORT; + case PROXY_FTP_HOST: + return PROXY_FTP_PORT; + case PROXY_SOCKS_HOST: + return PROXY_SOCKS_PORT; + default: + NOTREACHED(); + return PROXY_HTTP_PORT; // Placate compiler. + } + } + + // Gets a string type value from the data source and stores it in + // |*result|. Returns false if the key is unset or on error. Must only be + // called after a successful call to Init(), and not after a failed call + // to SetUpNotifications() or after calling Release(). + virtual bool GetString(StringSetting key, std::string* result) = 0; + // Same thing for a bool typed value. + virtual bool GetBool(BoolSetting key, bool* result) = 0; + // Same for an int typed value. + virtual bool GetInt(IntSetting key, int* result) = 0; + // And for a string list. + virtual bool GetStringList(StringListSetting key, + std::vector<std::string>* result) = 0; + + // Returns true if the bypass list should be interpreted as a proxy + // whitelist rather than blacklist. (This is KDE-specific.) + virtual bool BypassListIsReversed() = 0; + + // Returns true if the bypass rules should be interpreted as + // suffix-matching rules. + virtual bool MatchHostsUsingSuffixMatching() = 0; + + private: + DISALLOW_COPY_AND_ASSIGN(SettingGetter); + }; + + // ProxyConfigServiceLinux is created on the UI thread, and + // SetUpAndFetchInitialConfig() is immediately called to synchronously + // fetch the original configuration and set up change notifications on + // the UI thread. + // + // Past that point, it is accessed periodically through the + // ProxyConfigService interface (GetLatestProxyConfig, AddObserver, + // RemoveObserver) from the IO thread. + // + // Setting change notification callbacks can occur at any time and are + // run on either the UI thread (gconf/gsettings) or the file thread + // (KDE). The new settings are fetched on that thread, and the resulting + // proxy config is posted to the IO thread through + // Delegate::SetNewProxyConfig(). We then notify observers on the IO + // thread of the configuration change. + // + // ProxyConfigServiceLinux is deleted from the IO thread. + // + // The substance of the ProxyConfigServiceLinux implementation is + // wrapped in the Delegate ref counted class. On deleting the + // ProxyConfigServiceLinux, Delegate::OnDestroy() is posted to either + // the UI thread (gconf/gsettings) or the file thread (KDE) where change + // notifications will be safely stopped before releasing Delegate. + + class Delegate : public base::RefCountedThreadSafe<Delegate> { + public: + // Constructor receives env var getter implementation to use, and + // takes ownership of it. This is the normal constructor. + explicit Delegate(base::Environment* env_var_getter); + // Constructor receives setting and env var getter implementations + // to use, and takes ownership of them. Used for testing. + Delegate(base::Environment* env_var_getter, SettingGetter* setting_getter); + + // Synchronously obtains the proxy configuration. If gconf, + // gsettings, or kioslaverc are used, also enables notifications for + // setting changes. gconf/gsettings must only be accessed from the + // thread running the default glib main loop, and so this method + // must be called from the UI thread. The message loop for the IO + // thread is specified so that notifications can post tasks to it + // (and for assertions). The message loop for the file thread is + // used to read any files needed to determine proxy settings. + void SetUpAndFetchInitialConfig( + base::SingleThreadTaskRunner* glib_thread_task_runner, + base::SingleThreadTaskRunner* io_thread_task_runner, + base::MessageLoopForIO* file_loop); + + // Handler for setting change notifications: fetches a new proxy + // configuration from settings, and if this config is different + // than what we had before, posts a task to have it stored in + // cached_config_. + // Left public for simplicity. + void OnCheckProxyConfigSettings(); + + // Called from IO thread. + void AddObserver(Observer* observer); + void RemoveObserver(Observer* observer); + ProxyConfigService::ConfigAvailability GetLatestProxyConfig( + ProxyConfig* config); + + // Posts a call to OnDestroy() to the UI or FILE thread, depending on the + // setting getter in use. Called from ProxyConfigServiceLinux's destructor. + void PostDestroyTask(); + // Safely stops change notifications. Posted to either the UI or FILE + // thread, depending on the setting getter in use. + void OnDestroy(); + + private: + friend class base::RefCountedThreadSafe<Delegate>; + + ~Delegate(); + + // Obtains an environment variable's value. Parses a proxy server + // specification from it and puts it in result. Returns true if the + // requested variable is defined and the value valid. + bool GetProxyFromEnvVarForScheme(const char* variable, + ProxyServer::Scheme scheme, + ProxyServer* result_server); + // As above but with scheme set to HTTP, for convenience. + bool GetProxyFromEnvVar(const char* variable, ProxyServer* result_server); + // Fills proxy config from environment variables. Returns true if + // variables were found and the configuration is valid. + bool GetConfigFromEnv(ProxyConfig* config); + + // Obtains host and port config settings and parses a proxy server + // specification from it and puts it in result. Returns true if the + // requested variable is defined and the value valid. + bool GetProxyFromSettings(SettingGetter::StringSetting host_key, + ProxyServer* result_server); + // Fills proxy config from settings. Returns true if settings were found + // and the configuration is valid. + bool GetConfigFromSettings(ProxyConfig* config); + + // This method is posted from the UI thread to the IO thread to + // carry the new config information. + void SetNewProxyConfig(const ProxyConfig& new_config); + + // This method is run on the getter's notification thread. + void SetUpNotifications(); + + scoped_ptr<base::Environment> env_var_getter_; + scoped_ptr<SettingGetter> setting_getter_; + + // Cached proxy configuration, to be returned by + // GetLatestProxyConfig. Initially populated from the UI thread, but + // afterwards only accessed from the IO thread. + ProxyConfig cached_config_; + + // A copy kept on the UI thread of the last seen proxy config, so as + // to avoid posting a call to SetNewProxyConfig when we get a + // notification but the config has not actually changed. + ProxyConfig reference_config_; + + // The task runner for the glib thread, aka main browser thread. This thread + // is where we run the glib main loop (see + // base/message_loop/message_pump_glib.h). It is the glib default loop in + // the sense that it runs the glib default context: as in the context where + // sources are added by g_timeout_add and g_idle_add, and returned by + // g_main_context_default. gconf uses glib timeouts and idles and possibly + // other callbacks that will all be dispatched on this thread. Since gconf + // is not thread safe, any use of gconf must be done on the thread running + // this loop. + scoped_refptr<base::SingleThreadTaskRunner> glib_thread_task_runner_; + // Task runner for the IO thread. GetLatestProxyConfig() is called from + // the thread running this loop. + scoped_refptr<base::SingleThreadTaskRunner> io_thread_task_runner_; + + ObserverList<Observer> observers_; + + DISALLOW_COPY_AND_ASSIGN(Delegate); + }; + + // Thin wrapper shell around Delegate. + + // Usual constructor + ProxyConfigServiceLinux(); + // For testing: take alternate setting and env var getter implementations. + explicit ProxyConfigServiceLinux(base::Environment* env_var_getter); + ProxyConfigServiceLinux(base::Environment* env_var_getter, + SettingGetter* setting_getter); + + virtual ~ProxyConfigServiceLinux(); + + void SetupAndFetchInitialConfig( + base::SingleThreadTaskRunner* glib_thread_task_runner, + base::SingleThreadTaskRunner* io_thread_task_runner, + base::MessageLoopForIO* file_loop) { + delegate_->SetUpAndFetchInitialConfig(glib_thread_task_runner, + io_thread_task_runner, file_loop); + } + void OnCheckProxyConfigSettings() { + delegate_->OnCheckProxyConfigSettings(); + } + + // ProxyConfigService methods: + // Called from IO thread. + virtual void AddObserver(Observer* observer) OVERRIDE; + virtual void RemoveObserver(Observer* observer) OVERRIDE; + virtual ProxyConfigService::ConfigAvailability GetLatestProxyConfig( + ProxyConfig* config) OVERRIDE; + + private: + scoped_refptr<Delegate> delegate_; + + DISALLOW_COPY_AND_ASSIGN(ProxyConfigServiceLinux); +}; + +} // namespace net + +#endif // NET_PROXY_PROXY_CONFIG_SERVICE_LINUX_H_ diff --git a/chromium/net/proxy/proxy_config_service_linux_unittest.cc b/chromium/net/proxy/proxy_config_service_linux_unittest.cc new file mode 100644 index 00000000000..03bba04d994 --- /dev/null +++ b/chromium/net/proxy/proxy_config_service_linux_unittest.cc @@ -0,0 +1,1617 @@ +// 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/proxy/proxy_config_service_linux.h" + +#include <map> +#include <string> +#include <vector> + +#include "base/bind.h" +#include "base/compiler_specific.h" +#include "base/file_util.h" +#include "base/files/file_path.h" +#include "base/format_macros.h" +#include "base/logging.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/synchronization/waitable_event.h" +#include "base/threading/thread.h" +#include "net/proxy/proxy_config.h" +#include "net/proxy/proxy_config_service_common_unittest.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" + +namespace net { +namespace { + +// Set of values for all environment variables that we might +// query. NULL represents an unset variable. +struct EnvVarValues { + // The strange capitalization is so that the field matches the + // environment variable name exactly. + const char *DESKTOP_SESSION, *HOME, + *KDEHOME, *KDE_SESSION_VERSION, + *auto_proxy, *all_proxy, + *http_proxy, *https_proxy, *ftp_proxy, + *SOCKS_SERVER, *SOCKS_VERSION, + *no_proxy; +}; + +// Undo macro pollution from GDK includes (from message_loop.h). +#undef TRUE +#undef FALSE + +// So as to distinguish between an unset gconf boolean variable and +// one that is false. +enum BoolSettingValue { + UNSET = 0, TRUE, FALSE +}; + +// Set of values for all gconf settings that we might query. +struct GConfValues { + // strings + const char *mode, *autoconfig_url, + *http_host, *secure_host, *ftp_host, *socks_host; + // integers + int http_port, secure_port, ftp_port, socks_port; + // booleans + BoolSettingValue use_proxy, same_proxy, use_auth; + // string list + std::vector<std::string> ignore_hosts; +}; + +// Mapping from a setting name to the location of the corresponding +// value (inside a EnvVarValues or GConfValues struct). +template<typename key_type, typename value_type> +struct SettingsTable { + typedef std::map<key_type, value_type*> map_type; + + // Gets the value from its location + value_type Get(key_type key) { + typename map_type::const_iterator it = settings.find(key); + // In case there's a typo or the unittest becomes out of sync. + CHECK(it != settings.end()) << "key " << key << " not found"; + value_type* value_ptr = it->second; + return *value_ptr; + } + + map_type settings; +}; + +class MockEnvironment : public base::Environment { + public: + MockEnvironment() { +#define ENTRY(x) table[#x] = &values.x + ENTRY(DESKTOP_SESSION); + ENTRY(HOME); + ENTRY(KDEHOME); + ENTRY(KDE_SESSION_VERSION); + ENTRY(auto_proxy); + ENTRY(all_proxy); + ENTRY(http_proxy); + ENTRY(https_proxy); + ENTRY(ftp_proxy); + ENTRY(no_proxy); + ENTRY(SOCKS_SERVER); + ENTRY(SOCKS_VERSION); +#undef ENTRY + Reset(); + } + + // Zeroes all environment values. + void Reset() { + EnvVarValues zero_values = { 0 }; + values = zero_values; + } + + // Begin base::Environment implementation. + virtual bool GetVar(const char* variable_name, std::string* result) OVERRIDE { + std::map<std::string, const char**>::iterator it = + table.find(variable_name); + if (it != table.end() && *(it->second) != NULL) { + // Note that the variable may be defined but empty. + *result = *(it->second); + return true; + } + return false; + } + + virtual bool SetVar(const char* variable_name, const std::string& new_value) + OVERRIDE { + ADD_FAILURE(); + return false; + } + + virtual bool UnSetVar(const char* variable_name) OVERRIDE { + ADD_FAILURE(); + return false; + } + // End base::Environment implementation. + + // Intentionally public, for convenience when setting up a test. + EnvVarValues values; + + private: + std::map<std::string, const char**> table; +}; + +class MockSettingGetter + : public ProxyConfigServiceLinux::SettingGetter { + public: + typedef ProxyConfigServiceLinux::SettingGetter SettingGetter; + MockSettingGetter() { +#define ENTRY(key, field) \ + strings_table.settings[SettingGetter::key] = &values.field + ENTRY(PROXY_MODE, mode); + ENTRY(PROXY_AUTOCONF_URL, autoconfig_url); + ENTRY(PROXY_HTTP_HOST, http_host); + ENTRY(PROXY_HTTPS_HOST, secure_host); + ENTRY(PROXY_FTP_HOST, ftp_host); + ENTRY(PROXY_SOCKS_HOST, socks_host); +#undef ENTRY +#define ENTRY(key, field) \ + ints_table.settings[SettingGetter::key] = &values.field + ENTRY(PROXY_HTTP_PORT, http_port); + ENTRY(PROXY_HTTPS_PORT, secure_port); + ENTRY(PROXY_FTP_PORT, ftp_port); + ENTRY(PROXY_SOCKS_PORT, socks_port); +#undef ENTRY +#define ENTRY(key, field) \ + bools_table.settings[SettingGetter::key] = &values.field + ENTRY(PROXY_USE_HTTP_PROXY, use_proxy); + ENTRY(PROXY_USE_SAME_PROXY, same_proxy); + ENTRY(PROXY_USE_AUTHENTICATION, use_auth); +#undef ENTRY + string_lists_table.settings[SettingGetter::PROXY_IGNORE_HOSTS] = + &values.ignore_hosts; + Reset(); + } + + // Zeros all environment values. + void Reset() { + GConfValues zero_values = { 0 }; + values = zero_values; + } + + virtual bool Init(base::SingleThreadTaskRunner* glib_thread_task_runner, + base::MessageLoopForIO* file_loop) OVERRIDE { + return true; + } + + virtual void ShutDown() OVERRIDE {} + + virtual bool SetUpNotifications(ProxyConfigServiceLinux::Delegate* delegate) + OVERRIDE { + return true; + } + + virtual base::SingleThreadTaskRunner* GetNotificationTaskRunner() OVERRIDE { + return NULL; + } + + virtual ProxyConfigSource GetConfigSource() OVERRIDE { + return PROXY_CONFIG_SOURCE_TEST; + } + + virtual bool GetString(StringSetting key, std::string* result) OVERRIDE { + const char* value = strings_table.Get(key); + if (value) { + *result = value; + return true; + } + return false; + } + + virtual bool GetBool(BoolSetting key, bool* result) OVERRIDE { + BoolSettingValue value = bools_table.Get(key); + switch (value) { + case UNSET: + return false; + case TRUE: + *result = true; + break; + case FALSE: + *result = false; + } + return true; + } + + virtual bool GetInt(IntSetting key, int* result) OVERRIDE { + // We don't bother to distinguish unset keys from 0 values. + *result = ints_table.Get(key); + return true; + } + + virtual bool GetStringList(StringListSetting key, + std::vector<std::string>* result) OVERRIDE { + *result = string_lists_table.Get(key); + // We don't bother to distinguish unset keys from empty lists. + return !result->empty(); + } + + virtual bool BypassListIsReversed() OVERRIDE { + return false; + } + + virtual bool MatchHostsUsingSuffixMatching() OVERRIDE { + return false; + } + + // Intentionally public, for convenience when setting up a test. + GConfValues values; + + private: + SettingsTable<StringSetting, const char*> strings_table; + SettingsTable<BoolSetting, BoolSettingValue> bools_table; + SettingsTable<IntSetting, int> ints_table; + SettingsTable<StringListSetting, + std::vector<std::string> > string_lists_table; +}; + +} // namespace +} // namespace net + +// This helper class runs ProxyConfigServiceLinux::GetLatestProxyConfig() on +// the IO thread and synchronously waits for the result. +// Some code duplicated from proxy_script_fetcher_unittest.cc. +class SynchConfigGetter { + public: + // Takes ownership of |config_service|. + explicit SynchConfigGetter(net::ProxyConfigServiceLinux* config_service) + : event_(false, false), + io_thread_("IO_Thread"), + config_service_(config_service) { + // Start an IO thread. + base::Thread::Options options; + options.message_loop_type = base::MessageLoop::TYPE_IO; + io_thread_.StartWithOptions(options); + + // Make sure the thread started. + io_thread_.message_loop()->PostTask(FROM_HERE, + base::Bind(&SynchConfigGetter::Init, base::Unretained(this))); + Wait(); + } + + ~SynchConfigGetter() { + // Let the config service post a destroy message to the IO thread + // before cleaning up that thread. + delete config_service_; + // Clean up the IO thread. + io_thread_.message_loop()->PostTask(FROM_HERE, + base::Bind(&SynchConfigGetter::CleanUp, base::Unretained(this))); + Wait(); + } + + // Does gconf setup and initial fetch of the proxy config, + // all on the calling thread (meant to be the thread with the + // default glib main loop, which is the UI thread). + void SetupAndInitialFetch() { + base::MessageLoop* file_loop = io_thread_.message_loop(); + DCHECK_EQ(base::MessageLoop::TYPE_IO, file_loop->type()); + // We pass the mock IO thread as both the IO and file threads. + config_service_->SetupAndFetchInitialConfig( + base::MessageLoopProxy::current().get(), + io_thread_.message_loop_proxy().get(), + static_cast<base::MessageLoopForIO*>(file_loop)); + } + // Synchronously gets the proxy config. + net::ProxyConfigService::ConfigAvailability SyncGetLatestProxyConfig( + net::ProxyConfig* config) { + io_thread_.message_loop()->PostTask(FROM_HERE, + base::Bind(&SynchConfigGetter::GetLatestConfigOnIOThread, + base::Unretained(this))); + Wait(); + *config = proxy_config_; + return get_latest_config_result_; + } + + private: + // [Runs on |io_thread_|] + void Init() { + event_.Signal(); + } + + // Calls GetLatestProxyConfig, running on |io_thread_| Signals |event_| + // on completion. + void GetLatestConfigOnIOThread() { + get_latest_config_result_ = + config_service_->GetLatestProxyConfig(&proxy_config_); + event_.Signal(); + } + + // [Runs on |io_thread_|] Signals |event_| on cleanup completion. + void CleanUp() { + base::MessageLoop::current()->RunUntilIdle(); + event_.Signal(); + } + + void Wait() { + event_.Wait(); + event_.Reset(); + } + + base::WaitableEvent event_; + base::Thread io_thread_; + + net::ProxyConfigServiceLinux* config_service_; + + // The config obtained by |io_thread_| and read back by the main + // thread. + net::ProxyConfig proxy_config_; + + // Return value from GetLatestProxyConfig(). + net::ProxyConfigService::ConfigAvailability get_latest_config_result_; +}; + +namespace net { + +// This test fixture is only really needed for the KDEConfigParser test case, +// but all the test cases with the same prefix ("ProxyConfigServiceLinuxTest") +// must use the same test fixture class (also "ProxyConfigServiceLinuxTest"). +class ProxyConfigServiceLinuxTest : public PlatformTest { + protected: + virtual void SetUp() OVERRIDE { + PlatformTest::SetUp(); + // Set up a temporary KDE home directory. + std::string prefix("ProxyConfigServiceLinuxTest_user_home"); + file_util::CreateNewTempDirectory(prefix, &user_home_); + kde_home_ = user_home_.Append(FILE_PATH_LITERAL(".kde")); + base::FilePath path = kde_home_.Append(FILE_PATH_LITERAL("share")); + path = path.Append(FILE_PATH_LITERAL("config")); + file_util::CreateDirectory(path); + kioslaverc_ = path.Append(FILE_PATH_LITERAL("kioslaverc")); + // Set up paths but do not create the directory for .kde4. + kde4_home_ = user_home_.Append(FILE_PATH_LITERAL(".kde4")); + path = kde4_home_.Append(FILE_PATH_LITERAL("share")); + kde4_config_ = path.Append(FILE_PATH_LITERAL("config")); + kioslaverc4_ = kde4_config_.Append(FILE_PATH_LITERAL("kioslaverc")); + } + + virtual void TearDown() OVERRIDE { + // Delete the temporary KDE home directory. + base::DeleteFile(user_home_, true); + PlatformTest::TearDown(); + } + + base::FilePath user_home_; + // KDE3 paths. + base::FilePath kde_home_; + base::FilePath kioslaverc_; + // KDE4 paths. + base::FilePath kde4_home_; + base::FilePath kde4_config_; + base::FilePath kioslaverc4_; +}; + +// Builds an identifier for each test in an array. +#define TEST_DESC(desc) base::StringPrintf("at line %d <%s>", __LINE__, desc) + +TEST_F(ProxyConfigServiceLinuxTest, BasicGConfTest) { + std::vector<std::string> empty_ignores; + + std::vector<std::string> google_ignores; + google_ignores.push_back("*.google.com"); + + // Inspired from proxy_config_service_win_unittest.cc. + // Very neat, but harder to track down failures though. + const struct { + // Short description to identify the test + std::string description; + + // Input. + GConfValues values; + + // Expected outputs (availability and fields of ProxyConfig). + ProxyConfigService::ConfigAvailability availability; + bool auto_detect; + GURL pac_url; + ProxyRulesExpectation proxy_rules; + } tests[] = { + { + TEST_DESC("No proxying"), + { // Input. + "none", // mode + "", // autoconfig_url + "", "", "", "", // hosts + 0, 0, 0, 0, // ports + FALSE, FALSE, FALSE, // use, same, auth + empty_ignores, // ignore_hosts + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Empty(), + }, + + { + TEST_DESC("Auto detect"), + { // Input. + "auto", // mode + "", // autoconfig_url + "", "", "", "", // hosts + 0, 0, 0, 0, // ports + FALSE, FALSE, FALSE, // use, same, auth + empty_ignores, // ignore_hosts + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + true, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Empty(), + }, + + { + TEST_DESC("Valid PAC URL"), + { // Input. + "auto", // mode + "http://wpad/wpad.dat", // autoconfig_url + "", "", "", "", // hosts + 0, 0, 0, 0, // ports + FALSE, FALSE, FALSE, // use, same, auth + empty_ignores, // ignore_hosts + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL("http://wpad/wpad.dat"), // pac_url + ProxyRulesExpectation::Empty(), + }, + + { + TEST_DESC("Invalid PAC URL"), + { // Input. + "auto", // mode + "wpad.dat", // autoconfig_url + "", "", "", "", // hosts + 0, 0, 0, 0, // ports + FALSE, FALSE, FALSE, // use, same, auth + empty_ignores, // ignore_hosts + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Empty(), + }, + + { + TEST_DESC("Single-host in proxy list"), + { // Input. + "manual", // mode + "", // autoconfig_url + "www.google.com", "", "", "", // hosts + 80, 0, 0, 0, // ports + TRUE, TRUE, FALSE, // use, same, auth + empty_ignores, // ignore_hosts + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Single( + "www.google.com:80", // single proxy + ""), // bypass rules + }, + + { + TEST_DESC("use_http_proxy is honored"), + { // Input. + "manual", // mode + "", // autoconfig_url + "www.google.com", "", "", "", // hosts + 80, 0, 0, 0, // ports + FALSE, TRUE, FALSE, // use, same, auth + empty_ignores, // ignore_hosts + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Empty(), + }, + + { + TEST_DESC("use_http_proxy and use_same_proxy are optional"), + { // Input. + "manual", // mode + "", // autoconfig_url + "www.google.com", "", "", "", // hosts + 80, 0, 0, 0, // ports + UNSET, UNSET, FALSE, // use, same, auth + empty_ignores, // ignore_hosts + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::PerScheme( + "www.google.com:80", // http + "", // https + "", // ftp + ""), // bypass rules + }, + + { + TEST_DESC("Single-host, different port"), + { // Input. + "manual", // mode + "", // autoconfig_url + "www.google.com", "", "", "", // hosts + 88, 0, 0, 0, // ports + TRUE, TRUE, FALSE, // use, same, auth + empty_ignores, // ignore_hosts + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Single( + "www.google.com:88", // single proxy + ""), // bypass rules + }, + + { + TEST_DESC("Per-scheme proxy rules"), + { // Input. + "manual", // mode + "", // autoconfig_url + "www.google.com", // http_host + "www.foo.com", // secure_host + "ftp.foo.com", // ftp + "", // socks + 88, 110, 121, 0, // ports + TRUE, FALSE, FALSE, // use, same, auth + empty_ignores, // ignore_hosts + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::PerScheme( + "www.google.com:88", // http + "www.foo.com:110", // https + "ftp.foo.com:121", // ftp + ""), // bypass rules + }, + + { + TEST_DESC("socks"), + { // Input. + "manual", // mode + "", // autoconfig_url + "", "", "", "socks.com", // hosts + 0, 0, 0, 99, // ports + TRUE, FALSE, FALSE, // use, same, auth + empty_ignores, // ignore_hosts + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Single( + "socks5://socks.com:99", // single proxy + "") // bypass rules + }, + + { + TEST_DESC("Per-scheme proxy rules with fallback to SOCKS"), + { // Input. + "manual", // mode + "", // autoconfig_url + "www.google.com", // http_host + "www.foo.com", // secure_host + "ftp.foo.com", // ftp + "foobar.net", // socks + 88, 110, 121, 99, // ports + TRUE, FALSE, FALSE, // use, same, auth + empty_ignores, // ignore_hosts + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::PerSchemeWithSocks( + "www.google.com:88", // http + "www.foo.com:110", // https + "ftp.foo.com:121", // ftp + "socks5://foobar.net:99", // socks + ""), // bypass rules + }, + + { + TEST_DESC("Per-scheme proxy rules (just HTTP) with fallback to SOCKS"), + { // Input. + "manual", // mode + "", // autoconfig_url + "www.google.com", // http_host + "", // secure_host + "", // ftp + "foobar.net", // socks + 88, 0, 0, 99, // ports + TRUE, FALSE, FALSE, // use, same, auth + empty_ignores, // ignore_hosts + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::PerSchemeWithSocks( + "www.google.com:88", // http + "", // https + "", // ftp + "socks5://foobar.net:99", // socks + ""), // bypass rules + }, + + { + TEST_DESC("Bypass *.google.com"), + { // Input. + "manual", // mode + "", // autoconfig_url + "www.google.com", "", "", "", // hosts + 80, 0, 0, 0, // ports + TRUE, TRUE, FALSE, // use, same, auth + google_ignores, // ignore_hosts + }, + + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Single( + "www.google.com:80", // single proxy + "*.google.com"), // bypass rules + }, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { + SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "] %s", i, + tests[i].description.c_str())); + MockEnvironment* env = new MockEnvironment; + MockSettingGetter* setting_getter = new MockSettingGetter; + SynchConfigGetter sync_config_getter( + new ProxyConfigServiceLinux(env, setting_getter)); + ProxyConfig config; + setting_getter->values = tests[i].values; + sync_config_getter.SetupAndInitialFetch(); + ProxyConfigService::ConfigAvailability availability = + sync_config_getter.SyncGetLatestProxyConfig(&config); + EXPECT_EQ(tests[i].availability, availability); + + if (availability == ProxyConfigService::CONFIG_VALID) { + EXPECT_EQ(tests[i].auto_detect, config.auto_detect()); + EXPECT_EQ(tests[i].pac_url, config.pac_url()); + EXPECT_TRUE(tests[i].proxy_rules.Matches(config.proxy_rules())); + } + } +} + +TEST_F(ProxyConfigServiceLinuxTest, BasicEnvTest) { + // Inspired from proxy_config_service_win_unittest.cc. + const struct { + // Short description to identify the test + std::string description; + + // Input. + EnvVarValues values; + + // Expected outputs (availability and fields of ProxyConfig). + ProxyConfigService::ConfigAvailability availability; + bool auto_detect; + GURL pac_url; + ProxyRulesExpectation proxy_rules; + } tests[] = { + { + TEST_DESC("No proxying"), + { // Input. + NULL, // DESKTOP_SESSION + NULL, // HOME + NULL, // KDEHOME + NULL, // KDE_SESSION_VERSION + NULL, // auto_proxy + NULL, // all_proxy + NULL, NULL, NULL, // per-proto proxies + NULL, NULL, // SOCKS + "*", // no_proxy + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Empty(), + }, + + { + TEST_DESC("Auto detect"), + { // Input. + NULL, // DESKTOP_SESSION + NULL, // HOME + NULL, // KDEHOME + NULL, // KDE_SESSION_VERSION + "", // auto_proxy + NULL, // all_proxy + NULL, NULL, NULL, // per-proto proxies + NULL, NULL, // SOCKS + NULL, // no_proxy + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + true, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Empty(), + }, + + { + TEST_DESC("Valid PAC URL"), + { // Input. + NULL, // DESKTOP_SESSION + NULL, // HOME + NULL, // KDEHOME + NULL, // KDE_SESSION_VERSION + "http://wpad/wpad.dat", // auto_proxy + NULL, // all_proxy + NULL, NULL, NULL, // per-proto proxies + NULL, NULL, // SOCKS + NULL, // no_proxy + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL("http://wpad/wpad.dat"), // pac_url + ProxyRulesExpectation::Empty(), + }, + + { + TEST_DESC("Invalid PAC URL"), + { // Input. + NULL, // DESKTOP_SESSION + NULL, // HOME + NULL, // KDEHOME + NULL, // KDE_SESSION_VERSION + "wpad.dat", // auto_proxy + NULL, // all_proxy + NULL, NULL, NULL, // per-proto proxies + NULL, NULL, // SOCKS + NULL, // no_proxy + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Empty(), + }, + + { + TEST_DESC("Single-host in proxy list"), + { // Input. + NULL, // DESKTOP_SESSION + NULL, // HOME + NULL, // KDEHOME + NULL, // KDE_SESSION_VERSION + NULL, // auto_proxy + "www.google.com", // all_proxy + NULL, NULL, NULL, // per-proto proxies + NULL, NULL, // SOCKS + NULL, // no_proxy + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Single( + "www.google.com:80", // single proxy + ""), // bypass rules + }, + + { + TEST_DESC("Single-host, different port"), + { // Input. + NULL, // DESKTOP_SESSION + NULL, // HOME + NULL, // KDEHOME + NULL, // KDE_SESSION_VERSION + NULL, // auto_proxy + "www.google.com:99", // all_proxy + NULL, NULL, NULL, // per-proto proxies + NULL, NULL, // SOCKS + NULL, // no_proxy + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Single( + "www.google.com:99", // single + ""), // bypass rules + }, + + { + TEST_DESC("Tolerate a scheme"), + { // Input. + NULL, // DESKTOP_SESSION + NULL, // HOME + NULL, // KDEHOME + NULL, // KDE_SESSION_VERSION + NULL, // auto_proxy + "http://www.google.com:99", // all_proxy + NULL, NULL, NULL, // per-proto proxies + NULL, NULL, // SOCKS + NULL, // no_proxy + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Single( + "www.google.com:99", // single proxy + ""), // bypass rules + }, + + { + TEST_DESC("Per-scheme proxy rules"), + { // Input. + NULL, // DESKTOP_SESSION + NULL, // HOME + NULL, // KDEHOME + NULL, // KDE_SESSION_VERSION + NULL, // auto_proxy + NULL, // all_proxy + "www.google.com:80", "www.foo.com:110", "ftp.foo.com:121", // per-proto + NULL, NULL, // SOCKS + NULL, // no_proxy + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::PerScheme( + "www.google.com:80", // http + "www.foo.com:110", // https + "ftp.foo.com:121", // ftp + ""), // bypass rules + }, + + { + TEST_DESC("socks"), + { // Input. + NULL, // DESKTOP_SESSION + NULL, // HOME + NULL, // KDEHOME + NULL, // KDE_SESSION_VERSION + NULL, // auto_proxy + "", // all_proxy + NULL, NULL, NULL, // per-proto proxies + "socks.com:888", NULL, // SOCKS + NULL, // no_proxy + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Single( + "socks5://socks.com:888", // single proxy + ""), // bypass rules + }, + + { + TEST_DESC("socks4"), + { // Input. + NULL, // DESKTOP_SESSION + NULL, // HOME + NULL, // KDEHOME + NULL, // KDE_SESSION_VERSION + NULL, // auto_proxy + "", // all_proxy + NULL, NULL, NULL, // per-proto proxies + "socks.com:888", "4", // SOCKS + NULL, // no_proxy + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Single( + "socks4://socks.com:888", // single proxy + ""), // bypass rules + }, + + { + TEST_DESC("socks default port"), + { // Input. + NULL, // DESKTOP_SESSION + NULL, // HOME + NULL, // KDEHOME + NULL, // KDE_SESSION_VERSION + NULL, // auto_proxy + "", // all_proxy + NULL, NULL, NULL, // per-proto proxies + "socks.com", NULL, // SOCKS + NULL, // no_proxy + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Single( + "socks5://socks.com:1080", // single proxy + ""), // bypass rules + }, + + { + TEST_DESC("bypass"), + { // Input. + NULL, // DESKTOP_SESSION + NULL, // HOME + NULL, // KDEHOME + NULL, // KDE_SESSION_VERSION + NULL, // auto_proxy + "www.google.com", // all_proxy + NULL, NULL, NULL, // per-proto + NULL, NULL, // SOCKS + ".google.com, foo.com:99, 1.2.3.4:22, 127.0.0.1/8", // no_proxy + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Single( + "www.google.com:80", + "*.google.com,*foo.com:99,1.2.3.4:22,127.0.0.1/8"), + }, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { + SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "] %s", i, + tests[i].description.c_str())); + MockEnvironment* env = new MockEnvironment; + MockSettingGetter* setting_getter = new MockSettingGetter; + SynchConfigGetter sync_config_getter( + new ProxyConfigServiceLinux(env, setting_getter)); + ProxyConfig config; + env->values = tests[i].values; + sync_config_getter.SetupAndInitialFetch(); + ProxyConfigService::ConfigAvailability availability = + sync_config_getter.SyncGetLatestProxyConfig(&config); + EXPECT_EQ(tests[i].availability, availability); + + if (availability == ProxyConfigService::CONFIG_VALID) { + EXPECT_EQ(tests[i].auto_detect, config.auto_detect()); + EXPECT_EQ(tests[i].pac_url, config.pac_url()); + EXPECT_TRUE(tests[i].proxy_rules.Matches(config.proxy_rules())); + } + } +} + +TEST_F(ProxyConfigServiceLinuxTest, GconfNotification) { + MockEnvironment* env = new MockEnvironment; + MockSettingGetter* setting_getter = new MockSettingGetter; + ProxyConfigServiceLinux* service = + new ProxyConfigServiceLinux(env, setting_getter); + SynchConfigGetter sync_config_getter(service); + ProxyConfig config; + + // Start with no proxy. + setting_getter->values.mode = "none"; + sync_config_getter.SetupAndInitialFetch(); + EXPECT_EQ(ProxyConfigService::CONFIG_VALID, + sync_config_getter.SyncGetLatestProxyConfig(&config)); + EXPECT_FALSE(config.auto_detect()); + + // Now set to auto-detect. + setting_getter->values.mode = "auto"; + // Simulate setting change notification callback. + service->OnCheckProxyConfigSettings(); + EXPECT_EQ(ProxyConfigService::CONFIG_VALID, + sync_config_getter.SyncGetLatestProxyConfig(&config)); + EXPECT_TRUE(config.auto_detect()); +} + +TEST_F(ProxyConfigServiceLinuxTest, KDEConfigParser) { + // One of the tests below needs a worst-case long line prefix. We build it + // programmatically so that it will always be the right size. + std::string long_line; + size_t limit = ProxyConfigServiceLinux::SettingGetter::BUFFER_SIZE - 1; + for (size_t i = 0; i < limit; ++i) + long_line += "-"; + + // Inspired from proxy_config_service_win_unittest.cc. + const struct { + // Short description to identify the test + std::string description; + + // Input. + std::string kioslaverc; + EnvVarValues env_values; + + // Expected outputs (availability and fields of ProxyConfig). + ProxyConfigService::ConfigAvailability availability; + bool auto_detect; + GURL pac_url; + ProxyRulesExpectation proxy_rules; + } tests[] = { + { + TEST_DESC("No proxying"), + + // Input. + "[Proxy Settings]\nProxyType=0\n", + {}, // env_values + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Empty(), + }, + + { + TEST_DESC("Auto detect"), + + // Input. + "[Proxy Settings]\nProxyType=3\n", + {}, // env_values + + // Expected result. + ProxyConfigService::CONFIG_VALID, + true, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Empty(), + }, + + { + TEST_DESC("Valid PAC URL"), + + // Input. + "[Proxy Settings]\nProxyType=2\n" + "Proxy Config Script=http://wpad/wpad.dat\n", + {}, // env_values + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL("http://wpad/wpad.dat"), // pac_url + ProxyRulesExpectation::Empty(), + }, + + { + TEST_DESC("Valid PAC file without file://"), + + // Input. + "[Proxy Settings]\nProxyType=2\n" + "Proxy Config Script=/wpad/wpad.dat\n", + {}, // env_values + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL("file:///wpad/wpad.dat"), // pac_url + ProxyRulesExpectation::Empty(), + }, + + { + TEST_DESC("Per-scheme proxy rules"), + + // Input. + "[Proxy Settings]\nProxyType=1\nhttpProxy=www.google.com\n" + "httpsProxy=www.foo.com\nftpProxy=ftp.foo.com\n", + {}, // env_values + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::PerScheme( + "www.google.com:80", // http + "www.foo.com:80", // https + "ftp.foo.com:80", // http + ""), // bypass rules + }, + + { + TEST_DESC("Only HTTP proxy specified"), + + // Input. + "[Proxy Settings]\nProxyType=1\n" + "httpProxy=www.google.com\n", + {}, // env_values + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::PerScheme( + "www.google.com:80", // http + "", // https + "", // ftp + ""), // bypass rules + }, + + { + TEST_DESC("Only HTTP proxy specified, different port"), + + // Input. + "[Proxy Settings]\nProxyType=1\n" + "httpProxy=www.google.com:88\n", + {}, // env_values + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::PerScheme( + "www.google.com:88", // http + "", // https + "", // ftp + ""), // bypass rules + }, + + { + TEST_DESC("Only HTTP proxy specified, different port, space-delimited"), + + // Input. + "[Proxy Settings]\nProxyType=1\n" + "httpProxy=www.google.com 88\n", + {}, // env_values + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::PerScheme( + "www.google.com:88", // http + "", // https + "", // ftp + ""), // bypass rules + }, + + { + TEST_DESC("Bypass *.google.com"), + + // Input. + "[Proxy Settings]\nProxyType=1\nhttpProxy=www.google.com\n" + "NoProxyFor=.google.com\n", + {}, // env_values + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::PerScheme( + "www.google.com:80", // http + "", // https + "", // ftp + "*.google.com"), // bypass rules + }, + + { + TEST_DESC("Bypass *.google.com and *.kde.org"), + + // Input. + "[Proxy Settings]\nProxyType=1\nhttpProxy=www.google.com\n" + "NoProxyFor=.google.com,.kde.org\n", + {}, // env_values + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::PerScheme( + "www.google.com:80", // http + "", // https + "", // ftp + "*.google.com,*.kde.org"), // bypass rules + }, + + { + TEST_DESC("Correctly parse bypass list with ReversedException"), + + // Input. + "[Proxy Settings]\nProxyType=1\nhttpProxy=www.google.com\n" + "NoProxyFor=.google.com\nReversedException=true\n", + {}, // env_values + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::PerSchemeWithBypassReversed( + "www.google.com:80", // http + "", // https + "", // ftp + "*.google.com"), // bypass rules + }, + + { + TEST_DESC("socks"), + + // Input. + "[Proxy Settings]\nProxyType=1\nsocksProxy=socks.com 888\n", + {}, // env_values + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Single( + "socks5://socks.com:888", // single proxy + ""), // bypass rules + }, + + { + TEST_DESC("socks4"), + + // Input. + "[Proxy Settings]\nProxyType=1\nsocksProxy=socks4://socks.com 888\n", + {}, // env_values + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Single( + "socks4://socks.com:888", // single proxy + ""), // bypass rules + }, + + { + TEST_DESC("Treat all hostname patterns as wildcard patterns"), + + // Input. + "[Proxy Settings]\nProxyType=1\nhttpProxy=www.google.com\n" + "NoProxyFor=google.com,kde.org,<local>\n", + {}, // env_values + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::PerScheme( + "www.google.com:80", // http + "", // https + "", // ftp + "*google.com,*kde.org,<local>"), // bypass rules + }, + + { + TEST_DESC("Allow trailing whitespace after boolean value"), + + // Input. + "[Proxy Settings]\nProxyType=1\nhttpProxy=www.google.com\n" + "NoProxyFor=.google.com\nReversedException=true \n", + {}, // env_values + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::PerSchemeWithBypassReversed( + "www.google.com:80", // http + "", // https + "", // ftp + "*.google.com"), // bypass rules + }, + + { + TEST_DESC("Ignore settings outside [Proxy Settings]"), + + // Input. + "httpsProxy=www.foo.com\n[Proxy Settings]\nProxyType=1\n" + "httpProxy=www.google.com\n[Other Section]\nftpProxy=ftp.foo.com\n", + {}, // env_values + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::PerScheme( + "www.google.com:80", // http + "", // https + "", // ftp + ""), // bypass rules + }, + + { + TEST_DESC("Handle CRLF line endings"), + + // Input. + "[Proxy Settings]\r\nProxyType=1\r\nhttpProxy=www.google.com\r\n", + {}, // env_values + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::PerScheme( + "www.google.com:80", // http + "", // https + "", // ftp + ""), // bypass rules + }, + + { + TEST_DESC("Handle blank lines and mixed line endings"), + + // Input. + "[Proxy Settings]\r\n\nProxyType=1\n\r\nhttpProxy=www.google.com\n\n", + {}, // env_values + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::PerScheme( + "www.google.com:80", // http + "", // https + "", // ftp + ""), // bypass rules + }, + + { + TEST_DESC("Handle localized settings"), + + // Input. + "[Proxy Settings]\nProxyType[$e]=1\nhttpProxy[$e]=www.google.com\n", + {}, // env_values + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::PerScheme( + "www.google.com:80", // http + "", // https + "", // ftp + ""), // bypass rules + }, + + { + TEST_DESC("Ignore malformed localized settings"), + + // Input. + "[Proxy Settings]\nProxyType=1\nhttpProxy=www.google.com\n" + "httpsProxy$e]=www.foo.com\nftpProxy=ftp.foo.com\n", + {}, // env_values + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::PerScheme( + "www.google.com:80", // http + "", // https + "ftp.foo.com:80", // ftp + ""), // bypass rules + }, + + { + TEST_DESC("Handle strange whitespace"), + + // Input. + "[Proxy Settings]\nProxyType [$e] =2\n" + " Proxy Config Script = http:// foo\n", + {}, // env_values + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL("http:// foo"), // pac_url + ProxyRulesExpectation::Empty(), + }, + + { + TEST_DESC("Ignore all of a line which is too long"), + + // Input. + std::string("[Proxy Settings]\nProxyType=1\nftpProxy=ftp.foo.com\n") + + long_line + "httpsProxy=www.foo.com\nhttpProxy=www.google.com\n", + {}, // env_values + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::PerScheme( + "www.google.com:80", // http + "", // https + "ftp.foo.com:80", // ftp + ""), // bypass rules + }, + + { + TEST_DESC("Indirect Proxy - no env vars set"), + + // Input. + "[Proxy Settings]\nProxyType=4\nhttpProxy=http_proxy\n" + "httpsProxy=https_proxy\nftpProxy=ftp_proxy\nNoProxyFor=no_proxy\n", + {}, // env_values + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Empty(), + }, + + { + TEST_DESC("Indirect Proxy - with env vars set"), + + // Input. + "[Proxy Settings]\nProxyType=4\nhttpProxy=http_proxy\n" + "httpsProxy=https_proxy\nftpProxy=ftp_proxy\nNoProxyFor=no_proxy\n", + { // env_values + NULL, // DESKTOP_SESSION + NULL, // HOME + NULL, // KDEHOME + NULL, // KDE_SESSION_VERSION + NULL, // auto_proxy + NULL, // all_proxy + "www.normal.com", // http_proxy + "www.secure.com", // https_proxy + "ftp.foo.com", // ftp_proxy + NULL, NULL, // SOCKS + ".google.com, .kde.org", // no_proxy + }, + + // Expected result. + ProxyConfigService::CONFIG_VALID, + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::PerScheme( + "www.normal.com:80", // http + "www.secure.com:80", // https + "ftp.foo.com:80", // ftp + "*.google.com,*.kde.org"), // bypass rules + }, + + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { + SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "] %s", i, + tests[i].description.c_str())); + MockEnvironment* env = new MockEnvironment; + env->values = tests[i].env_values; + // Force the KDE getter to be used and tell it where the test is. + env->values.DESKTOP_SESSION = "kde4"; + env->values.KDEHOME = kde_home_.value().c_str(); + SynchConfigGetter sync_config_getter( + new ProxyConfigServiceLinux(env)); + ProxyConfig config; + // Overwrite the kioslaverc file. + file_util::WriteFile(kioslaverc_, tests[i].kioslaverc.c_str(), + tests[i].kioslaverc.length()); + sync_config_getter.SetupAndInitialFetch(); + ProxyConfigService::ConfigAvailability availability = + sync_config_getter.SyncGetLatestProxyConfig(&config); + EXPECT_EQ(tests[i].availability, availability); + + if (availability == ProxyConfigService::CONFIG_VALID) { + EXPECT_EQ(tests[i].auto_detect, config.auto_detect()); + EXPECT_EQ(tests[i].pac_url, config.pac_url()); + EXPECT_TRUE(tests[i].proxy_rules.Matches(config.proxy_rules())); + } + } +} + +TEST_F(ProxyConfigServiceLinuxTest, KDEHomePicker) { + // Auto detect proxy settings. + std::string slaverc3 = "[Proxy Settings]\nProxyType=3\n"; + // Valid PAC URL. + std::string slaverc4 = "[Proxy Settings]\nProxyType=2\n" + "Proxy Config Script=http://wpad/wpad.dat\n"; + GURL slaverc4_pac_url("http://wpad/wpad.dat"); + + // Overwrite the .kde kioslaverc file. + file_util::WriteFile(kioslaverc_, slaverc3.c_str(), slaverc3.length()); + + // If .kde4 exists it will mess up the first test. It should not, as + // we created the directory for $HOME in the test setup. + CHECK(!base::DirectoryExists(kde4_home_)); + + { SCOPED_TRACE("KDE4, no .kde4 directory, verify fallback"); + MockEnvironment* env = new MockEnvironment; + env->values.DESKTOP_SESSION = "kde4"; + env->values.HOME = user_home_.value().c_str(); + SynchConfigGetter sync_config_getter( + new ProxyConfigServiceLinux(env)); + ProxyConfig config; + sync_config_getter.SetupAndInitialFetch(); + EXPECT_EQ(ProxyConfigService::CONFIG_VALID, + sync_config_getter.SyncGetLatestProxyConfig(&config)); + EXPECT_TRUE(config.auto_detect()); + EXPECT_EQ(GURL(), config.pac_url()); + } + + // Now create .kde4 and put a kioslaverc in the config directory. + // Note that its timestamp will be at least as new as the .kde one. + file_util::CreateDirectory(kde4_config_); + file_util::WriteFile(kioslaverc4_, slaverc4.c_str(), slaverc4.length()); + CHECK(base::PathExists(kioslaverc4_)); + + { SCOPED_TRACE("KDE4, .kde4 directory present, use it"); + MockEnvironment* env = new MockEnvironment; + env->values.DESKTOP_SESSION = "kde4"; + env->values.HOME = user_home_.value().c_str(); + SynchConfigGetter sync_config_getter( + new ProxyConfigServiceLinux(env)); + ProxyConfig config; + sync_config_getter.SetupAndInitialFetch(); + EXPECT_EQ(ProxyConfigService::CONFIG_VALID, + sync_config_getter.SyncGetLatestProxyConfig(&config)); + EXPECT_FALSE(config.auto_detect()); + EXPECT_EQ(slaverc4_pac_url, config.pac_url()); + } + + { SCOPED_TRACE("KDE3, .kde4 directory present, ignore it"); + MockEnvironment* env = new MockEnvironment; + env->values.DESKTOP_SESSION = "kde"; + env->values.HOME = user_home_.value().c_str(); + SynchConfigGetter sync_config_getter( + new ProxyConfigServiceLinux(env)); + ProxyConfig config; + sync_config_getter.SetupAndInitialFetch(); + EXPECT_EQ(ProxyConfigService::CONFIG_VALID, + sync_config_getter.SyncGetLatestProxyConfig(&config)); + EXPECT_TRUE(config.auto_detect()); + EXPECT_EQ(GURL(), config.pac_url()); + } + + { SCOPED_TRACE("KDE4, .kde4 directory present, KDEHOME set to .kde"); + MockEnvironment* env = new MockEnvironment; + env->values.DESKTOP_SESSION = "kde4"; + env->values.HOME = user_home_.value().c_str(); + env->values.KDEHOME = kde_home_.value().c_str(); + SynchConfigGetter sync_config_getter( + new ProxyConfigServiceLinux(env)); + ProxyConfig config; + sync_config_getter.SetupAndInitialFetch(); + EXPECT_EQ(ProxyConfigService::CONFIG_VALID, + sync_config_getter.SyncGetLatestProxyConfig(&config)); + EXPECT_TRUE(config.auto_detect()); + EXPECT_EQ(GURL(), config.pac_url()); + } + + // Finally, make the .kde4 config directory older than the .kde directory + // and make sure we then use .kde instead of .kde4 since it's newer. + file_util::SetLastModifiedTime(kde4_config_, base::Time()); + + { SCOPED_TRACE("KDE4, very old .kde4 directory present, use .kde"); + MockEnvironment* env = new MockEnvironment; + env->values.DESKTOP_SESSION = "kde4"; + env->values.HOME = user_home_.value().c_str(); + SynchConfigGetter sync_config_getter( + new ProxyConfigServiceLinux(env)); + ProxyConfig config; + sync_config_getter.SetupAndInitialFetch(); + EXPECT_EQ(ProxyConfigService::CONFIG_VALID, + sync_config_getter.SyncGetLatestProxyConfig(&config)); + EXPECT_TRUE(config.auto_detect()); + EXPECT_EQ(GURL(), config.pac_url()); + } +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_config_service_mac.cc b/chromium/net/proxy/proxy_config_service_mac.cc new file mode 100644 index 00000000000..c1bc5337e5d --- /dev/null +++ b/chromium/net/proxy/proxy_config_service_mac.cc @@ -0,0 +1,286 @@ +// 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/proxy/proxy_config_service_mac.h" + +#include <CoreFoundation/CoreFoundation.h> +#include <SystemConfiguration/SystemConfiguration.h> + +#include "base/bind.h" +#include "base/logging.h" +#include "base/mac/foundation_util.h" +#include "base/mac/scoped_cftyperef.h" +#include "base/message_loop/message_loop.h" +#include "base/strings/sys_string_conversions.h" +#include "net/base/net_errors.h" +#include "net/proxy/proxy_config.h" +#include "net/proxy/proxy_info.h" +#include "net/proxy/proxy_server.h" + +namespace net { + +namespace { + +const int kPollIntervalSec = 5; + +// Utility function to pull out a boolean value from a dictionary and return it, +// returning a default value if the key is not present. +bool GetBoolFromDictionary(CFDictionaryRef dict, + CFStringRef key, + bool default_value) { + CFNumberRef number = base::mac::GetValueFromDictionary<CFNumberRef>(dict, + key); + if (!number) + return default_value; + + int int_value; + if (CFNumberGetValue(number, kCFNumberIntType, &int_value)) + return int_value; + else + return default_value; +} + +void GetCurrentProxyConfig(ProxyConfig* config) { + base::ScopedCFTypeRef<CFDictionaryRef> config_dict( + SCDynamicStoreCopyProxies(NULL)); + DCHECK(config_dict); + + // auto-detect + + // There appears to be no UI for this configuration option, and we're not sure + // if Apple's proxy code even takes it into account. But the constant is in + // the header file so we'll use it. + config->set_auto_detect( + GetBoolFromDictionary(config_dict.get(), + kSCPropNetProxiesProxyAutoDiscoveryEnable, + false)); + + // PAC file + + if (GetBoolFromDictionary(config_dict.get(), + kSCPropNetProxiesProxyAutoConfigEnable, + false)) { + CFStringRef pac_url_ref = base::mac::GetValueFromDictionary<CFStringRef>( + config_dict.get(), kSCPropNetProxiesProxyAutoConfigURLString); + if (pac_url_ref) + config->set_pac_url(GURL(base::SysCFStringRefToUTF8(pac_url_ref))); + } + + // proxies (for now ftp, http, https, and SOCKS) + + if (GetBoolFromDictionary(config_dict.get(), + kSCPropNetProxiesFTPEnable, + false)) { + ProxyServer proxy_server = + ProxyServer::FromDictionary(ProxyServer::SCHEME_HTTP, + config_dict.get(), + kSCPropNetProxiesFTPProxy, + kSCPropNetProxiesFTPPort); + if (proxy_server.is_valid()) { + config->proxy_rules().type = + ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME; + config->proxy_rules().proxies_for_ftp.SetSingleProxyServer(proxy_server); + } + } + if (GetBoolFromDictionary(config_dict.get(), + kSCPropNetProxiesHTTPEnable, + false)) { + ProxyServer proxy_server = + ProxyServer::FromDictionary(ProxyServer::SCHEME_HTTP, + config_dict.get(), + kSCPropNetProxiesHTTPProxy, + kSCPropNetProxiesHTTPPort); + if (proxy_server.is_valid()) { + config->proxy_rules().type = + ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME; + config->proxy_rules().proxies_for_http.SetSingleProxyServer(proxy_server); + } + } + if (GetBoolFromDictionary(config_dict.get(), + kSCPropNetProxiesHTTPSEnable, + false)) { + ProxyServer proxy_server = + ProxyServer::FromDictionary(ProxyServer::SCHEME_HTTP, + config_dict.get(), + kSCPropNetProxiesHTTPSProxy, + kSCPropNetProxiesHTTPSPort); + if (proxy_server.is_valid()) { + config->proxy_rules().type = + ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME; + config->proxy_rules().proxies_for_https. + SetSingleProxyServer(proxy_server); + } + } + if (GetBoolFromDictionary(config_dict.get(), + kSCPropNetProxiesSOCKSEnable, + false)) { + ProxyServer proxy_server = + ProxyServer::FromDictionary(ProxyServer::SCHEME_SOCKS5, + config_dict.get(), + kSCPropNetProxiesSOCKSProxy, + kSCPropNetProxiesSOCKSPort); + if (proxy_server.is_valid()) { + config->proxy_rules().type = + ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME; + config->proxy_rules().fallback_proxies.SetSingleProxyServer(proxy_server); + } + } + + // proxy bypass list + + CFArrayRef bypass_array_ref = base::mac::GetValueFromDictionary<CFArrayRef>( + config_dict.get(), kSCPropNetProxiesExceptionsList); + if (bypass_array_ref) { + CFIndex bypass_array_count = CFArrayGetCount(bypass_array_ref); + for (CFIndex i = 0; i < bypass_array_count; ++i) { + CFStringRef bypass_item_ref = base::mac::CFCast<CFStringRef>( + CFArrayGetValueAtIndex(bypass_array_ref, i)); + if (!bypass_item_ref) { + LOG(WARNING) << "Expected value for item " << i + << " in the kSCPropNetProxiesExceptionsList" + " to be a CFStringRef but it was not"; + + } else { + config->proxy_rules().bypass_rules.AddRuleFromString( + base::SysCFStringRefToUTF8(bypass_item_ref)); + } + } + } + + // proxy bypass boolean + + if (GetBoolFromDictionary(config_dict.get(), + kSCPropNetProxiesExcludeSimpleHostnames, + false)) { + config->proxy_rules().bypass_rules.AddRuleToBypassLocal(); + } + + // Source + config->set_source(PROXY_CONFIG_SOURCE_SYSTEM); +} + +} // namespace + +// Reference-counted helper for posting a task to +// ProxyConfigServiceMac::OnProxyConfigChanged between the notifier and IO +// thread. This helper object may outlive the ProxyConfigServiceMac. +class ProxyConfigServiceMac::Helper + : public base::RefCountedThreadSafe<ProxyConfigServiceMac::Helper> { + public: + explicit Helper(ProxyConfigServiceMac* parent) : parent_(parent) { + DCHECK(parent); + } + + // Called when the parent is destroyed. + void Orphan() { + parent_ = NULL; + } + + void OnProxyConfigChanged(const ProxyConfig& new_config) { + if (parent_) + parent_->OnProxyConfigChanged(new_config); + } + + private: + friend class base::RefCountedThreadSafe<Helper>; + ~Helper() {} + + ProxyConfigServiceMac* parent_; +}; + +void ProxyConfigServiceMac::Forwarder::SetDynamicStoreNotificationKeys( + SCDynamicStoreRef store) { + proxy_config_service_->SetDynamicStoreNotificationKeys(store); +} + +void ProxyConfigServiceMac::Forwarder::OnNetworkConfigChange( + CFArrayRef changed_keys) { + proxy_config_service_->OnNetworkConfigChange(changed_keys); +} + +ProxyConfigServiceMac::ProxyConfigServiceMac( + base::SingleThreadTaskRunner* io_thread_task_runner) + : forwarder_(this), + has_fetched_config_(false), + helper_(new Helper(this)), + io_thread_task_runner_(io_thread_task_runner) { + DCHECK(io_thread_task_runner_.get()); + config_watcher_.reset(new NetworkConfigWatcherMac(&forwarder_)); +} + +ProxyConfigServiceMac::~ProxyConfigServiceMac() { + DCHECK(io_thread_task_runner_->BelongsToCurrentThread()); + // Delete the config_watcher_ to ensure the notifier thread finishes before + // this object is destroyed. + config_watcher_.reset(); + helper_->Orphan(); +} + +void ProxyConfigServiceMac::AddObserver(Observer* observer) { + DCHECK(io_thread_task_runner_->BelongsToCurrentThread()); + observers_.AddObserver(observer); +} + +void ProxyConfigServiceMac::RemoveObserver(Observer* observer) { + DCHECK(io_thread_task_runner_->BelongsToCurrentThread()); + observers_.RemoveObserver(observer); +} + +net::ProxyConfigService::ConfigAvailability + ProxyConfigServiceMac::GetLatestProxyConfig(ProxyConfig* config) { + DCHECK(io_thread_task_runner_->BelongsToCurrentThread()); + + // Lazy-initialize by fetching the proxy setting from this thread. + if (!has_fetched_config_) { + GetCurrentProxyConfig(&last_config_fetched_); + has_fetched_config_ = true; + } + + *config = last_config_fetched_; + return has_fetched_config_ ? CONFIG_VALID : CONFIG_PENDING; +} + +void ProxyConfigServiceMac::SetDynamicStoreNotificationKeys( + SCDynamicStoreRef store) { + // Called on notifier thread. + + CFStringRef proxies_key = SCDynamicStoreKeyCreateProxies(NULL); + CFArrayRef key_array = CFArrayCreate( + NULL, (const void **)(&proxies_key), 1, &kCFTypeArrayCallBacks); + + bool ret = SCDynamicStoreSetNotificationKeys(store, key_array, NULL); + // TODO(willchan): Figure out a proper way to handle this rather than crash. + CHECK(ret); + + CFRelease(key_array); + CFRelease(proxies_key); +} + +void ProxyConfigServiceMac::OnNetworkConfigChange(CFArrayRef changed_keys) { + // Called on notifier thread. + + // Fetch the new system proxy configuration. + ProxyConfig new_config; + GetCurrentProxyConfig(&new_config); + + // Call OnProxyConfigChanged() on the IO thread to notify our observers. + io_thread_task_runner_->PostTask( + FROM_HERE, + base::Bind(&Helper::OnProxyConfigChanged, helper_.get(), new_config)); +} + +void ProxyConfigServiceMac::OnProxyConfigChanged( + const ProxyConfig& new_config) { + DCHECK(io_thread_task_runner_->BelongsToCurrentThread()); + + // Keep track of the last value we have seen. + has_fetched_config_ = true; + last_config_fetched_ = new_config; + + // Notify all the observers. + FOR_EACH_OBSERVER(Observer, observers_, + OnProxyConfigChanged(new_config, CONFIG_VALID)); +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_config_service_mac.h b/chromium/net/proxy/proxy_config_service_mac.h new file mode 100644 index 00000000000..cf513a0aeef --- /dev/null +++ b/chromium/net/proxy/proxy_config_service_mac.h @@ -0,0 +1,88 @@ +// 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. + +#ifndef NET_PROXY_PROXY_CONFIG_SERVICE_MAC_H_ +#define NET_PROXY_PROXY_CONFIG_SERVICE_MAC_H_ + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/observer_list.h" +#include "net/base/network_config_watcher_mac.h" +#include "net/proxy/proxy_config.h" +#include "net/proxy/proxy_config_service.h" + +namespace base { +class SingleThreadTaskRunner; +} // namespace base + +namespace net { + +// TODO(sergeyu): This class needs to be exported because remoting code +// creates it directly. Fix that and remove NET_EXPORT here. +// crbug.com/125104 +class NET_EXPORT ProxyConfigServiceMac : public ProxyConfigService { + public: + // Constructs a ProxyConfigService that watches the Mac OS system settings. + // This instance is expected to be operated and deleted on the same thread + // (however it may be constructed from a different thread). + explicit ProxyConfigServiceMac( + base::SingleThreadTaskRunner* io_thread_task_runner); + virtual ~ProxyConfigServiceMac(); + + public: + // ProxyConfigService implementation: + virtual void AddObserver(Observer* observer) OVERRIDE; + virtual void RemoveObserver(Observer* observer) OVERRIDE; + virtual ConfigAvailability GetLatestProxyConfig(ProxyConfig* config) OVERRIDE; + + private: + class Helper; + + // Forwarder just exists to keep the NetworkConfigWatcherMac API out of + // ProxyConfigServiceMac's public API. + class Forwarder : public NetworkConfigWatcherMac::Delegate { + public: + explicit Forwarder(ProxyConfigServiceMac* proxy_config_service) + : proxy_config_service_(proxy_config_service) {} + + // NetworkConfigWatcherMac::Delegate implementation: + virtual void StartReachabilityNotifications() OVERRIDE {} + virtual void SetDynamicStoreNotificationKeys( + SCDynamicStoreRef store) OVERRIDE; + virtual void OnNetworkConfigChange(CFArrayRef changed_keys) OVERRIDE; + + private: + ProxyConfigServiceMac* const proxy_config_service_; + DISALLOW_COPY_AND_ASSIGN(Forwarder); + }; + + // Methods directly called by the NetworkConfigWatcherMac::Delegate: + void SetDynamicStoreNotificationKeys(SCDynamicStoreRef store); + void OnNetworkConfigChange(CFArrayRef changed_keys); + + // Called when the proxy configuration has changed, to notify the observers. + void OnProxyConfigChanged(const ProxyConfig& new_config); + + Forwarder forwarder_; + scoped_ptr<const NetworkConfigWatcherMac> config_watcher_; + + ObserverList<Observer> observers_; + + // Holds the last system proxy settings that we fetched. + bool has_fetched_config_; + ProxyConfig last_config_fetched_; + + scoped_refptr<Helper> helper_; + + // The thread that we expect to be operated on. + const scoped_refptr<base::SingleThreadTaskRunner> io_thread_task_runner_; + + DISALLOW_COPY_AND_ASSIGN(ProxyConfigServiceMac); +}; + +} // namespace net + +#endif // NET_PROXY_PROXY_CONFIG_SERVICE_MAC_H_ diff --git a/chromium/net/proxy/proxy_config_service_win.cc b/chromium/net/proxy/proxy_config_service_win.cc new file mode 100644 index 00000000000..a1295f33de8 --- /dev/null +++ b/chromium/net/proxy/proxy_config_service_win.cc @@ -0,0 +1,194 @@ +// 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/proxy/proxy_config_service_win.h" + +#include <windows.h> +#include <winhttp.h> + +#include "base/logging.h" +#include "base/memory/scoped_ptr.h" +#include "base/stl_util.h" +#include "base/strings/string_util.h" +#include "base/strings/string_tokenizer.h" +#include "base/threading/thread_restrictions.h" +#include "base/win/registry.h" +#include "net/base/net_errors.h" +#include "net/proxy/proxy_config.h" + +#pragma comment(lib, "winhttp.lib") + +namespace net { + +namespace { + +const int kPollIntervalSec = 10; + +void FreeIEConfig(WINHTTP_CURRENT_USER_IE_PROXY_CONFIG* ie_config) { + if (ie_config->lpszAutoConfigUrl) + GlobalFree(ie_config->lpszAutoConfigUrl); + if (ie_config->lpszProxy) + GlobalFree(ie_config->lpszProxy); + if (ie_config->lpszProxyBypass) + GlobalFree(ie_config->lpszProxyBypass); +} + +} // namespace + +// RegKey and ObjectWatcher pair. +class ProxyConfigServiceWin::KeyEntry { + public: + bool StartWatching(base::win::ObjectWatcher::Delegate* delegate) { + // Try to create a watch event for the registry key (which watches the + // sibling tree as well). + if (key_.StartWatching() != ERROR_SUCCESS) + return false; + + // Now setup an ObjectWatcher for this event, so we get OnObjectSignaled() + // invoked on this message loop once it is signalled. + if (!watcher_.StartWatching(key_.watch_event(), delegate)) + return false; + + return true; + } + + bool CreateRegKey(HKEY rootkey, const wchar_t* subkey) { + return key_.Create(rootkey, subkey, KEY_NOTIFY) == ERROR_SUCCESS; + } + + HANDLE watch_event() const { + return key_.watch_event(); + } + + private: + base::win::RegKey key_; + base::win::ObjectWatcher watcher_; +}; + +ProxyConfigServiceWin::ProxyConfigServiceWin() + : PollingProxyConfigService( + base::TimeDelta::FromSeconds(kPollIntervalSec), + &ProxyConfigServiceWin::GetCurrentProxyConfig) { +} + +ProxyConfigServiceWin::~ProxyConfigServiceWin() { + // The registry functions below will end up going to disk. Do this on another + // thread to avoid slowing the IO thread. http://crbug.com/61453 + base::ThreadRestrictions::ScopedAllowIO allow_io; + STLDeleteElements(&keys_to_watch_); +} + +void ProxyConfigServiceWin::AddObserver(Observer* observer) { + // Lazily-initialize our registry watcher. + StartWatchingRegistryForChanges(); + + // Let the super-class do its work now. + PollingProxyConfigService::AddObserver(observer); +} + +void ProxyConfigServiceWin::StartWatchingRegistryForChanges() { + if (!keys_to_watch_.empty()) + return; // Already initialized. + + // The registry functions below will end up going to disk. Do this on another + // thread to avoid slowing the IO thread. http://crbug.com/61453 + base::ThreadRestrictions::ScopedAllowIO allow_io; + + // There are a number of different places where proxy settings can live + // in the registry. In some cases it appears in a binary value, in other + // cases string values. Furthermore winhttp and wininet appear to have + // separate stores, and proxy settings can be configured per-machine + // or per-user. + // + // This function is probably not exhaustive in the registry locations it + // watches for changes, however it should catch the majority of the + // cases. In case we have missed some less common triggers (likely), we + // will catch them during the periodic (10 second) polling, so things + // will recover. + + AddKeyToWatchList( + HKEY_CURRENT_USER, + L"Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"); + + AddKeyToWatchList( + HKEY_LOCAL_MACHINE, + L"Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"); + + AddKeyToWatchList( + HKEY_LOCAL_MACHINE, + L"SOFTWARE\\Policies\\Microsoft\\Windows\\CurrentVersion\\" + L"Internet Settings"); +} + +bool ProxyConfigServiceWin::AddKeyToWatchList(HKEY rootkey, + const wchar_t* subkey) { + scoped_ptr<KeyEntry> entry(new KeyEntry); + if (!entry->CreateRegKey(rootkey, subkey)) + return false; + + if (!entry->StartWatching(this)) + return false; + + keys_to_watch_.push_back(entry.release()); + return true; +} + +void ProxyConfigServiceWin::OnObjectSignaled(HANDLE object) { + // Figure out which registry key signalled this change. + KeyEntryList::iterator it; + for (it = keys_to_watch_.begin(); it != keys_to_watch_.end(); ++it) { + if ((*it)->watch_event() == object) + break; + } + + DCHECK(it != keys_to_watch_.end()); + + // Keep watching the registry key. + if (!(*it)->StartWatching(this)) + keys_to_watch_.erase(it); + + // Have the PollingProxyConfigService test for changes. + CheckForChangesNow(); +} + +// static +void ProxyConfigServiceWin::GetCurrentProxyConfig(ProxyConfig* config) { + WINHTTP_CURRENT_USER_IE_PROXY_CONFIG ie_config = {0}; + if (!WinHttpGetIEProxyConfigForCurrentUser(&ie_config)) { + LOG(ERROR) << "WinHttpGetIEProxyConfigForCurrentUser failed: " << + GetLastError(); + *config = ProxyConfig::CreateDirect(); + config->set_source(PROXY_CONFIG_SOURCE_SYSTEM_FAILED); + return; + } + SetFromIEConfig(config, ie_config); + FreeIEConfig(&ie_config); +} + +// static +void ProxyConfigServiceWin::SetFromIEConfig( + ProxyConfig* config, + const WINHTTP_CURRENT_USER_IE_PROXY_CONFIG& ie_config) { + if (ie_config.fAutoDetect) + config->set_auto_detect(true); + if (ie_config.lpszProxy) { + // lpszProxy may be a single proxy, or a proxy per scheme. The format + // is compatible with ProxyConfig::ProxyRules's string format. + config->proxy_rules().ParseFromString(WideToASCII(ie_config.lpszProxy)); + } + if (ie_config.lpszProxyBypass) { + std::string proxy_bypass = WideToASCII(ie_config.lpszProxyBypass); + + base::StringTokenizer proxy_server_bypass_list(proxy_bypass, ";, \t\n\r"); + while (proxy_server_bypass_list.GetNext()) { + std::string bypass_url_domain = proxy_server_bypass_list.token(); + config->proxy_rules().bypass_rules.AddRuleFromString(bypass_url_domain); + } + } + if (ie_config.lpszAutoConfigUrl) + config->set_pac_url(GURL(ie_config.lpszAutoConfigUrl)); + config->set_source(PROXY_CONFIG_SOURCE_SYSTEM); +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_config_service_win.h b/chromium/net/proxy/proxy_config_service_win.h new file mode 100644 index 00000000000..aa91b686c4e --- /dev/null +++ b/chromium/net/proxy/proxy_config_service_win.h @@ -0,0 +1,80 @@ +// 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. + +#ifndef NET_PROXY_PROXY_CONFIG_SERVICE_WIN_H_ +#define NET_PROXY_PROXY_CONFIG_SERVICE_WIN_H_ + +#include <windows.h> +#include <winhttp.h> + +#include <vector> + +#include "base/compiler_specific.h" +#include "base/gtest_prod_util.h" +#include "base/win/object_watcher.h" +#include "net/proxy/polling_proxy_config_service.h" + +namespace net { + +// Implementation of ProxyConfigService that retrieves the system proxy +// settings. +// +// It works by calling WinHttpGetIEProxyConfigForCurrentUser() to fetch the +// Internet Explorer proxy settings. +// +// We use two different strategies to notice when the configuration has +// changed: +// +// (1) Watch the internet explorer settings registry keys for changes. When +// one of the registry keys pertaining to proxy settings has changed, we +// call WinHttpGetIEProxyConfigForCurrentUser() again to read the +// configuration's new value. +// +// (2) Do regular polling every 10 seconds during network activity to see if +// WinHttpGetIEProxyConfigForCurrentUser() returns something different. +// +// Ideally strategy (1) should be sufficient to pick up all of the changes. +// However we still do the regular polling as a precaution in case the +// implementation details of WinHttpGetIEProxyConfigForCurrentUser() ever +// change, or in case we got it wrong (and are not checking all possible +// registry dependencies). +class NET_EXPORT_PRIVATE ProxyConfigServiceWin + : public PollingProxyConfigService, + public base::win::ObjectWatcher::Delegate { + public: + ProxyConfigServiceWin(); + virtual ~ProxyConfigServiceWin(); + + // Overrides a function from PollingProxyConfigService. + virtual void AddObserver(Observer* observer) OVERRIDE; + + private: + FRIEND_TEST_ALL_PREFIXES(ProxyConfigServiceWinTest, SetFromIEConfig); + class KeyEntry; + typedef std::vector<KeyEntry*> KeyEntryList; + + // Registers change observers on the registry keys relating to proxy settings. + void StartWatchingRegistryForChanges(); + + // Creates a new KeyEntry and appends it to |keys_to_watch_|. If the key + // fails to be created, it is not appended to the list and we return false. + bool AddKeyToWatchList(HKEY rootkey, const wchar_t* subkey); + + // ObjectWatcher::Delegate methods: + // This is called whenever one of the registry keys we are watching change. + virtual void OnObjectSignaled(HANDLE object) OVERRIDE; + + static void GetCurrentProxyConfig(ProxyConfig* config); + + // Set |config| using the proxy configuration values of |ie_config|. + static void SetFromIEConfig( + ProxyConfig* config, + const WINHTTP_CURRENT_USER_IE_PROXY_CONFIG& ie_config); + + KeyEntryList keys_to_watch_; +}; + +} // namespace net + +#endif // NET_PROXY_PROXY_CONFIG_SERVICE_WIN_H_ diff --git a/chromium/net/proxy/proxy_config_service_win_unittest.cc b/chromium/net/proxy/proxy_config_service_win_unittest.cc new file mode 100644 index 00000000000..911949d5994 --- /dev/null +++ b/chromium/net/proxy/proxy_config_service_win_unittest.cc @@ -0,0 +1,203 @@ +// 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/proxy/proxy_config_service_win.h" + +#include "net/base/net_errors.h" +#include "net/proxy/proxy_config.h" +#include "net/proxy/proxy_config_service_common_unittest.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { + +TEST(ProxyConfigServiceWinTest, SetFromIEConfig) { + const struct { + // Input. + WINHTTP_CURRENT_USER_IE_PROXY_CONFIG ie_config; + + // Expected outputs (fields of the ProxyConfig). + bool auto_detect; + GURL pac_url; + ProxyRulesExpectation proxy_rules; + const char* proxy_bypass_list; // newline separated + } tests[] = { + // Auto detect. + { + { // Input. + TRUE, // fAutoDetect + NULL, // lpszAutoConfigUrl + NULL, // lpszProxy + NULL, // lpszProxyBypass + }, + + // Expected result. + true, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Empty(), + }, + + // Valid PAC url + { + { // Input. + FALSE, // fAutoDetect + L"http://wpad/wpad.dat", // lpszAutoConfigUrl + NULL, // lpszProxy + NULL, // lpszProxy_bypass + }, + + // Expected result. + false, // auto_detect + GURL("http://wpad/wpad.dat"), // pac_url + ProxyRulesExpectation::Empty(), + }, + + // Invalid PAC url string. + { + { // Input. + FALSE, // fAutoDetect + L"wpad.dat", // lpszAutoConfigUrl + NULL, // lpszProxy + NULL, // lpszProxy_bypass + }, + + // Expected result. + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Empty(), + }, + + // Single-host in proxy list. + { + { // Input. + FALSE, // fAutoDetect + NULL, // lpszAutoConfigUrl + L"www.google.com", // lpszProxy + NULL, // lpszProxy_bypass + }, + + // Expected result. + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Single( + "www.google.com:80", // single proxy + ""), // bypass rules + }, + + // Per-scheme proxy rules. + { + { // Input. + FALSE, // fAutoDetect + NULL, // lpszAutoConfigUrl + L"http=www.google.com:80;https=www.foo.com:110", // lpszProxy + NULL, // lpszProxy_bypass + }, + + // Expected result. + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::PerScheme( + "www.google.com:80", // http + "www.foo.com:110", // https + "", // ftp + ""), // bypass rules + }, + + // SOCKS proxy configuration. + { + { // Input. + FALSE, // fAutoDetect + NULL, // lpszAutoConfigUrl + L"http=www.google.com:80;https=www.foo.com:110;" + L"ftp=ftpproxy:20;socks=foopy:130", // lpszProxy + NULL, // lpszProxy_bypass + }, + + // Expected result. + // Note that "socks" is interprted as meaning "socks4", since that is how + // Internet Explorer applies the settings. For more details on this + // policy, see: + // http://code.google.com/p/chromium/issues/detail?id=55912#c2 + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::PerSchemeWithSocks( + "www.google.com:80", // http + "www.foo.com:110", // https + "ftpproxy:20", // ftp + "socks4://foopy:130", // socks + ""), // bypass rules + }, + + // Bypass local names. + { + { // Input. + TRUE, // fAutoDetect + NULL, // lpszAutoConfigUrl + NULL, // lpszProxy + L"<local>", // lpszProxy_bypass + }, + + true, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::EmptyWithBypass("<local>"), + }, + + // Bypass "google.com" and local names, using semicolon as delimiter + // (ignoring white space). + { + { // Input. + TRUE, // fAutoDetect + NULL, // lpszAutoConfigUrl + NULL, // lpszProxy + L"<local> ; google.com", // lpszProxy_bypass + }, + + // Expected result. + true, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::EmptyWithBypass("<local>,google.com"), + }, + + // Bypass "foo.com" and "google.com", using lines as delimiter. + { + { // Input. + TRUE, // fAutoDetect + NULL, // lpszAutoConfigUrl + NULL, // lpszProxy + L"foo.com\r\ngoogle.com", // lpszProxy_bypass + }, + + // Expected result. + true, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::EmptyWithBypass("foo.com,google.com"), + }, + + // Bypass "foo.com" and "google.com", using commas as delimiter. + { + { // Input. + TRUE, // fAutoDetect + NULL, // lpszAutoConfigUrl + NULL, // lpszProxy + L"foo.com, google.com", // lpszProxy_bypass + }, + + // Expected result. + true, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::EmptyWithBypass("foo.com,google.com"), + }, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { + ProxyConfig config; + ProxyConfigServiceWin::SetFromIEConfig(&config, tests[i].ie_config); + + EXPECT_EQ(tests[i].auto_detect, config.auto_detect()); + EXPECT_EQ(tests[i].pac_url, config.pac_url()); + EXPECT_TRUE(tests[i].proxy_rules.Matches(config.proxy_rules())); + EXPECT_EQ(PROXY_CONFIG_SOURCE_SYSTEM, config.source()); + } +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_config_source.cc b/chromium/net/proxy/proxy_config_source.cc new file mode 100644 index 00000000000..5695b9bf6e6 --- /dev/null +++ b/chromium/net/proxy/proxy_config_source.cc @@ -0,0 +1,35 @@ +// 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/proxy/proxy_config_source.h" + +#include "base/basictypes.h" +#include "base/logging.h" + +namespace net { + +namespace { + +const char* kSourceNames[] = { + "UNKNOWN", + "SYSTEM", + "SYSTEM FAILED", + "GCONF", + "GSETTINGS", + "KDE", + "ENV", + "CUSTOM", + "TEST" +}; +COMPILE_ASSERT(ARRAYSIZE_UNSAFE(kSourceNames) == NUM_PROXY_CONFIG_SOURCES, + source_names_incorrect_size); + +} // namespace + +const char* ProxyConfigSourceToString(ProxyConfigSource source) { + DCHECK_GT(NUM_PROXY_CONFIG_SOURCES, source); + return kSourceNames[source]; +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_config_source.h b/chromium/net/proxy/proxy_config_source.h new file mode 100644 index 00000000000..a7e375ab749 --- /dev/null +++ b/chromium/net/proxy/proxy_config_source.h @@ -0,0 +1,37 @@ +// 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. + +#ifndef NET_PROXY_PROXY_CONFIG_SOURCE_H_ +#define NET_PROXY_PROXY_CONFIG_SOURCE_H_ + +namespace net { + +// Source of the configuration settings encapsulated in a ProxyConfig object. + +// The source information is used for determining how credentials are used and +// for logging. When adding new values, remember to add a string to +// kSourceNames[] in proxy_config_source.cc. +enum ProxyConfigSource { + PROXY_CONFIG_SOURCE_UNKNOWN, // The source hasn't been set. + PROXY_CONFIG_SOURCE_SYSTEM, // System settings (Win/Mac). + PROXY_CONFIG_SOURCE_SYSTEM_FAILED, // Default settings after failure to + // determine system settings. + PROXY_CONFIG_SOURCE_GCONF, // GConf (Linux) + PROXY_CONFIG_SOURCE_GSETTINGS, // GSettings (Linux). + PROXY_CONFIG_SOURCE_KDE, // KDE (Linux). + PROXY_CONFIG_SOURCE_ENV, // Environment variables. + PROXY_CONFIG_SOURCE_CUSTOM, // Custom settings local to the + // application (command line, + // extensions, application + // specific preferences, etc.) + PROXY_CONFIG_SOURCE_TEST, // Test settings. + NUM_PROXY_CONFIG_SOURCES +}; + +// Returns a textual representation of the source. +const char* ProxyConfigSourceToString(ProxyConfigSource source); + +} // namespace net + +#endif // NET_PROXY_PROXY_CONFIG_SOURCE_H_ diff --git a/chromium/net/proxy/proxy_config_unittest.cc b/chromium/net/proxy/proxy_config_unittest.cc new file mode 100644 index 00000000000..4b041b34340 --- /dev/null +++ b/chromium/net/proxy/proxy_config_unittest.cc @@ -0,0 +1,357 @@ +// 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/proxy/proxy_config.h" +#include "net/proxy/proxy_config_service_common_unittest.h" +#include "net/proxy/proxy_info.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { +namespace { + +void ExpectProxyServerEquals(const char* expectation, + const ProxyList& proxy_servers) { + if (expectation == NULL) { + EXPECT_TRUE(proxy_servers.IsEmpty()); + } else { + EXPECT_EQ(expectation, proxy_servers.ToPacString()); + } +} + +TEST(ProxyConfigTest, Equals) { + // Test |ProxyConfig::auto_detect|. + + ProxyConfig config1; + config1.set_auto_detect(true); + + ProxyConfig config2; + config2.set_auto_detect(false); + + EXPECT_FALSE(config1.Equals(config2)); + EXPECT_FALSE(config2.Equals(config1)); + + config2.set_auto_detect(true); + + EXPECT_TRUE(config1.Equals(config2)); + EXPECT_TRUE(config2.Equals(config1)); + + // Test |ProxyConfig::pac_url|. + + config2.set_pac_url(GURL("http://wpad/wpad.dat")); + + EXPECT_FALSE(config1.Equals(config2)); + EXPECT_FALSE(config2.Equals(config1)); + + config1.set_pac_url(GURL("http://wpad/wpad.dat")); + + EXPECT_TRUE(config1.Equals(config2)); + EXPECT_TRUE(config2.Equals(config1)); + + // Test |ProxyConfig::proxy_rules|. + + config2.proxy_rules().type = ProxyConfig::ProxyRules::TYPE_SINGLE_PROXY; + config2.proxy_rules().single_proxies.SetSingleProxyServer( + ProxyServer::FromURI("myproxy:80", ProxyServer::SCHEME_HTTP)); + + EXPECT_FALSE(config1.Equals(config2)); + EXPECT_FALSE(config2.Equals(config1)); + + config1.proxy_rules().type = ProxyConfig::ProxyRules::TYPE_SINGLE_PROXY; + config1.proxy_rules().single_proxies.SetSingleProxyServer( + ProxyServer::FromURI("myproxy:100", ProxyServer::SCHEME_HTTP)); + + EXPECT_FALSE(config1.Equals(config2)); + EXPECT_FALSE(config2.Equals(config1)); + + config1.proxy_rules().single_proxies.SetSingleProxyServer( + ProxyServer::FromURI("myproxy", ProxyServer::SCHEME_HTTP)); + + EXPECT_TRUE(config1.Equals(config2)); + EXPECT_TRUE(config2.Equals(config1)); + + // Test |ProxyConfig::bypass_rules|. + + config2.proxy_rules().bypass_rules.AddRuleFromString("*.google.com"); + + EXPECT_FALSE(config1.Equals(config2)); + EXPECT_FALSE(config2.Equals(config1)); + + config1.proxy_rules().bypass_rules.AddRuleFromString("*.google.com"); + + EXPECT_TRUE(config1.Equals(config2)); + EXPECT_TRUE(config2.Equals(config1)); + + // Test |ProxyConfig::proxy_rules.reverse_bypass|. + + config2.proxy_rules().reverse_bypass = true; + + EXPECT_FALSE(config1.Equals(config2)); + EXPECT_FALSE(config2.Equals(config1)); + + config1.proxy_rules().reverse_bypass = true; + + EXPECT_TRUE(config1.Equals(config2)); + EXPECT_TRUE(config2.Equals(config1)); +} + +TEST(ProxyConfigTest, ParseProxyRules) { + const struct { + const char* proxy_rules; + + ProxyConfig::ProxyRules::Type type; + // These will be PAC-stle strings, eg 'PROXY foo.com' + const char* single_proxy; + const char* proxy_for_http; + const char* proxy_for_https; + const char* proxy_for_ftp; + const char* fallback_proxy; + } tests[] = { + // One HTTP proxy for all schemes. + { + "myproxy:80", + + ProxyConfig::ProxyRules::TYPE_SINGLE_PROXY, + "PROXY myproxy:80", + NULL, + NULL, + NULL, + NULL, + }, + + // Multiple HTTP proxies for all schemes. + { + "myproxy:80,https://myotherproxy", + + ProxyConfig::ProxyRules::TYPE_SINGLE_PROXY, + "PROXY myproxy:80;HTTPS myotherproxy:443", + NULL, + NULL, + NULL, + NULL, + }, + + // Only specify a proxy server for "http://" urls. + { + "http=myproxy:80", + + ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME, + NULL, + "PROXY myproxy:80", + NULL, + NULL, + NULL, + }, + + // Specify an HTTP proxy for "ftp://" and a SOCKS proxy for "https://" urls. + { + "ftp=ftp-proxy ; https=socks4://foopy", + + ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME, + NULL, + NULL, + "SOCKS foopy:1080", + "PROXY ftp-proxy:80", + NULL, + }, + + // Give a scheme-specific proxy as well as a non-scheme specific. + // The first entry "foopy" takes precedance marking this list as + // TYPE_SINGLE_PROXY. + { + "foopy ; ftp=ftp-proxy", + + ProxyConfig::ProxyRules::TYPE_SINGLE_PROXY, + "PROXY foopy:80", + NULL, + NULL, + NULL, + NULL, + }, + + // Give a scheme-specific proxy as well as a non-scheme specific. + // The first entry "ftp=ftp-proxy" takes precedance marking this list as + // TYPE_PROXY_PER_SCHEME. + { + "ftp=ftp-proxy ; foopy", + + ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME, + NULL, + NULL, + NULL, + "PROXY ftp-proxy:80", + NULL, + }, + + // Include a list of entries for a single scheme. + { + "ftp=ftp1,ftp2,ftp3", + + ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME, + NULL, + NULL, + NULL, + "PROXY ftp1:80;PROXY ftp2:80;PROXY ftp3:80", + NULL, + }, + + // Include multiple entries for the same scheme -- they accumulate. + { + "http=http1,http2; http=http3", + + ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME, + NULL, + "PROXY http1:80;PROXY http2:80;PROXY http3:80", + NULL, + NULL, + NULL, + }, + + // Include lists of entries for multiple schemes. + { + "ftp=ftp1,ftp2,ftp3 ; http=http1,http2; ", + + ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME, + NULL, + "PROXY http1:80;PROXY http2:80", + NULL, + "PROXY ftp1:80;PROXY ftp2:80;PROXY ftp3:80", + NULL, + }, + + // Include non-default proxy schemes. + { + "http=https://secure_proxy; ftp=socks4://socks_proxy; https=socks://foo", + + ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME, + NULL, + "HTTPS secure_proxy:443", + "SOCKS5 foo:1080", + "SOCKS socks_proxy:1080", + NULL, + }, + + // Only SOCKS proxy present, others being blank. + { + "socks=foopy", + + ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME, + NULL, + NULL, + NULL, + NULL, + "SOCKS foopy:1080", + }, + + // SOCKS proxy present along with other proxies too + { + "http=httpproxy ; https=httpsproxy ; ftp=ftpproxy ; socks=foopy ", + + ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME, + NULL, + "PROXY httpproxy:80", + "PROXY httpsproxy:80", + "PROXY ftpproxy:80", + "SOCKS foopy:1080", + }, + + // SOCKS proxy (with modifier) present along with some proxies + // (FTP being blank) + { + "http=httpproxy ; https=httpsproxy ; socks=socks5://foopy ", + + ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME, + NULL, + "PROXY httpproxy:80", + "PROXY httpsproxy:80", + NULL, + "SOCKS5 foopy:1080", + }, + + // Include unsupported schemes -- they are discarded. + { + "crazy=foopy ; foo=bar ; https=myhttpsproxy", + + ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME, + NULL, + NULL, + "PROXY myhttpsproxy:80", + NULL, + NULL, + }, + + // direct:// as first option for a scheme. + { + "http=direct://,myhttpproxy; https=direct://", + + ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME, + NULL, + "DIRECT;PROXY myhttpproxy:80", + "DIRECT", + NULL, + NULL, + }, + + // direct:// as a second option for a scheme. + { + "http=myhttpproxy,direct://", + + ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME, + NULL, + "PROXY myhttpproxy:80;DIRECT", + NULL, + NULL, + NULL, + }, + + }; + + ProxyConfig config; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { + config.proxy_rules().ParseFromString(tests[i].proxy_rules); + + EXPECT_EQ(tests[i].type, config.proxy_rules().type); + ExpectProxyServerEquals(tests[i].single_proxy, + config.proxy_rules().single_proxies); + ExpectProxyServerEquals(tests[i].proxy_for_http, + config.proxy_rules().proxies_for_http); + ExpectProxyServerEquals(tests[i].proxy_for_https, + config.proxy_rules().proxies_for_https); + ExpectProxyServerEquals(tests[i].proxy_for_ftp, + config.proxy_rules().proxies_for_ftp); + ExpectProxyServerEquals(tests[i].fallback_proxy, + config.proxy_rules().fallback_proxies); + } +} + +TEST(ProxyConfigTest, ProxyRulesSetBypassFlag) { + // Test whether the did_bypass_proxy() flag is set in proxy info correctly. + ProxyConfig::ProxyRules rules; + ProxyInfo result; + + rules.ParseFromString("http=httpproxy:80"); + rules.bypass_rules.AddRuleFromString(".com"); + + rules.Apply(GURL("http://example.com"), &result); + EXPECT_TRUE(result.is_direct_only()); + EXPECT_TRUE(result.did_bypass_proxy()); + + rules.Apply(GURL("http://example.org"), &result); + EXPECT_FALSE(result.is_direct()); + EXPECT_FALSE(result.did_bypass_proxy()); + + // Try with reversed bypass rules. + rules.reverse_bypass = true; + + rules.Apply(GURL("http://example.org"), &result); + EXPECT_TRUE(result.is_direct_only()); + EXPECT_TRUE(result.did_bypass_proxy()); + + rules.Apply(GURL("http://example.com"), &result); + EXPECT_FALSE(result.is_direct()); + EXPECT_FALSE(result.did_bypass_proxy()); +} + +} // namespace +} // namespace net diff --git a/chromium/net/proxy/proxy_info.cc b/chromium/net/proxy/proxy_info.cc new file mode 100644 index 00000000000..b6fcc86767f --- /dev/null +++ b/chromium/net/proxy/proxy_info.cc @@ -0,0 +1,90 @@ +// 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/proxy/proxy_info.h" + +#include "net/proxy/proxy_retry_info.h" + +namespace net { + +ProxyInfo::ProxyInfo() + : config_id_(ProxyConfig::kInvalidConfigID), + config_source_(PROXY_CONFIG_SOURCE_UNKNOWN), + did_bypass_proxy_(false), + did_use_pac_script_(false) { +} + +ProxyInfo::~ProxyInfo() { +} + +void ProxyInfo::Use(const ProxyInfo& other) { + proxy_resolve_start_time_ = other.proxy_resolve_start_time_; + proxy_resolve_end_time_ = other.proxy_resolve_end_time_; + proxy_list_ = other.proxy_list_; + proxy_retry_info_ = other.proxy_retry_info_; + config_id_ = other.config_id_; + config_source_ = other.config_source_; + did_bypass_proxy_ = other.did_bypass_proxy_; + did_use_pac_script_ = other.did_use_pac_script_; +} + +void ProxyInfo::UseDirect() { + Reset(); + proxy_list_.SetSingleProxyServer(ProxyServer::Direct()); +} + +void ProxyInfo::UseDirectWithBypassedProxy() { + UseDirect(); + did_bypass_proxy_ = true; +} + +void ProxyInfo::UseNamedProxy(const std::string& proxy_uri_list) { + Reset(); + proxy_list_.Set(proxy_uri_list); +} + +void ProxyInfo::UseProxyServer(const ProxyServer& proxy_server) { + Reset(); + proxy_list_.SetSingleProxyServer(proxy_server); +} + +void ProxyInfo::UsePacString(const std::string& pac_string) { + Reset(); + proxy_list_.SetFromPacString(pac_string); +} + +void ProxyInfo::UseProxyList(const ProxyList& proxy_list) { + Reset(); + proxy_list_ = proxy_list; +} + +std::string ProxyInfo::ToPacString() const { + return proxy_list_.ToPacString(); +} + +bool ProxyInfo::Fallback(const BoundNetLog& net_log) { + return proxy_list_.Fallback(&proxy_retry_info_, net_log); +} + +void ProxyInfo::DeprioritizeBadProxies( + const ProxyRetryInfoMap& proxy_retry_info) { + proxy_list_.DeprioritizeBadProxies(proxy_retry_info); +} + +void ProxyInfo::RemoveProxiesWithoutScheme(int scheme_bit_field) { + proxy_list_.RemoveProxiesWithoutScheme(scheme_bit_field); +} + +void ProxyInfo::Reset() { + proxy_resolve_start_time_ = base::TimeTicks(); + proxy_resolve_end_time_ = base::TimeTicks(); + proxy_list_.Clear(); + proxy_retry_info_.clear(); + config_id_ = ProxyConfig::kInvalidConfigID; + config_source_ = PROXY_CONFIG_SOURCE_UNKNOWN; + did_bypass_proxy_ = false; + did_use_pac_script_ = false; +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_info.h b/chromium/net/proxy/proxy_info.h new file mode 100644 index 00000000000..243cbba80fd --- /dev/null +++ b/chromium/net/proxy/proxy_info.h @@ -0,0 +1,169 @@ +// 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. + +#ifndef NET_PROXY_PROXY_INFO_H_ +#define NET_PROXY_PROXY_INFO_H_ + +#include <string> + +#include "base/time/time.h" +#include "net/base/net_export.h" +#include "net/base/net_log.h" +#include "net/proxy/proxy_config.h" +#include "net/proxy/proxy_list.h" +#include "net/proxy/proxy_retry_info.h" +#include "net/proxy/proxy_server.h" + +namespace net { + +// This object holds proxy information returned by ResolveProxy. +class NET_EXPORT ProxyInfo { + public: + ProxyInfo(); + ~ProxyInfo(); + // Default copy-constructor and assignment operator are OK! + + // Uses the same proxy server as the given |proxy_info|. + void Use(const ProxyInfo& proxy_info); + + // Uses a direct connection. + void UseDirect(); + + // Uses a direct connection. did_bypass_proxy() will return true to indicate + // that the direct connection is the result of configured proxy bypass rules. + void UseDirectWithBypassedProxy(); + + // Uses a specific proxy server, of the form: + // proxy-uri = [<scheme> "://"] <hostname> [":" <port>] + // This may optionally be a semi-colon delimited list of <proxy-uri>. + // It is OK to have LWS between entries. + void UseNamedProxy(const std::string& proxy_uri_list); + + // Sets the proxy list to a single entry, |proxy_server|. + void UseProxyServer(const ProxyServer& proxy_server); + + // Parses from the given PAC result. + void UsePacString(const std::string& pac_string); + + // Use the proxies from the given list. + void UseProxyList(const ProxyList& proxy_list); + + // Returns true if this proxy info specifies a direct connection. + bool is_direct() const { + // We don't implicitly fallback to DIRECT unless it was added to the list. + if (is_empty()) + return false; + return proxy_list_.Get().is_direct(); + } + + bool is_direct_only() const { + return is_direct() && proxy_list_.size() == 1 && proxy_retry_info_.empty(); + } + + // Returns true if the first valid proxy server is an https proxy. + bool is_https() const { + if (is_empty()) + return false; + return proxy_server().is_https(); + } + + // Returns true if the first valid proxy server is an http proxy. + bool is_http() const { + if (is_empty()) + return false; + return proxy_server().is_http(); + } + + // Returns true if the first valid proxy server is a socks server. + bool is_socks() const { + if (is_empty()) + return false; + return proxy_server().is_socks(); + } + + // Returns true if this proxy info has no proxies left to try. + bool is_empty() const { + return proxy_list_.IsEmpty(); + } + + // Returns true if this proxy resolution is using a direct connection due to + // proxy bypass rules. + bool did_bypass_proxy() const { + return did_bypass_proxy_; + } + + // Returns true if the proxy resolution was done using a PAC script. + bool did_use_pac_script() const { + return did_use_pac_script_; + } + + // Returns the first valid proxy server. is_empty() must be false to be able + // to call this function. + const ProxyServer& proxy_server() const { return proxy_list_.Get(); } + + // Returns the source for configuration settings used for proxy resolution. + ProxyConfigSource config_source() const { return config_source_; } + + // See description in ProxyList::ToPacString(). + std::string ToPacString() const; + + // Marks the current proxy as bad. Returns true if there is another proxy + // available to try in proxy list_. + bool Fallback(const BoundNetLog& net_log); + + // De-prioritizes the proxies that we have cached as not working, by moving + // them to the end of the proxy list. + void DeprioritizeBadProxies(const ProxyRetryInfoMap& proxy_retry_info); + + // Deletes any entry which doesn't have one of the specified proxy schemes. + void RemoveProxiesWithoutScheme(int scheme_bit_field); + + ProxyConfig::ID config_id() const { return config_id_; } + + base::TimeTicks proxy_resolve_start_time() const { + return proxy_resolve_start_time_; + } + + base::TimeTicks proxy_resolve_end_time() const { + return proxy_resolve_end_time_; + } + + private: + friend class ProxyService; + + const ProxyRetryInfoMap& proxy_retry_info() const { + return proxy_retry_info_; + } + + // Reset proxy and config settings. + void Reset(); + + // The ordered list of proxy servers (including DIRECT attempts) remaining to + // try. If proxy_list_ is empty, then there is nothing left to fall back to. + ProxyList proxy_list_; + + // List of proxies that have been tried already. + ProxyRetryInfoMap proxy_retry_info_; + + // This value identifies the proxy config used to initialize this object. + ProxyConfig::ID config_id_; + + // The source of the proxy settings used, + ProxyConfigSource config_source_; + + // Whether the proxy result represent a proxy bypass. + bool did_bypass_proxy_; + + // Whether we used a PAC script for resolving the proxy. + bool did_use_pac_script_; + + // How long it took to resolve the proxy. Times are both null if proxy was + // determined synchronously without running a PAC. + base::TimeTicks proxy_resolve_start_time_; + base::TimeTicks proxy_resolve_end_time_; +}; + +} // namespace net + +#endif // NET_PROXY_PROXY_INFO_H_ diff --git a/chromium/net/proxy/proxy_info_unittest.cc b/chromium/net/proxy/proxy_info_unittest.cc new file mode 100644 index 00000000000..377cff3438a --- /dev/null +++ b/chromium/net/proxy/proxy_info_unittest.cc @@ -0,0 +1,41 @@ +// 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/proxy/proxy_info.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { +namespace { + +TEST(ProxyInfoTest, ProxyInfoIsDirectOnly) { + // Test the is_direct_only() predicate. + ProxyInfo info; + + // An empty ProxyInfo is not considered direct. + EXPECT_FALSE(info.is_direct_only()); + + info.UseDirect(); + EXPECT_TRUE(info.is_direct_only()); + + info.UsePacString("DIRECT"); + EXPECT_TRUE(info.is_direct_only()); + + info.UsePacString("PROXY myproxy:80"); + EXPECT_FALSE(info.is_direct_only()); + + info.UsePacString("DIRECT; PROXY myproxy:80"); + EXPECT_TRUE(info.is_direct()); + EXPECT_FALSE(info.is_direct_only()); + + info.UsePacString("PROXY myproxy:80; DIRECT"); + EXPECT_FALSE(info.is_direct()); + EXPECT_FALSE(info.is_direct_only()); + // After falling back to direct, we shouldn't consider it DIRECT only. + EXPECT_TRUE(info.Fallback(BoundNetLog())); + EXPECT_TRUE(info.is_direct()); + EXPECT_FALSE(info.is_direct_only()); +} + +} // namespace +} // namespace net diff --git a/chromium/net/proxy/proxy_list.cc b/chromium/net/proxy/proxy_list.cc new file mode 100644 index 00000000000..baa638f6c7d --- /dev/null +++ b/chromium/net/proxy/proxy_list.cc @@ -0,0 +1,230 @@ +// 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/proxy/proxy_list.h" + +#include "base/callback.h" +#include "base/logging.h" +#include "base/rand_util.h" +#include "base/strings/string_tokenizer.h" +#include "base/time/time.h" +#include "base/values.h" +#include "net/proxy/proxy_server.h" + +using base::TimeDelta; +using base::TimeTicks; + +namespace net { + +ProxyList::ProxyList() { +} + +ProxyList::~ProxyList() { +} + +void ProxyList::Set(const std::string& proxy_uri_list) { + proxies_.clear(); + base::StringTokenizer str_tok(proxy_uri_list, ";"); + while (str_tok.GetNext()) { + ProxyServer uri = ProxyServer::FromURI( + str_tok.token_begin(), str_tok.token_end(), ProxyServer::SCHEME_HTTP); + // Silently discard malformed inputs. + if (uri.is_valid()) + proxies_.push_back(uri); + } +} + +void ProxyList::SetSingleProxyServer(const ProxyServer& proxy_server) { + proxies_.clear(); + AddProxyServer(proxy_server); +} + +void ProxyList::AddProxyServer(const ProxyServer& proxy_server) { + if (proxy_server.is_valid()) + proxies_.push_back(proxy_server); +} + +void ProxyList::DeprioritizeBadProxies( + const ProxyRetryInfoMap& proxy_retry_info) { + // Partition the proxy list in two: + // (1) the known bad proxies + // (2) everything else + std::vector<ProxyServer> good_proxies; + std::vector<ProxyServer> bad_proxies; + + std::vector<ProxyServer>::const_iterator iter = proxies_.begin(); + for (; iter != proxies_.end(); ++iter) { + ProxyRetryInfoMap::const_iterator bad_proxy = + proxy_retry_info.find(iter->ToURI()); + if (bad_proxy != proxy_retry_info.end()) { + // This proxy is bad. Check if it's time to retry. + if (bad_proxy->second.bad_until >= TimeTicks::Now()) { + // still invalid. + bad_proxies.push_back(*iter); + continue; + } + } + good_proxies.push_back(*iter); + } + + // "proxies_ = good_proxies + bad_proxies" + proxies_.swap(good_proxies); + proxies_.insert(proxies_.end(), bad_proxies.begin(), bad_proxies.end()); +} + +bool ProxyList::HasUntriedProxies( + const ProxyRetryInfoMap& proxy_retry_info) const { + std::vector<ProxyServer>::const_iterator iter = proxies_.begin(); + for (; iter != proxies_.end(); ++iter) { + ProxyRetryInfoMap::const_iterator bad_proxy = + proxy_retry_info.find(iter->ToURI()); + if (bad_proxy != proxy_retry_info.end()) { + // This proxy is bad. Check if it's time to retry. + if (bad_proxy->second.bad_until >= TimeTicks::Now()) { + continue; + } + } + // Either we've found the entry in the retry map and it's expired or we + // didn't find a corresponding entry in the retry map. In either case, we + // have a proxy to try. + return true; + } + return false; +} + +void ProxyList::RemoveProxiesWithoutScheme(int scheme_bit_field) { + for (std::vector<ProxyServer>::iterator it = proxies_.begin(); + it != proxies_.end(); ) { + if (!(scheme_bit_field & it->scheme())) { + it = proxies_.erase(it); + continue; + } + ++it; + } +} + +void ProxyList::Clear() { + proxies_.clear(); +} + +bool ProxyList::IsEmpty() const { + return proxies_.empty(); +} + +size_t ProxyList::size() const { + return proxies_.size(); +} + +// Returns true if |*this| lists the same proxies as |other|. +bool ProxyList::Equals(const ProxyList& other) const { + if (size() != other.size()) + return false; + return proxies_ == other.proxies_; +} + +const ProxyServer& ProxyList::Get() const { + DCHECK(!proxies_.empty()); + return proxies_[0]; +} + +void ProxyList::SetFromPacString(const std::string& pac_string) { + base::StringTokenizer entry_tok(pac_string, ";"); + proxies_.clear(); + while (entry_tok.GetNext()) { + ProxyServer uri = ProxyServer::FromPacString( + entry_tok.token_begin(), entry_tok.token_end()); + // Silently discard malformed inputs. + if (uri.is_valid()) + proxies_.push_back(uri); + } + + // If we failed to parse anything from the PAC results list, fallback to + // DIRECT (this basically means an error in the PAC script). + if (proxies_.empty()) { + proxies_.push_back(ProxyServer::Direct()); + } +} + +std::string ProxyList::ToPacString() const { + std::string proxy_list; + std::vector<ProxyServer>::const_iterator iter = proxies_.begin(); + for (; iter != proxies_.end(); ++iter) { + if (!proxy_list.empty()) + proxy_list += ";"; + proxy_list += iter->ToPacString(); + } + return proxy_list.empty() ? std::string() : proxy_list; +} + +base::ListValue* ProxyList::ToValue() const { + base::ListValue* list = new base::ListValue(); + for (size_t i = 0; i < proxies_.size(); ++i) + list->AppendString(proxies_[i].ToURI()); + return list; +} + +bool ProxyList::Fallback(ProxyRetryInfoMap* proxy_retry_info, + const BoundNetLog& net_log) { + + // TODO(eroman): It would be good if instead of removing failed proxies + // from the list, we simply annotated them with the error code they failed + // with. Of course, ProxyService::ReconsiderProxyAfterError() would need to + // be given this information by the network transaction. + // + // The advantage of this approach is when the network transaction + // fails, we could output the full list of proxies that were attempted, and + // why each one of those failed (as opposed to just the last failure). + // + // And also, before failing the transaction wholesale, we could go back and + // retry the "bad proxies" which we never tried to begin with. + // (RemoveBadProxies would annotate them as 'expected bad' rather then delete + // them from the list, so we would know what they were). + + if (proxies_.empty()) { + NOTREACHED(); + return false; + } + UpdateRetryInfoOnFallback(proxy_retry_info, net_log); + + // Remove this proxy from our list. + proxies_.erase(proxies_.begin()); + return !proxies_.empty(); +} + +void ProxyList::UpdateRetryInfoOnFallback( + ProxyRetryInfoMap* proxy_retry_info, const BoundNetLog& net_log) const { + // Time to wait before retrying a bad proxy server. +#if defined(OS_ANDROID) || defined(OS_IOS) + // Randomize the timeout over a range from one to five minutes. + const TimeDelta proxy_retry_delay = + TimeDelta::FromMilliseconds(base::RandInt(1 * 60 * 1000, 5 * 60 * 1000)); +#else + const TimeDelta proxy_retry_delay = TimeDelta::FromMinutes(5); +#endif + + if (proxies_.empty()) { + NOTREACHED(); + return; + } + + if (!proxies_[0].is_direct()) { + std::string key = proxies_[0].ToURI(); + // Mark this proxy as bad. + ProxyRetryInfoMap::iterator iter = proxy_retry_info->find(key); + if (iter != proxy_retry_info->end()) { + // TODO(nsylvain): This is not the first time we get this. We should + // double the retry time. Bug 997660. + iter->second.bad_until = TimeTicks::Now() + iter->second.current_delay; + } else { + ProxyRetryInfo retry_info; + retry_info.current_delay = proxy_retry_delay; + retry_info.bad_until = TimeTicks().Now() + retry_info.current_delay; + (*proxy_retry_info)[key] = retry_info; + } + net_log.AddEvent(NetLog::TYPE_PROXY_LIST_FALLBACK, + NetLog::StringCallback("bad_proxy", &key)); + } +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_list.h b/chromium/net/proxy/proxy_list.h new file mode 100644 index 00000000000..9f5fa59db06 --- /dev/null +++ b/chromium/net/proxy/proxy_list.h @@ -0,0 +1,103 @@ +// 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. + +#ifndef NET_PROXY_PROXY_LIST_H_ +#define NET_PROXY_PROXY_LIST_H_ + +#include <string> +#include <vector> + +#include "net/base/net_export.h" +#include "net/base/net_log.h" +#include "net/proxy/proxy_retry_info.h" + +namespace base { +class ListValue; +} + +namespace net { + +class ProxyServer; + +// This class is used to hold a list of proxies returned by GetProxyForUrl or +// manually configured. It handles proxy fallback if multiple servers are +// specified. +class NET_EXPORT_PRIVATE ProxyList { + public: + ProxyList(); + ~ProxyList(); + + // Initializes the proxy list to a string containing one or more proxy servers + // delimited by a semicolon. + void Set(const std::string& proxy_uri_list); + + // Set the proxy list to a single entry, |proxy_server|. + void SetSingleProxyServer(const ProxyServer& proxy_server); + + // Append a single proxy server to the end of the proxy list. + void AddProxyServer(const ProxyServer& proxy_server); + + // De-prioritizes the proxies that we have cached as not working, by moving + // them to the end of the fallback list. + void DeprioritizeBadProxies(const ProxyRetryInfoMap& proxy_retry_info); + + // Returns true if this proxy list contains at least one proxy that is + // not currently present in |proxy_retry_info|. + bool HasUntriedProxies(const ProxyRetryInfoMap& proxy_retry_info) const; + + // Delete any entry which doesn't have one of the specified proxy schemes. + // |scheme_bit_field| is a bunch of ProxyServer::Scheme bitwise ORed together. + void RemoveProxiesWithoutScheme(int scheme_bit_field); + + // Clear the proxy list. + void Clear(); + + // Returns true if there is nothing left in the ProxyList. + bool IsEmpty() const; + + // Returns the number of proxy servers in this list. + size_t size() const; + + // Returns true if |*this| lists the same proxies as |other|. + bool Equals(const ProxyList& other) const; + + // Returns the first proxy server in the list. It is only valid to call + // this if !IsEmpty(). + const ProxyServer& Get() const; + + // Sets the list by parsing the pac result |pac_string|. + // Some examples for |pac_string|: + // "DIRECT" + // "PROXY foopy1" + // "PROXY foopy1; SOCKS4 foopy2:1188" + // Does a best-effort parse, and silently discards any errors. + void SetFromPacString(const std::string& pac_string); + + // Returns a PAC-style semicolon-separated list of valid proxy servers. + // For example: "PROXY xxx.xxx.xxx.xxx:xx; SOCKS yyy.yyy.yyy:yy". + std::string ToPacString() const; + + // Returns a serialized value for the list. The caller takes ownership of it. + base::ListValue* ToValue() const; + + // Marks the current proxy server as bad and deletes it from the list. The + // list of known bad proxies is given by proxy_retry_info. Returns true if + // there is another server available in the list. + bool Fallback(ProxyRetryInfoMap* proxy_retry_info, + const BoundNetLog& net_log); + + // Updates |proxy_retry_info| to indicate that the first proxy in the list + // is bad. This is distinct from Fallback(), above, to allow updating proxy + // retry information without modifying a given transction's proxy list. + void UpdateRetryInfoOnFallback(ProxyRetryInfoMap* proxy_retry_info, + const BoundNetLog& net_log) const; + + private: + // List of proxies. + std::vector<ProxyServer> proxies_; +}; + +} // namespace net + +#endif // NET_PROXY_PROXY_LIST_H_ diff --git a/chromium/net/proxy/proxy_list_unittest.cc b/chromium/net/proxy/proxy_list_unittest.cc new file mode 100644 index 00000000000..470a9000882 --- /dev/null +++ b/chromium/net/proxy/proxy_list_unittest.cc @@ -0,0 +1,183 @@ +// Copyright (c) 2006-2008 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/proxy/proxy_list.h" + +#include "net/proxy/proxy_server.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { + +namespace { + +// Test parsing from a PAC string. +TEST(ProxyListTest, SetFromPacString) { + const struct { + const char* pac_input; + const char* pac_output; + } tests[] = { + // Valid inputs: + { "PROXY foopy:10", + "PROXY foopy:10", + }, + { " DIRECT", // leading space. + "DIRECT", + }, + { "PROXY foopy1 ; proxy foopy2;\t DIRECT", + "PROXY foopy1:80;PROXY foopy2:80;DIRECT", + }, + { "proxy foopy1 ; SOCKS foopy2", + "PROXY foopy1:80;SOCKS foopy2:1080", + }, + // Try putting DIRECT first. + { "DIRECT ; proxy foopy1 ; DIRECT ; SOCKS5 foopy2;DIRECT ", + "DIRECT;PROXY foopy1:80;DIRECT;SOCKS5 foopy2:1080;DIRECT", + }, + // Try putting DIRECT consecutively. + { "DIRECT ; proxy foopy1:80; DIRECT ; DIRECT", + "DIRECT;PROXY foopy1:80;DIRECT;DIRECT", + }, + + // Invalid inputs (parts which aren't understood get + // silently discarded): + // + // If the proxy list string parsed to empty, automatically fall-back to + // DIRECT. + { "PROXY-foopy:10", + "DIRECT", + }, + { "PROXY", + "DIRECT", + }, + { "PROXY foopy1 ; JUNK ; JUNK ; SOCKS5 foopy2 ; ;", + "PROXY foopy1:80;SOCKS5 foopy2:1080", + }, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { + ProxyList list; + list.SetFromPacString(tests[i].pac_input); + EXPECT_EQ(tests[i].pac_output, list.ToPacString()); + EXPECT_FALSE(list.IsEmpty()); + } +} + +TEST(ProxyListTest, RemoveProxiesWithoutScheme) { + const struct { + const char* pac_input; + int filter; + const char* filtered_pac_output; + } tests[] = { + { "PROXY foopy:10 ; SOCKS5 foopy2 ; SOCKS foopy11 ; PROXY foopy3 ; DIRECT", + // Remove anything that isn't HTTP or DIRECT. + ProxyServer::SCHEME_DIRECT | ProxyServer::SCHEME_HTTP, + "PROXY foopy:10;PROXY foopy3:80;DIRECT", + }, + { "PROXY foopy:10 ; SOCKS5 foopy2", + // Remove anything that isn't HTTP or SOCKS5. + ProxyServer::SCHEME_DIRECT | ProxyServer::SCHEME_SOCKS4, + "", + }, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { + ProxyList list; + list.SetFromPacString(tests[i].pac_input); + list.RemoveProxiesWithoutScheme(tests[i].filter); + EXPECT_EQ(tests[i].filtered_pac_output, list.ToPacString()); + } +} + +TEST(ProxyListTest, HasUntriedProxies) { + // As in DeprioritizeBadProxies, we use a lengthy timeout to avoid depending + // on the current time. + ProxyRetryInfo proxy_retry_info; + proxy_retry_info.bad_until = + base::TimeTicks::Now() + base::TimeDelta::FromDays(1); + + // An empty list has nothing to try. + { + ProxyList list; + ProxyRetryInfoMap proxy_retry_info; + EXPECT_FALSE(list.HasUntriedProxies(proxy_retry_info)); + } + + // A list with one bad proxy has something to try. With two bad proxies, + // there's nothing to try. + { + ProxyList list; + list.SetFromPacString("PROXY bad1:80; PROXY bad2:80"); + ProxyRetryInfoMap retry_info_map; + retry_info_map["bad1:80"] = proxy_retry_info; + EXPECT_TRUE(list.HasUntriedProxies(retry_info_map)); + retry_info_map["bad2:80"] = proxy_retry_info; + EXPECT_FALSE(list.HasUntriedProxies(retry_info_map)); + } + + // A list with one bad proxy and a DIRECT entry has something to try. + { + ProxyList list; + list.SetFromPacString("PROXY bad1:80; DIRECT"); + ProxyRetryInfoMap retry_info_map; + retry_info_map["bad1:80"] = proxy_retry_info; + EXPECT_TRUE(list.HasUntriedProxies(retry_info_map)); + } +} + +TEST(ProxyListTest, DeprioritizeBadProxies) { + // Retry info that marks a proxy as being bad for a *very* long time (to avoid + // the test depending on the current time.) + ProxyRetryInfo proxy_retry_info; + proxy_retry_info.bad_until = + base::TimeTicks::Now() + base::TimeDelta::FromDays(1); + + // Call DeprioritizeBadProxies with an empty map -- should have no effect. + { + ProxyList list; + list.SetFromPacString("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80"); + + ProxyRetryInfoMap retry_info_map; + list.DeprioritizeBadProxies(retry_info_map); + EXPECT_EQ("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80", + list.ToPacString()); + } + + // Call DeprioritizeBadProxies with 2 of the three proxies marked as bad. + // These proxies should be retried last. + { + ProxyList list; + list.SetFromPacString("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80"); + + ProxyRetryInfoMap retry_info_map; + retry_info_map["foopy1:80"] = proxy_retry_info; + retry_info_map["foopy3:80"] = proxy_retry_info; + retry_info_map["socks5://localhost:1080"] = proxy_retry_info; + + list.DeprioritizeBadProxies(retry_info_map); + + EXPECT_EQ("PROXY foopy2:80;PROXY foopy1:80;PROXY foopy3:80", + list.ToPacString()); + } + + // Call DeprioritizeBadProxies where ALL of the proxies are marked as bad. + // This should have no effect on the order. + { + ProxyList list; + list.SetFromPacString("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80"); + + ProxyRetryInfoMap retry_info_map; + retry_info_map["foopy1:80"] = proxy_retry_info; + retry_info_map["foopy2:80"] = proxy_retry_info; + retry_info_map["foopy3:80"] = proxy_retry_info; + + list.DeprioritizeBadProxies(retry_info_map); + + EXPECT_EQ("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80", + list.ToPacString()); + } +} + +} // namesapce + +} // namespace net diff --git a/chromium/net/proxy/proxy_resolver.h b/chromium/net/proxy/proxy_resolver.h new file mode 100644 index 00000000000..47ec31af5c6 --- /dev/null +++ b/chromium/net/proxy/proxy_resolver.h @@ -0,0 +1,83 @@ +// 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. + +#ifndef NET_PROXY_PROXY_RESOLVER_H_ +#define NET_PROXY_PROXY_RESOLVER_H_ + +#include "base/logging.h" +#include "base/memory/ref_counted.h" +#include "base/strings/string16.h" +#include "net/base/completion_callback.h" +#include "net/base/load_states.h" +#include "net/base/net_export.h" +#include "net/proxy/proxy_resolver_script_data.h" +#include "url/gurl.h" + +namespace net { + +class BoundNetLog; +class ProxyInfo; + +// Interface for "proxy resolvers". A ProxyResolver fills in a list of proxies +// to use for a particular URL. Generally the backend for a ProxyResolver is +// a PAC script, but it doesn't need to be. ProxyResolver can service multiple +// requests at a time. +class NET_EXPORT_PRIVATE ProxyResolver { + public: + // Opaque pointer type, to return a handle to cancel outstanding requests. + typedef void* RequestHandle; + + // See |expects_pac_bytes()| for the meaning of |expects_pac_bytes|. + explicit ProxyResolver(bool expects_pac_bytes) + : expects_pac_bytes_(expects_pac_bytes) {} + + virtual ~ProxyResolver() {} + + // Gets a list of proxy servers to use for |url|. If the request will + // complete asynchronously returns ERR_IO_PENDING and notifies the result + // by running |callback|. If the result code is OK then + // the request was successful and |results| contains the proxy + // resolution information. In the case of asynchronous completion + // |*request| is written to, and can be passed to CancelRequest(). + virtual int GetProxyForURL(const GURL& url, + ProxyInfo* results, + const net::CompletionCallback& callback, + RequestHandle* request, + const BoundNetLog& net_log) = 0; + + // Cancels |request|. + virtual void CancelRequest(RequestHandle request) = 0; + + // Gets the LoadState for |request|. + virtual LoadState GetLoadState(RequestHandle request) const = 0; + + // The PAC script backend can be specified to the ProxyResolver either via + // URL, or via the javascript text itself. If |expects_pac_bytes| is true, + // then the ProxyResolverScriptData passed to SetPacScript() should + // contain the actual script bytes rather than just the URL. + bool expects_pac_bytes() const { return expects_pac_bytes_; } + + virtual void CancelSetPacScript() = 0; + + // Frees any unneeded memory held by the resolver, e.g. garbage in the JS + // engine. Most subclasses don't need to do anything, so we provide a default + // no-op implementation. + virtual void PurgeMemory() {} + + // Called to set the PAC script backend to use. + // Returns ERR_IO_PENDING in the case of asynchronous completion, and notifies + // the result through |callback|. + virtual int SetPacScript( + const scoped_refptr<ProxyResolverScriptData>& pac_script, + const net::CompletionCallback& callback) = 0; + + private: + const bool expects_pac_bytes_; + + DISALLOW_COPY_AND_ASSIGN(ProxyResolver); +}; + +} // namespace net + +#endif // NET_PROXY_PROXY_RESOLVER_H_ diff --git a/chromium/net/proxy/proxy_resolver_error_observer.h b/chromium/net/proxy/proxy_resolver_error_observer.h new file mode 100644 index 00000000000..190b78759ba --- /dev/null +++ b/chromium/net/proxy/proxy_resolver_error_observer.h @@ -0,0 +1,37 @@ +// 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. + +#ifndef NET_PROXY_PROXY_RESOLVER_ERROR_OBSERVER_H_ +#define NET_PROXY_PROXY_RESOLVER_ERROR_OBSERVER_H_ + +#include "base/basictypes.h" +#include "base/strings/string16.h" +#include "net/base/net_export.h" + +namespace net { + +// Interface for observing JavaScript error messages from PAC scripts. +class NET_EXPORT_PRIVATE ProxyResolverErrorObserver { + public: + ProxyResolverErrorObserver() {} + virtual ~ProxyResolverErrorObserver() {} + + // Handler for when an error is encountered. |line_number| may be -1 + // if a line number is not applicable to this error. |error| is a message + // describing the error. + // + // Note on threading: This may get called from a worker thread. If the + // backing proxy resolver is ProxyResolverV8Tracing, then it will not + // be called concurrently, however it will be called from a different + // thread than the proxy resolver's origin thread. + virtual void OnPACScriptError(int line_number, + const base::string16& error) = 0; + + private: + DISALLOW_COPY_AND_ASSIGN(ProxyResolverErrorObserver); +}; + +} // namespace net + +#endif // NET_PROXY_PROXY_RESOLVER_ERROR_OBSERVER_H_ diff --git a/chromium/net/proxy/proxy_resolver_mac.cc b/chromium/net/proxy/proxy_resolver_mac.cc new file mode 100644 index 00000000000..8ddf81fffe4 --- /dev/null +++ b/chromium/net/proxy/proxy_resolver_mac.cc @@ -0,0 +1,204 @@ +// 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/proxy/proxy_resolver_mac.h" + +#include <CoreFoundation/CoreFoundation.h> + +#include "base/logging.h" +#include "base/mac/foundation_util.h" +#include "base/mac/scoped_cftyperef.h" +#include "base/strings/string_util.h" +#include "base/strings/sys_string_conversions.h" +#include "net/base/net_errors.h" +#include "net/proxy/proxy_info.h" +#include "net/proxy/proxy_server.h" + +#if defined(OS_IOS) +#include <CFNetwork/CFProxySupport.h> +#else +#include <CoreServices/CoreServices.h> +#endif + +namespace { + +// Utility function to map a CFProxyType to a ProxyServer::Scheme. +// If the type is unknown, returns ProxyServer::SCHEME_INVALID. +net::ProxyServer::Scheme GetProxyServerScheme(CFStringRef proxy_type) { + if (CFEqual(proxy_type, kCFProxyTypeNone)) + return net::ProxyServer::SCHEME_DIRECT; + if (CFEqual(proxy_type, kCFProxyTypeHTTP)) + return net::ProxyServer::SCHEME_HTTP; + if (CFEqual(proxy_type, kCFProxyTypeHTTPS)) { + // The "HTTPS" on the Mac side here means "proxy applies to https://" URLs; + // the proxy itself is still expected to be an HTTP proxy. + return net::ProxyServer::SCHEME_HTTP; + } + if (CFEqual(proxy_type, kCFProxyTypeSOCKS)) { + // We can't tell whether this was v4 or v5. We will assume it is + // v5 since that is the only version OS X supports. + return net::ProxyServer::SCHEME_SOCKS5; + } + return net::ProxyServer::SCHEME_INVALID; +} + +// Callback for CFNetworkExecuteProxyAutoConfigurationURL. |client| is a pointer +// to a CFTypeRef. This stashes either |error| or |proxies| in that location. +void ResultCallback(void* client, CFArrayRef proxies, CFErrorRef error) { + DCHECK((proxies != NULL) == (error == NULL)); + + CFTypeRef* result_ptr = reinterpret_cast<CFTypeRef*>(client); + DCHECK(result_ptr != NULL); + DCHECK(*result_ptr == NULL); + + if (error != NULL) { + *result_ptr = CFRetain(error); + } else { + *result_ptr = CFRetain(proxies); + } + CFRunLoopStop(CFRunLoopGetCurrent()); +} + +} // namespace + +namespace net { + +ProxyResolverMac::ProxyResolverMac() + : ProxyResolver(false /*expects_pac_bytes*/) { +} + +ProxyResolverMac::~ProxyResolverMac() {} + +// Gets the proxy information for a query URL from a PAC. Implementation +// inspired by http://developer.apple.com/samplecode/CFProxySupportTool/ +int ProxyResolverMac::GetProxyForURL(const GURL& query_url, + ProxyInfo* results, + const CompletionCallback& /*callback*/, + RequestHandle* /*request*/, + const BoundNetLog& net_log) { + base::ScopedCFTypeRef<CFStringRef> query_ref( + base::SysUTF8ToCFStringRef(query_url.spec())); + base::ScopedCFTypeRef<CFURLRef> query_url_ref( + CFURLCreateWithString(kCFAllocatorDefault, query_ref.get(), NULL)); + if (!query_url_ref.get()) + return ERR_FAILED; + base::ScopedCFTypeRef<CFStringRef> pac_ref(base::SysUTF8ToCFStringRef( + script_data_->type() == ProxyResolverScriptData::TYPE_AUTO_DETECT + ? std::string() + : script_data_->url().spec())); + base::ScopedCFTypeRef<CFURLRef> pac_url_ref( + CFURLCreateWithString(kCFAllocatorDefault, pac_ref.get(), NULL)); + if (!pac_url_ref.get()) + return ERR_FAILED; + + // Work around <rdar://problem/5530166>. This dummy call to + // CFNetworkCopyProxiesForURL initializes some state within CFNetwork that is + // required by CFNetworkExecuteProxyAutoConfigurationURL. + + CFArrayRef dummy_result = CFNetworkCopyProxiesForURL(query_url_ref.get(), + NULL); + if (dummy_result) + CFRelease(dummy_result); + + // We cheat here. We need to act as if we were synchronous, so we pump the + // runloop ourselves. Our caller moved us to a new thread anyway, so this is + // OK to do. (BTW, CFNetworkExecuteProxyAutoConfigurationURL returns a + // runloop source we need to release despite its name.) + + CFTypeRef result = NULL; + CFStreamClientContext context = { 0, &result, NULL, NULL, NULL }; + base::ScopedCFTypeRef<CFRunLoopSourceRef> runloop_source( + CFNetworkExecuteProxyAutoConfigurationURL( + pac_url_ref.get(), query_url_ref.get(), ResultCallback, &context)); + if (!runloop_source) + return ERR_FAILED; + + const CFStringRef private_runloop_mode = + CFSTR("org.chromium.ProxyResolverMac"); + + CFRunLoopAddSource(CFRunLoopGetCurrent(), runloop_source.get(), + private_runloop_mode); + CFRunLoopRunInMode(private_runloop_mode, DBL_MAX, false); + CFRunLoopRemoveSource(CFRunLoopGetCurrent(), runloop_source.get(), + private_runloop_mode); + DCHECK(result != NULL); + + if (CFGetTypeID(result) == CFErrorGetTypeID()) { + // TODO(avi): do something better than this + CFRelease(result); + return ERR_FAILED; + } + base::ScopedCFTypeRef<CFArrayRef> proxy_array_ref( + base::mac::CFCastStrict<CFArrayRef>(result)); + DCHECK(proxy_array_ref != NULL); + + // This string will be an ordered list of <proxy-uri> entries, separated by + // semi-colons. It is the format that ProxyInfo::UseNamedProxy() expects. + // proxy-uri = [<proxy-scheme>"://"]<proxy-host>":"<proxy-port> + // (This also includes entries for direct connection, as "direct://"). + std::string proxy_uri_list; + + CFIndex proxy_array_count = CFArrayGetCount(proxy_array_ref.get()); + for (CFIndex i = 0; i < proxy_array_count; ++i) { + CFDictionaryRef proxy_dictionary = base::mac::CFCastStrict<CFDictionaryRef>( + CFArrayGetValueAtIndex(proxy_array_ref.get(), i)); + DCHECK(proxy_dictionary != NULL); + + // The dictionary may have the following keys: + // - kCFProxyTypeKey : The type of the proxy + // - kCFProxyHostNameKey + // - kCFProxyPortNumberKey : The meat we're after. + // - kCFProxyUsernameKey + // - kCFProxyPasswordKey : Despite the existence of these keys in the + // documentation, they're never populated. Even if a + // username/password were to be set in the network + // proxy system preferences, we'd need to fetch it + // from the Keychain ourselves. CFProxy is such a + // tease. + // - kCFProxyAutoConfigurationURLKey : If the PAC file specifies another + // PAC file, I'm going home. + + CFStringRef proxy_type = base::mac::GetValueFromDictionary<CFStringRef>( + proxy_dictionary, kCFProxyTypeKey); + ProxyServer proxy_server = ProxyServer::FromDictionary( + GetProxyServerScheme(proxy_type), + proxy_dictionary, + kCFProxyHostNameKey, + kCFProxyPortNumberKey); + if (!proxy_server.is_valid()) + continue; + + if (!proxy_uri_list.empty()) + proxy_uri_list += ";"; + proxy_uri_list += proxy_server.ToURI(); + } + + if (!proxy_uri_list.empty()) + results->UseNamedProxy(proxy_uri_list); + // Else do nothing (results is already guaranteed to be in the default state). + + return OK; +} + +void ProxyResolverMac::CancelRequest(RequestHandle request) { + NOTREACHED(); +} + +LoadState ProxyResolverMac::GetLoadState(RequestHandle request) const { + NOTREACHED(); + return LOAD_STATE_IDLE; +} + +void ProxyResolverMac::CancelSetPacScript() { + NOTREACHED(); +} + +int ProxyResolverMac::SetPacScript( + const scoped_refptr<ProxyResolverScriptData>& script_data, + const CompletionCallback& /*callback*/) { + script_data_ = script_data; + return OK; +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_resolver_mac.h b/chromium/net/proxy/proxy_resolver_mac.h new file mode 100644 index 00000000000..24eb10dac86 --- /dev/null +++ b/chromium/net/proxy/proxy_resolver_mac.h @@ -0,0 +1,46 @@ +// 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. + +#ifndef NET_PROXY_PROXY_RESOLVER_MAC_H_ +#define NET_PROXY_PROXY_RESOLVER_MAC_H_ + +#include "base/compiler_specific.h" +#include "net/base/net_errors.h" +#include "net/base/net_export.h" +#include "net/proxy/proxy_resolver.h" +#include "url/gurl.h" + +namespace net { + +// Implementation of ProxyResolver that uses the Mac CFProxySupport to implement +// proxies. +class NET_EXPORT ProxyResolverMac : public ProxyResolver { + public: + ProxyResolverMac(); + virtual ~ProxyResolverMac(); + + // ProxyResolver methods: + virtual int GetProxyForURL(const GURL& url, + ProxyInfo* results, + const net::CompletionCallback& callback, + RequestHandle* request, + const BoundNetLog& net_log) OVERRIDE; + + virtual void CancelRequest(RequestHandle request) OVERRIDE; + + virtual LoadState GetLoadState(RequestHandle request) const OVERRIDE; + + virtual void CancelSetPacScript() OVERRIDE; + + virtual int SetPacScript( + const scoped_refptr<ProxyResolverScriptData>& script_data, + const net::CompletionCallback& /*callback*/) OVERRIDE; + + private: + scoped_refptr<ProxyResolverScriptData> script_data_; +}; + +} // namespace net + +#endif // NET_PROXY_PROXY_RESOLVER_MAC_H_ diff --git a/chromium/net/proxy/proxy_resolver_perftest.cc b/chromium/net/proxy/proxy_resolver_perftest.cc new file mode 100644 index 00000000000..3faf3961b81 --- /dev/null +++ b/chromium/net/proxy/proxy_resolver_perftest.cc @@ -0,0 +1,229 @@ +// 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 "base/base_paths.h" +#include "base/compiler_specific.h" +#include "base/file_util.h" +#include "base/path_service.h" +#include "base/perftimer.h" +#include "base/strings/string_util.h" +#include "net/base/net_errors.h" +#include "net/dns/mock_host_resolver.h" +#include "net/proxy/proxy_info.h" +#include "net/proxy/proxy_resolver_v8.h" +#include "net/test/spawned_test_server/spawned_test_server.h" +#include "testing/gtest/include/gtest/gtest.h" + +#if defined(OS_WIN) +#include "net/proxy/proxy_resolver_winhttp.h" +#elif defined(OS_MACOSX) +#include "net/proxy/proxy_resolver_mac.h" +#endif + +// This class holds the URL to use for resolving, and the expected result. +// We track the expected result in order to make sure the performance +// test is actually resolving URLs properly, otherwise the perf numbers +// are meaningless :-) +struct PacQuery { + const char* query_url; + const char* expected_result; +}; + +// Entry listing which PAC scripts to load, and which URLs to try resolving. +// |queries| should be terminated by {NULL, NULL}. A sentinel is used +// rather than a length, to simplify using initializer lists. +struct PacPerfTest { + const char* pac_name; + PacQuery queries[100]; + + // Returns the actual number of entries in |queries| (assumes NULL sentinel). + int NumQueries() const; +}; + +// List of performance tests. +static PacPerfTest kPerfTests[] = { + // This test uses an ad-blocker PAC script. This script is very heavily + // regular expression oriented, and has no dependencies on the current + // IP address, or DNS resolving of hosts. + { "no-ads.pac", + { // queries: + {"http://www.google.com", "DIRECT"}, + {"http://www.imdb.com/photos/cmsicons/x", "PROXY 0.0.0.0:3421"}, + {"http://www.imdb.com/x", "DIRECT"}, + {"http://www.staples.com/", "DIRECT"}, + {"http://www.staples.com/pixeltracker/x", "PROXY 0.0.0.0:3421"}, + {"http://www.staples.com/pixel/x", "DIRECT"}, + {"http://www.foobar.com", "DIRECT"}, + {"http://www.foobarbaz.com/x/y/z", "DIRECT"}, + {"http://www.testurl1.com/index.html", "DIRECT"}, + {"http://www.testurl2.com", "DIRECT"}, + {"https://www.sample/pirate/arrrrrr", "DIRECT"}, + {NULL, NULL} + }, + }, +}; + +int PacPerfTest::NumQueries() const { + for (size_t i = 0; i < arraysize(queries); ++i) { + if (queries[i].query_url == NULL) + return i; + } + NOTREACHED(); // Bad definition. + return 0; +} + +// The number of URLs to resolve when testing a PAC script. +const int kNumIterations = 500; + +// Helper class to run through all the performance tests using the specified +// proxy resolver implementation. +class PacPerfSuiteRunner { + public: + // |resolver_name| is the label used when logging the results. + PacPerfSuiteRunner(net::ProxyResolver* resolver, + const std::string& resolver_name) + : resolver_(resolver), + resolver_name_(resolver_name), + test_server_( + net::SpawnedTestServer::TYPE_HTTP, + net::SpawnedTestServer::kLocalhost, + base::FilePath( + FILE_PATH_LITERAL("net/data/proxy_resolver_perftest"))) { + } + + void RunAllTests() { + ASSERT_TRUE(test_server_.Start()); + for (size_t i = 0; i < arraysize(kPerfTests); ++i) { + const PacPerfTest& test_data = kPerfTests[i]; + RunTest(test_data.pac_name, + test_data.queries, + test_data.NumQueries()); + } + } + + private: + void RunTest(const std::string& script_name, + const PacQuery* queries, + int queries_len) { + if (!resolver_->expects_pac_bytes()) { + GURL pac_url = + test_server_.GetURL(std::string("files/") + script_name); + int rv = resolver_->SetPacScript( + net::ProxyResolverScriptData::FromURL(pac_url), + net::CompletionCallback()); + EXPECT_EQ(net::OK, rv); + } else { + LoadPacScriptIntoResolver(script_name); + } + + // Do a query to warm things up. In the case of internal-fetch proxy + // resolvers, the first resolve will be slow since it has to download + // the PAC script. + { + net::ProxyInfo proxy_info; + int result = resolver_->GetProxyForURL( + GURL("http://www.warmup.com"), &proxy_info, net::CompletionCallback(), + NULL, net::BoundNetLog()); + ASSERT_EQ(net::OK, result); + } + + // Start the perf timer. + std::string perf_test_name = resolver_name_ + "_" + script_name; + PerfTimeLogger timer(perf_test_name.c_str()); + + for (int i = 0; i < kNumIterations; ++i) { + // Round-robin between URLs to resolve. + const PacQuery& query = queries[i % queries_len]; + + // Resolve. + net::ProxyInfo proxy_info; + int result = resolver_->GetProxyForURL( + GURL(query.query_url), &proxy_info, net::CompletionCallback(), NULL, + net::BoundNetLog()); + + // Check that the result was correct. Note that ToPacString() and + // ASSERT_EQ() are fast, so they won't skew the results. + ASSERT_EQ(net::OK, result); + ASSERT_EQ(query.expected_result, proxy_info.ToPacString()); + } + + // Print how long the test ran for. + timer.Done(); + } + + // Read the PAC script from disk and initialize the proxy resolver with it. + void LoadPacScriptIntoResolver(const std::string& script_name) { + base::FilePath path; + PathService::Get(base::DIR_SOURCE_ROOT, &path); + path = path.AppendASCII("net"); + path = path.AppendASCII("data"); + path = path.AppendASCII("proxy_resolver_perftest"); + path = path.AppendASCII(script_name); + + // Try to read the file from disk. + std::string file_contents; + bool ok = file_util::ReadFileToString(path, &file_contents); + + // If we can't load the file from disk, something is misconfigured. + LOG_IF(ERROR, !ok) << "Failed to read file: " << path.value(); + ASSERT_TRUE(ok); + + // Load the PAC script into the ProxyResolver. + int rv = resolver_->SetPacScript( + net::ProxyResolverScriptData::FromUTF8(file_contents), + net::CompletionCallback()); + EXPECT_EQ(net::OK, rv); + } + + net::ProxyResolver* resolver_; + std::string resolver_name_; + net::SpawnedTestServer test_server_; +}; + +#if defined(OS_WIN) +TEST(ProxyResolverPerfTest, ProxyResolverWinHttp) { + net::ProxyResolverWinHttp resolver; + PacPerfSuiteRunner runner(&resolver, "ProxyResolverWinHttp"); + runner.RunAllTests(); +} +#elif defined(OS_MACOSX) +TEST(ProxyResolverPerfTest, ProxyResolverMac) { + net::ProxyResolverMac resolver; + PacPerfSuiteRunner runner(&resolver, "ProxyResolverMac"); + runner.RunAllTests(); +} +#endif + +class MockJSBindings : public net::ProxyResolverV8::JSBindings { + public: + MockJSBindings() {} + + virtual void Alert(const base::string16& message) OVERRIDE { + CHECK(false); + } + + virtual bool ResolveDns(const std::string& host, + ResolveDnsOperation op, + std::string* output, + bool* terminate) OVERRIDE { + CHECK(false); + return false; + } + + virtual void OnError(int line_number, + const base::string16& message) OVERRIDE { + CHECK(false); + } +}; + +TEST(ProxyResolverPerfTest, ProxyResolverV8) { + // This has to be done on the main thread. + net::ProxyResolverV8::RememberDefaultIsolate(); + + MockJSBindings js_bindings; + net::ProxyResolverV8 resolver; + resolver.set_js_bindings(&js_bindings); + PacPerfSuiteRunner runner(&resolver, "ProxyResolverV8"); + runner.RunAllTests(); +} diff --git a/chromium/net/proxy/proxy_resolver_script.h b/chromium/net/proxy/proxy_resolver_script.h new file mode 100644 index 00000000000..283eff91454 --- /dev/null +++ b/chromium/net/proxy/proxy_resolver_script.h @@ -0,0 +1,276 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is mozilla.org code. + * + * The Initial Developer of the Original Code is + * Netscape Communications Corporation. + * Portions created by the Initial Developer are Copyright (C) 1998 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Akhil Arora <akhil.arora@sun.com> + * Tomi Leppikangas <Tomi.Leppikangas@oulu.fi> + * Darin Fisher <darin@meer.net> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#ifndef NET_PROXY_PROXY_RESOLVER_SCRIPT_H_ +#define NET_PROXY_PROXY_RESOLVER_SCRIPT_H_ + +// The following code was formatted from: +// 'mozilla/netwerk/base/src/nsProxyAutoConfig.js' (1.55) +// +// Using the command: +// $ cat nsProxyAutoConfig.js | +// awk '/var pacUtils/,/EOF/' | +// sed -e 's/^\s*$/""/g' | +// sed -e 's/"\s*[+]\s*$/"/g' | +// sed -e 's/"$/" \\/g' | +// sed -e 's/\/(ipaddr);/\/.exec(ipaddr);/g' | +// grep -v '^var pacUtils =' +#define PROXY_RESOLVER_SCRIPT \ + "function dnsDomainIs(host, domain) {\n" \ + " return (host.length >= domain.length &&\n" \ + " host.substring(host.length - domain.length) == domain);\n" \ + "}\n" \ + "" \ + "function dnsDomainLevels(host) {\n" \ + " return host.split('.').length-1;\n" \ + "}\n" \ + "" \ + "function convert_addr(ipchars) {\n" \ + " var bytes = ipchars.split('.');\n" \ + " var result = ((bytes[0] & 0xff) << 24) |\n" \ + " ((bytes[1] & 0xff) << 16) |\n" \ + " ((bytes[2] & 0xff) << 8) |\n" \ + " (bytes[3] & 0xff);\n" \ + " return result;\n" \ + "}\n" \ + "" \ + "function isInNet(ipaddr, pattern, maskstr) {\n" \ + " var test = /^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/.exec(ipaddr);\n" \ + " if (test == null) {\n" \ + " ipaddr = dnsResolve(ipaddr);\n" \ + " if (ipaddr == null)\n" \ + " return false;\n" \ + " } else if (test[1] > 255 || test[2] > 255 || \n" \ + " test[3] > 255 || test[4] > 255) {\n" \ + " return false; // not an IP address\n" \ + " }\n" \ + " var host = convert_addr(ipaddr);\n" \ + " var pat = convert_addr(pattern);\n" \ + " var mask = convert_addr(maskstr);\n" \ + " return ((host & mask) == (pat & mask));\n" \ + " \n" \ + "}\n" \ + "" \ + "function isPlainHostName(host) {\n" \ + " return (host.search('\\\\.') == -1);\n" \ + "}\n" \ + "" \ + "function isResolvable(host) {\n" \ + " var ip = dnsResolve(host);\n" \ + " return (ip != null);\n" \ + "}\n" \ + "" \ + "function localHostOrDomainIs(host, hostdom) {\n" \ + " return (host == hostdom) ||\n" \ + " (hostdom.lastIndexOf(host + '.', 0) == 0);\n" \ + "}\n" \ + "" \ + "function shExpMatch(url, pattern) {\n" \ + " pattern = pattern.replace(/\\./g, '\\\\.');\n" \ + " pattern = pattern.replace(/\\*/g, '.*');\n" \ + " pattern = pattern.replace(/\\?/g, '.');\n" \ + " var newRe = new RegExp('^'+pattern+'$');\n" \ + " return newRe.test(url);\n" \ + "}\n" \ + "" \ + "var wdays = {SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6};\n" \ + "" \ + "var months = {JAN: 0, FEB: 1, MAR: 2, APR: 3, MAY: 4, JUN: 5, JUL: 6, AUG: 7, SEP: 8, OCT: 9, NOV: 10, DEC: 11};\n" \ + "" \ + "function weekdayRange() {\n" \ + " function getDay(weekday) {\n" \ + " if (weekday in wdays) {\n" \ + " return wdays[weekday];\n" \ + " }\n" \ + " return -1;\n" \ + " }\n" \ + " var date = new Date();\n" \ + " var argc = arguments.length;\n" \ + " var wday;\n" \ + " if (argc < 1)\n" \ + " return false;\n" \ + " if (arguments[argc - 1] == 'GMT') {\n" \ + " argc--;\n" \ + " wday = date.getUTCDay();\n" \ + " } else {\n" \ + " wday = date.getDay();\n" \ + " }\n" \ + " var wd1 = getDay(arguments[0]);\n" \ + " var wd2 = (argc == 2) ? getDay(arguments[1]) : wd1;\n" \ + " return (wd1 == -1 || wd2 == -1) ? false\n" \ + " : (wd1 <= wday && wday <= wd2);\n" \ + "}\n" \ + "" \ + "function dateRange() {\n" \ + " function getMonth(name) {\n" \ + " if (name in months) {\n" \ + " return months[name];\n" \ + " }\n" \ + " return -1;\n" \ + " }\n" \ + " var date = new Date();\n" \ + " var argc = arguments.length;\n" \ + " if (argc < 1) {\n" \ + " return false;\n" \ + " }\n" \ + " var isGMT = (arguments[argc - 1] == 'GMT');\n" \ + "\n" \ + " if (isGMT) {\n" \ + " argc--;\n" \ + " }\n" \ + " // function will work even without explict handling of this case\n" \ + " if (argc == 1) {\n" \ + " var tmp = parseInt(arguments[0]);\n" \ + " if (isNaN(tmp)) {\n" \ + " return ((isGMT ? date.getUTCMonth() : date.getMonth()) ==\n" \ + "getMonth(arguments[0]));\n" \ + " } else if (tmp < 32) {\n" \ + " return ((isGMT ? date.getUTCDate() : date.getDate()) == tmp);\n" \ + " } else { \n" \ + " return ((isGMT ? date.getUTCFullYear() : date.getFullYear()) ==\n" \ + "tmp);\n" \ + " }\n" \ + " }\n" \ + " var year = date.getFullYear();\n" \ + " var date1, date2;\n" \ + " date1 = new Date(year, 0, 1, 0, 0, 0);\n" \ + " date2 = new Date(year, 11, 31, 23, 59, 59);\n" \ + " var adjustMonth = false;\n" \ + " for (var i = 0; i < (argc >> 1); i++) {\n" \ + " var tmp = parseInt(arguments[i]);\n" \ + " if (isNaN(tmp)) {\n" \ + " var mon = getMonth(arguments[i]);\n" \ + " date1.setMonth(mon);\n" \ + " } else if (tmp < 32) {\n" \ + " adjustMonth = (argc <= 2);\n" \ + " date1.setDate(tmp);\n" \ + " } else {\n" \ + " date1.setFullYear(tmp);\n" \ + " }\n" \ + " }\n" \ + " for (var i = (argc >> 1); i < argc; i++) {\n" \ + " var tmp = parseInt(arguments[i]);\n" \ + " if (isNaN(tmp)) {\n" \ + " var mon = getMonth(arguments[i]);\n" \ + " date2.setMonth(mon);\n" \ + " } else if (tmp < 32) {\n" \ + " date2.setDate(tmp);\n" \ + " } else {\n" \ + " date2.setFullYear(tmp);\n" \ + " }\n" \ + " }\n" \ + " if (adjustMonth) {\n" \ + " date1.setMonth(date.getMonth());\n" \ + " date2.setMonth(date.getMonth());\n" \ + " }\n" \ + " if (isGMT) {\n" \ + " var tmp = date;\n" \ + " tmp.setFullYear(date.getUTCFullYear());\n" \ + " tmp.setMonth(date.getUTCMonth());\n" \ + " tmp.setDate(date.getUTCDate());\n" \ + " tmp.setHours(date.getUTCHours());\n" \ + " tmp.setMinutes(date.getUTCMinutes());\n" \ + " tmp.setSeconds(date.getUTCSeconds());\n" \ + " date = tmp;\n" \ + " }\n" \ + " return ((date1 <= date) && (date <= date2));\n" \ + "}\n" \ + "" \ + "function timeRange() {\n" \ + " var argc = arguments.length;\n" \ + " var date = new Date();\n" \ + " var isGMT= false;\n" \ + "\n" \ + " if (argc < 1) {\n" \ + " return false;\n" \ + " }\n" \ + " if (arguments[argc - 1] == 'GMT') {\n" \ + " isGMT = true;\n" \ + " argc--;\n" \ + " }\n" \ + "\n" \ + " var hour = isGMT ? date.getUTCHours() : date.getHours();\n" \ + " var date1, date2;\n" \ + " date1 = new Date();\n" \ + " date2 = new Date();\n" \ + "\n" \ + " if (argc == 1) {\n" \ + " return (hour == arguments[0]);\n" \ + " } else if (argc == 2) {\n" \ + " return ((arguments[0] <= hour) && (hour <= arguments[1]));\n" \ + " } else {\n" \ + " switch (argc) {\n" \ + " case 6:\n" \ + " date1.setSeconds(arguments[2]);\n" \ + " date2.setSeconds(arguments[5]);\n" \ + " case 4:\n" \ + " var middle = argc >> 1;\n" \ + " date1.setHours(arguments[0]);\n" \ + " date1.setMinutes(arguments[1]);\n" \ + " date2.setHours(arguments[middle]);\n" \ + " date2.setMinutes(arguments[middle + 1]);\n" \ + " if (middle == 2) {\n" \ + " date2.setSeconds(59);\n" \ + " }\n" \ + " break;\n" \ + " default:\n" \ + " throw 'timeRange: bad number of arguments'\n" \ + " }\n" \ + " }\n" \ + "\n" \ + " if (isGMT) {\n" \ + " date.setFullYear(date.getUTCFullYear());\n" \ + " date.setMonth(date.getUTCMonth());\n" \ + " date.setDate(date.getUTCDate());\n" \ + " date.setHours(date.getUTCHours());\n" \ + " date.setMinutes(date.getUTCMinutes());\n" \ + " date.setSeconds(date.getUTCSeconds());\n" \ + " }\n" \ + " return ((date1 <= date) && (date <= date2));\n" \ + "}\n" + +// This is a Microsoft extension to PAC for IPv6, see: +// http://blogs.msdn.com/b/wndp/archive/2006/07/13/ipv6-pac-extensions-v0-9.aspx +#define PROXY_RESOLVER_SCRIPT_EX \ + "function isResolvableEx(host) {\n" \ + " var ipList = dnsResolveEx(host);\n" \ + " return (ipList != '');\n" \ + "}\n" + +#endif // NET_PROXY_PROXY_RESOLVER_SCRIPT_H_ diff --git a/chromium/net/proxy/proxy_resolver_script_data.cc b/chromium/net/proxy/proxy_resolver_script_data.cc new file mode 100644 index 00000000000..7ee07a5d794 --- /dev/null +++ b/chromium/net/proxy/proxy_resolver_script_data.cc @@ -0,0 +1,76 @@ +// 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/proxy/proxy_resolver_script_data.h" + +#include "base/logging.h" +#include "base/strings/utf_string_conversions.h" + +namespace net { + +// static +scoped_refptr<ProxyResolverScriptData> ProxyResolverScriptData::FromUTF8( + const std::string& utf8) { + return new ProxyResolverScriptData(TYPE_SCRIPT_CONTENTS, + GURL(), + UTF8ToUTF16(utf8)); +} + +// static +scoped_refptr<ProxyResolverScriptData> ProxyResolverScriptData::FromUTF16( + const base::string16& utf16) { + return new ProxyResolverScriptData(TYPE_SCRIPT_CONTENTS, GURL(), utf16); +} + +// static +scoped_refptr<ProxyResolverScriptData> ProxyResolverScriptData::FromURL( + const GURL& url) { + return new ProxyResolverScriptData(TYPE_SCRIPT_URL, url, base::string16()); +} + +// static +scoped_refptr<ProxyResolverScriptData> +ProxyResolverScriptData::ForAutoDetect() { + return new ProxyResolverScriptData(TYPE_AUTO_DETECT, GURL(), + base::string16()); +} + +const base::string16& ProxyResolverScriptData::utf16() const { + DCHECK_EQ(TYPE_SCRIPT_CONTENTS, type_); + return utf16_; +} + +const GURL& ProxyResolverScriptData::url() const { + DCHECK_EQ(TYPE_SCRIPT_URL, type_); + return url_; +} + +bool ProxyResolverScriptData::Equals( + const ProxyResolverScriptData* other) const { + if (type() != other->type()) + return false; + + switch (type()) { + case TYPE_SCRIPT_CONTENTS: + return utf16() == other->utf16(); + case TYPE_SCRIPT_URL: + return url() == other->url(); + case TYPE_AUTO_DETECT: + return true; + } + + return false; // Shouldn't be reached. +} + +ProxyResolverScriptData::ProxyResolverScriptData(Type type, + const GURL& url, + const base::string16& utf16) + : type_(type), + url_(url), + utf16_(utf16) { +} + +ProxyResolverScriptData::~ProxyResolverScriptData() {} + +} // namespace net diff --git a/chromium/net/proxy/proxy_resolver_script_data.h b/chromium/net/proxy/proxy_resolver_script_data.h new file mode 100644 index 00000000000..16e17fd5293 --- /dev/null +++ b/chromium/net/proxy/proxy_resolver_script_data.h @@ -0,0 +1,74 @@ +// 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. + +#ifndef NET_PROXY_PROXY_RESOLVER_SCRIPT_DATA_H_ +#define NET_PROXY_PROXY_RESOLVER_SCRIPT_DATA_H_ + +#include "base/memory/ref_counted.h" +#include "base/strings/string16.h" +#include "net/base/net_export.h" +#include "url/gurl.h" + +namespace net { + +// Reference-counted wrapper for passing around a PAC script specification. +// The PAC script can be either specified via a URL, a deferred URL for +// auto-detect, or the actual javascript program text. +// +// This is thread-safe so it can be used by multi-threaded implementations of +// ProxyResolver to share the data between threads. +class NET_EXPORT_PRIVATE ProxyResolverScriptData + : public base::RefCountedThreadSafe<ProxyResolverScriptData> { + public: + enum Type { + TYPE_SCRIPT_CONTENTS, + TYPE_SCRIPT_URL, + TYPE_AUTO_DETECT, + }; + + // Creates a script data given the UTF8 bytes of the content. + static scoped_refptr<ProxyResolverScriptData> FromUTF8( + const std::string& utf8); + + // Creates a script data given the UTF16 bytes of the content. + static scoped_refptr<ProxyResolverScriptData> FromUTF16( + const base::string16& utf16); + + // Creates a script data given a URL to the PAC script. + static scoped_refptr<ProxyResolverScriptData> FromURL(const GURL& url); + + // Creates a script data for using an automatically detected PAC URL. + static scoped_refptr<ProxyResolverScriptData> ForAutoDetect(); + + Type type() const { + return type_; + } + + // Returns the contents of the script as UTF16. + // (only valid for type() == TYPE_SCRIPT_CONTENTS). + const base::string16& utf16() const; + + // Returns the URL of the script. + // (only valid for type() == TYPE_SCRIPT_URL). + const GURL& url() const; + + // Returns true if |this| matches |other|. + bool Equals(const ProxyResolverScriptData* other) const; + + private: + friend class base::RefCountedThreadSafe<ProxyResolverScriptData>; + ProxyResolverScriptData(Type type, + const GURL& url, + const base::string16& utf16); + virtual ~ProxyResolverScriptData(); + + + const Type type_; + const GURL url_; + const base::string16 utf16_; +}; + +} // namespace net + +#endif // NET_PROXY_PROXY_RESOLVER_SCRIPT_DATA_H_ diff --git a/chromium/net/proxy/proxy_resolver_v8.cc b/chromium/net/proxy/proxy_resolver_v8.cc new file mode 100644 index 00000000000..87f61028039 --- /dev/null +++ b/chromium/net/proxy/proxy_resolver_v8.cc @@ -0,0 +1,808 @@ +// 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/proxy/proxy_resolver_v8.h" + +#include <algorithm> +#include <cstdio> + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "base/logging.h" +#include "base/strings/string_tokenizer.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/synchronization/lock.h" +#include "net/base/net_errors.h" +#include "net/base/net_log.h" +#include "net/base/net_util.h" +#include "net/proxy/proxy_info.h" +#include "net/proxy/proxy_resolver_script.h" +#include "url/gurl.h" +#include "url/url_canon.h" +#include "v8/include/v8.h" + +// Notes on the javascript environment: +// +// For the majority of the PAC utility functions, we use the same code +// as Firefox. See the javascript library that proxy_resolver_scipt.h +// pulls in. +// +// In addition, we implement a subset of Microsoft's extensions to PAC. +// - myIpAddressEx() +// - dnsResolveEx() +// - isResolvableEx() +// - isInNetEx() +// - sortIpAddressList() +// +// It is worth noting that the original PAC specification does not describe +// the return values on failure. Consequently, there are compatibility +// differences between browsers on what to return on failure, which are +// illustrated below: +// +// --------------------+-------------+-------------------+-------------- +// | Firefox3 | InternetExplorer8 | --> Us <--- +// --------------------+-------------+-------------------+-------------- +// myIpAddress() | "127.0.0.1" | ??? | "127.0.0.1" +// dnsResolve() | null | false | null +// myIpAddressEx() | N/A | "" | "" +// sortIpAddressList() | N/A | false | false +// dnsResolveEx() | N/A | "" | "" +// isInNetEx() | N/A | false | false +// --------------------+-------------+-------------------+-------------- +// +// TODO(eroman): The cell above reading ??? means I didn't test it. +// +// Another difference is in how dnsResolve() and myIpAddress() are +// implemented -- whether they should restrict to IPv4 results, or +// include both IPv4 and IPv6. The following table illustrates the +// differences: +// +// --------------------+-------------+-------------------+-------------- +// | Firefox3 | InternetExplorer8 | --> Us <--- +// --------------------+-------------+-------------------+-------------- +// myIpAddress() | IPv4/IPv6 | IPv4 | IPv4 +// dnsResolve() | IPv4/IPv6 | IPv4 | IPv4 +// isResolvable() | IPv4/IPv6 | IPv4 | IPv4 +// myIpAddressEx() | N/A | IPv4/IPv6 | IPv4/IPv6 +// dnsResolveEx() | N/A | IPv4/IPv6 | IPv4/IPv6 +// sortIpAddressList() | N/A | IPv4/IPv6 | IPv4/IPv6 +// isResolvableEx() | N/A | IPv4/IPv6 | IPv4/IPv6 +// isInNetEx() | N/A | IPv4/IPv6 | IPv4/IPv6 +// -----------------+-------------+-------------------+-------------- + +namespace net { + +namespace { + +// Pseudo-name for the PAC script. +const char kPacResourceName[] = "proxy-pac-script.js"; +// Pseudo-name for the PAC utility script. +const char kPacUtilityResourceName[] = "proxy-pac-utility-script.js"; + +// External string wrapper so V8 can access the UTF16 string wrapped by +// ProxyResolverScriptData. +class V8ExternalStringFromScriptData + : public v8::String::ExternalStringResource { + public: + explicit V8ExternalStringFromScriptData( + const scoped_refptr<ProxyResolverScriptData>& script_data) + : script_data_(script_data) {} + + virtual const uint16_t* data() const OVERRIDE { + return reinterpret_cast<const uint16*>(script_data_->utf16().data()); + } + + virtual size_t length() const OVERRIDE { + return script_data_->utf16().size(); + } + + private: + const scoped_refptr<ProxyResolverScriptData> script_data_; + DISALLOW_COPY_AND_ASSIGN(V8ExternalStringFromScriptData); +}; + +// External string wrapper so V8 can access a string literal. +class V8ExternalASCIILiteral : public v8::String::ExternalAsciiStringResource { + public: + // |ascii| must be a NULL-terminated C string, and must remain valid + // throughout this object's lifetime. + V8ExternalASCIILiteral(const char* ascii, size_t length) + : ascii_(ascii), length_(length) { + DCHECK(IsStringASCII(ascii)); + } + + virtual const char* data() const OVERRIDE { + return ascii_; + } + + virtual size_t length() const OVERRIDE { + return length_; + } + + private: + const char* ascii_; + size_t length_; + DISALLOW_COPY_AND_ASSIGN(V8ExternalASCIILiteral); +}; + +// When creating a v8::String from a C++ string we have two choices: create +// a copy, or create a wrapper that shares the same underlying storage. +// For small strings it is better to just make a copy, whereas for large +// strings there are savings by sharing the storage. This number identifies +// the cutoff length for when to start wrapping rather than creating copies. +const size_t kMaxStringBytesForCopy = 256; + +// Converts a V8 String to a UTF8 std::string. +std::string V8StringToUTF8(v8::Handle<v8::String> s) { + int len = s->Length(); + std::string result; + if (len > 0) + s->WriteUtf8(WriteInto(&result, len + 1)); + return result; +} + +// Converts a V8 String to a UTF16 base::string16. +base::string16 V8StringToUTF16(v8::Handle<v8::String> s) { + int len = s->Length(); + base::string16 result; + // Note that the reinterpret cast is because on Windows string16 is an alias + // to wstring, and hence has character type wchar_t not uint16_t. + if (len > 0) + s->Write(reinterpret_cast<uint16_t*>(WriteInto(&result, len + 1)), 0, len); + return result; +} + +// Converts an ASCII std::string to a V8 string. +v8::Local<v8::String> ASCIIStringToV8String(const std::string& s) { + DCHECK(IsStringASCII(s)); + return v8::String::New(s.data(), s.size()); +} + +// Converts a UTF16 base::string16 (warpped by a ProxyResolverScriptData) to a +// V8 string. +v8::Local<v8::String> ScriptDataToV8String( + const scoped_refptr<ProxyResolverScriptData>& s) { + if (s->utf16().size() * 2 <= kMaxStringBytesForCopy) { + return v8::String::New( + reinterpret_cast<const uint16_t*>(s->utf16().data()), + s->utf16().size()); + } + return v8::String::NewExternal(new V8ExternalStringFromScriptData(s)); +} + +// Converts an ASCII string literal to a V8 string. +v8::Local<v8::String> ASCIILiteralToV8String(const char* ascii) { + DCHECK(IsStringASCII(ascii)); + size_t length = strlen(ascii); + if (length <= kMaxStringBytesForCopy) + return v8::String::New(ascii, length); + return v8::String::NewExternal(new V8ExternalASCIILiteral(ascii, length)); +} + +// Stringizes a V8 object by calling its toString() method. Returns true +// on success. This may fail if the toString() throws an exception. +bool V8ObjectToUTF16String(v8::Handle<v8::Value> object, + base::string16* utf16_result) { + if (object.IsEmpty()) + return false; + + v8::HandleScope scope; + v8::Local<v8::String> str_object = object->ToString(); + if (str_object.IsEmpty()) + return false; + *utf16_result = V8StringToUTF16(str_object); + return true; +} + +// Extracts an hostname argument from |args|. On success returns true +// and fills |*hostname| with the result. +bool GetHostnameArgument(const v8::FunctionCallbackInfo<v8::Value>& args, + std::string* hostname) { + // The first argument should be a string. + if (args.Length() == 0 || args[0].IsEmpty() || !args[0]->IsString()) + return false; + + const base::string16 hostname_utf16 = V8StringToUTF16(args[0]->ToString()); + + // If the hostname is already in ASCII, simply return it as is. + if (IsStringASCII(hostname_utf16)) { + *hostname = UTF16ToASCII(hostname_utf16); + return true; + } + + // Otherwise try to convert it from IDN to punycode. + const int kInitialBufferSize = 256; + url_canon::RawCanonOutputT<char16, kInitialBufferSize> punycode_output; + if (!url_canon::IDNToASCII(hostname_utf16.data(), + hostname_utf16.length(), + &punycode_output)) { + return false; + } + + // |punycode_output| should now be ASCII; convert it to a std::string. + // (We could use UTF16ToASCII() instead, but that requires an extra string + // copy. Since ASCII is a subset of UTF8 the following is equivalent). + bool success = UTF16ToUTF8(punycode_output.data(), + punycode_output.length(), + hostname); + DCHECK(success); + DCHECK(IsStringASCII(*hostname)); + return success; +} + +// Wrapper for passing around IP address strings and IPAddressNumber objects. +struct IPAddress { + IPAddress(const std::string& ip_string, const IPAddressNumber& ip_number) + : string_value(ip_string), + ip_address_number(ip_number) { + } + + // Used for sorting IP addresses in ascending order in SortIpAddressList(). + // IP6 addresses are placed ahead of IPv4 addresses. + bool operator<(const IPAddress& rhs) const { + const IPAddressNumber& ip1 = this->ip_address_number; + const IPAddressNumber& ip2 = rhs.ip_address_number; + if (ip1.size() != ip2.size()) + return ip1.size() > ip2.size(); // IPv6 before IPv4. + DCHECK(ip1.size() == ip2.size()); + return memcmp(&ip1[0], &ip2[0], ip1.size()) < 0; // Ascending order. + } + + std::string string_value; + IPAddressNumber ip_address_number; +}; + +// Handler for "sortIpAddressList(IpAddressList)". |ip_address_list| is a +// semi-colon delimited string containing IP addresses. +// |sorted_ip_address_list| is the resulting list of sorted semi-colon delimited +// IP addresses or an empty string if unable to sort the IP address list. +// Returns 'true' if the sorting was successful, and 'false' if the input was an +// empty string, a string of separators (";" in this case), or if any of the IP +// addresses in the input list failed to parse. +bool SortIpAddressList(const std::string& ip_address_list, + std::string* sorted_ip_address_list) { + sorted_ip_address_list->clear(); + + // Strip all whitespace (mimics IE behavior). + std::string cleaned_ip_address_list; + RemoveChars(ip_address_list, " \t", &cleaned_ip_address_list); + if (cleaned_ip_address_list.empty()) + return false; + + // Split-up IP addresses and store them in a vector. + std::vector<IPAddress> ip_vector; + IPAddressNumber ip_num; + base::StringTokenizer str_tok(cleaned_ip_address_list, ";"); + while (str_tok.GetNext()) { + if (!ParseIPLiteralToNumber(str_tok.token(), &ip_num)) + return false; + ip_vector.push_back(IPAddress(str_tok.token(), ip_num)); + } + + if (ip_vector.empty()) // Can happen if we have something like + return false; // sortIpAddressList(";") or sortIpAddressList("; ;") + + DCHECK(!ip_vector.empty()); + + // Sort lists according to ascending numeric value. + if (ip_vector.size() > 1) + std::stable_sort(ip_vector.begin(), ip_vector.end()); + + // Return a semi-colon delimited list of sorted addresses (IPv6 followed by + // IPv4). + for (size_t i = 0; i < ip_vector.size(); ++i) { + if (i > 0) + *sorted_ip_address_list += ";"; + *sorted_ip_address_list += ip_vector[i].string_value; + } + return true; +} + +// Handler for "isInNetEx(ip_address, ip_prefix)". |ip_address| is a string +// containing an IPv4/IPv6 address, and |ip_prefix| is a string containg a +// slash-delimited IP prefix with the top 'n' bits specified in the bit +// field. This returns 'true' if the address is in the same subnet, and +// 'false' otherwise. Also returns 'false' if the prefix is in an incorrect +// format, or if an address and prefix of different types are used (e.g. IPv6 +// address and IPv4 prefix). +bool IsInNetEx(const std::string& ip_address, const std::string& ip_prefix) { + IPAddressNumber address; + if (!ParseIPLiteralToNumber(ip_address, &address)) + return false; + + IPAddressNumber prefix; + size_t prefix_length_in_bits; + if (!ParseCIDRBlock(ip_prefix, &prefix, &prefix_length_in_bits)) + return false; + + // Both |address| and |prefix| must be of the same type (IPv4 or IPv6). + if (address.size() != prefix.size()) + return false; + + DCHECK((address.size() == 4 && prefix.size() == 4) || + (address.size() == 16 && prefix.size() == 16)); + + return IPNumberMatchesPrefix(address, prefix, prefix_length_in_bits); +} + +} // namespace + +// ProxyResolverV8::Context --------------------------------------------------- + +class ProxyResolverV8::Context { + public: + Context(ProxyResolverV8* parent, v8::Isolate* isolate) + : parent_(parent), + isolate_(isolate) { + DCHECK(isolate); + } + + ~Context() { + v8::Locker locked(isolate_); + + v8_this_.Dispose(isolate_); + v8_context_.Dispose(isolate_); + } + + JSBindings* js_bindings() { + return parent_->js_bindings_; + } + + int ResolveProxy(const GURL& query_url, ProxyInfo* results) { + v8::Locker locked(isolate_); + v8::HandleScope scope(isolate_); + + v8::Local<v8::Context> context = + v8::Local<v8::Context>::New(isolate_, v8_context_); + v8::Context::Scope function_scope(context); + + v8::Local<v8::Value> function; + if (!GetFindProxyForURL(&function)) { + js_bindings()->OnError( + -1, ASCIIToUTF16("FindProxyForURL() is undefined.")); + return ERR_PAC_SCRIPT_FAILED; + } + + v8::Handle<v8::Value> argv[] = { + ASCIIStringToV8String(query_url.spec()), + ASCIIStringToV8String(query_url.HostNoBrackets()), + }; + + v8::TryCatch try_catch; + v8::Local<v8::Value> ret = v8::Function::Cast(*function)->Call( + context->Global(), arraysize(argv), argv); + + if (try_catch.HasCaught()) { + HandleError(try_catch.Message()); + return ERR_PAC_SCRIPT_FAILED; + } + + if (!ret->IsString()) { + js_bindings()->OnError( + -1, ASCIIToUTF16("FindProxyForURL() did not return a string.")); + return ERR_PAC_SCRIPT_FAILED; + } + + base::string16 ret_str = V8StringToUTF16(ret->ToString()); + + if (!IsStringASCII(ret_str)) { + // TODO(eroman): Rather than failing when a wide string is returned, we + // could extend the parsing to handle IDNA hostnames by + // converting them to ASCII punycode. + // crbug.com/47234 + base::string16 error_message = + ASCIIToUTF16("FindProxyForURL() returned a non-ASCII string " + "(crbug.com/47234): ") + ret_str; + js_bindings()->OnError(-1, error_message); + return ERR_PAC_SCRIPT_FAILED; + } + + results->UsePacString(UTF16ToASCII(ret_str)); + return OK; + } + + int InitV8(const scoped_refptr<ProxyResolverScriptData>& pac_script) { + v8::Locker locked(isolate_); + v8::HandleScope scope(isolate_); + + v8_this_.Reset(isolate_, v8::External::New(this)); + v8::Local<v8::External> v8_this = + v8::Local<v8::External>::New(isolate_, v8_this_); + v8::Local<v8::ObjectTemplate> global_template = v8::ObjectTemplate::New(); + + // Attach the javascript bindings. + v8::Local<v8::FunctionTemplate> alert_template = + v8::FunctionTemplate::New(&AlertCallback, v8_this); + global_template->Set(ASCIILiteralToV8String("alert"), alert_template); + + v8::Local<v8::FunctionTemplate> my_ip_address_template = + v8::FunctionTemplate::New(&MyIpAddressCallback, v8_this); + global_template->Set(ASCIILiteralToV8String("myIpAddress"), + my_ip_address_template); + + v8::Local<v8::FunctionTemplate> dns_resolve_template = + v8::FunctionTemplate::New(&DnsResolveCallback, v8_this); + global_template->Set(ASCIILiteralToV8String("dnsResolve"), + dns_resolve_template); + + // Microsoft's PAC extensions: + + v8::Local<v8::FunctionTemplate> dns_resolve_ex_template = + v8::FunctionTemplate::New(&DnsResolveExCallback, v8_this); + global_template->Set(ASCIILiteralToV8String("dnsResolveEx"), + dns_resolve_ex_template); + + v8::Local<v8::FunctionTemplate> my_ip_address_ex_template = + v8::FunctionTemplate::New(&MyIpAddressExCallback, v8_this); + global_template->Set(ASCIILiteralToV8String("myIpAddressEx"), + my_ip_address_ex_template); + + v8::Local<v8::FunctionTemplate> sort_ip_address_list_template = + v8::FunctionTemplate::New(&SortIpAddressListCallback, v8_this); + global_template->Set(ASCIILiteralToV8String("sortIpAddressList"), + sort_ip_address_list_template); + + v8::Local<v8::FunctionTemplate> is_in_net_ex_template = + v8::FunctionTemplate::New(&IsInNetExCallback, v8_this); + global_template->Set(ASCIILiteralToV8String("isInNetEx"), + is_in_net_ex_template); + + v8_context_.Reset( + isolate_, v8::Context::New(isolate_, NULL, global_template)); + + v8::Local<v8::Context> context = + v8::Local<v8::Context>::New(isolate_, v8_context_); + v8::Context::Scope ctx(context); + + // Add the PAC utility functions to the environment. + // (This script should never fail, as it is a string literal!) + // Note that the two string literals are concatenated. + int rv = RunScript( + ASCIILiteralToV8String( + PROXY_RESOLVER_SCRIPT + PROXY_RESOLVER_SCRIPT_EX), + kPacUtilityResourceName); + if (rv != OK) { + NOTREACHED(); + return rv; + } + + // Add the user's PAC code to the environment. + rv = RunScript(ScriptDataToV8String(pac_script), kPacResourceName); + if (rv != OK) + return rv; + + // At a minimum, the FindProxyForURL() function must be defined for this + // to be a legitimiate PAC script. + v8::Local<v8::Value> function; + if (!GetFindProxyForURL(&function)) { + js_bindings()->OnError( + -1, ASCIIToUTF16("FindProxyForURL() is undefined.")); + return ERR_PAC_SCRIPT_FAILED; + } + + return OK; + } + + void PurgeMemory() { + v8::Locker locked(isolate_); + v8::V8::LowMemoryNotification(); + } + + private: + bool GetFindProxyForURL(v8::Local<v8::Value>* function) { + v8::Local<v8::Context> context = + v8::Local<v8::Context>::New(v8::Isolate::GetCurrent(), v8_context_); + *function = + context->Global()->Get(ASCIILiteralToV8String("FindProxyForURL")); + return (*function)->IsFunction(); + } + + // Handle an exception thrown by V8. + void HandleError(v8::Handle<v8::Message> message) { + base::string16 error_message; + int line_number = -1; + + if (!message.IsEmpty()) { + line_number = message->GetLineNumber(); + V8ObjectToUTF16String(message->Get(), &error_message); + } + + js_bindings()->OnError(line_number, error_message); + } + + // Compiles and runs |script| in the current V8 context. + // Returns OK on success, otherwise an error code. + int RunScript(v8::Handle<v8::String> script, const char* script_name) { + v8::TryCatch try_catch; + + // Compile the script. + v8::ScriptOrigin origin = + v8::ScriptOrigin(ASCIILiteralToV8String(script_name)); + v8::Local<v8::Script> code = v8::Script::Compile(script, &origin); + + // Execute. + if (!code.IsEmpty()) + code->Run(); + + // Check for errors. + if (try_catch.HasCaught()) { + HandleError(try_catch.Message()); + return ERR_PAC_SCRIPT_FAILED; + } + + return OK; + } + + // V8 callback for when "alert()" is invoked by the PAC script. + static void AlertCallback(const v8::FunctionCallbackInfo<v8::Value>& args) { + Context* context = + static_cast<Context*>(v8::External::Cast(*args.Data())->Value()); + + // Like firefox we assume "undefined" if no argument was specified, and + // disregard any arguments beyond the first. + base::string16 message; + if (args.Length() == 0) { + message = ASCIIToUTF16("undefined"); + } else { + if (!V8ObjectToUTF16String(args[0], &message)) + return; // toString() threw an exception. + } + + context->js_bindings()->Alert(message); + } + + // V8 callback for when "myIpAddress()" is invoked by the PAC script. + static void MyIpAddressCallback( + const v8::FunctionCallbackInfo<v8::Value>& args) { + DnsResolveCallbackHelper(args, JSBindings::MY_IP_ADDRESS); + } + + // V8 callback for when "myIpAddressEx()" is invoked by the PAC script. + static void MyIpAddressExCallback( + const v8::FunctionCallbackInfo<v8::Value>& args) { + DnsResolveCallbackHelper(args, JSBindings::MY_IP_ADDRESS_EX); + } + + // V8 callback for when "dnsResolve()" is invoked by the PAC script. + static void DnsResolveCallback( + const v8::FunctionCallbackInfo<v8::Value>& args) { + DnsResolveCallbackHelper(args, JSBindings::DNS_RESOLVE); + } + + // V8 callback for when "dnsResolveEx()" is invoked by the PAC script. + static void DnsResolveExCallback( + const v8::FunctionCallbackInfo<v8::Value>& args) { + DnsResolveCallbackHelper(args, JSBindings::DNS_RESOLVE_EX); + } + + // Shared code for implementing: + // - myIpAddress(), myIpAddressEx(), dnsResolve(), dnsResolveEx(). + static void DnsResolveCallbackHelper( + const v8::FunctionCallbackInfo<v8::Value>& args, + JSBindings::ResolveDnsOperation op) { + Context* context = + static_cast<Context*>(v8::External::Cast(*args.Data())->Value()); + + std::string hostname; + + // dnsResolve() and dnsResolveEx() need at least 1 argument. + if (op == JSBindings::DNS_RESOLVE || op == JSBindings::DNS_RESOLVE_EX) { + if (!GetHostnameArgument(args, &hostname)) { + if (op == JSBindings::DNS_RESOLVE) + args.GetReturnValue().SetNull(); + return; + } + } + + std::string result; + bool success; + bool terminate = false; + + { + v8::Unlocker unlocker(args.GetIsolate()); + success = context->js_bindings()->ResolveDns( + hostname, op, &result, &terminate); + } + + if (terminate) + v8::V8::TerminateExecution(args.GetIsolate()); + + if (success) { + args.GetReturnValue().Set(ASCIIStringToV8String(result)); + return; + } + + // Each function handles resolution errors differently. + switch (op) { + case JSBindings::DNS_RESOLVE: + args.GetReturnValue().SetNull(); + return; + case JSBindings::DNS_RESOLVE_EX: + args.GetReturnValue().SetEmptyString(); + return; + case JSBindings::MY_IP_ADDRESS: + args.GetReturnValue().Set(ASCIILiteralToV8String("127.0.0.1")); + return; + case JSBindings::MY_IP_ADDRESS_EX: + args.GetReturnValue().SetEmptyString(); + return; + } + + NOTREACHED(); + } + + // V8 callback for when "sortIpAddressList()" is invoked by the PAC script. + static void SortIpAddressListCallback( + const v8::FunctionCallbackInfo<v8::Value>& args) { + // We need at least one string argument. + if (args.Length() == 0 || args[0].IsEmpty() || !args[0]->IsString()) { + args.GetReturnValue().SetNull(); + return; + } + + std::string ip_address_list = V8StringToUTF8(args[0]->ToString()); + if (!IsStringASCII(ip_address_list)) { + args.GetReturnValue().SetNull(); + return; + } + std::string sorted_ip_address_list; + bool success = SortIpAddressList(ip_address_list, &sorted_ip_address_list); + if (!success) { + args.GetReturnValue().Set(false); + return; + } + args.GetReturnValue().Set(ASCIIStringToV8String(sorted_ip_address_list)); + } + + // V8 callback for when "isInNetEx()" is invoked by the PAC script. + static void IsInNetExCallback( + const v8::FunctionCallbackInfo<v8::Value>& args) { + // We need at least 2 string arguments. + if (args.Length() < 2 || args[0].IsEmpty() || !args[0]->IsString() || + args[1].IsEmpty() || !args[1]->IsString()) { + args.GetReturnValue().SetNull(); + return; + } + + std::string ip_address = V8StringToUTF8(args[0]->ToString()); + if (!IsStringASCII(ip_address)) { + args.GetReturnValue().Set(false); + return; + } + std::string ip_prefix = V8StringToUTF8(args[1]->ToString()); + if (!IsStringASCII(ip_prefix)) { + args.GetReturnValue().Set(false); + return; + } + args.GetReturnValue().Set(IsInNetEx(ip_address, ip_prefix)); + } + + mutable base::Lock lock_; + ProxyResolverV8* parent_; + v8::Isolate* isolate_; + v8::Persistent<v8::External> v8_this_; + v8::Persistent<v8::Context> v8_context_; +}; + +// ProxyResolverV8 ------------------------------------------------------------ + +ProxyResolverV8::ProxyResolverV8() + : ProxyResolver(true /*expects_pac_bytes*/), + js_bindings_(NULL) { +} + +ProxyResolverV8::~ProxyResolverV8() {} + +int ProxyResolverV8::GetProxyForURL( + const GURL& query_url, ProxyInfo* results, + const CompletionCallback& /*callback*/, + RequestHandle* /*request*/, + const BoundNetLog& net_log) { + DCHECK(js_bindings_); + + // If the V8 instance has not been initialized (either because + // SetPacScript() wasn't called yet, or because it failed. + if (!context_) + return ERR_FAILED; + + // Otherwise call into V8. + int rv = context_->ResolveProxy(query_url, results); + + return rv; +} + +void ProxyResolverV8::CancelRequest(RequestHandle request) { + // This is a synchronous ProxyResolver; no possibility for async requests. + NOTREACHED(); +} + +LoadState ProxyResolverV8::GetLoadState(RequestHandle request) const { + NOTREACHED(); + return LOAD_STATE_IDLE; +} + +void ProxyResolverV8::CancelSetPacScript() { + NOTREACHED(); +} + +void ProxyResolverV8::PurgeMemory() { + context_->PurgeMemory(); +} + +int ProxyResolverV8::SetPacScript( + const scoped_refptr<ProxyResolverScriptData>& script_data, + const CompletionCallback& /*callback*/) { + DCHECK(script_data.get()); + DCHECK(js_bindings_); + + context_.reset(); + if (script_data->utf16().empty()) + return ERR_PAC_SCRIPT_FAILED; + + // Try parsing the PAC script. + scoped_ptr<Context> context(new Context(this, GetDefaultIsolate())); + int rv = context->InitV8(script_data); + if (rv == OK) + context_.reset(context.release()); + return rv; +} + +// static +void ProxyResolverV8::RememberDefaultIsolate() { + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + DCHECK(isolate) + << "ProxyResolverV8::RememberDefaultIsolate called on wrong thread"; + DCHECK(g_default_isolate_ == NULL || g_default_isolate_ == isolate) + << "Default Isolate can not be changed"; + g_default_isolate_ = isolate; +} + +#if defined(OS_WIN) +// static +void ProxyResolverV8::CreateIsolate() { + v8::Isolate* isolate = v8::Isolate::New(); + DCHECK(isolate); + DCHECK(g_default_isolate_ == NULL) << "Default Isolate can not be set twice"; + + isolate->Enter(); + v8::V8::Initialize(); + + g_default_isolate_ = isolate; +} +#endif // defined(OS_WIN) + +// static +v8::Isolate* ProxyResolverV8::GetDefaultIsolate() { + DCHECK(g_default_isolate_) + << "Must call ProxyResolverV8::RememberDefaultIsolate() first"; + return g_default_isolate_; +} + +v8::Isolate* ProxyResolverV8::g_default_isolate_ = NULL; + +// static +size_t ProxyResolverV8::GetTotalHeapSize() { + if (!g_default_isolate_) + return 0; + + v8::Locker locked(g_default_isolate_); + v8::HeapStatistics heap_statistics; + g_default_isolate_->GetHeapStatistics(&heap_statistics); + return heap_statistics.total_heap_size(); +} + +// static +size_t ProxyResolverV8::GetUsedHeapSize() { + if (!g_default_isolate_) + return 0; + + v8::Locker locked(g_default_isolate_); + v8::HeapStatistics heap_statistics; + g_default_isolate_->GetHeapStatistics(&heap_statistics); + return heap_statistics.used_heap_size(); +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_resolver_v8.h b/chromium/net/proxy/proxy_resolver_v8.h new file mode 100644 index 00000000000..76ec7252700 --- /dev/null +++ b/chromium/net/proxy/proxy_resolver_v8.h @@ -0,0 +1,131 @@ +// 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. + +#ifndef NET_PROXY_PROXY_RESOLVER_V8_H_ +#define NET_PROXY_PROXY_RESOLVER_V8_H_ + +#include "base/compiler_specific.h" +#include "base/memory/scoped_ptr.h" +#include "net/base/net_export.h" +#include "net/proxy/proxy_resolver.h" + +namespace v8 { +class HeapStatistics; +class Isolate; +} // namespace v8 + +namespace net { + +// Implementation of ProxyResolver that uses V8 to evaluate PAC scripts. +// +// ---------------------------------------------------------------------------- +// !!! Important note on threading model: +// ---------------------------------------------------------------------------- +// There can be only one instance of V8 running at a time. To enforce this +// constraint, ProxyResolverV8 holds a v8::Locker during execution. Therefore +// it is OK to run multiple instances of ProxyResolverV8 on different threads, +// since only one will be running inside V8 at a time. +// +// It is important that *ALL* instances of V8 in the process be using +// v8::Locker. If not there can be race conditions between the non-locked V8 +// instances and the locked V8 instances used by ProxyResolverV8 (assuming they +// run on different threads). +// +// This is the case with the V8 instance used by chromium's renderer -- it runs +// on a different thread from ProxyResolver (renderer thread vs PAC thread), +// and does not use locking since it expects to be alone. +class NET_EXPORT_PRIVATE ProxyResolverV8 : public ProxyResolver { + public: + // Interface for the javascript bindings. + class NET_EXPORT_PRIVATE JSBindings { + public: + enum ResolveDnsOperation { + DNS_RESOLVE, + DNS_RESOLVE_EX, + MY_IP_ADDRESS, + MY_IP_ADDRESS_EX, + }; + + JSBindings() {} + + // Handler for "dnsResolve()", "dnsResolveEx()", "myIpAddress()", + // "myIpAddressEx()". Returns true on success and fills |*output| with the + // result. If |*terminate| is set to true, then the script execution will + // be aborted. Note that termination may not happen right away. + virtual bool ResolveDns(const std::string& host, + ResolveDnsOperation op, + std::string* output, + bool* terminate) = 0; + + // Handler for "alert(message)" + virtual void Alert(const base::string16& message) = 0; + + // Handler for when an error is encountered. |line_number| may be -1 + // if a line number is not applicable to this error. + virtual void OnError(int line_number, const base::string16& error) = 0; + + protected: + virtual ~JSBindings() {} + }; + + // Constructs a ProxyResolverV8. + ProxyResolverV8(); + + virtual ~ProxyResolverV8(); + + JSBindings* js_bindings() const { return js_bindings_; } + void set_js_bindings(JSBindings* js_bindings) { js_bindings_ = js_bindings; } + + // ProxyResolver implementation: + virtual int GetProxyForURL(const GURL& url, + ProxyInfo* results, + const net::CompletionCallback& /*callback*/, + RequestHandle* /*request*/, + const BoundNetLog& net_log) OVERRIDE; + virtual void CancelRequest(RequestHandle request) OVERRIDE; + virtual LoadState GetLoadState(RequestHandle request) const OVERRIDE; + virtual void CancelSetPacScript() OVERRIDE; + virtual void PurgeMemory() OVERRIDE; + virtual int SetPacScript( + const scoped_refptr<ProxyResolverScriptData>& script_data, + const net::CompletionCallback& /*callback*/) OVERRIDE; + + // Remember the default Isolate, must be called from the main thread. This + // hack can be removed when the "default Isolate" concept is gone. + static void RememberDefaultIsolate(); + +#if defined(OS_WIN) + // Create an isolate to use for the proxy resolver. Until the "default + // Isolate" concept is gone, it is preferable to invoke + // RememberDefaultIsolate() as creating a new Isolate in additional to the + // default Isolate will waste a few MB of memory and the runtime it took to + // create the default Isolate. + static void CreateIsolate(); +#endif + + static v8::Isolate* GetDefaultIsolate(); + + // Get total/ued heap memory usage of all v8 instances used by the proxy + // resolver. + static size_t GetTotalHeapSize(); + static size_t GetUsedHeapSize(); + + private: + static v8::Isolate* g_default_isolate_; + + // Context holds the Javascript state for the most recently loaded PAC + // script. It corresponds with the data from the last call to + // SetPacScript(). + class Context; + + scoped_ptr<Context> context_; + + JSBindings* js_bindings_; + + DISALLOW_COPY_AND_ASSIGN(ProxyResolverV8); +}; + +} // namespace net + +#endif // NET_PROXY_PROXY_RESOLVER_V8_H_ diff --git a/chromium/net/proxy/proxy_resolver_v8_tracing.cc b/chromium/net/proxy/proxy_resolver_v8_tracing.cc new file mode 100644 index 00000000000..4f6f5fc17d9 --- /dev/null +++ b/chromium/net/proxy/proxy_resolver_v8_tracing.cc @@ -0,0 +1,1181 @@ +// Copyright (c) 2013 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/proxy/proxy_resolver_v8_tracing.h" + +#include "base/bind.h" +#include "base/message_loop/message_loop_proxy.h" +#include "base/metrics/histogram.h" +#include "base/strings/stringprintf.h" +#include "base/synchronization/cancellation_flag.h" +#include "base/synchronization/waitable_event.h" +#include "base/threading/thread.h" +#include "base/threading/thread_restrictions.h" +#include "base/values.h" +#include "net/base/address_list.h" +#include "net/base/net_errors.h" +#include "net/base/net_log.h" +#include "net/dns/host_resolver.h" +#include "net/proxy/proxy_info.h" +#include "net/proxy/proxy_resolver_error_observer.h" +#include "net/proxy/proxy_resolver_v8.h" + +// The intent of this class is explained in the design document: +// https://docs.google.com/a/chromium.org/document/d/16Ij5OcVnR3s0MH4Z5XkhI9VTPoMJdaBn9rKreAmGOdE/edit +// +// In a nutshell, PAC scripts are Javascript programs and may depend on +// network I/O, by calling functions like dnsResolve(). +// +// This is problematic since functions such as dnsResolve() will block the +// Javascript execution until the DNS result is availble, thereby stalling the +// PAC thread, which hurts the ability to process parallel proxy resolves. +// An obvious solution is to simply start more PAC threads, however this scales +// poorly, which hurts the ability to process parallel proxy resolves. +// +// The solution in ProxyResolverV8Tracing is to model PAC scripts as being +// deterministic, and depending only on the inputted URL. When the script +// issues a dnsResolve() for a yet unresolved hostname, the Javascript +// execution is "aborted", and then re-started once the DNS result is +// known. +namespace net { + +namespace { + +// Upper bound on how many *unique* DNS resolves a PAC script is allowed +// to make. This is a failsafe both for scripts that do a ridiculous +// number of DNS resolves, as well as scripts which are misbehaving +// under the tracing optimization. It is not expected to hit this normally. +const size_t kMaxUniqueResolveDnsPerExec = 20; + +// Approximate number of bytes to use for buffering alerts() and errors. +// This is a failsafe in case repeated executions of the script causes +// too much memory bloat. It is not expected for well behaved scripts to +// hit this. (In fact normal scripts should not even have alerts() or errors). +const size_t kMaxAlertsAndErrorsBytes = 2048; + +// Returns event parameters for a PAC error message (line number + message). +base::Value* NetLogErrorCallback(int line_number, + const base::string16* message, + NetLog::LogLevel /* log_level */) { + base::DictionaryValue* dict = new base::DictionaryValue(); + dict->SetInteger("line_number", line_number); + dict->SetString("message", *message); + return dict; +} + +void IncrementWithoutOverflow(uint8* x) { + if (*x != 0xFF) + *x += 1; +} + +} // namespace + +// The Job class is responsible for executing GetProxyForURL() and +// SetPacScript(), since both of these operations share similar code. +// +// The DNS for these operations can operate in either blocking or +// non-blocking mode. Blocking mode is used as a fallback when the PAC script +// seems to be misbehaving under the tracing optimization. +// +// Note that this class runs on both the origin thread and a worker +// thread. Most methods are expected to be used exclusively on one thread +// or the other. +// +// The lifetime of Jobs does not exceed that of the ProxyResolverV8Tracing that +// spawned it. Destruction might happen on either the origin thread or the +// worker thread. +class ProxyResolverV8Tracing::Job + : public base::RefCountedThreadSafe<ProxyResolverV8Tracing::Job>, + public ProxyResolverV8::JSBindings { + public: + // |parent| is non-owned. It is the ProxyResolverV8Tracing that spawned this + // Job, and must oulive it. + explicit Job(ProxyResolverV8Tracing* parent); + + // Called from origin thread. + void StartSetPacScript( + const scoped_refptr<ProxyResolverScriptData>& script_data, + const CompletionCallback& callback); + + // Called from origin thread. + void StartGetProxyForURL(const GURL& url, + ProxyInfo* results, + const BoundNetLog& net_log, + const CompletionCallback& callback); + + // Called from origin thread. + void Cancel(); + + // Called from origin thread. + LoadState GetLoadState() const; + + private: + typedef std::map<std::string, std::string> DnsCache; + friend class base::RefCountedThreadSafe<ProxyResolverV8Tracing::Job>; + + enum Operation { + SET_PAC_SCRIPT, + GET_PROXY_FOR_URL, + }; + + struct AlertOrError { + bool is_alert; + int line_number; + base::string16 message; + }; + + virtual ~Job(); + + void CheckIsOnWorkerThread() const; + void CheckIsOnOriginThread() const; + + void SetCallback(const CompletionCallback& callback); + void ReleaseCallback(); + + ProxyResolverV8* v8_resolver(); + base::MessageLoop* worker_loop(); + HostResolver* host_resolver(); + ProxyResolverErrorObserver* error_observer(); + NetLog* net_log(); + + // Invokes the user's callback. + void NotifyCaller(int result); + void NotifyCallerOnOriginLoop(int result); + + void RecordMetrics() const; + + void Start(Operation op, bool blocking_dns, + const CompletionCallback& callback); + + void ExecuteBlocking(); + void ExecuteNonBlocking(); + int ExecuteProxyResolver(); + + // Implementation of ProxyResolverv8::JSBindings + virtual bool ResolveDns(const std::string& host, + ResolveDnsOperation op, + std::string* output, + bool* terminate) OVERRIDE; + virtual void Alert(const base::string16& message) OVERRIDE; + virtual void OnError(int line_number, const base::string16& error) OVERRIDE; + + bool ResolveDnsBlocking(const std::string& host, + ResolveDnsOperation op, + std::string* output); + + bool ResolveDnsNonBlocking(const std::string& host, + ResolveDnsOperation op, + std::string* output, + bool* terminate); + + bool PostDnsOperationAndWait(const std::string& host, + ResolveDnsOperation op, + bool* completed_synchronously) + WARN_UNUSED_RESULT; + + void DoDnsOperation(); + void OnDnsOperationComplete(int result); + + void ScheduleRestartWithBlockingDns(); + + bool GetDnsFromLocalCache(const std::string& host, ResolveDnsOperation op, + std::string* output, bool* return_value); + + void SaveDnsToLocalCache(const std::string& host, ResolveDnsOperation op, + int net_error, const net::AddressList& addresses); + + // Builds a RequestInfo to service the specified PAC DNS operation. + static HostResolver::RequestInfo MakeDnsRequestInfo(const std::string& host, + ResolveDnsOperation op); + + // Makes a key for looking up |host, op| in |dns_cache_|. Strings are used for + // convenience, to avoid defining custom comparators. + static std::string MakeDnsCacheKey(const std::string& host, + ResolveDnsOperation op); + + void HandleAlertOrError(bool is_alert, int line_number, + const base::string16& message); + void DispatchBufferedAlertsAndErrors(); + void DispatchAlertOrError(bool is_alert, int line_number, + const base::string16& message); + + void LogEventToCurrentRequestAndGlobally( + NetLog::EventType type, + const NetLog::ParametersCallback& parameters_callback); + + // The thread which called into ProxyResolverV8Tracing, and on which the + // completion callback is expected to run. + scoped_refptr<base::MessageLoopProxy> origin_loop_; + + // The ProxyResolverV8Tracing which spawned this Job. + // Initialized on origin thread and then accessed from both threads. + ProxyResolverV8Tracing* parent_; + + // The callback to run (on the origin thread) when the Job finishes. + // Should only be accessed from origin thread. + CompletionCallback callback_; + + // Flag to indicate whether the request has been cancelled. + base::CancellationFlag cancelled_; + + // The operation that this Job is running. + // Initialized on origin thread and then accessed from both threads. + Operation operation_; + + // The DNS mode for this Job. + // Initialized on origin thread, mutated on worker thread, and accessed + // by both the origin thread and worker thread. + bool blocking_dns_; + + // Used to block the worker thread on a DNS operation taking place on the + // origin thread. + base::WaitableEvent event_; + + // Map of DNS operations completed so far. Written into on the origin thread + // and read on the worker thread. + DnsCache dns_cache_; + + // The job holds a reference to itself to ensure that it remains alive until + // either completion or cancellation. + scoped_refptr<Job> owned_self_reference_; + + // ------------------------------------------------------- + // State specific to SET_PAC_SCRIPT. + // ------------------------------------------------------- + + scoped_refptr<ProxyResolverScriptData> script_data_; + + // ------------------------------------------------------- + // State specific to GET_PROXY_FOR_URL. + // ------------------------------------------------------- + + ProxyInfo* user_results_; // Owned by caller, lives on origin thread. + GURL url_; + ProxyInfo results_; + BoundNetLog bound_net_log_; + + // --------------------------------------------------------------------------- + // State for ExecuteNonBlocking() + // --------------------------------------------------------------------------- + // These variables are used exclusively on the worker thread and are only + // meaningful when executing inside of ExecuteNonBlocking(). + + // Whether this execution was abandoned due to a missing DNS dependency. + bool abandoned_; + + // Number of calls made to ResolveDns() by this execution. + int num_dns_; + + // Sequence of calls made to Alert() or OnError() by this execution. + std::vector<AlertOrError> alerts_and_errors_; + size_t alerts_and_errors_byte_cost_; // Approximate byte cost of the above. + + // Number of calls made to ResolveDns() by the PREVIOUS execution. + int last_num_dns_; + + // Whether the current execution needs to be restarted in blocking mode. + bool should_restart_with_blocking_dns_; + + // --------------------------------------------------------------------------- + // State for pending DNS request. + // --------------------------------------------------------------------------- + + // Handle to the outstanding request in the HostResolver, or NULL. + // This is mutated and used on the origin thread, however it may be read by + // the worker thread for some DCHECKS(). + HostResolver::RequestHandle pending_dns_; + + // Indicates if the outstanding DNS request completed synchronously. Written + // on the origin thread, and read by the worker thread. + bool pending_dns_completed_synchronously_; + + // These are the inputs to DoDnsOperation(). Written on the worker thread, + // read by the origin thread. + std::string pending_dns_host_; + ResolveDnsOperation pending_dns_op_; + + // This contains the resolved address list that DoDnsOperation() fills in. + // Used exclusively on the origin thread. + AddressList pending_dns_addresses_; + + // --------------------------------------------------------------------------- + // Metrics for histograms + // --------------------------------------------------------------------------- + // These values are used solely for logging histograms. They do not affect + // the execution flow of requests. + + // The time when the proxy resolve request started. Used exclusively on the + // origin thread. + base::TimeTicks metrics_start_time_; + + // The time when the proxy resolve request completes on the worker thread. + // Written on the worker thread, read on the origin thread. + base::TimeTicks metrics_end_time_; + + // The time when PostDnsOperationAndWait() was called. Written on the worker + // thread, read by the origin thread. + base::TimeTicks metrics_pending_dns_start_; + + // The total amount of time that has been spent by the script waiting for + // DNS dependencies. This includes the time spent posting the task to + // the origin thread, up until the DNS result is found on the origin + // thread. It does not include any time spent waiting in the message loop + // for the worker thread, nor any time restarting or executing the + // script. Used exclusively on the origin thread. + base::TimeDelta metrics_dns_total_time_; + + // The following variables are initialized on the origin thread, + // incremented on the worker thread, and then read upon completion on the + // origin thread. The values are not expected to exceed the range of a uint8. + // If they do, then they will be clamped to 0xFF. + uint8 metrics_num_executions_; + uint8 metrics_num_unique_dns_; + uint8 metrics_num_alerts_; + uint8 metrics_num_errors_; + + // The time that the latest execution took (time spent inside of + // ExecuteProxyResolver(), which includes time spent in bindings too). + // Written on the worker thread, read on the origin thread. + base::TimeDelta metrics_execution_time_; + + // The cumulative time spent in ExecuteProxyResolver() that was ultimately + // discarded work. + // Written on the worker thread, read on the origin thread. + base::TimeDelta metrics_abandoned_execution_total_time_; + + // The duration that the worker thread was blocked waiting on DNS results from + // the origin thread, when operating in nonblocking mode. + // Written on the worker thread, read on the origin thread. + base::TimeDelta metrics_nonblocking_dns_wait_total_time_; +}; + +ProxyResolverV8Tracing::Job::Job(ProxyResolverV8Tracing* parent) + : origin_loop_(base::MessageLoopProxy::current()), + parent_(parent), + event_(true, false), + last_num_dns_(0), + pending_dns_(NULL), + metrics_num_executions_(0), + metrics_num_unique_dns_(0), + metrics_num_alerts_(0), + metrics_num_errors_(0) { + CheckIsOnOriginThread(); +} + +void ProxyResolverV8Tracing::Job::StartSetPacScript( + const scoped_refptr<ProxyResolverScriptData>& script_data, + const CompletionCallback& callback) { + CheckIsOnOriginThread(); + + script_data_ = script_data; + + // Script initialization uses blocking DNS since there isn't any + // advantage to using non-blocking mode here. That is because the + // parent ProxyService can't submit any ProxyResolve requests until + // initialization has completed successfully! + Start(SET_PAC_SCRIPT, true /*blocking*/, callback); +} + +void ProxyResolverV8Tracing::Job::StartGetProxyForURL( + const GURL& url, + ProxyInfo* results, + const BoundNetLog& net_log, + const CompletionCallback& callback) { + CheckIsOnOriginThread(); + + url_ = url; + user_results_ = results; + bound_net_log_ = net_log; + + Start(GET_PROXY_FOR_URL, false /*non-blocking*/, callback); +} + +void ProxyResolverV8Tracing::Job::Cancel() { + CheckIsOnOriginThread(); + + // There are several possibilities to consider for cancellation: + // (a) The job has been posted to the worker thread, however script execution + // has not yet started. + // (b) The script is executing on the worker thread. + // (c) The script is executing on the worker thread, however is blocked inside + // of dnsResolve() waiting for a response from the origin thread. + // (d) Nothing is running on the worker thread, however the host resolver has + // a pending DNS request which upon completion will restart the script + // execution. + // (e) The worker thread has a pending task to restart execution, which was + // posted after the DNS dependency was resolved and saved to local cache. + // (f) The script execution completed entirely, and posted a task to the + // origin thread to notify the caller. + // + // |cancelled_| is read on both the origin thread and worker thread. The + // code that runs on the worker thread is littered with checks on + // |cancelled_| to break out early. + cancelled_.Set(); + + ReleaseCallback(); + + if (pending_dns_) { + host_resolver()->CancelRequest(pending_dns_); + pending_dns_ = NULL; + } + + // The worker thread might be blocked waiting for DNS. + event_.Signal(); + + owned_self_reference_ = NULL; +} + +LoadState ProxyResolverV8Tracing::Job::GetLoadState() const { + CheckIsOnOriginThread(); + + if (pending_dns_) + return LOAD_STATE_RESOLVING_HOST_IN_PROXY_SCRIPT; + + return LOAD_STATE_RESOLVING_PROXY_FOR_URL; +} + +ProxyResolverV8Tracing::Job::~Job() { + DCHECK(!pending_dns_); + DCHECK(callback_.is_null()); +} + +void ProxyResolverV8Tracing::Job::CheckIsOnWorkerThread() const { + DCHECK_EQ(base::MessageLoop::current(), parent_->thread_->message_loop()); +} + +void ProxyResolverV8Tracing::Job::CheckIsOnOriginThread() const { + DCHECK(origin_loop_->BelongsToCurrentThread()); +} + +void ProxyResolverV8Tracing::Job::SetCallback( + const CompletionCallback& callback) { + CheckIsOnOriginThread(); + DCHECK(callback_.is_null()); + parent_->num_outstanding_callbacks_++; + callback_ = callback; +} + +void ProxyResolverV8Tracing::Job::ReleaseCallback() { + CheckIsOnOriginThread(); + DCHECK(!callback_.is_null()); + CHECK_GT(parent_->num_outstanding_callbacks_, 0); + parent_->num_outstanding_callbacks_--; + callback_.Reset(); + + // For good measure, clear this other user-owned pointer. + user_results_ = NULL; +} + +ProxyResolverV8* ProxyResolverV8Tracing::Job::v8_resolver() { + return parent_->v8_resolver_.get(); +} + +base::MessageLoop* ProxyResolverV8Tracing::Job::worker_loop() { + return parent_->thread_->message_loop(); +} + +HostResolver* ProxyResolverV8Tracing::Job::host_resolver() { + return parent_->host_resolver_; +} + +ProxyResolverErrorObserver* ProxyResolverV8Tracing::Job::error_observer() { + return parent_->error_observer_.get(); +} + +NetLog* ProxyResolverV8Tracing::Job::net_log() { + return parent_->net_log_; +} + +void ProxyResolverV8Tracing::Job::NotifyCaller(int result) { + CheckIsOnWorkerThread(); + + metrics_end_time_ = base::TimeTicks::Now(); + + origin_loop_->PostTask( + FROM_HERE, + base::Bind(&Job::NotifyCallerOnOriginLoop, this, result)); +} + +void ProxyResolverV8Tracing::Job::NotifyCallerOnOriginLoop(int result) { + CheckIsOnOriginThread(); + + if (cancelled_.IsSet()) + return; + + DCHECK(!callback_.is_null()); + DCHECK(!pending_dns_); + + if (operation_ == GET_PROXY_FOR_URL) { + RecordMetrics(); + *user_results_ = results_; + } + + // There is only ever 1 outstanding SET_PAC_SCRIPT job. It needs to be + // tracked to support cancellation. + if (operation_ == SET_PAC_SCRIPT) { + DCHECK_EQ(parent_->set_pac_script_job_.get(), this); + parent_->set_pac_script_job_ = NULL; + } + + CompletionCallback callback = callback_; + ReleaseCallback(); + callback.Run(result); + + owned_self_reference_ = NULL; +} + +void ProxyResolverV8Tracing::Job::RecordMetrics() const { + CheckIsOnOriginThread(); + DCHECK_EQ(GET_PROXY_FOR_URL, operation_); + + base::TimeTicks now = base::TimeTicks::Now(); + + // Metrics are output for each completed request to GetProxyForURL()). + // + // Note that a different set of histograms is used to record the metrics for + // requests that completed in non-blocking mode versus blocking mode. The + // expectation is for requests to complete in non-blocking mode each time. + // If they don't then something strange is happening, and the purpose of the + // seprate statistics is to better understand that trend. +#define UPDATE_HISTOGRAMS(base_name) \ + do {\ + UMA_HISTOGRAM_MEDIUM_TIMES(base_name "TotalTime", now - metrics_start_time_);\ + UMA_HISTOGRAM_MEDIUM_TIMES(base_name "TotalTimeWorkerThread",\ + metrics_end_time_ - metrics_start_time_);\ + UMA_HISTOGRAM_TIMES(base_name "OriginThreadLatency",\ + now - metrics_end_time_);\ + UMA_HISTOGRAM_MEDIUM_TIMES(base_name "TotalTimeDNS",\ + metrics_dns_total_time_);\ + UMA_HISTOGRAM_MEDIUM_TIMES(base_name "ExecutionTime",\ + metrics_execution_time_);\ + UMA_HISTOGRAM_MEDIUM_TIMES(base_name "AbandonedExecutionTotalTime",\ + metrics_abandoned_execution_total_time_);\ + UMA_HISTOGRAM_MEDIUM_TIMES(base_name "DnsWaitTotalTime",\ + metrics_nonblocking_dns_wait_total_time_);\ + UMA_HISTOGRAM_CUSTOM_COUNTS(\ + base_name "NumRestarts", metrics_num_executions_ - 1,\ + 1, kMaxUniqueResolveDnsPerExec, kMaxUniqueResolveDnsPerExec);\ + UMA_HISTOGRAM_CUSTOM_COUNTS(\ + base_name "UniqueDNS", metrics_num_unique_dns_,\ + 1, kMaxUniqueResolveDnsPerExec, kMaxUniqueResolveDnsPerExec);\ + UMA_HISTOGRAM_COUNTS_100(base_name "NumAlerts", metrics_num_alerts_);\ + UMA_HISTOGRAM_CUSTOM_COUNTS(\ + base_name "NumErrors", metrics_num_errors_, 1, 10, 10);\ + } while (false) + + if (!blocking_dns_) + UPDATE_HISTOGRAMS("Net.ProxyResolver."); + else + UPDATE_HISTOGRAMS("Net.ProxyResolver.BlockingDNSMode."); + +#undef UPDATE_HISTOGRAMS + + // Histograms to better understand http://crbug.com/240536 -- long + // URLs can cause a significant slowdown in PAC execution. Figure out how + // severe this is in the wild. + if (!blocking_dns_) { + size_t url_size = url_.spec().size(); + + UMA_HISTOGRAM_CUSTOM_COUNTS( + "Net.ProxyResolver.URLSize", url_size, 1, 200000, 50); + + if (url_size > 2048) { + UMA_HISTOGRAM_MEDIUM_TIMES("Net.ProxyResolver.ExecutionTime_UrlOver2K", + metrics_execution_time_); + } + + if (url_size > 4096) { + UMA_HISTOGRAM_MEDIUM_TIMES("Net.ProxyResolver.ExecutionTime_UrlOver4K", + metrics_execution_time_); + } + + if (url_size > 8192) { + UMA_HISTOGRAM_MEDIUM_TIMES("Net.ProxyResolver.ExecutionTime_UrlOver8K", + metrics_execution_time_); + } + + if (url_size > 131072) { + UMA_HISTOGRAM_MEDIUM_TIMES("Net.ProxyResolver.ExecutionTime_UrlOver128K", + metrics_execution_time_); + } + } +} + + +void ProxyResolverV8Tracing::Job::Start(Operation op, bool blocking_dns, + const CompletionCallback& callback) { + CheckIsOnOriginThread(); + + metrics_start_time_ = base::TimeTicks::Now(); + operation_ = op; + blocking_dns_ = blocking_dns; + SetCallback(callback); + + owned_self_reference_ = this; + + worker_loop()->PostTask(FROM_HERE, + blocking_dns_ ? base::Bind(&Job::ExecuteBlocking, this) : + base::Bind(&Job::ExecuteNonBlocking, this)); +} + +void ProxyResolverV8Tracing::Job::ExecuteBlocking() { + CheckIsOnWorkerThread(); + DCHECK(blocking_dns_); + + if (cancelled_.IsSet()) + return; + + NotifyCaller(ExecuteProxyResolver()); +} + +void ProxyResolverV8Tracing::Job::ExecuteNonBlocking() { + CheckIsOnWorkerThread(); + DCHECK(!blocking_dns_); + + if (cancelled_.IsSet()) + return; + + // Reset state for the current execution. + abandoned_ = false; + num_dns_ = 0; + alerts_and_errors_.clear(); + alerts_and_errors_byte_cost_ = 0; + should_restart_with_blocking_dns_ = false; + + int result = ExecuteProxyResolver(); + + if (abandoned_) + metrics_abandoned_execution_total_time_ += metrics_execution_time_; + + if (should_restart_with_blocking_dns_) { + DCHECK(!blocking_dns_); + DCHECK(abandoned_); + blocking_dns_ = true; + ExecuteBlocking(); + return; + } + + if (abandoned_) + return; + + DispatchBufferedAlertsAndErrors(); + NotifyCaller(result); +} + +int ProxyResolverV8Tracing::Job::ExecuteProxyResolver() { + IncrementWithoutOverflow(&metrics_num_executions_); + + base::TimeTicks start = base::TimeTicks::Now(); + + JSBindings* prev_bindings = v8_resolver()->js_bindings(); + v8_resolver()->set_js_bindings(this); + + int result = ERR_UNEXPECTED; // Initialized to silence warnings. + + switch (operation_) { + case SET_PAC_SCRIPT: + result = v8_resolver()->SetPacScript( + script_data_, CompletionCallback()); + break; + case GET_PROXY_FOR_URL: + result = v8_resolver()->GetProxyForURL( + url_, + // Important: Do not write directly into |user_results_|, since if the + // request were to be cancelled from the origin thread, must guarantee + // that |user_results_| is not accessed anymore. + &results_, + CompletionCallback(), + NULL, + bound_net_log_); + break; + } + + v8_resolver()->set_js_bindings(prev_bindings); + + metrics_execution_time_ = base::TimeTicks::Now() - start; + + return result; +} + +bool ProxyResolverV8Tracing::Job::ResolveDns(const std::string& host, + ResolveDnsOperation op, + std::string* output, + bool* terminate) { + if (cancelled_.IsSet()) { + *terminate = true; + return false; + } + + if ((op == DNS_RESOLVE || op == DNS_RESOLVE_EX) && host.empty()) { + // a DNS resolve with an empty hostname is considered an error. + return false; + } + + return blocking_dns_ ? + ResolveDnsBlocking(host, op, output) : + ResolveDnsNonBlocking(host, op, output, terminate); +} + +void ProxyResolverV8Tracing::Job::Alert(const base::string16& message) { + HandleAlertOrError(true, -1, message); +} + +void ProxyResolverV8Tracing::Job::OnError(int line_number, + const base::string16& error) { + HandleAlertOrError(false, line_number, error); +} + +bool ProxyResolverV8Tracing::Job::ResolveDnsBlocking(const std::string& host, + ResolveDnsOperation op, + std::string* output) { + CheckIsOnWorkerThread(); + + // Check if the DNS result for this host has already been cached. + bool rv; + if (GetDnsFromLocalCache(host, op, output, &rv)) { + // Yay, cache hit! + return rv; + } + + // If the host was not in the local cache, this is a new hostname. + IncrementWithoutOverflow(&metrics_num_unique_dns_); + + if (dns_cache_.size() >= kMaxUniqueResolveDnsPerExec) { + // Safety net for scripts with unexpectedly many DNS calls. + // We will continue running to completion, but will fail every + // subsequent DNS request. + return false; + } + + if (!PostDnsOperationAndWait(host, op, NULL)) + return false; // Was cancelled. + + CHECK(GetDnsFromLocalCache(host, op, output, &rv)); + return rv; +} + +bool ProxyResolverV8Tracing::Job::ResolveDnsNonBlocking(const std::string& host, + ResolveDnsOperation op, + std::string* output, + bool* terminate) { + CheckIsOnWorkerThread(); + + if (abandoned_) { + // If this execution was already abandoned can fail right away. Only 1 DNS + // dependency will be traced at a time (for more predictable outcomes). + return false; + } + + num_dns_ += 1; + + // Check if the DNS result for this host has already been cached. + bool rv; + if (GetDnsFromLocalCache(host, op, output, &rv)) { + // Yay, cache hit! + return rv; + } + + // If the host was not in the local cache, then this is a new hostname. + IncrementWithoutOverflow(&metrics_num_unique_dns_); + + if (num_dns_ <= last_num_dns_) { + // The sequence of DNS operations is different from last time! + ScheduleRestartWithBlockingDns(); + *terminate = true; + return false; + } + + if (dns_cache_.size() >= kMaxUniqueResolveDnsPerExec) { + // Safety net for scripts with unexpectedly many DNS calls. + return false; + } + + DCHECK(!should_restart_with_blocking_dns_); + + bool completed_synchronously; + if (!PostDnsOperationAndWait(host, op, &completed_synchronously)) + return false; // Was cancelled. + + if (completed_synchronously) { + CHECK(GetDnsFromLocalCache(host, op, output, &rv)); + return rv; + } + + // Otherwise if the result was not in the cache, then a DNS request has + // been started. Abandon this invocation of FindProxyForURL(), it will be + // restarted once the DNS request completes. + abandoned_ = true; + *terminate = true; + last_num_dns_ = num_dns_; + return false; +} + +bool ProxyResolverV8Tracing::Job::PostDnsOperationAndWait( + const std::string& host, ResolveDnsOperation op, + bool* completed_synchronously) { + + base::TimeTicks start = base::TimeTicks::Now(); + + // Post the DNS request to the origin thread. + DCHECK(!pending_dns_); + metrics_pending_dns_start_ = base::TimeTicks::Now(); + pending_dns_host_ = host; + pending_dns_op_ = op; + origin_loop_->PostTask(FROM_HERE, base::Bind(&Job::DoDnsOperation, this)); + + event_.Wait(); + event_.Reset(); + + if (cancelled_.IsSet()) + return false; + + if (completed_synchronously) + *completed_synchronously = pending_dns_completed_synchronously_; + + if (!blocking_dns_) + metrics_nonblocking_dns_wait_total_time_ += base::TimeTicks::Now() - start; + + return true; +} + +void ProxyResolverV8Tracing::Job::DoDnsOperation() { + CheckIsOnOriginThread(); + DCHECK(!pending_dns_); + + if (cancelled_.IsSet()) + return; + + HostResolver::RequestHandle dns_request = NULL; + int result = host_resolver()->Resolve( + MakeDnsRequestInfo(pending_dns_host_, pending_dns_op_), + &pending_dns_addresses_, + base::Bind(&Job::OnDnsOperationComplete, this), + &dns_request, + bound_net_log_); + + pending_dns_completed_synchronously_ = result != ERR_IO_PENDING; + + // Check if the request was cancelled as a side-effect of calling into the + // HostResolver. This isn't the ordinary execution flow, however it is + // exercised by unit-tests. + if (cancelled_.IsSet()) { + if (!pending_dns_completed_synchronously_) + host_resolver()->CancelRequest(dns_request); + return; + } + + if (pending_dns_completed_synchronously_) { + OnDnsOperationComplete(result); + } else { + DCHECK(dns_request); + pending_dns_ = dns_request; + // OnDnsOperationComplete() will be called by host resolver on completion. + } + + if (!blocking_dns_) { + // The worker thread always blocks waiting to see if the result can be + // serviced from cache before restarting. + event_.Signal(); + } +} + +void ProxyResolverV8Tracing::Job::OnDnsOperationComplete(int result) { + CheckIsOnOriginThread(); + + DCHECK(!cancelled_.IsSet()); + DCHECK(pending_dns_completed_synchronously_ == (pending_dns_ == NULL)); + + SaveDnsToLocalCache(pending_dns_host_, pending_dns_op_, result, + pending_dns_addresses_); + pending_dns_ = NULL; + + metrics_dns_total_time_ += + base::TimeTicks::Now() - metrics_pending_dns_start_; + + if (blocking_dns_) { + event_.Signal(); + return; + } + + if (!blocking_dns_ && !pending_dns_completed_synchronously_) { + // Restart. This time it should make more progress due to having + // cached items. + worker_loop()->PostTask(FROM_HERE, + base::Bind(&Job::ExecuteNonBlocking, this)); + } +} + +void ProxyResolverV8Tracing::Job::ScheduleRestartWithBlockingDns() { + CheckIsOnWorkerThread(); + + DCHECK(!should_restart_with_blocking_dns_); + DCHECK(!abandoned_); + DCHECK(!blocking_dns_); + + abandoned_ = true; + + // The restart will happen after ExecuteNonBlocking() finishes. + should_restart_with_blocking_dns_ = true; +} + +bool ProxyResolverV8Tracing::Job::GetDnsFromLocalCache( + const std::string& host, + ResolveDnsOperation op, + std::string* output, + bool* return_value) { + CheckIsOnWorkerThread(); + + DnsCache::const_iterator it = dns_cache_.find(MakeDnsCacheKey(host, op)); + if (it == dns_cache_.end()) + return false; + + *output = it->second; + *return_value = !it->second.empty(); + return true; +} + +void ProxyResolverV8Tracing::Job::SaveDnsToLocalCache( + const std::string& host, + ResolveDnsOperation op, + int net_error, + const net::AddressList& addresses) { + CheckIsOnOriginThread(); + + // Serialize the result into a string to save to the cache. + std::string cache_value; + if (net_error != OK) { + cache_value = std::string(); + } else if (op == DNS_RESOLVE || op == MY_IP_ADDRESS) { + // dnsResolve() and myIpAddress() are expected to return a single IP + // address. + cache_value = addresses.front().ToStringWithoutPort(); + } else { + // The *Ex versions are expected to return a semi-colon separated list. + for (AddressList::const_iterator iter = addresses.begin(); + iter != addresses.end(); ++iter) { + if (!cache_value.empty()) + cache_value += ";"; + cache_value += iter->ToStringWithoutPort(); + } + } + + dns_cache_[MakeDnsCacheKey(host, op)] = cache_value; +} + +// static +HostResolver::RequestInfo ProxyResolverV8Tracing::Job::MakeDnsRequestInfo( + const std::string& host, ResolveDnsOperation op) { + HostPortPair host_port = HostPortPair(host, 80); + if (op == MY_IP_ADDRESS || op == MY_IP_ADDRESS_EX) { + host_port.set_host(GetHostName()); + } + + HostResolver::RequestInfo info(host_port); + + // The non-ex flavors are limited to IPv4 results. + if (op == MY_IP_ADDRESS || op == DNS_RESOLVE) { + info.set_address_family(ADDRESS_FAMILY_IPV4); + } + + return info; +} + +std::string ProxyResolverV8Tracing::Job::MakeDnsCacheKey( + const std::string& host, ResolveDnsOperation op) { + return base::StringPrintf("%d:%s", op, host.c_str()); +} + +void ProxyResolverV8Tracing::Job::HandleAlertOrError( + bool is_alert, + int line_number, + const base::string16& message) { + CheckIsOnWorkerThread(); + + if (cancelled_.IsSet()) + return; + + if (blocking_dns_) { + // In blocking DNS mode the events can be dispatched immediately. + DispatchAlertOrError(is_alert, line_number, message); + return; + } + + // Otherwise in nonblocking mode, buffer all the messages until + // the end. + + if (abandoned_) + return; + + alerts_and_errors_byte_cost_ += sizeof(AlertOrError) + message.size() * 2; + + // If there have been lots of messages, enqueing could be expensive on + // memory. Consider a script which does megabytes worth of alerts(). + // Avoid this by falling back to blocking mode. + if (alerts_and_errors_byte_cost_ > kMaxAlertsAndErrorsBytes) { + ScheduleRestartWithBlockingDns(); + return; + } + + AlertOrError entry = {is_alert, line_number, message}; + alerts_and_errors_.push_back(entry); +} + +void ProxyResolverV8Tracing::Job::DispatchBufferedAlertsAndErrors() { + CheckIsOnWorkerThread(); + DCHECK(!blocking_dns_); + DCHECK(!abandoned_); + + for (size_t i = 0; i < alerts_and_errors_.size(); ++i) { + const AlertOrError& x = alerts_and_errors_[i]; + DispatchAlertOrError(x.is_alert, x.line_number, x.message); + } +} + +void ProxyResolverV8Tracing::Job::DispatchAlertOrError( + bool is_alert, int line_number, const base::string16& message) { + CheckIsOnWorkerThread(); + + // Note that the handling of cancellation is racy with regard to + // alerts/errors. The request might get cancelled shortly after this + // check! (There is no lock being held to guarantee otherwise). + // + // If this happens, then some information will get written to the NetLog + // needlessly, however the NetLog will still be alive so it shouldn't cause + // problems. + if (cancelled_.IsSet()) + return; + + if (is_alert) { + // ------------------- + // alert + // ------------------- + IncrementWithoutOverflow(&metrics_num_alerts_); + VLOG(1) << "PAC-alert: " << message; + + // Send to the NetLog. + LogEventToCurrentRequestAndGlobally( + NetLog::TYPE_PAC_JAVASCRIPT_ALERT, + NetLog::StringCallback("message", &message)); + } else { + // ------------------- + // error + // ------------------- + IncrementWithoutOverflow(&metrics_num_errors_); + if (line_number == -1) + VLOG(1) << "PAC-error: " << message; + else + VLOG(1) << "PAC-error: " << "line: " << line_number << ": " << message; + + // Send the error to the NetLog. + LogEventToCurrentRequestAndGlobally( + NetLog::TYPE_PAC_JAVASCRIPT_ERROR, + base::Bind(&NetLogErrorCallback, line_number, &message)); + + if (error_observer()) + error_observer()->OnPACScriptError(line_number, message); + } +} + +void ProxyResolverV8Tracing::Job::LogEventToCurrentRequestAndGlobally( + NetLog::EventType type, + const NetLog::ParametersCallback& parameters_callback) { + CheckIsOnWorkerThread(); + bound_net_log_.AddEvent(type, parameters_callback); + + // Emit to the global NetLog event stream. + if (net_log()) + net_log()->AddGlobalEntry(type, parameters_callback); +} + +ProxyResolverV8Tracing::ProxyResolverV8Tracing( + HostResolver* host_resolver, + ProxyResolverErrorObserver* error_observer, + NetLog* net_log) + : ProxyResolver(true /*expects_pac_bytes*/), + host_resolver_(host_resolver), + error_observer_(error_observer), + net_log_(net_log), + num_outstanding_callbacks_(0) { + DCHECK(host_resolver); + // Start up the thread. + thread_.reset(new base::Thread("Proxy resolver")); + CHECK(thread_->Start()); + + v8_resolver_.reset(new ProxyResolverV8); +} + +ProxyResolverV8Tracing::~ProxyResolverV8Tracing() { + // Note, all requests should have been cancelled. + CHECK(!set_pac_script_job_.get()); + CHECK_EQ(0, num_outstanding_callbacks_); + + // Join the worker thread. See http://crbug.com/69710. Note that we call + // Stop() here instead of simply clearing thread_ since there may be pending + // callbacks on the worker thread which want to dereference thread_. + base::ThreadRestrictions::ScopedAllowIO allow_io; + thread_->Stop(); +} + +int ProxyResolverV8Tracing::GetProxyForURL(const GURL& url, + ProxyInfo* results, + const CompletionCallback& callback, + RequestHandle* request, + const BoundNetLog& net_log) { + DCHECK(CalledOnValidThread()); + DCHECK(!callback.is_null()); + DCHECK(!set_pac_script_job_.get()); + + scoped_refptr<Job> job = new Job(this); + + if (request) + *request = job.get(); + + job->StartGetProxyForURL(url, results, net_log, callback); + return ERR_IO_PENDING; +} + +void ProxyResolverV8Tracing::CancelRequest(RequestHandle request) { + Job* job = reinterpret_cast<Job*>(request); + job->Cancel(); +} + +LoadState ProxyResolverV8Tracing::GetLoadState(RequestHandle request) const { + Job* job = reinterpret_cast<Job*>(request); + return job->GetLoadState(); +} + +void ProxyResolverV8Tracing::CancelSetPacScript() { + DCHECK(set_pac_script_job_.get()); + set_pac_script_job_->Cancel(); + set_pac_script_job_ = NULL; +} + +void ProxyResolverV8Tracing::PurgeMemory() { + thread_->message_loop()->PostTask( + FROM_HERE, + base::Bind(&ProxyResolverV8::PurgeMemory, + // The use of unretained is safe, since the worker thread + // cannot outlive |this|. + base::Unretained(v8_resolver_.get()))); +} + +int ProxyResolverV8Tracing::SetPacScript( + const scoped_refptr<ProxyResolverScriptData>& script_data, + const CompletionCallback& callback) { + DCHECK(CalledOnValidThread()); + DCHECK(!callback.is_null()); + + // Note that there should not be any outstanding (non-cancelled) Jobs when + // setting the PAC script (ProxyService should guarantee this). If there are, + // then they might complete in strange ways after the new script is set. + DCHECK(!set_pac_script_job_.get()); + CHECK_EQ(0, num_outstanding_callbacks_); + + set_pac_script_job_ = new Job(this); + set_pac_script_job_->StartSetPacScript(script_data, callback); + + return ERR_IO_PENDING; +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_resolver_v8_tracing.h b/chromium/net/proxy/proxy_resolver_v8_tracing.h new file mode 100644 index 00000000000..5877aa2088d --- /dev/null +++ b/chromium/net/proxy/proxy_resolver_v8_tracing.h @@ -0,0 +1,85 @@ +// Copyright (c) 2013 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. + +#ifndef NET_PROXY_PROXY_RESOLVER_V8_TRACING_H_ +#define NET_PROXY_PROXY_RESOLVER_V8_TRACING_H_ + +#include "base/basictypes.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/threading/non_thread_safe.h" +#include "net/base/net_export.h" +#include "net/proxy/proxy_resolver.h" + +namespace base { +class Thread; +class MessageLoopProxy; +} // namespace base + +namespace net { + +class HostResolver; +class NetLog; +class ProxyResolverErrorObserver; +class ProxyResolverV8; + +// ProxyResolverV8Tracing is a non-blocking ProxyResolver. It executes +// ProxyResolverV8 on a single helper thread, and does some magic to avoid +// blocking in DNS. For more details see the design document: +// https://docs.google.com/a/google.com/document/d/16Ij5OcVnR3s0MH4Z5XkhI9VTPoMJdaBn9rKreAmGOdE/edit?pli=1 +class NET_EXPORT_PRIVATE ProxyResolverV8Tracing + : public ProxyResolver, + NON_EXPORTED_BASE(public base::NonThreadSafe) { + public: + // Constructs a ProxyResolver that will issue DNS requests through + // |host_resolver|, forward Javascript errors through |error_observer|, and + // log Javascript errors and alerts to |net_log|. + // + // Note that the constructor takes ownership of |error_observer|, whereas + // |host_resolver| and |net_log| are expected to outlive |this|. + ProxyResolverV8Tracing(HostResolver* host_resolver, + ProxyResolverErrorObserver* error_observer, + NetLog* net_log); + + virtual ~ProxyResolverV8Tracing(); + + // ProxyResolver implementation: + virtual int GetProxyForURL(const GURL& url, + ProxyInfo* results, + const CompletionCallback& callback, + RequestHandle* request, + const BoundNetLog& net_log) OVERRIDE; + virtual void CancelRequest(RequestHandle request) OVERRIDE; + virtual LoadState GetLoadState(RequestHandle request) const OVERRIDE; + virtual void CancelSetPacScript() OVERRIDE; + virtual void PurgeMemory() OVERRIDE; + virtual int SetPacScript( + const scoped_refptr<ProxyResolverScriptData>& script_data, + const CompletionCallback& callback) OVERRIDE; + + private: + class Job; + + // The worker thread on which the ProxyResolverV8 will be run. + scoped_ptr<base::Thread> thread_; + scoped_ptr<ProxyResolverV8> v8_resolver_; + + // Non-owned host resolver, which is to be operated on the origin thread. + HostResolver* host_resolver_; + + scoped_ptr<ProxyResolverErrorObserver> error_observer_; + NetLog* net_log_; + + // The outstanding SetPacScript operation, or NULL. + scoped_refptr<Job> set_pac_script_job_; + + // The number of outstanding (non-cancelled) jobs. + int num_outstanding_callbacks_; + + DISALLOW_COPY_AND_ASSIGN(ProxyResolverV8Tracing); +}; + +} // namespace net + +#endif // NET_PROXY_PROXY_RESOLVER_V8_TRACING_H_ diff --git a/chromium/net/proxy/proxy_resolver_v8_tracing_unittest.cc b/chromium/net/proxy/proxy_resolver_v8_tracing_unittest.cc new file mode 100644 index 00000000000..e597402c89c --- /dev/null +++ b/chromium/net/proxy/proxy_resolver_v8_tracing_unittest.cc @@ -0,0 +1,1098 @@ +// Copyright (c) 2013 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/proxy/proxy_resolver_v8_tracing.h" + +#include "base/file_util.h" +#include "base/message_loop/message_loop.h" +#include "base/path_service.h" +#include "base/stl_util.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "base/synchronization/waitable_event.h" +#include "base/threading/platform_thread.h" +#include "base/values.h" +#include "net/base/net_errors.h" +#include "net/base/net_log.h" +#include "net/base/net_log_unittest.h" +#include "net/base/test_completion_callback.h" +#include "net/dns/host_cache.h" +#include "net/dns/mock_host_resolver.h" +#include "net/proxy/proxy_info.h" +#include "net/proxy/proxy_resolver_error_observer.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace net { + +namespace { + +class ProxyResolverV8TracingTest : public testing::Test { + public: + virtual void TearDown() OVERRIDE { + // Drain any pending messages, which may be left over from cancellation. + // This way they get reliably run as part of the current test, rather than + // spilling into the next test's execution. + base::MessageLoop::current()->RunUntilIdle(); + } +}; + +scoped_refptr<ProxyResolverScriptData> LoadScriptData(const char* filename) { + base::FilePath path; + PathService::Get(base::DIR_SOURCE_ROOT, &path); + path = path.AppendASCII("net"); + path = path.AppendASCII("data"); + path = path.AppendASCII("proxy_resolver_v8_tracing_unittest"); + path = path.AppendASCII(filename); + + // Try to read the file from disk. + std::string file_contents; + bool ok = file_util::ReadFileToString(path, &file_contents); + + // If we can't load the file from disk, something is misconfigured. + EXPECT_TRUE(ok) << "Failed to read file: " << path.value(); + + // Load the PAC script into the ProxyResolver. + return ProxyResolverScriptData::FromUTF8(file_contents); +} + +void InitResolver(ProxyResolverV8Tracing* resolver, const char* filename) { + TestCompletionCallback callback; + int rv = + resolver->SetPacScript(LoadScriptData(filename), callback.callback()); + EXPECT_EQ(ERR_IO_PENDING, rv); + EXPECT_EQ(OK, callback.WaitForResult()); +} + +class MockErrorObserver : public ProxyResolverErrorObserver { + public: + MockErrorObserver() : event_(true, false) {} + + virtual void OnPACScriptError(int line_number, + const base::string16& error) OVERRIDE { + { + base::AutoLock l(lock_); + output += base::StringPrintf("Error: line %d: %s\n", line_number, + UTF16ToASCII(error).c_str()); + } + event_.Signal(); + } + + std::string GetOutput() { + base::AutoLock l(lock_); + return output; + } + + void WaitForOutput() { + event_.Wait(); + } + + private: + base::Lock lock_; + std::string output; + + base::WaitableEvent event_; +}; + +TEST_F(ProxyResolverV8TracingTest, Simple) { + CapturingNetLog log; + CapturingBoundNetLog request_log; + MockCachingHostResolver host_resolver; + MockErrorObserver* error_observer = new MockErrorObserver; + ProxyResolverV8Tracing resolver(&host_resolver, error_observer, &log); + + InitResolver(&resolver, "simple.js"); + + TestCompletionCallback callback; + ProxyInfo proxy_info; + + int rv = resolver.GetProxyForURL( + GURL("http://foo/"), &proxy_info, callback.callback(), + NULL, request_log.bound()); + + EXPECT_EQ(ERR_IO_PENDING, rv); + EXPECT_EQ(OK, callback.WaitForResult()); + + EXPECT_EQ("foo:99", proxy_info.proxy_server().ToURI()); + + EXPECT_EQ(0u, host_resolver.num_resolve()); + + // There were no errors. + EXPECT_EQ("", error_observer->GetOutput()); + + // Check the NetLogs -- nothing was logged. + EXPECT_EQ(0u, log.GetSize()); + EXPECT_EQ(0u, request_log.GetSize()); +} + +TEST_F(ProxyResolverV8TracingTest, JavascriptError) { + CapturingNetLog log; + CapturingBoundNetLog request_log; + MockCachingHostResolver host_resolver; + MockErrorObserver* error_observer = new MockErrorObserver; + ProxyResolverV8Tracing resolver(&host_resolver, error_observer, &log); + + InitResolver(&resolver, "error.js"); + + TestCompletionCallback callback; + ProxyInfo proxy_info; + + int rv = resolver.GetProxyForURL( + GURL("http://throw-an-error/"), &proxy_info, callback.callback(), NULL, + request_log.bound()); + + EXPECT_EQ(ERR_IO_PENDING, rv); + EXPECT_EQ(ERR_PAC_SCRIPT_FAILED, callback.WaitForResult()); + + EXPECT_EQ(0u, host_resolver.num_resolve()); + + EXPECT_EQ("Error: line 5: Uncaught TypeError: Cannot call method 'split' " + "of null\n", error_observer->GetOutput()); + + // Check the NetLogs -- there was 1 alert and 1 javascript error, and they + // were output to both the global log, and per-request log. + CapturingNetLog::CapturedEntryList entries_list[2]; + log.GetEntries(&entries_list[0]); + request_log.GetEntries(&entries_list[1]); + + for (size_t list_i = 0; list_i < arraysize(entries_list); list_i++) { + const CapturingNetLog::CapturedEntryList& entries = entries_list[list_i]; + EXPECT_EQ(2u, entries.size()); + EXPECT_TRUE( + LogContainsEvent(entries, 0, NetLog::TYPE_PAC_JAVASCRIPT_ALERT, + NetLog::PHASE_NONE)); + EXPECT_TRUE( + LogContainsEvent(entries, 1, NetLog::TYPE_PAC_JAVASCRIPT_ERROR, + NetLog::PHASE_NONE)); + + EXPECT_EQ("{\"message\":\"Prepare to DIE!\"}", entries[0].GetParamsJson()); + EXPECT_EQ("{\"line_number\":5,\"message\":\"Uncaught TypeError: Cannot " + "call method 'split' of null\"}", entries[1].GetParamsJson()); + } +} + +TEST_F(ProxyResolverV8TracingTest, TooManyAlerts) { + CapturingNetLog log; + CapturingBoundNetLog request_log; + MockCachingHostResolver host_resolver; + MockErrorObserver* error_observer = new MockErrorObserver; + ProxyResolverV8Tracing resolver(&host_resolver, error_observer, &log); + + InitResolver(&resolver, "too_many_alerts.js"); + + TestCompletionCallback callback; + ProxyInfo proxy_info; + + int rv = resolver.GetProxyForURL( + GURL("http://foo/"), + &proxy_info, + callback.callback(), + NULL, + request_log.bound()); + + EXPECT_EQ(ERR_IO_PENDING, rv); + EXPECT_EQ(OK, callback.WaitForResult()); + + // Iteration1 does a DNS resolve + // Iteration2 exceeds the alert buffer + // Iteration3 runs in blocking mode and completes + EXPECT_EQ("foo:3", proxy_info.proxy_server().ToURI()); + + EXPECT_EQ(1u, host_resolver.num_resolve()); + + // No errors. + EXPECT_EQ("", error_observer->GetOutput()); + + // Check the NetLogs -- the script generated 50 alerts, which were mirrored + // to both the global and per-request logs. + CapturingNetLog::CapturedEntryList entries_list[2]; + log.GetEntries(&entries_list[0]); + request_log.GetEntries(&entries_list[1]); + + for (size_t list_i = 0; list_i < arraysize(entries_list); list_i++) { + const CapturingNetLog::CapturedEntryList& entries = entries_list[list_i]; + EXPECT_EQ(50u, entries.size()); + for (size_t i = 0; i < entries.size(); ++i) { + ASSERT_TRUE( + LogContainsEvent(entries, i, NetLog::TYPE_PAC_JAVASCRIPT_ALERT, + NetLog::PHASE_NONE)); + } + } +} + +// Verify that buffered alerts cannot grow unboundedly, even when the message is +// empty string. +TEST_F(ProxyResolverV8TracingTest, TooManyEmptyAlerts) { + CapturingNetLog log; + CapturingBoundNetLog request_log; + MockCachingHostResolver host_resolver; + MockErrorObserver* error_observer = new MockErrorObserver; + ProxyResolverV8Tracing resolver(&host_resolver, error_observer, &log); + + InitResolver(&resolver, "too_many_empty_alerts.js"); + + TestCompletionCallback callback; + ProxyInfo proxy_info; + + int rv = resolver.GetProxyForURL( + GURL("http://foo/"), + &proxy_info, + callback.callback(), + NULL, + request_log.bound()); + + EXPECT_EQ(ERR_IO_PENDING, rv); + EXPECT_EQ(OK, callback.WaitForResult()); + + EXPECT_EQ("foo:3", proxy_info.proxy_server().ToURI()); + + EXPECT_EQ(1u, host_resolver.num_resolve()); + + // No errors. + EXPECT_EQ("", error_observer->GetOutput()); + + // Check the NetLogs -- the script generated 50 alerts, which were mirrored + // to both the global and per-request logs. + CapturingNetLog::CapturedEntryList entries_list[2]; + log.GetEntries(&entries_list[0]); + request_log.GetEntries(&entries_list[1]); + + for (size_t list_i = 0; list_i < arraysize(entries_list); list_i++) { + const CapturingNetLog::CapturedEntryList& entries = entries_list[list_i]; + EXPECT_EQ(1000u, entries.size()); + for (size_t i = 0; i < entries.size(); ++i) { + ASSERT_TRUE( + LogContainsEvent(entries, i, NetLog::TYPE_PAC_JAVASCRIPT_ALERT, + NetLog::PHASE_NONE)); + } + } +} + +// This test runs a PAC script that issues a sequence of DNS resolves. The test +// verifies the final result, and that the underlying DNS resolver received +// the correct set of queries. +TEST_F(ProxyResolverV8TracingTest, Dns) { + CapturingNetLog log; + CapturingBoundNetLog request_log; + MockCachingHostResolver host_resolver; + MockErrorObserver* error_observer = new MockErrorObserver; + ProxyResolverV8Tracing resolver(&host_resolver, error_observer, &log); + + host_resolver.rules()->AddRuleForAddressFamily( + "host1", ADDRESS_FAMILY_IPV4, "166.155.144.44"); + host_resolver.rules() + ->AddIPLiteralRule("host1", "::1,192.168.1.1", std::string()); + host_resolver.rules()->AddSimulatedFailure("host2"); + host_resolver.rules()->AddRule("host3", "166.155.144.33"); + host_resolver.rules()->AddRule("host5", "166.155.144.55"); + host_resolver.rules()->AddSimulatedFailure("host6"); + host_resolver.rules()->AddRuleForAddressFamily( + "*", ADDRESS_FAMILY_IPV4, "122.133.144.155"); + host_resolver.rules()->AddRule("*", "133.122.100.200"); + + InitResolver(&resolver, "dns.js"); + + TestCompletionCallback callback; + ProxyInfo proxy_info; + + int rv = resolver.GetProxyForURL( + GURL("http://foo/"), + &proxy_info, + callback.callback(), + NULL, + request_log.bound()); + + EXPECT_EQ(ERR_IO_PENDING, rv); + EXPECT_EQ(OK, callback.WaitForResult()); + + // The test does 13 DNS resolution, however only 7 of them are unique. + EXPECT_EQ(7u, host_resolver.num_resolve()); + + const char* kExpectedResult = + "122.133.144.155-" // myIpAddress() + "null-" // dnsResolve('') + "__1_192.168.1.1-" // dnsResolveEx('host1') + "null-" // dnsResolve('host2') + "166.155.144.33-" // dnsResolve('host3') + "122.133.144.155-" // myIpAddress() + "166.155.144.33-" // dnsResolve('host3') + "__1_192.168.1.1-" // dnsResolveEx('host1') + "122.133.144.155-" // myIpAddress() + "null-" // dnsResolve('host2') + "-" // dnsResolveEx('host6') + "133.122.100.200-" // myIpAddressEx() + "166.155.144.44" // dnsResolve('host1') + ":99"; + + EXPECT_EQ(kExpectedResult, proxy_info.proxy_server().ToURI()); + + // No errors. + EXPECT_EQ("", error_observer->GetOutput()); + + // Check the NetLogs -- the script generated 1 alert, mirrored to both + // the per-request and global logs. + CapturingNetLog::CapturedEntryList entries_list[2]; + log.GetEntries(&entries_list[0]); + request_log.GetEntries(&entries_list[1]); + + for (size_t list_i = 0; list_i < arraysize(entries_list); list_i++) { + const CapturingNetLog::CapturedEntryList& entries = entries_list[list_i]; + EXPECT_EQ(1u, entries.size()); + EXPECT_TRUE( + LogContainsEvent(entries, 0, NetLog::TYPE_PAC_JAVASCRIPT_ALERT, + NetLog::PHASE_NONE)); + EXPECT_EQ("{\"message\":\"iteration: 7\"}", entries[0].GetParamsJson()); + } +} + +// This test runs a PAC script that does "myIpAddress()" followed by +// "dnsResolve()". This requires 2 restarts. However once the HostResolver's +// cache is warmed, subsequent calls should take 0 restarts. +TEST_F(ProxyResolverV8TracingTest, DnsChecksCache) { + CapturingNetLog log; + CapturingBoundNetLog request_log; + MockCachingHostResolver host_resolver; + MockErrorObserver* error_observer = new MockErrorObserver; + ProxyResolverV8Tracing resolver(&host_resolver, error_observer, &log); + + host_resolver.rules()->AddRule("foopy", "166.155.144.11"); + host_resolver.rules()->AddRule("*", "122.133.144.155"); + + InitResolver(&resolver, "simple_dns.js"); + + TestCompletionCallback callback1; + TestCompletionCallback callback2; + ProxyInfo proxy_info; + + int rv = resolver.GetProxyForURL( + GURL("http://foopy/req1"), + &proxy_info, + callback1.callback(), + NULL, + request_log.bound()); + + EXPECT_EQ(ERR_IO_PENDING, rv); + EXPECT_EQ(OK, callback1.WaitForResult()); + + // The test does 2 DNS resolutions. + EXPECT_EQ(2u, host_resolver.num_resolve()); + + // The first request took 2 restarts, hence on g_iteration=3. + EXPECT_EQ("166.155.144.11:3", proxy_info.proxy_server().ToURI()); + + rv = resolver.GetProxyForURL( + GURL("http://foopy/req2"), + &proxy_info, + callback2.callback(), + NULL, + request_log.bound()); + + EXPECT_EQ(ERR_IO_PENDING, rv); + EXPECT_EQ(OK, callback2.WaitForResult()); + + EXPECT_EQ(4u, host_resolver.num_resolve()); + + // This time no restarts were required, so g_iteration incremented by 1. + EXPECT_EQ("166.155.144.11:4", proxy_info.proxy_server().ToURI()); + + // No errors. + EXPECT_EQ("", error_observer->GetOutput()); + + EXPECT_EQ(0u, log.GetSize()); + EXPECT_EQ(0u, request_log.GetSize()); +} + +// This test runs a weird PAC script that was designed to defeat the DNS tracing +// optimization. The proxy resolver should detect the inconsistency and +// fall-back to synchronous mode execution. +TEST_F(ProxyResolverV8TracingTest, FallBackToSynchronous1) { + CapturingNetLog log; + CapturingBoundNetLog request_log; + MockCachingHostResolver host_resolver; + MockErrorObserver* error_observer = new MockErrorObserver; + ProxyResolverV8Tracing resolver(&host_resolver, error_observer, &log); + + host_resolver.rules()->AddRule("host1", "166.155.144.11"); + host_resolver.rules()->AddRule("crazy4", "133.199.111.4"); + host_resolver.rules()->AddRule("*", "122.133.144.155"); + + InitResolver(&resolver, "global_sideffects1.js"); + + TestCompletionCallback callback; + ProxyInfo proxy_info; + + int rv = resolver.GetProxyForURL( + GURL("http://foo/"), &proxy_info, callback.callback(), NULL, + request_log.bound()); + EXPECT_EQ(ERR_IO_PENDING, rv); + EXPECT_EQ(OK, callback.WaitForResult()); + + // The script itself only does 2 DNS resolves per execution, however it + // constructs the hostname using a global counter which changes on each + // invocation. + EXPECT_EQ(3u, host_resolver.num_resolve()); + + EXPECT_EQ("166.155.144.11-133.199.111.4:100", + proxy_info.proxy_server().ToURI()); + + // No errors. + EXPECT_EQ("", error_observer->GetOutput()); + + // Check the NetLogs -- the script generated 1 alert, mirrored to both + // the per-request and global logs. + CapturingNetLog::CapturedEntryList entries_list[2]; + log.GetEntries(&entries_list[0]); + request_log.GetEntries(&entries_list[1]); + + for (size_t list_i = 0; list_i < arraysize(entries_list); list_i++) { + const CapturingNetLog::CapturedEntryList& entries = entries_list[list_i]; + EXPECT_EQ(1u, entries.size()); + EXPECT_TRUE( + LogContainsEvent(entries, 0, NetLog::TYPE_PAC_JAVASCRIPT_ALERT, + NetLog::PHASE_NONE)); + EXPECT_EQ("{\"message\":\"iteration: 4\"}", entries[0].GetParamsJson()); + } +} + +// This test runs a weird PAC script that was designed to defeat the DNS tracing +// optimization. The proxy resolver should detect the inconsistency and +// fall-back to synchronous mode execution. +TEST_F(ProxyResolverV8TracingTest, FallBackToSynchronous2) { + CapturingNetLog log; + CapturingBoundNetLog request_log; + MockCachingHostResolver host_resolver; + MockErrorObserver* error_observer = new MockErrorObserver; + ProxyResolverV8Tracing resolver(&host_resolver, error_observer, &log); + + host_resolver.rules()->AddRule("host1", "166.155.144.11"); + host_resolver.rules()->AddRule("host2", "166.155.144.22"); + host_resolver.rules()->AddRule("host3", "166.155.144.33"); + host_resolver.rules()->AddRule("host4", "166.155.144.44"); + host_resolver.rules()->AddRule("*", "122.133.144.155"); + + InitResolver(&resolver, "global_sideffects2.js"); + + TestCompletionCallback callback; + ProxyInfo proxy_info; + + int rv = resolver.GetProxyForURL( + GURL("http://foo/"), &proxy_info, callback.callback(), NULL, + request_log.bound()); + EXPECT_EQ(ERR_IO_PENDING, rv); + EXPECT_EQ(OK, callback.WaitForResult()); + + EXPECT_EQ(3u, host_resolver.num_resolve()); + + EXPECT_EQ("166.155.144.44:100", proxy_info.proxy_server().ToURI()); + + // No errors. + EXPECT_EQ("", error_observer->GetOutput()); + + // Check the NetLogs -- nothing was logged. + EXPECT_EQ(0u, log.GetSize()); + EXPECT_EQ(0u, request_log.GetSize()); +} + +// This test runs a weird PAC script that yields a never ending sequence +// of DNS resolves when restarting. Running it will hit the maximum +// DNS resolves per request limit (20) after which every DNS resolve will +// fail. +TEST_F(ProxyResolverV8TracingTest, InfiniteDNSSequence) { + CapturingNetLog log; + CapturingBoundNetLog request_log; + MockCachingHostResolver host_resolver; + MockErrorObserver* error_observer = new MockErrorObserver; + ProxyResolverV8Tracing resolver(&host_resolver, error_observer, &log); + + host_resolver.rules()->AddRule("host*", "166.155.144.11"); + host_resolver.rules()->AddRule("*", "122.133.144.155"); + + InitResolver(&resolver, "global_sideffects3.js"); + + TestCompletionCallback callback; + ProxyInfo proxy_info; + + int rv = resolver.GetProxyForURL( + GURL("http://foo/"), &proxy_info, callback.callback(), NULL, + request_log.bound()); + EXPECT_EQ(ERR_IO_PENDING, rv); + EXPECT_EQ(OK, callback.WaitForResult()); + + EXPECT_EQ(20u, host_resolver.num_resolve()); + + EXPECT_EQ( + "166.155.144.11-166.155.144.11-166.155.144.11-166.155.144.11-" + "166.155.144.11-166.155.144.11-166.155.144.11-166.155.144.11-" + "166.155.144.11-166.155.144.11-166.155.144.11-166.155.144.11-" + "166.155.144.11-166.155.144.11-166.155.144.11-166.155.144.11-" + "166.155.144.11-166.155.144.11-166.155.144.11-166.155.144.11-" + "null:21", proxy_info.proxy_server().ToURI()); + + // No errors. + EXPECT_EQ("", error_observer->GetOutput()); + + // Check the NetLogs -- 1 alert was logged. + EXPECT_EQ(1u, log.GetSize()); + EXPECT_EQ(1u, request_log.GetSize()); +} + +// This test runs a weird PAC script that yields a never ending sequence +// of DNS resolves when restarting. Running it will hit the maximum +// DNS resolves per request limit (20) after which every DNS resolve will +// fail. +TEST_F(ProxyResolverV8TracingTest, InfiniteDNSSequence2) { + CapturingNetLog log; + CapturingBoundNetLog request_log; + MockCachingHostResolver host_resolver; + MockErrorObserver* error_observer = new MockErrorObserver; + ProxyResolverV8Tracing resolver(&host_resolver, error_observer, &log); + + host_resolver.rules()->AddRule("host*", "166.155.144.11"); + host_resolver.rules()->AddRule("*", "122.133.144.155"); + + InitResolver(&resolver, "global_sideffects4.js"); + + TestCompletionCallback callback; + ProxyInfo proxy_info; + + int rv = resolver.GetProxyForURL( + GURL("http://foo/"), &proxy_info, callback.callback(), NULL, + request_log.bound()); + EXPECT_EQ(ERR_IO_PENDING, rv); + EXPECT_EQ(OK, callback.WaitForResult()); + + EXPECT_EQ(20u, host_resolver.num_resolve()); + + EXPECT_EQ("null21:34", proxy_info.proxy_server().ToURI()); + + // No errors. + EXPECT_EQ("", error_observer->GetOutput()); + + // Check the NetLogs -- 1 alert was logged. + EXPECT_EQ(1u, log.GetSize()); + EXPECT_EQ(1u, request_log.GetSize()); +} + +void DnsDuringInitHelper(bool synchronous_host_resolver) { + CapturingNetLog log; + CapturingBoundNetLog request_log; + MockCachingHostResolver host_resolver; + host_resolver.set_synchronous_mode(synchronous_host_resolver); + MockErrorObserver* error_observer = new MockErrorObserver; + ProxyResolverV8Tracing resolver(&host_resolver, error_observer, &log); + + host_resolver.rules()->AddRule("host1", "91.13.12.1"); + host_resolver.rules()->AddRule("host2", "91.13.12.2"); + + InitResolver(&resolver, "dns_during_init.js"); + + // Initialization did 2 dnsResolves. + EXPECT_EQ(2u, host_resolver.num_resolve()); + + host_resolver.rules()->ClearRules(); + host_resolver.GetHostCache()->clear(); + + host_resolver.rules()->AddRule("host1", "145.88.13.3"); + host_resolver.rules()->AddRule("host2", "137.89.8.45"); + + TestCompletionCallback callback; + ProxyInfo proxy_info; + + int rv = resolver.GetProxyForURL( + GURL("http://foo/"), &proxy_info, callback.callback(), NULL, + request_log.bound()); + EXPECT_EQ(ERR_IO_PENDING, rv); + EXPECT_EQ(OK, callback.WaitForResult()); + + // Fetched host1 and host2 again, since the ones done during initialization + // should not have been cached. + EXPECT_EQ(4u, host_resolver.num_resolve()); + + EXPECT_EQ("91.13.12.1-91.13.12.2-145.88.13.3-137.89.8.45:99", + proxy_info.proxy_server().ToURI()); + + // Check the NetLogs -- the script generated 2 alerts during initialization. + EXPECT_EQ(0u, request_log.GetSize()); + CapturingNetLog::CapturedEntryList entries; + log.GetEntries(&entries); + + ASSERT_EQ(2u, entries.size()); + EXPECT_TRUE( + LogContainsEvent(entries, 0, NetLog::TYPE_PAC_JAVASCRIPT_ALERT, + NetLog::PHASE_NONE)); + EXPECT_TRUE( + LogContainsEvent(entries, 1, NetLog::TYPE_PAC_JAVASCRIPT_ALERT, + NetLog::PHASE_NONE)); + + EXPECT_EQ("{\"message\":\"Watsup\"}", entries[0].GetParamsJson()); + EXPECT_EQ("{\"message\":\"Watsup2\"}", entries[1].GetParamsJson()); +} + +// Tests a PAC script which does DNS resolves during initialization. +TEST_F(ProxyResolverV8TracingTest, DnsDuringInit) { + // Test with both both a host resolver that always completes asynchronously, + // and then again with one that completes synchronously. + DnsDuringInitHelper(false); + DnsDuringInitHelper(true); +} + +void CrashCallback(int) { + // Be extra sure that if the callback ever gets invoked, the test will fail. + CHECK(false); +} + +// Start some requests, cancel them all, and then destroy the resolver. +// Note the execution order for this test can vary. Since multiple +// threads are involved, the cancellation may be received a different +// times. +TEST_F(ProxyResolverV8TracingTest, CancelAll) { + MockCachingHostResolver host_resolver; + MockErrorObserver* error_observer = new MockErrorObserver; + ProxyResolverV8Tracing resolver(&host_resolver, error_observer, NULL); + + host_resolver.rules()->AddSimulatedFailure("*"); + + InitResolver(&resolver, "dns.js"); + + const size_t kNumRequests = 5; + ProxyInfo proxy_info[kNumRequests]; + ProxyResolver::RequestHandle request[kNumRequests]; + + for (size_t i = 0; i < kNumRequests; ++i) { + int rv = resolver.GetProxyForURL( + GURL("http://foo/"), &proxy_info[i], + base::Bind(&CrashCallback), &request[i], BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + } + + for (size_t i = 0; i < kNumRequests; ++i) { + resolver.CancelRequest(request[i]); + } +} + +// Note the execution order for this test can vary. Since multiple +// threads are involved, the cancellation may be received a different +// times. +TEST_F(ProxyResolverV8TracingTest, CancelSome) { + MockCachingHostResolver host_resolver; + MockErrorObserver* error_observer = new MockErrorObserver; + ProxyResolverV8Tracing resolver(&host_resolver, error_observer, NULL); + + host_resolver.rules()->AddSimulatedFailure("*"); + + InitResolver(&resolver, "dns.js"); + + ProxyInfo proxy_info1; + ProxyInfo proxy_info2; + ProxyResolver::RequestHandle request1; + ProxyResolver::RequestHandle request2; + TestCompletionCallback callback; + + int rv = resolver.GetProxyForURL( + GURL("http://foo/"), &proxy_info1, + base::Bind(&CrashCallback), &request1, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + rv = resolver.GetProxyForURL( + GURL("http://foo/"), &proxy_info2, + callback.callback(), &request2, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + resolver.CancelRequest(request1); + + EXPECT_EQ(OK, callback.WaitForResult()); +} + +// Cancel a request after it has finished running on the worker thread, and has +// posted a task the completion task back to origin thread. +TEST_F(ProxyResolverV8TracingTest, CancelWhilePendingCompletionTask) { + MockCachingHostResolver host_resolver; + MockErrorObserver* error_observer = new MockErrorObserver; + ProxyResolverV8Tracing resolver(&host_resolver, error_observer, NULL); + + host_resolver.rules()->AddSimulatedFailure("*"); + + InitResolver(&resolver, "error.js"); + + ProxyInfo proxy_info1; + ProxyInfo proxy_info2; + ProxyInfo proxy_info3; + ProxyResolver::RequestHandle request1; + ProxyResolver::RequestHandle request2; + ProxyResolver::RequestHandle request3; + TestCompletionCallback callback; + + int rv = resolver.GetProxyForURL( + GURL("http://foo/"), &proxy_info1, + base::Bind(&CrashCallback), &request1, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + rv = resolver.GetProxyForURL( + GURL("http://throw-an-error/"), &proxy_info2, + callback.callback(), &request2, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Wait until the first request has finished running on the worker thread. + // (The second request will output an error). + error_observer->WaitForOutput(); + + // Cancel the first request, while it has a pending completion task on + // the origin thread. + resolver.CancelRequest(request1); + + EXPECT_EQ(ERR_PAC_SCRIPT_FAILED, callback.WaitForResult()); + + // Start another request, to make sure it is able to complete. + rv = resolver.GetProxyForURL( + GURL("http://i-have-no-idea-what-im-doing/"), &proxy_info3, + callback.callback(), &request3, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + EXPECT_EQ(OK, callback.WaitForResult()); + + EXPECT_EQ("i-approve-this-message:42", + proxy_info3.proxy_server().ToURI()); +} + +// This implementation of HostResolver allows blocking until a resolve request +// has been received. The resolve requests it receives will never be completed. +class BlockableHostResolver : public HostResolver { + public: + BlockableHostResolver() + : num_cancelled_requests_(0), waiting_for_resolve_(false) {} + + virtual int Resolve(const RequestInfo& info, + AddressList* addresses, + const CompletionCallback& callback, + RequestHandle* out_req, + const BoundNetLog& net_log) OVERRIDE { + EXPECT_FALSE(callback.is_null()); + EXPECT_TRUE(out_req); + + if (!action_.is_null()) + action_.Run(); + + // Indicate to the caller that a request was received. + EXPECT_TRUE(waiting_for_resolve_); + base::MessageLoop::current()->Quit(); + + // This line is intentionally after action_.Run(), since one of the + // tests does a cancellation inside of Resolve(), and it is more + // interesting if *out_req hasn't been written yet at that point. + *out_req = reinterpret_cast<RequestHandle*>(1); // Magic value. + + // Return ERR_IO_PENDING as this request will NEVER be completed. + // Expectation is for the caller to later cancel the request. + return ERR_IO_PENDING; + } + + virtual int ResolveFromCache(const RequestInfo& info, + AddressList* addresses, + const BoundNetLog& net_log) OVERRIDE { + NOTREACHED(); + return ERR_DNS_CACHE_MISS; + } + + virtual void CancelRequest(RequestHandle req) OVERRIDE { + EXPECT_EQ(reinterpret_cast<RequestHandle*>(1), req); + num_cancelled_requests_++; + } + + void SetAction(const base::Callback<void(void)>& action) { + action_ = action; + } + + // Waits until Resolve() has been called. + void WaitUntilRequestIsReceived() { + waiting_for_resolve_ = true; + base::MessageLoop::current()->Run(); + DCHECK(waiting_for_resolve_); + waiting_for_resolve_ = false; + } + + int num_cancelled_requests() const { + return num_cancelled_requests_; + } + + private: + int num_cancelled_requests_; + bool waiting_for_resolve_; + base::Callback<void(void)> action_; +}; + +// This cancellation test exercises a more predictable cancellation codepath -- +// when the request has an outstanding DNS request in flight. +TEST_F(ProxyResolverV8TracingTest, CancelWhileOutstandingNonBlockingDns) { + BlockableHostResolver host_resolver; + MockErrorObserver* error_observer = new MockErrorObserver; + ProxyResolverV8Tracing resolver(&host_resolver, error_observer, NULL); + + InitResolver(&resolver, "dns.js"); + + ProxyInfo proxy_info1; + ProxyInfo proxy_info2; + ProxyResolver::RequestHandle request1; + ProxyResolver::RequestHandle request2; + + int rv = resolver.GetProxyForURL( + GURL("http://foo/req1"), &proxy_info1, + base::Bind(&CrashCallback), &request1, BoundNetLog()); + + EXPECT_EQ(ERR_IO_PENDING, rv); + + host_resolver.WaitUntilRequestIsReceived(); + + rv = resolver.GetProxyForURL( + GURL("http://foo/req2"), &proxy_info2, + base::Bind(&CrashCallback), &request2, BoundNetLog()); + + EXPECT_EQ(ERR_IO_PENDING, rv); + + host_resolver.WaitUntilRequestIsReceived(); + + resolver.CancelRequest(request1); + resolver.CancelRequest(request2); + + EXPECT_EQ(2, host_resolver.num_cancelled_requests()); + + // After leaving this scope, the ProxyResolver is destroyed. + // This should not cause any problems, as the outstanding work + // should have been cancelled. +} + +void CancelRequestAndPause(ProxyResolverV8Tracing* resolver, + ProxyResolver::RequestHandle request) { + resolver->CancelRequest(request); + + // Sleep for a little bit. This makes it more likely for the worker + // thread to have returned from its call, and serves as a regression + // test for http://crbug.com/173373. + base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(30)); +} + +// In non-blocking mode, the worker thread actually does block for +// a short time to see if the result is in the DNS cache. Test +// cancellation while the worker thread is waiting on this event. +TEST_F(ProxyResolverV8TracingTest, CancelWhileBlockedInNonBlockingDns) { + BlockableHostResolver host_resolver; + MockErrorObserver* error_observer = new MockErrorObserver; + ProxyResolverV8Tracing resolver(&host_resolver, error_observer, NULL); + + InitResolver(&resolver, "dns.js"); + + ProxyInfo proxy_info; + ProxyResolver::RequestHandle request; + + int rv = resolver.GetProxyForURL( + GURL("http://foo/"), &proxy_info, + base::Bind(&CrashCallback), &request, BoundNetLog()); + + EXPECT_EQ(ERR_IO_PENDING, rv); + + host_resolver.SetAction( + base::Bind(CancelRequestAndPause, &resolver, request)); + + host_resolver.WaitUntilRequestIsReceived(); + + // At this point the host resolver ran Resolve(), and should have cancelled + // the request. + + EXPECT_EQ(1, host_resolver.num_cancelled_requests()); +} + +// Cancel the request while there is a pending DNS request, however before +// the request is sent to the host resolver. +TEST_F(ProxyResolverV8TracingTest, CancelWhileBlockedInNonBlockingDns2) { + MockCachingHostResolver host_resolver; + MockErrorObserver* error_observer = new MockErrorObserver; + ProxyResolverV8Tracing resolver(&host_resolver, error_observer, NULL); + + InitResolver(&resolver, "dns.js"); + + ProxyInfo proxy_info; + ProxyResolver::RequestHandle request; + + int rv = resolver.GetProxyForURL( + GURL("http://foo/"), &proxy_info, + base::Bind(&CrashCallback), &request, BoundNetLog()); + + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Wait a bit, so the DNS task has hopefully been posted. The test will + // work whatever the delay is here, but it is most useful if the delay + // is large enough to allow a task to be posted back. + base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(10)); + resolver.CancelRequest(request); + + EXPECT_EQ(0u, host_resolver.num_resolve()); +} + +TEST_F(ProxyResolverV8TracingTest, CancelSetPacWhileOutstandingBlockingDns) { + BlockableHostResolver host_resolver; + MockErrorObserver* error_observer = new MockErrorObserver; + + ProxyResolverV8Tracing resolver(&host_resolver, error_observer, NULL); + + int rv = + resolver.SetPacScript(LoadScriptData("dns_during_init.js"), + base::Bind(&CrashCallback)); + EXPECT_EQ(ERR_IO_PENDING, rv); + + host_resolver.WaitUntilRequestIsReceived(); + + resolver.CancelSetPacScript(); + EXPECT_EQ(1, host_resolver.num_cancelled_requests()); +} + +// This tests that the execution of a PAC script is terminated when the DNS +// dependencies are missing. If the test fails, then it will hang. +TEST_F(ProxyResolverV8TracingTest, Terminate) { + CapturingNetLog log; + CapturingBoundNetLog request_log; + MockCachingHostResolver host_resolver; + MockErrorObserver* error_observer = new MockErrorObserver; + ProxyResolverV8Tracing resolver(&host_resolver, error_observer, &log); + + host_resolver.rules()->AddRule("host1", "182.111.0.222"); + host_resolver.rules()->AddRule("host2", "111.33.44.55"); + + InitResolver(&resolver, "terminate.js"); + + TestCompletionCallback callback; + ProxyInfo proxy_info; + + int rv = resolver.GetProxyForURL( + GURL("http://foopy/req1"), + &proxy_info, + callback.callback(), + NULL, + request_log.bound()); + + EXPECT_EQ(ERR_IO_PENDING, rv); + EXPECT_EQ(OK, callback.WaitForResult()); + + // The test does 2 DNS resolutions. + EXPECT_EQ(2u, host_resolver.num_resolve()); + + EXPECT_EQ("foopy:3", proxy_info.proxy_server().ToURI()); + + // No errors. + EXPECT_EQ("", error_observer->GetOutput()); + + EXPECT_EQ(0u, log.GetSize()); + EXPECT_EQ(0u, request_log.GetSize()); +} + +// Tests that multiple instances of ProxyResolverV8Tracing can coexist and run +// correctly at the same time. This is relevant because at the moment (time +// this test was written) each ProxyResolverV8Tracing creates its own thread to +// run V8 on, however each thread is operating on the same v8::Isolate. +TEST_F(ProxyResolverV8TracingTest, MultipleResolvers) { + // ------------------------ + // Setup resolver0 + // ------------------------ + MockHostResolver host_resolver0; + host_resolver0.rules()->AddRuleForAddressFamily( + "host1", ADDRESS_FAMILY_IPV4, "166.155.144.44"); + host_resolver0.rules() + ->AddIPLiteralRule("host1", "::1,192.168.1.1", std::string()); + host_resolver0.rules()->AddSimulatedFailure("host2"); + host_resolver0.rules()->AddRule("host3", "166.155.144.33"); + host_resolver0.rules()->AddRule("host5", "166.155.144.55"); + host_resolver0.rules()->AddSimulatedFailure("host6"); + host_resolver0.rules()->AddRuleForAddressFamily( + "*", ADDRESS_FAMILY_IPV4, "122.133.144.155"); + host_resolver0.rules()->AddRule("*", "133.122.100.200"); + ProxyResolverV8Tracing resolver0( + &host_resolver0, new MockErrorObserver, NULL); + InitResolver(&resolver0, "dns.js"); + + // ------------------------ + // Setup resolver1 + // ------------------------ + ProxyResolverV8Tracing resolver1( + &host_resolver0, new MockErrorObserver, NULL); + InitResolver(&resolver1, "dns.js"); + + // ------------------------ + // Setup resolver2 + // ------------------------ + ProxyResolverV8Tracing resolver2( + &host_resolver0, new MockErrorObserver, NULL); + InitResolver(&resolver2, "simple.js"); + + // ------------------------ + // Setup resolver3 + // ------------------------ + MockHostResolver host_resolver3; + host_resolver3.rules()->AddRule("foo", "166.155.144.33"); + ProxyResolverV8Tracing resolver3( + &host_resolver3, new MockErrorObserver, NULL); + InitResolver(&resolver3, "simple_dns.js"); + + // ------------------------ + // Queue up work for each resolver (which will be running in parallel). + // ------------------------ + + ProxyResolverV8Tracing* resolver[] = { + &resolver0, &resolver1, &resolver2, &resolver3, + }; + + const size_t kNumResolvers = arraysize(resolver); + const size_t kNumIterations = 20; + const size_t kNumResults = kNumResolvers * kNumIterations; + TestCompletionCallback callback[kNumResults]; + ProxyInfo proxy_info[kNumResults]; + + for (size_t i = 0; i < kNumResults; ++i) { + size_t resolver_i = i % kNumResolvers; + int rv = resolver[resolver_i]->GetProxyForURL( + GURL("http://foo/"), &proxy_info[i], callback[i].callback(), NULL, + BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + } + + // ------------------------ + // Verify all of the results. + // ------------------------ + + const char* kExpectedForDnsJs = + "122.133.144.155-" // myIpAddress() + "null-" // dnsResolve('') + "__1_192.168.1.1-" // dnsResolveEx('host1') + "null-" // dnsResolve('host2') + "166.155.144.33-" // dnsResolve('host3') + "122.133.144.155-" // myIpAddress() + "166.155.144.33-" // dnsResolve('host3') + "__1_192.168.1.1-" // dnsResolveEx('host1') + "122.133.144.155-" // myIpAddress() + "null-" // dnsResolve('host2') + "-" // dnsResolveEx('host6') + "133.122.100.200-" // myIpAddressEx() + "166.155.144.44" // dnsResolve('host1') + ":99"; + + for (size_t i = 0; i < kNumResults; ++i) { + size_t resolver_i = i % kNumResolvers; + EXPECT_EQ(OK, callback[i].WaitForResult()); + + std::string proxy_uri = proxy_info[i].proxy_server().ToURI(); + + if (resolver_i == 0 || resolver_i == 1) { + EXPECT_EQ(kExpectedForDnsJs, proxy_uri); + } else if (resolver_i == 2) { + EXPECT_EQ("foo:99", proxy_uri); + } else if (resolver_i == 3) { + EXPECT_EQ("166.155.144.33:", + proxy_uri.substr(0, proxy_uri.find(':') + 1)); + } else { + NOTREACHED(); + } + } +} + +} // namespace + +} // namespace net diff --git a/chromium/net/proxy/proxy_resolver_v8_unittest.cc b/chromium/net/proxy/proxy_resolver_v8_unittest.cc new file mode 100644 index 00000000000..67e77397a8c --- /dev/null +++ b/chromium/net/proxy/proxy_resolver_v8_unittest.cc @@ -0,0 +1,631 @@ +// 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 "base/compiler_specific.h" +#include "base/file_util.h" +#include "base/path_service.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "net/base/net_errors.h" +#include "net/base/net_log_unittest.h" +#include "net/proxy/proxy_info.h" +#include "net/proxy/proxy_resolver_v8.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace net { +namespace { + +// Javascript bindings for ProxyResolverV8, which returns mock values. +// Each time one of the bindings is called into, we push the input into a +// list, for later verification. +class MockJSBindings : public ProxyResolverV8::JSBindings { + public: + MockJSBindings() : my_ip_address_count(0), my_ip_address_ex_count(0), + should_terminate(false) {} + + virtual void Alert(const base::string16& message) OVERRIDE { + VLOG(1) << "PAC-alert: " << message; // Helpful when debugging. + alerts.push_back(UTF16ToUTF8(message)); + } + + virtual bool ResolveDns(const std::string& host, + ResolveDnsOperation op, + std::string* output, + bool* terminate) OVERRIDE { + *terminate = should_terminate; + + if (op == MY_IP_ADDRESS) { + my_ip_address_count++; + *output = my_ip_address_result; + return !my_ip_address_result.empty(); + } + + if (op == MY_IP_ADDRESS_EX) { + my_ip_address_ex_count++; + *output = my_ip_address_ex_result; + return !my_ip_address_ex_result.empty(); + } + + if (op == DNS_RESOLVE) { + dns_resolves.push_back(host); + *output = dns_resolve_result; + return !dns_resolve_result.empty(); + } + + if (op == DNS_RESOLVE_EX) { + dns_resolves_ex.push_back(host); + *output = dns_resolve_ex_result; + return !dns_resolve_ex_result.empty(); + } + + CHECK(false); + return false; + } + + virtual void OnError(int line_number, + const base::string16& message) OVERRIDE { + // Helpful when debugging. + VLOG(1) << "PAC-error: [" << line_number << "] " << message; + + errors.push_back(UTF16ToUTF8(message)); + errors_line_number.push_back(line_number); + } + + // Mock values to return. + std::string my_ip_address_result; + std::string my_ip_address_ex_result; + std::string dns_resolve_result; + std::string dns_resolve_ex_result; + + // Inputs we got called with. + std::vector<std::string> alerts; + std::vector<std::string> errors; + std::vector<int> errors_line_number; + std::vector<std::string> dns_resolves; + std::vector<std::string> dns_resolves_ex; + int my_ip_address_count; + int my_ip_address_ex_count; + + // Whether ResolveDns() should terminate script execution. + bool should_terminate; +}; + +// This is the same as ProxyResolverV8, but it uses mock bindings in place of +// the default bindings, and has a helper function to load PAC scripts from +// disk. +class ProxyResolverV8WithMockBindings : public ProxyResolverV8 { + public: + ProxyResolverV8WithMockBindings() { + set_js_bindings(&mock_js_bindings_); + } + + MockJSBindings* mock_js_bindings() { + return &mock_js_bindings_; + } + + // Initialize with the PAC script data at |filename|. + int SetPacScriptFromDisk(const char* filename) { + base::FilePath path; + PathService::Get(base::DIR_SOURCE_ROOT, &path); + path = path.AppendASCII("net"); + path = path.AppendASCII("data"); + path = path.AppendASCII("proxy_resolver_v8_unittest"); + path = path.AppendASCII(filename); + + // Try to read the file from disk. + std::string file_contents; + bool ok = file_util::ReadFileToString(path, &file_contents); + + // If we can't load the file from disk, something is misconfigured. + if (!ok) { + LOG(ERROR) << "Failed to read file: " << path.value(); + return ERR_UNEXPECTED; + } + + // Load the PAC script into the ProxyResolver. + return SetPacScript(ProxyResolverScriptData::FromUTF8(file_contents), + CompletionCallback()); + } + + private: + MockJSBindings mock_js_bindings_; +}; + +// Doesn't really matter what these values are for many of the tests. +const GURL kQueryUrl("http://www.google.com"); +const GURL kPacUrl; + +TEST(ProxyResolverV8Test, Direct) { + ProxyResolverV8WithMockBindings resolver; + int result = resolver.SetPacScriptFromDisk("direct.js"); + EXPECT_EQ(OK, result); + + ProxyInfo proxy_info; + CapturingBoundNetLog log; + result = resolver.GetProxyForURL( + kQueryUrl, &proxy_info, CompletionCallback(), NULL, log.bound()); + + EXPECT_EQ(OK, result); + EXPECT_TRUE(proxy_info.is_direct()); + + EXPECT_EQ(0U, resolver.mock_js_bindings()->alerts.size()); + EXPECT_EQ(0U, resolver.mock_js_bindings()->errors.size()); + + net::CapturingNetLog::CapturedEntryList entries; + log.GetEntries(&entries); + // No bindings were called, so no log entries. + EXPECT_EQ(0u, entries.size()); +} + +TEST(ProxyResolverV8Test, ReturnEmptyString) { + ProxyResolverV8WithMockBindings resolver; + int result = resolver.SetPacScriptFromDisk("return_empty_string.js"); + EXPECT_EQ(OK, result); + + ProxyInfo proxy_info; + result = resolver.GetProxyForURL( + kQueryUrl, &proxy_info, CompletionCallback(), NULL, BoundNetLog()); + + EXPECT_EQ(OK, result); + EXPECT_TRUE(proxy_info.is_direct()); + + EXPECT_EQ(0U, resolver.mock_js_bindings()->alerts.size()); + EXPECT_EQ(0U, resolver.mock_js_bindings()->errors.size()); +} + +TEST(ProxyResolverV8Test, Basic) { + ProxyResolverV8WithMockBindings resolver; + int result = resolver.SetPacScriptFromDisk("passthrough.js"); + EXPECT_EQ(OK, result); + + // The "FindProxyForURL" of this PAC script simply concatenates all of the + // arguments into a pseudo-host. The purpose of this test is to verify that + // the correct arguments are being passed to FindProxyForURL(). + { + ProxyInfo proxy_info; + result = resolver.GetProxyForURL(GURL("http://query.com/path"), &proxy_info, + CompletionCallback(), NULL, BoundNetLog()); + EXPECT_EQ(OK, result); + EXPECT_EQ("http.query.com.path.query.com:80", + proxy_info.proxy_server().ToURI()); + } + { + ProxyInfo proxy_info; + int result = resolver.GetProxyForURL( + GURL("ftp://query.com:90/path"), &proxy_info, CompletionCallback(), + NULL, BoundNetLog()); + EXPECT_EQ(OK, result); + // Note that FindProxyForURL(url, host) does not expect |host| to contain + // the port number. + EXPECT_EQ("ftp.query.com.90.path.query.com:80", + proxy_info.proxy_server().ToURI()); + + EXPECT_EQ(0U, resolver.mock_js_bindings()->alerts.size()); + EXPECT_EQ(0U, resolver.mock_js_bindings()->errors.size()); + } + + // We call this so we'll have code coverage of the function and valgrind will + // make sure nothing bad happens. + // + // NOTE: This is here instead of in its own test so that we'll be calling it + // after having done something, in hopes it won't be a no-op. + resolver.PurgeMemory(); +} + +TEST(ProxyResolverV8Test, BadReturnType) { + // These are the filenames of PAC scripts which each return a non-string + // types for FindProxyForURL(). They should all fail with + // ERR_PAC_SCRIPT_FAILED. + static const char* const filenames[] = { + "return_undefined.js", + "return_integer.js", + "return_function.js", + "return_object.js", + // TODO(eroman): Should 'null' be considered equivalent to "DIRECT" ? + "return_null.js" + }; + + for (size_t i = 0; i < arraysize(filenames); ++i) { + ProxyResolverV8WithMockBindings resolver; + int result = resolver.SetPacScriptFromDisk(filenames[i]); + EXPECT_EQ(OK, result); + + ProxyInfo proxy_info; + result = resolver.GetProxyForURL( + kQueryUrl, &proxy_info, CompletionCallback(), NULL, BoundNetLog()); + + EXPECT_EQ(ERR_PAC_SCRIPT_FAILED, result); + + MockJSBindings* bindings = resolver.mock_js_bindings(); + EXPECT_EQ(0U, bindings->alerts.size()); + ASSERT_EQ(1U, bindings->errors.size()); + EXPECT_EQ("FindProxyForURL() did not return a string.", + bindings->errors[0]); + EXPECT_EQ(-1, bindings->errors_line_number[0]); + } +} + +// Try using a PAC script which defines no "FindProxyForURL" function. +TEST(ProxyResolverV8Test, NoEntryPoint) { + ProxyResolverV8WithMockBindings resolver; + int result = resolver.SetPacScriptFromDisk("no_entrypoint.js"); + EXPECT_EQ(ERR_PAC_SCRIPT_FAILED, result); + + ProxyInfo proxy_info; + result = resolver.GetProxyForURL( + kQueryUrl, &proxy_info, CompletionCallback(), NULL, BoundNetLog()); + + EXPECT_EQ(ERR_FAILED, result); +} + +// Try loading a malformed PAC script. +TEST(ProxyResolverV8Test, ParseError) { + ProxyResolverV8WithMockBindings resolver; + int result = resolver.SetPacScriptFromDisk("missing_close_brace.js"); + EXPECT_EQ(ERR_PAC_SCRIPT_FAILED, result); + + ProxyInfo proxy_info; + result = resolver.GetProxyForURL( + kQueryUrl, &proxy_info, CompletionCallback(), NULL, BoundNetLog()); + + EXPECT_EQ(ERR_FAILED, result); + + MockJSBindings* bindings = resolver.mock_js_bindings(); + EXPECT_EQ(0U, bindings->alerts.size()); + + // We get one error during compilation. + ASSERT_EQ(1U, bindings->errors.size()); + + EXPECT_EQ("Uncaught SyntaxError: Unexpected end of input", + bindings->errors[0]); + EXPECT_EQ(0, bindings->errors_line_number[0]); +} + +// Run a PAC script several times, which has side-effects. +TEST(ProxyResolverV8Test, SideEffects) { + ProxyResolverV8WithMockBindings resolver; + int result = resolver.SetPacScriptFromDisk("side_effects.js"); + + // The PAC script increments a counter each time we invoke it. + for (int i = 0; i < 3; ++i) { + ProxyInfo proxy_info; + result = resolver.GetProxyForURL( + kQueryUrl, &proxy_info, CompletionCallback(), NULL, BoundNetLog()); + EXPECT_EQ(OK, result); + EXPECT_EQ(base::StringPrintf("sideffect_%d:80", i), + proxy_info.proxy_server().ToURI()); + } + + // Reload the script -- the javascript environment should be reset, hence + // the counter starts over. + result = resolver.SetPacScriptFromDisk("side_effects.js"); + EXPECT_EQ(OK, result); + + for (int i = 0; i < 3; ++i) { + ProxyInfo proxy_info; + result = resolver.GetProxyForURL( + kQueryUrl, &proxy_info, CompletionCallback(), NULL, BoundNetLog()); + EXPECT_EQ(OK, result); + EXPECT_EQ(base::StringPrintf("sideffect_%d:80", i), + proxy_info.proxy_server().ToURI()); + } +} + +// Execute a PAC script which throws an exception in FindProxyForURL. +TEST(ProxyResolverV8Test, UnhandledException) { + ProxyResolverV8WithMockBindings resolver; + int result = resolver.SetPacScriptFromDisk("unhandled_exception.js"); + EXPECT_EQ(OK, result); + + ProxyInfo proxy_info; + result = resolver.GetProxyForURL( + kQueryUrl, &proxy_info, CompletionCallback(), NULL, BoundNetLog()); + + EXPECT_EQ(ERR_PAC_SCRIPT_FAILED, result); + + MockJSBindings* bindings = resolver.mock_js_bindings(); + EXPECT_EQ(0U, bindings->alerts.size()); + ASSERT_EQ(1U, bindings->errors.size()); + EXPECT_EQ("Uncaught ReferenceError: undefined_variable is not defined", + bindings->errors[0]); + EXPECT_EQ(3, bindings->errors_line_number[0]); +} + +TEST(ProxyResolverV8Test, ReturnUnicode) { + ProxyResolverV8WithMockBindings resolver; + int result = resolver.SetPacScriptFromDisk("return_unicode.js"); + EXPECT_EQ(OK, result); + + ProxyInfo proxy_info; + result = resolver.GetProxyForURL( + kQueryUrl, &proxy_info, CompletionCallback(), NULL, BoundNetLog()); + + // The result from this resolve was unparseable, because it + // wasn't ASCII. + EXPECT_EQ(ERR_PAC_SCRIPT_FAILED, result); +} + +// Test the PAC library functions that we expose in the JS environment. +TEST(ProxyResolverV8Test, JavascriptLibrary) { + ProxyResolverV8WithMockBindings resolver; + int result = resolver.SetPacScriptFromDisk("pac_library_unittest.js"); + EXPECT_EQ(OK, result); + + ProxyInfo proxy_info; + result = resolver.GetProxyForURL( + kQueryUrl, &proxy_info, CompletionCallback(), NULL, BoundNetLog()); + + // If the javascript side of this unit-test fails, it will throw a javascript + // exception. Otherwise it will return "PROXY success:80". + EXPECT_EQ(OK, result); + EXPECT_EQ("success:80", proxy_info.proxy_server().ToURI()); + + EXPECT_EQ(0U, resolver.mock_js_bindings()->alerts.size()); + EXPECT_EQ(0U, resolver.mock_js_bindings()->errors.size()); +} + +// Try resolving when SetPacScriptByData() has not been called. +TEST(ProxyResolverV8Test, NoSetPacScript) { + ProxyResolverV8WithMockBindings resolver; + + ProxyInfo proxy_info; + + // Resolve should fail, as we are not yet initialized with a script. + int result = resolver.GetProxyForURL( + kQueryUrl, &proxy_info, CompletionCallback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_FAILED, result); + + // Initialize it. + result = resolver.SetPacScriptFromDisk("direct.js"); + EXPECT_EQ(OK, result); + + // Resolve should now succeed. + result = resolver.GetProxyForURL( + kQueryUrl, &proxy_info, CompletionCallback(), NULL, BoundNetLog()); + EXPECT_EQ(OK, result); + + // Clear it, by initializing with an empty string. + resolver.SetPacScript( + ProxyResolverScriptData::FromUTF16(base::string16()), + CompletionCallback()); + + // Resolve should fail again now. + result = resolver.GetProxyForURL( + kQueryUrl, &proxy_info, CompletionCallback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_FAILED, result); + + // Load a good script once more. + result = resolver.SetPacScriptFromDisk("direct.js"); + EXPECT_EQ(OK, result); + result = resolver.GetProxyForURL( + kQueryUrl, &proxy_info, CompletionCallback(), NULL, BoundNetLog()); + EXPECT_EQ(OK, result); + + EXPECT_EQ(0U, resolver.mock_js_bindings()->alerts.size()); + EXPECT_EQ(0U, resolver.mock_js_bindings()->errors.size()); +} + +// Test marshalling/un-marshalling of values between C++/V8. +TEST(ProxyResolverV8Test, V8Bindings) { + ProxyResolverV8WithMockBindings resolver; + MockJSBindings* bindings = resolver.mock_js_bindings(); + bindings->dns_resolve_result = "127.0.0.1"; + int result = resolver.SetPacScriptFromDisk("bindings.js"); + EXPECT_EQ(OK, result); + + ProxyInfo proxy_info; + result = resolver.GetProxyForURL( + kQueryUrl, &proxy_info, CompletionCallback(), NULL, BoundNetLog()); + + EXPECT_EQ(OK, result); + EXPECT_TRUE(proxy_info.is_direct()); + + EXPECT_EQ(0U, resolver.mock_js_bindings()->errors.size()); + + // Alert was called 5 times. + ASSERT_EQ(5U, bindings->alerts.size()); + EXPECT_EQ("undefined", bindings->alerts[0]); + EXPECT_EQ("null", bindings->alerts[1]); + EXPECT_EQ("undefined", bindings->alerts[2]); + EXPECT_EQ("[object Object]", bindings->alerts[3]); + EXPECT_EQ("exception from calling toString()", bindings->alerts[4]); + + // DnsResolve was called 8 times, however only 2 of those were string + // parameters. (so 6 of them failed immediately). + ASSERT_EQ(2U, bindings->dns_resolves.size()); + EXPECT_EQ("", bindings->dns_resolves[0]); + EXPECT_EQ("arg1", bindings->dns_resolves[1]); + + // MyIpAddress was called two times. + EXPECT_EQ(2, bindings->my_ip_address_count); + + // MyIpAddressEx was called once. + EXPECT_EQ(1, bindings->my_ip_address_ex_count); + + // DnsResolveEx was called 2 times. + ASSERT_EQ(2U, bindings->dns_resolves_ex.size()); + EXPECT_EQ("is_resolvable", bindings->dns_resolves_ex[0]); + EXPECT_EQ("foobar", bindings->dns_resolves_ex[1]); +} + +// Test calling a binding (myIpAddress()) from the script's global scope. +// http://crbug.com/40026 +TEST(ProxyResolverV8Test, BindingCalledDuringInitialization) { + ProxyResolverV8WithMockBindings resolver; + + int result = resolver.SetPacScriptFromDisk("binding_from_global.js"); + EXPECT_EQ(OK, result); + + MockJSBindings* bindings = resolver.mock_js_bindings(); + + // myIpAddress() got called during initialization of the script. + EXPECT_EQ(1, bindings->my_ip_address_count); + + ProxyInfo proxy_info; + result = resolver.GetProxyForURL( + kQueryUrl, &proxy_info, CompletionCallback(), NULL, BoundNetLog()); + + EXPECT_EQ(OK, result); + EXPECT_FALSE(proxy_info.is_direct()); + EXPECT_EQ("127.0.0.1:80", proxy_info.proxy_server().ToURI()); + + // Check that no other bindings were called. + EXPECT_EQ(0U, bindings->errors.size()); + ASSERT_EQ(0U, bindings->alerts.size()); + ASSERT_EQ(0U, bindings->dns_resolves.size()); + EXPECT_EQ(0, bindings->my_ip_address_ex_count); + ASSERT_EQ(0U, bindings->dns_resolves_ex.size()); +} + +// Try loading a PAC script that ends with a comment and has no terminal +// newline. This should not cause problems with the PAC utility functions +// that we add to the script's environment. +// http://crbug.com/22864 +TEST(ProxyResolverV8Test, EndsWithCommentNoNewline) { + ProxyResolverV8WithMockBindings resolver; + int result = resolver.SetPacScriptFromDisk("ends_with_comment.js"); + EXPECT_EQ(OK, result); + + ProxyInfo proxy_info; + result = resolver.GetProxyForURL( + kQueryUrl, &proxy_info, CompletionCallback(), NULL, BoundNetLog()); + + EXPECT_EQ(OK, result); + EXPECT_FALSE(proxy_info.is_direct()); + EXPECT_EQ("success:80", proxy_info.proxy_server().ToURI()); +} + +// Try loading a PAC script that ends with a statement and has no terminal +// newline. This should not cause problems with the PAC utility functions +// that we add to the script's environment. +// http://crbug.com/22864 +TEST(ProxyResolverV8Test, EndsWithStatementNoNewline) { + ProxyResolverV8WithMockBindings resolver; + int result = resolver.SetPacScriptFromDisk( + "ends_with_statement_no_semicolon.js"); + EXPECT_EQ(OK, result); + + ProxyInfo proxy_info; + result = resolver.GetProxyForURL( + kQueryUrl, &proxy_info, CompletionCallback(), NULL, BoundNetLog()); + + EXPECT_EQ(OK, result); + EXPECT_FALSE(proxy_info.is_direct()); + EXPECT_EQ("success:3", proxy_info.proxy_server().ToURI()); +} + +// Test the return values from myIpAddress(), myIpAddressEx(), dnsResolve(), +// dnsResolveEx(), isResolvable(), isResolvableEx(), when the the binding +// returns empty string (failure). This simulates the return values from +// those functions when the underlying DNS resolution fails. +TEST(ProxyResolverV8Test, DNSResolutionFailure) { + ProxyResolverV8WithMockBindings resolver; + int result = resolver.SetPacScriptFromDisk("dns_fail.js"); + EXPECT_EQ(OK, result); + + ProxyInfo proxy_info; + result = resolver.GetProxyForURL( + kQueryUrl, &proxy_info, CompletionCallback(), NULL, BoundNetLog()); + + EXPECT_EQ(OK, result); + EXPECT_FALSE(proxy_info.is_direct()); + EXPECT_EQ("success:80", proxy_info.proxy_server().ToURI()); +} + +TEST(ProxyResolverV8Test, DNSResolutionOfInternationDomainName) { + ProxyResolverV8WithMockBindings resolver; + int result = resolver.SetPacScriptFromDisk("international_domain_names.js"); + EXPECT_EQ(OK, result); + + // Execute FindProxyForURL(). + ProxyInfo proxy_info; + result = resolver.GetProxyForURL( + kQueryUrl, &proxy_info, CompletionCallback(), NULL, BoundNetLog()); + + EXPECT_EQ(OK, result); + EXPECT_TRUE(proxy_info.is_direct()); + + // Check that the international domain name was converted to punycode + // before passing it onto the bindings layer. + MockJSBindings* bindings = resolver.mock_js_bindings(); + + ASSERT_EQ(1u, bindings->dns_resolves.size()); + EXPECT_EQ("xn--bcher-kva.ch", bindings->dns_resolves[0]); + + ASSERT_EQ(1u, bindings->dns_resolves_ex.size()); + EXPECT_EQ("xn--bcher-kva.ch", bindings->dns_resolves_ex[0]); +} + +// Test that when resolving a URL which contains an IPv6 string literal, the +// brackets are removed from the host before passing it down to the PAC script. +// If we don't do this, then subsequent calls to dnsResolveEx(host) will be +// doomed to fail since it won't correspond with a valid name. +TEST(ProxyResolverV8Test, IPv6HostnamesNotBracketed) { + ProxyResolverV8WithMockBindings resolver; + int result = resolver.SetPacScriptFromDisk("resolve_host.js"); + EXPECT_EQ(OK, result); + + ProxyInfo proxy_info; + result = resolver.GetProxyForURL( + GURL("http://[abcd::efff]:99/watsupdawg"), &proxy_info, + CompletionCallback(), NULL, BoundNetLog()); + + EXPECT_EQ(OK, result); + EXPECT_TRUE(proxy_info.is_direct()); + + // We called dnsResolveEx() exactly once, by passing through the "host" + // argument to FindProxyForURL(). The brackets should have been stripped. + ASSERT_EQ(1U, resolver.mock_js_bindings()->dns_resolves_ex.size()); + EXPECT_EQ("abcd::efff", resolver.mock_js_bindings()->dns_resolves_ex[0]); +} + +// Test that terminating a script within DnsResolve() leads to eventual +// termination of the script. Also test that repeatedly calling terminate is +// safe, and running the script again after termination still works. +TEST(ProxyResolverV8Test, Terminate) { + ProxyResolverV8WithMockBindings resolver; + int result = resolver.SetPacScriptFromDisk("terminate.js"); + EXPECT_EQ(OK, result); + + MockJSBindings* bindings = resolver.mock_js_bindings(); + + // Terminate script execution upon reaching dnsResolve(). Note that + // termination may not take effect right away (so the subsequent dnsResolve() + // and alert() may be run). + bindings->should_terminate = true; + + ProxyInfo proxy_info; + result = resolver.GetProxyForURL( + GURL("http://hang/"), &proxy_info, + CompletionCallback(), NULL, BoundNetLog()); + + // The script execution was terminated. + EXPECT_EQ(ERR_PAC_SCRIPT_FAILED, result); + + EXPECT_EQ(1U, resolver.mock_js_bindings()->dns_resolves.size()); + EXPECT_GE(2U, resolver.mock_js_bindings()->dns_resolves_ex.size()); + EXPECT_GE(1U, bindings->alerts.size()); + + EXPECT_EQ(1U, bindings->errors.size()); + + // Termination shows up as an uncaught exception without any message. + EXPECT_EQ("", bindings->errors[0]); + + bindings->errors.clear(); + + // Try running the script again, this time with a different input which won't + // cause a termination+hang. + result = resolver.GetProxyForURL( + GURL("http://kittens/"), &proxy_info, + CompletionCallback(), NULL, BoundNetLog()); + + EXPECT_EQ(OK, result); + EXPECT_EQ(0u, bindings->errors.size()); + EXPECT_EQ("kittens:88", proxy_info.proxy_server().ToURI()); +} + +} // namespace +} // namespace net diff --git a/chromium/net/proxy/proxy_resolver_winhttp.cc b/chromium/net/proxy/proxy_resolver_winhttp.cc new file mode 100644 index 00000000000..32737875e3e --- /dev/null +++ b/chromium/net/proxy/proxy_resolver_winhttp.cc @@ -0,0 +1,173 @@ +// 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/proxy/proxy_resolver_winhttp.h" + +#include <windows.h> +#include <winhttp.h> + +#include "base/metrics/histogram.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "net/base/net_errors.h" +#include "net/proxy/proxy_info.h" +#include "url/gurl.h" + +#pragma comment(lib, "winhttp.lib") + +using base::TimeDelta; +using base::TimeTicks; + +namespace net { + +static void FreeInfo(WINHTTP_PROXY_INFO* info) { + if (info->lpszProxy) + GlobalFree(info->lpszProxy); + if (info->lpszProxyBypass) + GlobalFree(info->lpszProxyBypass); +} + +ProxyResolverWinHttp::ProxyResolverWinHttp() + : ProxyResolver(false /*expects_pac_bytes*/), session_handle_(NULL) { +} + +ProxyResolverWinHttp::~ProxyResolverWinHttp() { + CloseWinHttpSession(); +} + +int ProxyResolverWinHttp::GetProxyForURL(const GURL& query_url, + ProxyInfo* results, + const CompletionCallback& /*callback*/, + RequestHandle* /*request*/, + const BoundNetLog& /*net_log*/) { + // If we don't have a WinHTTP session, then create a new one. + if (!session_handle_ && !OpenWinHttpSession()) + return ERR_FAILED; + + // If we have been given an empty PAC url, then use auto-detection. + // + // NOTE: We just use DNS-based auto-detection here like Firefox. We do this + // to avoid WinHTTP's auto-detection code, which while more featureful (it + // supports DHCP based auto-detection) also appears to have issues. + // + WINHTTP_AUTOPROXY_OPTIONS options = {0}; + options.fAutoLogonIfChallenged = FALSE; + options.dwFlags = WINHTTP_AUTOPROXY_CONFIG_URL; + std::wstring pac_url_wide = ASCIIToWide(pac_url_.spec()); + options.lpszAutoConfigUrl = pac_url_wide.c_str(); + + WINHTTP_PROXY_INFO info = {0}; + DCHECK(session_handle_); + + // Per http://msdn.microsoft.com/en-us/library/aa383153(VS.85).aspx, it is + // necessary to first try resolving with fAutoLogonIfChallenged set to false. + // Otherwise, we fail over to trying it with a value of true. This way we + // get good performance in the case where WinHTTP uses an out-of-process + // resolver. This is important for Vista and Win2k3. + BOOL ok = WinHttpGetProxyForUrl( + session_handle_, ASCIIToWide(query_url.spec()).c_str(), &options, &info); + if (!ok) { + if (ERROR_WINHTTP_LOGIN_FAILURE == GetLastError()) { + options.fAutoLogonIfChallenged = TRUE; + ok = WinHttpGetProxyForUrl( + session_handle_, ASCIIToWide(query_url.spec()).c_str(), + &options, &info); + } + if (!ok) { + DWORD error = GetLastError(); + // If we got here because of RPC timeout during out of process PAC + // resolution, no further requests on this session are going to work. + if (ERROR_WINHTTP_TIMEOUT == error || + ERROR_WINHTTP_AUTO_PROXY_SERVICE_ERROR == error) { + CloseWinHttpSession(); + } + return ERR_FAILED; // TODO(darin): Bug 1189288: translate error code. + } + } + + int rv = OK; + + switch (info.dwAccessType) { + case WINHTTP_ACCESS_TYPE_NO_PROXY: + results->UseDirect(); + break; + case WINHTTP_ACCESS_TYPE_NAMED_PROXY: + // According to MSDN: + // + // The proxy server list contains one or more of the following strings + // separated by semicolons or whitespace. + // + // ([<scheme>=][<scheme>"://"]<server>[":"<port>]) + // + // Based on this description, ProxyInfo::UseNamedProxy() isn't + // going to handle all the variations (in particular <scheme>=). + // + // However in practice, it seems that WinHTTP is simply returning + // things like "foopy1:80;foopy2:80". It strips out the non-HTTP + // proxy types, and stops the list when PAC encounters a "DIRECT". + // So UseNamedProxy() should work OK. + results->UseNamedProxy(WideToASCII(info.lpszProxy)); + break; + default: + NOTREACHED(); + rv = ERR_FAILED; + } + + FreeInfo(&info); + return rv; +} + +void ProxyResolverWinHttp::CancelRequest(RequestHandle request) { + // This is a synchronous ProxyResolver; no possibility for async requests. + NOTREACHED(); +} + +LoadState ProxyResolverWinHttp::GetLoadState(RequestHandle request) const { + NOTREACHED(); + return LOAD_STATE_IDLE; +} + +void ProxyResolverWinHttp::CancelSetPacScript() { + NOTREACHED(); +} + +int ProxyResolverWinHttp::SetPacScript( + const scoped_refptr<ProxyResolverScriptData>& script_data, + const CompletionCallback& /*callback*/) { + if (script_data->type() == ProxyResolverScriptData::TYPE_AUTO_DETECT) { + pac_url_ = GURL("http://wpad/wpad.dat"); + } else { + pac_url_ = script_data->url(); + } + return OK; +} + +bool ProxyResolverWinHttp::OpenWinHttpSession() { + DCHECK(!session_handle_); + session_handle_ = WinHttpOpen(NULL, + WINHTTP_ACCESS_TYPE_NO_PROXY, + WINHTTP_NO_PROXY_NAME, + WINHTTP_NO_PROXY_BYPASS, + 0); + if (!session_handle_) + return false; + + // Since this session handle will never be used for WinHTTP connections, + // these timeouts don't really mean much individually. However, WinHTTP's + // out of process PAC resolution will use a combined (sum of all timeouts) + // value to wait for an RPC reply. + BOOL rv = WinHttpSetTimeouts(session_handle_, 10000, 10000, 5000, 5000); + DCHECK(rv); + + return true; +} + +void ProxyResolverWinHttp::CloseWinHttpSession() { + if (session_handle_) { + WinHttpCloseHandle(session_handle_); + session_handle_ = NULL; + } +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_resolver_winhttp.h b/chromium/net/proxy/proxy_resolver_winhttp.h new file mode 100644 index 00000000000..62b56436ba0 --- /dev/null +++ b/chromium/net/proxy/proxy_resolver_winhttp.h @@ -0,0 +1,53 @@ +// 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. + +#ifndef NET_PROXY_PROXY_RESOLVER_WINHTTP_H_ +#define NET_PROXY_PROXY_RESOLVER_WINHTTP_H_ + +#include "base/compiler_specific.h" +#include "net/proxy/proxy_resolver.h" +#include "url/gurl.h" + +typedef void* HINTERNET; // From winhttp.h + +namespace net { + +// An implementation of ProxyResolver that uses WinHTTP and the system +// proxy settings. +class NET_EXPORT_PRIVATE ProxyResolverWinHttp : public ProxyResolver { + public: + ProxyResolverWinHttp(); + virtual ~ProxyResolverWinHttp(); + + // ProxyResolver implementation: + virtual int GetProxyForURL(const GURL& url, + ProxyInfo* results, + const net::CompletionCallback& /*callback*/, + RequestHandle* /*request*/, + const BoundNetLog& /*net_log*/) OVERRIDE; + virtual void CancelRequest(RequestHandle request) OVERRIDE; + + virtual LoadState GetLoadState(RequestHandle request) const OVERRIDE; + + virtual void CancelSetPacScript() OVERRIDE; + + virtual int SetPacScript( + const scoped_refptr<ProxyResolverScriptData>& script_data, + const net::CompletionCallback& /*callback*/) OVERRIDE; + + private: + bool OpenWinHttpSession(); + void CloseWinHttpSession(); + + // Proxy configuration is cached on the session handle. + HINTERNET session_handle_; + + GURL pac_url_; + + DISALLOW_COPY_AND_ASSIGN(ProxyResolverWinHttp); +}; + +} // namespace net + +#endif // NET_PROXY_PROXY_RESOLVER_WINHTTP_H_ diff --git a/chromium/net/proxy/proxy_retry_info.h b/chromium/net/proxy/proxy_retry_info.h new file mode 100644 index 00000000000..8825289a352 --- /dev/null +++ b/chromium/net/proxy/proxy_retry_info.h @@ -0,0 +1,30 @@ +// Copyright (c) 2006-2008 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. + +#ifndef NET_PROXY_PROXY_RETRY_INFO_H_ +#define NET_PROXY_PROXY_RETRY_INFO_H_ + +#include <map> + +#include "base/time/time.h" + +namespace net { + +// Contains the information about when to retry a proxy server. +struct ProxyRetryInfo { + // We should not retry until this time. + base::TimeTicks bad_until; + + // This is the current delay. If the proxy is still bad, we need to increase + // this delay. + base::TimeDelta current_delay; +}; + +// Map of proxy servers with the associated RetryInfo structures. +// The key is a proxy URI string [<scheme>"://"]<host>":"<port>. +typedef std::map<std::string, ProxyRetryInfo> ProxyRetryInfoMap; + +} // namespace net + +#endif // NET_PROXY_PROXY_RETRY_INFO_H_ diff --git a/chromium/net/proxy/proxy_script_decider.cc b/chromium/net/proxy/proxy_script_decider.cc new file mode 100644 index 00000000000..38bf751cd4d --- /dev/null +++ b/chromium/net/proxy/proxy_script_decider.cc @@ -0,0 +1,414 @@ +// 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/proxy/proxy_script_decider.h" + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/compiler_specific.h" +#include "base/format_macros.h" +#include "base/logging.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/values.h" +#include "net/base/net_errors.h" +#include "net/proxy/dhcp_proxy_script_fetcher.h" +#include "net/proxy/dhcp_proxy_script_fetcher_factory.h" +#include "net/proxy/proxy_script_fetcher.h" + +namespace net { + +namespace { + +bool LooksLikePacScript(const base::string16& script) { + // Note: this is only an approximation! It may not always work correctly, + // however it is very likely that legitimate scripts have this exact string, + // since they must minimally define a function of this name. Conversely, a + // file not containing the string is not likely to be a PAC script. + // + // An exact test would have to load the script in a javascript evaluator. + return script.find(ASCIIToUTF16("FindProxyForURL")) != base::string16::npos; +} + +} + +// This is the hard-coded location used by the DNS portion of web proxy +// auto-discovery. +// +// Note that we not use DNS devolution to find the WPAD host, since that could +// be dangerous should our top level domain registry become out of date. +// +// Instead we directly resolve "wpad", and let the operating system apply the +// DNS suffix search paths. This is the same approach taken by Firefox, and +// compatibility hasn't been an issue. +// +// For more details, also check out this comment: +// http://code.google.com/p/chromium/issues/detail?id=18575#c20 +static const char kWpadUrl[] = "http://wpad/wpad.dat"; + +base::Value* ProxyScriptDecider::PacSource::NetLogCallback( + const GURL* effective_pac_url, + NetLog::LogLevel /* log_level */) const { + base::DictionaryValue* dict = new base::DictionaryValue(); + std::string source; + switch (type) { + case PacSource::WPAD_DHCP: + source = "WPAD DHCP"; + break; + case PacSource::WPAD_DNS: + source = "WPAD DNS: "; + source += effective_pac_url->possibly_invalid_spec(); + break; + case PacSource::CUSTOM: + source = "Custom PAC URL: "; + source += effective_pac_url->possibly_invalid_spec(); + break; + } + dict->SetString("source", source); + return dict; +} + +ProxyScriptDecider::ProxyScriptDecider( + ProxyScriptFetcher* proxy_script_fetcher, + DhcpProxyScriptFetcher* dhcp_proxy_script_fetcher, + NetLog* net_log) + : resolver_(NULL), + proxy_script_fetcher_(proxy_script_fetcher), + dhcp_proxy_script_fetcher_(dhcp_proxy_script_fetcher), + current_pac_source_index_(0u), + pac_mandatory_(false), + next_state_(STATE_NONE), + net_log_(BoundNetLog::Make( + net_log, NetLog::SOURCE_PROXY_SCRIPT_DECIDER)), + fetch_pac_bytes_(false) { +} + +ProxyScriptDecider::~ProxyScriptDecider() { + if (next_state_ != STATE_NONE) + Cancel(); +} + +int ProxyScriptDecider::Start( + const ProxyConfig& config, const base::TimeDelta wait_delay, + bool fetch_pac_bytes, const CompletionCallback& callback) { + DCHECK_EQ(STATE_NONE, next_state_); + DCHECK(!callback.is_null()); + DCHECK(config.HasAutomaticSettings()); + + net_log_.BeginEvent(NetLog::TYPE_PROXY_SCRIPT_DECIDER); + + fetch_pac_bytes_ = fetch_pac_bytes; + + // Save the |wait_delay| as a non-negative value. + wait_delay_ = wait_delay; + if (wait_delay_ < base::TimeDelta()) + wait_delay_ = base::TimeDelta(); + + pac_mandatory_ = config.pac_mandatory(); + + pac_sources_ = BuildPacSourcesFallbackList(config); + DCHECK(!pac_sources_.empty()); + + next_state_ = STATE_WAIT; + + int rv = DoLoop(OK); + if (rv == ERR_IO_PENDING) + callback_ = callback; + else + DidComplete(); + + return rv; +} + +const ProxyConfig& ProxyScriptDecider::effective_config() const { + DCHECK_EQ(STATE_NONE, next_state_); + return effective_config_; +} + +// TODO(eroman): Return a const-pointer. +ProxyResolverScriptData* ProxyScriptDecider::script_data() const { + DCHECK_EQ(STATE_NONE, next_state_); + return script_data_.get(); +} + +// Initialize the fallback rules. +// (1) WPAD (DHCP). +// (2) WPAD (DNS). +// (3) Custom PAC URL. +ProxyScriptDecider::PacSourceList ProxyScriptDecider:: + BuildPacSourcesFallbackList( + const ProxyConfig& config) const { + PacSourceList pac_sources; + if (config.auto_detect()) { + pac_sources.push_back(PacSource(PacSource::WPAD_DHCP, GURL())); + pac_sources.push_back(PacSource(PacSource::WPAD_DNS, GURL())); + } + if (config.has_pac_url()) + pac_sources.push_back(PacSource(PacSource::CUSTOM, config.pac_url())); + return pac_sources; +} + +void ProxyScriptDecider::OnIOCompletion(int result) { + DCHECK_NE(STATE_NONE, next_state_); + int rv = DoLoop(result); + if (rv != ERR_IO_PENDING) { + DidComplete(); + DoCallback(rv); + } +} + +int ProxyScriptDecider::DoLoop(int result) { + DCHECK_NE(next_state_, STATE_NONE); + int rv = result; + do { + State state = next_state_; + next_state_ = STATE_NONE; + switch (state) { + case STATE_WAIT: + DCHECK_EQ(OK, rv); + rv = DoWait(); + break; + case STATE_WAIT_COMPLETE: + rv = DoWaitComplete(rv); + break; + case STATE_FETCH_PAC_SCRIPT: + DCHECK_EQ(OK, rv); + rv = DoFetchPacScript(); + break; + case STATE_FETCH_PAC_SCRIPT_COMPLETE: + rv = DoFetchPacScriptComplete(rv); + break; + case STATE_VERIFY_PAC_SCRIPT: + DCHECK_EQ(OK, rv); + rv = DoVerifyPacScript(); + break; + case STATE_VERIFY_PAC_SCRIPT_COMPLETE: + rv = DoVerifyPacScriptComplete(rv); + break; + default: + NOTREACHED() << "bad state"; + rv = ERR_UNEXPECTED; + break; + } + } while (rv != ERR_IO_PENDING && next_state_ != STATE_NONE); + return rv; +} + +void ProxyScriptDecider::DoCallback(int result) { + DCHECK_NE(ERR_IO_PENDING, result); + DCHECK(!callback_.is_null()); + callback_.Run(result); +} + +int ProxyScriptDecider::DoWait() { + next_state_ = STATE_WAIT_COMPLETE; + + // If no waiting is required, continue on to the next state. + if (wait_delay_.ToInternalValue() == 0) + return OK; + + // Otherwise wait the specified amount of time. + wait_timer_.Start(FROM_HERE, wait_delay_, this, + &ProxyScriptDecider::OnWaitTimerFired); + net_log_.BeginEvent(NetLog::TYPE_PROXY_SCRIPT_DECIDER_WAIT); + return ERR_IO_PENDING; +} + +int ProxyScriptDecider::DoWaitComplete(int result) { + DCHECK_EQ(OK, result); + if (wait_delay_.ToInternalValue() != 0) { + net_log_.EndEventWithNetErrorCode(NetLog::TYPE_PROXY_SCRIPT_DECIDER_WAIT, + result); + } + next_state_ = GetStartState(); + return OK; +} + +int ProxyScriptDecider::DoFetchPacScript() { + DCHECK(fetch_pac_bytes_); + + next_state_ = STATE_FETCH_PAC_SCRIPT_COMPLETE; + + const PacSource& pac_source = current_pac_source(); + + GURL effective_pac_url; + DetermineURL(pac_source, &effective_pac_url); + + net_log_.BeginEvent(NetLog::TYPE_PROXY_SCRIPT_DECIDER_FETCH_PAC_SCRIPT, + base::Bind(&PacSource::NetLogCallback, + base::Unretained(&pac_source), + &effective_pac_url)); + + if (pac_source.type == PacSource::WPAD_DHCP) { + if (!dhcp_proxy_script_fetcher_) { + net_log_.AddEvent(NetLog::TYPE_PROXY_SCRIPT_DECIDER_HAS_NO_FETCHER); + return ERR_UNEXPECTED; + } + + return dhcp_proxy_script_fetcher_->Fetch( + &pac_script_, base::Bind(&ProxyScriptDecider::OnIOCompletion, + base::Unretained(this))); + } + + if (!proxy_script_fetcher_) { + net_log_.AddEvent(NetLog::TYPE_PROXY_SCRIPT_DECIDER_HAS_NO_FETCHER); + return ERR_UNEXPECTED; + } + + return proxy_script_fetcher_->Fetch( + effective_pac_url, &pac_script_, + base::Bind(&ProxyScriptDecider::OnIOCompletion, base::Unretained(this))); +} + +int ProxyScriptDecider::DoFetchPacScriptComplete(int result) { + DCHECK(fetch_pac_bytes_); + + net_log_.EndEventWithNetErrorCode( + NetLog::TYPE_PROXY_SCRIPT_DECIDER_FETCH_PAC_SCRIPT, result); + if (result != OK) + return TryToFallbackPacSource(result); + + next_state_ = STATE_VERIFY_PAC_SCRIPT; + return result; +} + +int ProxyScriptDecider::DoVerifyPacScript() { + next_state_ = STATE_VERIFY_PAC_SCRIPT_COMPLETE; + + // This is just a heuristic. Ideally we would try to parse the script. + if (fetch_pac_bytes_ && !LooksLikePacScript(pac_script_)) + return ERR_PAC_SCRIPT_FAILED; + + return OK; +} + +int ProxyScriptDecider::DoVerifyPacScriptComplete(int result) { + if (result != OK) + return TryToFallbackPacSource(result); + + const PacSource& pac_source = current_pac_source(); + + // Extract the current script data. + if (fetch_pac_bytes_) { + script_data_ = ProxyResolverScriptData::FromUTF16(pac_script_); + } else { + script_data_ = pac_source.type == PacSource::CUSTOM ? + ProxyResolverScriptData::FromURL(pac_source.url) : + ProxyResolverScriptData::ForAutoDetect(); + } + + // Let the caller know which automatic setting we ended up initializing the + // resolver for (there may have been multiple fallbacks to choose from.) + if (current_pac_source().type == PacSource::CUSTOM) { + effective_config_ = + ProxyConfig::CreateFromCustomPacURL(current_pac_source().url); + effective_config_.set_pac_mandatory(pac_mandatory_); + } else { + if (fetch_pac_bytes_) { + GURL auto_detected_url; + + switch (current_pac_source().type) { + case PacSource::WPAD_DHCP: + auto_detected_url = dhcp_proxy_script_fetcher_->GetPacURL(); + break; + + case PacSource::WPAD_DNS: + auto_detected_url = GURL(kWpadUrl); + break; + + default: + NOTREACHED(); + } + + effective_config_ = + ProxyConfig::CreateFromCustomPacURL(auto_detected_url); + } else { + // The resolver does its own resolution so we cannot know the + // URL. Just do the best we can and state that the configuration + // is to auto-detect proxy settings. + effective_config_ = ProxyConfig::CreateAutoDetect(); + } + } + + return OK; +} + +int ProxyScriptDecider::TryToFallbackPacSource(int error) { + DCHECK_LT(error, 0); + + if (current_pac_source_index_ + 1 >= pac_sources_.size()) { + // Nothing left to fall back to. + return error; + } + + // Advance to next URL in our list. + ++current_pac_source_index_; + + net_log_.AddEvent( + NetLog::TYPE_PROXY_SCRIPT_DECIDER_FALLING_BACK_TO_NEXT_PAC_SOURCE); + + next_state_ = GetStartState(); + + return OK; +} + +ProxyScriptDecider::State ProxyScriptDecider::GetStartState() const { + return fetch_pac_bytes_ ? STATE_FETCH_PAC_SCRIPT : STATE_VERIFY_PAC_SCRIPT; +} + +void ProxyScriptDecider::DetermineURL(const PacSource& pac_source, + GURL* effective_pac_url) { + DCHECK(effective_pac_url); + + switch (pac_source.type) { + case PacSource::WPAD_DHCP: + break; + case PacSource::WPAD_DNS: + *effective_pac_url = GURL(kWpadUrl); + break; + case PacSource::CUSTOM: + *effective_pac_url = pac_source.url; + break; + } +} + +const ProxyScriptDecider::PacSource& + ProxyScriptDecider::current_pac_source() const { + DCHECK_LT(current_pac_source_index_, pac_sources_.size()); + return pac_sources_[current_pac_source_index_]; +} + +void ProxyScriptDecider::OnWaitTimerFired() { + OnIOCompletion(OK); +} + +void ProxyScriptDecider::DidComplete() { + net_log_.EndEvent(NetLog::TYPE_PROXY_SCRIPT_DECIDER); +} + +void ProxyScriptDecider::Cancel() { + DCHECK_NE(STATE_NONE, next_state_); + + net_log_.AddEvent(NetLog::TYPE_CANCELLED); + + switch (next_state_) { + case STATE_WAIT_COMPLETE: + wait_timer_.Stop(); + break; + case STATE_FETCH_PAC_SCRIPT_COMPLETE: + proxy_script_fetcher_->Cancel(); + break; + default: + NOTREACHED(); + break; + } + + // This is safe to call in any state. + if (dhcp_proxy_script_fetcher_) + dhcp_proxy_script_fetcher_->Cancel(); + + DidComplete(); +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_script_decider.h b/chromium/net/proxy/proxy_script_decider.h new file mode 100644 index 00000000000..9a77938ec8e --- /dev/null +++ b/chromium/net/proxy/proxy_script_decider.h @@ -0,0 +1,184 @@ +// 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. + +#ifndef NET_PROXY_PROXY_SCRIPT_DECIDER_H_ +#define NET_PROXY_PROXY_SCRIPT_DECIDER_H_ + +#include <string> +#include <vector> + +#include "base/memory/ref_counted.h" +#include "base/strings/string16.h" +#include "base/time/time.h" +#include "base/timer/timer.h" +#include "net/base/completion_callback.h" +#include "net/base/net_export.h" +#include "net/base/net_log.h" +#include "net/proxy/proxy_config.h" +#include "net/proxy/proxy_resolver.h" +#include "url/gurl.h" + +namespace net { + +class DhcpProxyScriptFetcher; +class NetLogParameter; +class ProxyResolver; +class ProxyScriptFetcher; + +// ProxyScriptDecider is a helper class used by ProxyService to determine which +// PAC script to use given our proxy configuration. +// +// This involves trying to use PAC scripts in this order: +// +// (1) WPAD (DHCP) if auto-detect is on. +// (2) WPAD (DNS) if auto-detect is on. +// (3) Custom PAC script if a URL was given. +// +// If no PAC script was successfully selected, then it fails with either a +// network error, or PAC_SCRIPT_FAILED (indicating it did not pass our +// validation). +// +// On successful completion, the fetched PAC script data can be accessed using +// script_data(). +// +// Deleting ProxyScriptDecider while Init() is in progress, will +// cancel the request. +// +class NET_EXPORT_PRIVATE ProxyScriptDecider { + public: + // |proxy_script_fetcher|, |dhcp_proxy_script_fetcher| and + // |net_log| must remain valid for the lifespan of ProxyScriptDecider. + ProxyScriptDecider(ProxyScriptFetcher* proxy_script_fetcher, + DhcpProxyScriptFetcher* dhcp_proxy_script_fetcher, + NetLog* net_log); + + // Aborts any in-progress request. + ~ProxyScriptDecider(); + + // Evaluates the effective proxy settings for |config|, and downloads the + // associated PAC script. + // If |wait_delay| is positive, the initialization will pause for this + // amount of time before getting started. + // On successful completion, the "effective" proxy settings we ended up + // deciding on will be available vial the effective_settings() accessor. + // Note that this may differ from |config| since we will have stripped any + // manual settings, and decided whether to use auto-detect or the custom PAC + // URL. Finally, if auto-detect was used we may now have resolved that to a + // specific script URL. + int Start(const ProxyConfig& config, + const base::TimeDelta wait_delay, + bool fetch_pac_bytes, + const net::CompletionCallback& callback); + + const ProxyConfig& effective_config() const; + + // TODO(eroman): Return a const-pointer. + ProxyResolverScriptData* script_data() const; + + private: + // Represents the sources from which we can get PAC files; two types of + // auto-detect or a custom URL. + struct PacSource { + enum Type { + WPAD_DHCP, + WPAD_DNS, + CUSTOM + }; + + PacSource(Type type, const GURL& url) + : type(type), url(url) {} + + // Returns a Value representing the PacSource. |effective_pac_url| must + // be non-NULL and point to the URL derived from information contained in + // |this|, if Type is not WPAD_DHCP. + base::Value* NetLogCallback(const GURL* effective_pac_url, + NetLog::LogLevel log_level) const; + + Type type; + GURL url; // Empty unless |type == PAC_SOURCE_CUSTOM|. + }; + + typedef std::vector<PacSource> PacSourceList; + + enum State { + STATE_NONE, + STATE_WAIT, + STATE_WAIT_COMPLETE, + STATE_FETCH_PAC_SCRIPT, + STATE_FETCH_PAC_SCRIPT_COMPLETE, + STATE_VERIFY_PAC_SCRIPT, + STATE_VERIFY_PAC_SCRIPT_COMPLETE, + }; + + // Returns ordered list of PAC urls to try for |config|. + PacSourceList BuildPacSourcesFallbackList(const ProxyConfig& config) const; + + void OnIOCompletion(int result); + int DoLoop(int result); + void DoCallback(int result); + + int DoWait(); + int DoWaitComplete(int result); + + int DoFetchPacScript(); + int DoFetchPacScriptComplete(int result); + + int DoVerifyPacScript(); + int DoVerifyPacScriptComplete(int result); + + // Tries restarting using the next fallback PAC URL: + // |pac_sources_[++current_pac_source_index]|. + // Returns OK and rewinds the state machine when there + // is something to try, otherwise returns |error|. + int TryToFallbackPacSource(int error); + + // Gets the initial state (we skip fetching when the + // ProxyResolver doesn't |expect_pac_bytes()|. + State GetStartState() const; + + void DetermineURL(const PacSource& pac_source, GURL* effective_pac_url); + + // Returns the current PAC URL we are fetching/testing. + const PacSource& current_pac_source() const; + + void OnWaitTimerFired(); + void DidComplete(); + void Cancel(); + + ProxyResolver* resolver_; + ProxyScriptFetcher* proxy_script_fetcher_; + DhcpProxyScriptFetcher* dhcp_proxy_script_fetcher_; + + net::CompletionCallback callback_; + + size_t current_pac_source_index_; + + // Filled when the PAC script fetch completes. + base::string16 pac_script_; + + // Flag indicating whether the caller requested a mandatory pac script + // (i.e. fallback to direct connections are prohibited). + bool pac_mandatory_; + + PacSourceList pac_sources_; + State next_state_; + + BoundNetLog net_log_; + + bool fetch_pac_bytes_; + + base::TimeDelta wait_delay_; + base::OneShotTimer<ProxyScriptDecider> wait_timer_; + + // Results. + ProxyConfig effective_config_; + scoped_refptr<ProxyResolverScriptData> script_data_; + + + DISALLOW_COPY_AND_ASSIGN(ProxyScriptDecider); +}; + +} // namespace net + +#endif // NET_PROXY_PROXY_SCRIPT_DECIDER_H_ diff --git a/chromium/net/proxy/proxy_script_decider_unittest.cc b/chromium/net/proxy/proxy_script_decider_unittest.cc new file mode 100644 index 00000000000..977bd4df642 --- /dev/null +++ b/chromium/net/proxy/proxy_script_decider_unittest.cc @@ -0,0 +1,599 @@ +// 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 <vector> + +#include "base/bind.h" +#include "base/memory/weak_ptr.h" +#include "base/message_loop/message_loop.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/time/time.h" +#include "net/base/net_errors.h" +#include "net/base/net_log.h" +#include "net/base/net_log_unittest.h" +#include "net/base/test_completion_callback.h" +#include "net/proxy/dhcp_proxy_script_fetcher.h" +#include "net/proxy/proxy_config.h" +#include "net/proxy/proxy_resolver.h" +#include "net/proxy/proxy_script_decider.h" +#include "net/proxy/proxy_script_fetcher.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { +namespace { + +enum Error { + kFailedDownloading = -100, + kFailedParsing = ERR_PAC_SCRIPT_FAILED, +}; + +class Rules { + public: + struct Rule { + Rule(const GURL& url, int fetch_error, bool is_valid_script) + : url(url), + fetch_error(fetch_error), + is_valid_script(is_valid_script) { + } + + base::string16 text() const { + if (is_valid_script) + return UTF8ToUTF16(url.spec() + "!FindProxyForURL"); + if (fetch_error == OK) + return UTF8ToUTF16(url.spec() + "!invalid-script"); + return base::string16(); + } + + GURL url; + int fetch_error; + bool is_valid_script; + }; + + Rule AddSuccessRule(const char* url) { + Rule rule(GURL(url), OK /*fetch_error*/, true); + rules_.push_back(rule); + return rule; + } + + void AddFailDownloadRule(const char* url) { + rules_.push_back(Rule(GURL(url), kFailedDownloading /*fetch_error*/, + false)); + } + + void AddFailParsingRule(const char* url) { + rules_.push_back(Rule(GURL(url), OK /*fetch_error*/, false)); + } + + const Rule& GetRuleByUrl(const GURL& url) const { + for (RuleList::const_iterator it = rules_.begin(); it != rules_.end(); + ++it) { + if (it->url == url) + return *it; + } + LOG(FATAL) << "Rule not found for " << url; + return rules_[0]; + } + + const Rule& GetRuleByText(const base::string16& text) const { + for (RuleList::const_iterator it = rules_.begin(); it != rules_.end(); + ++it) { + if (it->text() == text) + return *it; + } + LOG(FATAL) << "Rule not found for " << text; + return rules_[0]; + } + + private: + typedef std::vector<Rule> RuleList; + RuleList rules_; +}; + +class RuleBasedProxyScriptFetcher : public ProxyScriptFetcher { + public: + explicit RuleBasedProxyScriptFetcher(const Rules* rules) : rules_(rules) {} + + // ProxyScriptFetcher implementation. + virtual int Fetch(const GURL& url, + base::string16* text, + const CompletionCallback& callback) OVERRIDE { + const Rules::Rule& rule = rules_->GetRuleByUrl(url); + int rv = rule.fetch_error; + EXPECT_NE(ERR_UNEXPECTED, rv); + if (rv == OK) + *text = rule.text(); + return rv; + } + + virtual void Cancel() OVERRIDE {} + + virtual URLRequestContext* GetRequestContext() const OVERRIDE { return NULL; } + + private: + const Rules* rules_; +}; + +// Succeed using custom PAC script. +TEST(ProxyScriptDeciderTest, CustomPacSucceeds) { + Rules rules; + RuleBasedProxyScriptFetcher fetcher(&rules); + DoNothingDhcpProxyScriptFetcher dhcp_fetcher; + + ProxyConfig config; + config.set_pac_url(GURL("http://custom/proxy.pac")); + + Rules::Rule rule = rules.AddSuccessRule("http://custom/proxy.pac"); + + TestCompletionCallback callback; + CapturingNetLog log; + ProxyScriptDecider decider(&fetcher, &dhcp_fetcher, &log); + EXPECT_EQ(OK, decider.Start( + config, base::TimeDelta(), true, callback.callback())); + EXPECT_EQ(rule.text(), decider.script_data()->utf16()); + + // Check the NetLog was filled correctly. + CapturingNetLog::CapturedEntryList entries; + log.GetEntries(&entries); + + EXPECT_EQ(4u, entries.size()); + EXPECT_TRUE(LogContainsBeginEvent( + entries, 0, NetLog::TYPE_PROXY_SCRIPT_DECIDER)); + EXPECT_TRUE(LogContainsBeginEvent( + entries, 1, NetLog::TYPE_PROXY_SCRIPT_DECIDER_FETCH_PAC_SCRIPT)); + EXPECT_TRUE(LogContainsEndEvent( + entries, 2, NetLog::TYPE_PROXY_SCRIPT_DECIDER_FETCH_PAC_SCRIPT)); + EXPECT_TRUE(LogContainsEndEvent( + entries, 3, NetLog::TYPE_PROXY_SCRIPT_DECIDER)); + + EXPECT_TRUE(decider.effective_config().has_pac_url()); + EXPECT_EQ(config.pac_url(), decider.effective_config().pac_url()); +} + +// Fail downloading the custom PAC script. +TEST(ProxyScriptDeciderTest, CustomPacFails1) { + Rules rules; + RuleBasedProxyScriptFetcher fetcher(&rules); + DoNothingDhcpProxyScriptFetcher dhcp_fetcher; + + ProxyConfig config; + config.set_pac_url(GURL("http://custom/proxy.pac")); + + rules.AddFailDownloadRule("http://custom/proxy.pac"); + + TestCompletionCallback callback; + CapturingNetLog log; + ProxyScriptDecider decider(&fetcher, &dhcp_fetcher, &log); + EXPECT_EQ(kFailedDownloading, + decider.Start(config, base::TimeDelta(), true, + callback.callback())); + EXPECT_EQ(NULL, decider.script_data()); + + // Check the NetLog was filled correctly. + CapturingNetLog::CapturedEntryList entries; + log.GetEntries(&entries); + + EXPECT_EQ(4u, entries.size()); + EXPECT_TRUE(LogContainsBeginEvent( + entries, 0, NetLog::TYPE_PROXY_SCRIPT_DECIDER)); + EXPECT_TRUE(LogContainsBeginEvent( + entries, 1, NetLog::TYPE_PROXY_SCRIPT_DECIDER_FETCH_PAC_SCRIPT)); + EXPECT_TRUE(LogContainsEndEvent( + entries, 2, NetLog::TYPE_PROXY_SCRIPT_DECIDER_FETCH_PAC_SCRIPT)); + EXPECT_TRUE(LogContainsEndEvent( + entries, 3, NetLog::TYPE_PROXY_SCRIPT_DECIDER)); + + EXPECT_FALSE(decider.effective_config().has_pac_url()); +} + +// Fail parsing the custom PAC script. +TEST(ProxyScriptDeciderTest, CustomPacFails2) { + Rules rules; + RuleBasedProxyScriptFetcher fetcher(&rules); + DoNothingDhcpProxyScriptFetcher dhcp_fetcher; + + ProxyConfig config; + config.set_pac_url(GURL("http://custom/proxy.pac")); + + rules.AddFailParsingRule("http://custom/proxy.pac"); + + TestCompletionCallback callback; + ProxyScriptDecider decider(&fetcher, &dhcp_fetcher, NULL); + EXPECT_EQ(kFailedParsing, + decider.Start(config, base::TimeDelta(), true, + callback.callback())); + EXPECT_EQ(NULL, decider.script_data()); +} + +// Fail downloading the custom PAC script, because the fetcher was NULL. +TEST(ProxyScriptDeciderTest, HasNullProxyScriptFetcher) { + Rules rules; + DoNothingDhcpProxyScriptFetcher dhcp_fetcher; + + ProxyConfig config; + config.set_pac_url(GURL("http://custom/proxy.pac")); + + TestCompletionCallback callback; + ProxyScriptDecider decider(NULL, &dhcp_fetcher, NULL); + EXPECT_EQ(ERR_UNEXPECTED, + decider.Start(config, base::TimeDelta(), true, + callback.callback())); + EXPECT_EQ(NULL, decider.script_data()); +} + +// Succeeds in choosing autodetect (WPAD DNS). +TEST(ProxyScriptDeciderTest, AutodetectSuccess) { + Rules rules; + RuleBasedProxyScriptFetcher fetcher(&rules); + DoNothingDhcpProxyScriptFetcher dhcp_fetcher; + + ProxyConfig config; + config.set_auto_detect(true); + + Rules::Rule rule = rules.AddSuccessRule("http://wpad/wpad.dat"); + + TestCompletionCallback callback; + ProxyScriptDecider decider(&fetcher, &dhcp_fetcher, NULL); + EXPECT_EQ(OK, decider.Start( + config, base::TimeDelta(), true, callback.callback())); + EXPECT_EQ(rule.text(), decider.script_data()->utf16()); + + EXPECT_TRUE(decider.effective_config().has_pac_url()); + EXPECT_EQ(rule.url, decider.effective_config().pac_url()); +} + +// Fails at WPAD (downloading), but succeeds in choosing the custom PAC. +TEST(ProxyScriptDeciderTest, AutodetectFailCustomSuccess1) { + Rules rules; + RuleBasedProxyScriptFetcher fetcher(&rules); + DoNothingDhcpProxyScriptFetcher dhcp_fetcher; + + ProxyConfig config; + config.set_auto_detect(true); + config.set_pac_url(GURL("http://custom/proxy.pac")); + + rules.AddFailDownloadRule("http://wpad/wpad.dat"); + Rules::Rule rule = rules.AddSuccessRule("http://custom/proxy.pac"); + + TestCompletionCallback callback; + ProxyScriptDecider decider(&fetcher, &dhcp_fetcher, NULL); + EXPECT_EQ(OK, decider.Start( + config, base::TimeDelta(), true, callback.callback())); + EXPECT_EQ(rule.text(), decider.script_data()->utf16()); + + EXPECT_TRUE(decider.effective_config().has_pac_url()); + EXPECT_EQ(rule.url, decider.effective_config().pac_url()); +} + +// Fails at WPAD (no DHCP config, DNS PAC fails parsing), but succeeds in +// choosing the custom PAC. +TEST(ProxyScriptDeciderTest, AutodetectFailCustomSuccess2) { + Rules rules; + RuleBasedProxyScriptFetcher fetcher(&rules); + DoNothingDhcpProxyScriptFetcher dhcp_fetcher; + + ProxyConfig config; + config.set_auto_detect(true); + config.set_pac_url(GURL("http://custom/proxy.pac")); + config.proxy_rules().ParseFromString("unused-manual-proxy:99"); + + rules.AddFailParsingRule("http://wpad/wpad.dat"); + Rules::Rule rule = rules.AddSuccessRule("http://custom/proxy.pac"); + + TestCompletionCallback callback; + CapturingNetLog log; + + ProxyScriptDecider decider(&fetcher, &dhcp_fetcher, &log); + EXPECT_EQ(OK, decider.Start(config, base::TimeDelta(), + true, callback.callback())); + EXPECT_EQ(rule.text(), decider.script_data()->utf16()); + + // Verify that the effective configuration no longer contains auto detect or + // any of the manual settings. + EXPECT_TRUE(decider.effective_config().Equals( + ProxyConfig::CreateFromCustomPacURL(GURL("http://custom/proxy.pac")))); + + // Check the NetLog was filled correctly. + // (Note that various states are repeated since both WPAD and custom + // PAC scripts are tried). + CapturingNetLog::CapturedEntryList entries; + log.GetEntries(&entries); + + EXPECT_EQ(10u, entries.size()); + EXPECT_TRUE(LogContainsBeginEvent( + entries, 0, NetLog::TYPE_PROXY_SCRIPT_DECIDER)); + // This is the DHCP phase, which fails fetching rather than parsing, so + // there is no pair of SET_PAC_SCRIPT events. + EXPECT_TRUE(LogContainsBeginEvent( + entries, 1, NetLog::TYPE_PROXY_SCRIPT_DECIDER_FETCH_PAC_SCRIPT)); + EXPECT_TRUE(LogContainsEndEvent( + entries, 2, NetLog::TYPE_PROXY_SCRIPT_DECIDER_FETCH_PAC_SCRIPT)); + EXPECT_TRUE(LogContainsEvent( + entries, 3, + NetLog::TYPE_PROXY_SCRIPT_DECIDER_FALLING_BACK_TO_NEXT_PAC_SOURCE, + NetLog::PHASE_NONE)); + // This is the DNS phase, which attempts a fetch but fails. + EXPECT_TRUE(LogContainsBeginEvent( + entries, 4, NetLog::TYPE_PROXY_SCRIPT_DECIDER_FETCH_PAC_SCRIPT)); + EXPECT_TRUE(LogContainsEndEvent( + entries, 5, NetLog::TYPE_PROXY_SCRIPT_DECIDER_FETCH_PAC_SCRIPT)); + EXPECT_TRUE(LogContainsEvent( + entries, 6, + NetLog::TYPE_PROXY_SCRIPT_DECIDER_FALLING_BACK_TO_NEXT_PAC_SOURCE, + NetLog::PHASE_NONE)); + // Finally, the custom PAC URL phase. + EXPECT_TRUE(LogContainsBeginEvent( + entries, 7, NetLog::TYPE_PROXY_SCRIPT_DECIDER_FETCH_PAC_SCRIPT)); + EXPECT_TRUE(LogContainsEndEvent( + entries, 8, NetLog::TYPE_PROXY_SCRIPT_DECIDER_FETCH_PAC_SCRIPT)); + EXPECT_TRUE(LogContainsEndEvent( + entries, 9, NetLog::TYPE_PROXY_SCRIPT_DECIDER)); +} + +// Fails at WPAD (downloading), and fails at custom PAC (downloading). +TEST(ProxyScriptDeciderTest, AutodetectFailCustomFails1) { + Rules rules; + RuleBasedProxyScriptFetcher fetcher(&rules); + DoNothingDhcpProxyScriptFetcher dhcp_fetcher; + + ProxyConfig config; + config.set_auto_detect(true); + config.set_pac_url(GURL("http://custom/proxy.pac")); + + rules.AddFailDownloadRule("http://wpad/wpad.dat"); + rules.AddFailDownloadRule("http://custom/proxy.pac"); + + TestCompletionCallback callback; + ProxyScriptDecider decider(&fetcher, &dhcp_fetcher, NULL); + EXPECT_EQ(kFailedDownloading, + decider.Start(config, base::TimeDelta(), true, + callback.callback())); + EXPECT_EQ(NULL, decider.script_data()); +} + +// Fails at WPAD (downloading), and fails at custom PAC (parsing). +TEST(ProxyScriptDeciderTest, AutodetectFailCustomFails2) { + Rules rules; + RuleBasedProxyScriptFetcher fetcher(&rules); + DoNothingDhcpProxyScriptFetcher dhcp_fetcher; + + ProxyConfig config; + config.set_auto_detect(true); + config.set_pac_url(GURL("http://custom/proxy.pac")); + + rules.AddFailDownloadRule("http://wpad/wpad.dat"); + rules.AddFailParsingRule("http://custom/proxy.pac"); + + TestCompletionCallback callback; + ProxyScriptDecider decider(&fetcher, &dhcp_fetcher, NULL); + EXPECT_EQ(kFailedParsing, + decider.Start(config, base::TimeDelta(), true, + callback.callback())); + EXPECT_EQ(NULL, decider.script_data()); +} + +// This is a copy-paste of CustomPacFails1, with the exception that we give it +// a 1 millisecond delay. This means it will now complete asynchronously. +// Moreover, we test the NetLog to make sure it logged the pause. +TEST(ProxyScriptDeciderTest, CustomPacFails1_WithPositiveDelay) { + Rules rules; + RuleBasedProxyScriptFetcher fetcher(&rules); + DoNothingDhcpProxyScriptFetcher dhcp_fetcher; + + ProxyConfig config; + config.set_pac_url(GURL("http://custom/proxy.pac")); + + rules.AddFailDownloadRule("http://custom/proxy.pac"); + + TestCompletionCallback callback; + CapturingNetLog log; + ProxyScriptDecider decider(&fetcher, &dhcp_fetcher, &log); + EXPECT_EQ(ERR_IO_PENDING, + decider.Start(config, base::TimeDelta::FromMilliseconds(1), + true, callback.callback())); + + EXPECT_EQ(kFailedDownloading, callback.WaitForResult()); + EXPECT_EQ(NULL, decider.script_data()); + + // Check the NetLog was filled correctly. + CapturingNetLog::CapturedEntryList entries; + log.GetEntries(&entries); + + EXPECT_EQ(6u, entries.size()); + EXPECT_TRUE(LogContainsBeginEvent( + entries, 0, NetLog::TYPE_PROXY_SCRIPT_DECIDER)); + EXPECT_TRUE(LogContainsBeginEvent( + entries, 1, NetLog::TYPE_PROXY_SCRIPT_DECIDER_WAIT)); + EXPECT_TRUE(LogContainsEndEvent( + entries, 2, NetLog::TYPE_PROXY_SCRIPT_DECIDER_WAIT)); + EXPECT_TRUE(LogContainsBeginEvent( + entries, 3, NetLog::TYPE_PROXY_SCRIPT_DECIDER_FETCH_PAC_SCRIPT)); + EXPECT_TRUE(LogContainsEndEvent( + entries, 4, NetLog::TYPE_PROXY_SCRIPT_DECIDER_FETCH_PAC_SCRIPT)); + EXPECT_TRUE(LogContainsEndEvent( + entries, 5, NetLog::TYPE_PROXY_SCRIPT_DECIDER)); +} + +// This is a copy-paste of CustomPacFails1, with the exception that we give it +// a -5 second delay instead of a 0 ms delay. This change should have no effect +// so the rest of the test is unchanged. +TEST(ProxyScriptDeciderTest, CustomPacFails1_WithNegativeDelay) { + Rules rules; + RuleBasedProxyScriptFetcher fetcher(&rules); + DoNothingDhcpProxyScriptFetcher dhcp_fetcher; + + ProxyConfig config; + config.set_pac_url(GURL("http://custom/proxy.pac")); + + rules.AddFailDownloadRule("http://custom/proxy.pac"); + + TestCompletionCallback callback; + CapturingNetLog log; + ProxyScriptDecider decider(&fetcher, &dhcp_fetcher, &log); + EXPECT_EQ(kFailedDownloading, + decider.Start(config, base::TimeDelta::FromSeconds(-5), + true, callback.callback())); + EXPECT_EQ(NULL, decider.script_data()); + + // Check the NetLog was filled correctly. + CapturingNetLog::CapturedEntryList entries; + log.GetEntries(&entries); + + EXPECT_EQ(4u, entries.size()); + EXPECT_TRUE(LogContainsBeginEvent( + entries, 0, NetLog::TYPE_PROXY_SCRIPT_DECIDER)); + EXPECT_TRUE(LogContainsBeginEvent( + entries, 1, NetLog::TYPE_PROXY_SCRIPT_DECIDER_FETCH_PAC_SCRIPT)); + EXPECT_TRUE(LogContainsEndEvent( + entries, 2, NetLog::TYPE_PROXY_SCRIPT_DECIDER_FETCH_PAC_SCRIPT)); + EXPECT_TRUE(LogContainsEndEvent( + entries, 3, NetLog::TYPE_PROXY_SCRIPT_DECIDER)); +} + +class SynchronousSuccessDhcpFetcher : public DhcpProxyScriptFetcher { + public: + explicit SynchronousSuccessDhcpFetcher(const base::string16& expected_text) + : gurl_("http://dhcppac/"), expected_text_(expected_text) { + } + + virtual int Fetch(base::string16* utf16_text, + const CompletionCallback& callback) OVERRIDE { + *utf16_text = expected_text_; + return OK; + } + + virtual void Cancel() OVERRIDE { + } + + virtual const GURL& GetPacURL() const OVERRIDE { + return gurl_; + } + + const base::string16& expected_text() const { + return expected_text_; + } + + private: + GURL gurl_; + base::string16 expected_text_; + + DISALLOW_COPY_AND_ASSIGN(SynchronousSuccessDhcpFetcher); +}; + +// All of the tests above that use ProxyScriptDecider have tested +// failure to fetch a PAC file via DHCP configuration, so we now test +// success at downloading and parsing, and then success at downloading, +// failure at parsing. + +TEST(ProxyScriptDeciderTest, AutodetectDhcpSuccess) { + Rules rules; + RuleBasedProxyScriptFetcher fetcher(&rules); + SynchronousSuccessDhcpFetcher dhcp_fetcher( + WideToUTF16(L"http://bingo/!FindProxyForURL")); + + ProxyConfig config; + config.set_auto_detect(true); + + rules.AddSuccessRule("http://bingo/"); + rules.AddFailDownloadRule("http://wpad/wpad.dat"); + + TestCompletionCallback callback; + ProxyScriptDecider decider(&fetcher, &dhcp_fetcher, NULL); + EXPECT_EQ(OK, decider.Start( + config, base::TimeDelta(), true, callback.callback())); + EXPECT_EQ(dhcp_fetcher.expected_text(), + decider.script_data()->utf16()); + + EXPECT_TRUE(decider.effective_config().has_pac_url()); + EXPECT_EQ(GURL("http://dhcppac/"), decider.effective_config().pac_url()); +} + +TEST(ProxyScriptDeciderTest, AutodetectDhcpFailParse) { + Rules rules; + RuleBasedProxyScriptFetcher fetcher(&rules); + SynchronousSuccessDhcpFetcher dhcp_fetcher( + WideToUTF16(L"http://bingo/!invalid-script")); + + ProxyConfig config; + config.set_auto_detect(true); + + rules.AddFailParsingRule("http://bingo/"); + rules.AddFailDownloadRule("http://wpad/wpad.dat"); + + TestCompletionCallback callback; + ProxyScriptDecider decider(&fetcher, &dhcp_fetcher, NULL); + // Since there is fallback to DNS-based WPAD, the final error will be that + // it failed downloading, not that it failed parsing. + EXPECT_EQ(kFailedDownloading, + decider.Start(config, base::TimeDelta(), true, callback.callback())); + EXPECT_EQ(NULL, decider.script_data()); + + EXPECT_FALSE(decider.effective_config().has_pac_url()); +} + +class AsyncFailDhcpFetcher + : public DhcpProxyScriptFetcher, + public base::SupportsWeakPtr<AsyncFailDhcpFetcher> { + public: + AsyncFailDhcpFetcher() {} + virtual ~AsyncFailDhcpFetcher() {} + + virtual int Fetch(base::string16* utf16_text, + const CompletionCallback& callback) OVERRIDE { + callback_ = callback; + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(&AsyncFailDhcpFetcher::CallbackWithFailure, AsWeakPtr())); + return ERR_IO_PENDING; + } + + virtual void Cancel() OVERRIDE { + callback_.Reset(); + } + + virtual const GURL& GetPacURL() const OVERRIDE { + return dummy_gurl_; + } + + void CallbackWithFailure() { + if (!callback_.is_null()) + callback_.Run(ERR_PAC_NOT_IN_DHCP); + } + + private: + GURL dummy_gurl_; + CompletionCallback callback_; +}; + +TEST(ProxyScriptDeciderTest, DhcpCancelledByDestructor) { + // This regression test would crash before + // http://codereview.chromium.org/7044058/ + // Thus, we don't care much about actual results (hence no EXPECT or ASSERT + // macros below), just that it doesn't crash. + Rules rules; + RuleBasedProxyScriptFetcher fetcher(&rules); + + scoped_ptr<AsyncFailDhcpFetcher> dhcp_fetcher(new AsyncFailDhcpFetcher()); + + ProxyConfig config; + config.set_auto_detect(true); + rules.AddFailDownloadRule("http://wpad/wpad.dat"); + + TestCompletionCallback callback; + + // Scope so ProxyScriptDecider gets destroyed early. + { + ProxyScriptDecider decider(&fetcher, dhcp_fetcher.get(), NULL); + decider.Start(config, base::TimeDelta(), true, callback.callback()); + } + + // Run the message loop to let the DHCP fetch complete and post the results + // back. Before the fix linked to above, this would try to invoke on + // the callback object provided by ProxyScriptDecider after it was + // no longer valid. + base::MessageLoop::current()->RunUntilIdle(); +} + +} // namespace +} // namespace net diff --git a/chromium/net/proxy/proxy_script_fetcher.h b/chromium/net/proxy/proxy_script_fetcher.h new file mode 100644 index 00000000000..02f3195381e --- /dev/null +++ b/chromium/net/proxy/proxy_script_fetcher.h @@ -0,0 +1,60 @@ +// 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. + +// ProxyScriptFetcher is an async interface for fetching a proxy auto config +// script. It is specific to fetching a PAC script; enforces timeout, max-size, +// status code. + +#ifndef NET_PROXY_PROXY_SCRIPT_FETCHER_H_ +#define NET_PROXY_PROXY_SCRIPT_FETCHER_H_ + +#include "base/strings/string16.h" +#include "net/base/completion_callback.h" +#include "net/base/net_export.h" + +class GURL; + +namespace net { + +class URLRequestContext; + +// Interface for downloading a PAC script. Implementations can enforce +// timeouts, maximum size constraints, content encoding, etc.. +class NET_EXPORT_PRIVATE ProxyScriptFetcher { + public: + // Destruction should cancel any outstanding requests. + virtual ~ProxyScriptFetcher() {} + + // Downloads the given PAC URL, and invokes |callback| on completion. + // Returns OK on success, otherwise the error code. If the return code is + // ERR_IO_PENDING, then the request completes asynchronously, and |callback| + // will be invoked later with the final error code. + // After synchronous or asynchronous completion with a result code of OK, + // |*utf16_text| is filled with the response. On failure, the result text is + // an empty string, and the result code is a network error. Some special + // network errors that may occur are: + // + // ERR_TIMED_OUT -- the fetch took too long to complete. + // ERR_FILE_TOO_BIG -- the response's body was too large. + // ERR_PAC_STATUS_NOT_OK -- non-200 HTTP status code. + // ERR_NOT_IMPLEMENTED -- the response required authentication. + // + // If the request is cancelled (either using the "Cancel()" method or by + // deleting |this|), then no callback is invoked. + // + // Only one fetch is allowed to be outstanding at a time. + virtual int Fetch(const GURL& url, base::string16* utf16_text, + const net::CompletionCallback& callback) = 0; + + // Aborts the in-progress fetch (if any). + virtual void Cancel() = 0; + + // Returns the request context that this fetcher uses to issue downloads, + // or NULL. + virtual URLRequestContext* GetRequestContext() const = 0; +}; + +} // namespace net + +#endif // NET_PROXY_PROXY_SCRIPT_FETCHER_H_ diff --git a/chromium/net/proxy/proxy_script_fetcher_impl.cc b/chromium/net/proxy/proxy_script_fetcher_impl.cc new file mode 100644 index 00000000000..2bf9e667792 --- /dev/null +++ b/chromium/net/proxy/proxy_script_fetcher_impl.cc @@ -0,0 +1,321 @@ +// 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/proxy/proxy_script_fetcher_impl.h" + +#include "base/compiler_specific.h" +#include "base/i18n/icu_string_conversions.h" +#include "base/logging.h" +#include "base/message_loop/message_loop.h" +#include "base/strings/string_util.h" +#include "net/base/data_url.h" +#include "net/base/io_buffer.h" +#include "net/base/load_flags.h" +#include "net/base/net_errors.h" +#include "net/cert/cert_status_flags.h" +#include "net/http/http_response_headers.h" +#include "net/url_request/url_request_context.h" + +// TODO(eroman): +// - Support auth-prompts (http://crbug.com/77366) + +namespace net { + +namespace { + +// The maximum size (in bytes) allowed for a PAC script. Responses exceeding +// this will fail with ERR_FILE_TOO_BIG. +const int kDefaultMaxResponseBytes = 1048576; // 1 megabyte + +// The maximum duration (in milliseconds) allowed for fetching the PAC script. +// Responses exceeding this will fail with ERR_TIMED_OUT. +const int kDefaultMaxDurationMs = 300000; // 5 minutes + +// Returns true if |mime_type| is one of the known PAC mime type. +bool IsPacMimeType(const std::string& mime_type) { + static const char * const kSupportedPacMimeTypes[] = { + "application/x-ns-proxy-autoconfig", + "application/x-javascript-config", + }; + for (size_t i = 0; i < arraysize(kSupportedPacMimeTypes); ++i) { + if (LowerCaseEqualsASCII(mime_type, kSupportedPacMimeTypes[i])) + return true; + } + return false; +} + +// Converts |bytes| (which is encoded by |charset|) to UTF16, saving the resul +// to |*utf16|. +// If |charset| is empty, then we don't know what it was and guess. +void ConvertResponseToUTF16(const std::string& charset, + const std::string& bytes, + base::string16* utf16) { + const char* codepage; + + if (charset.empty()) { + // Assume ISO-8859-1 if no charset was specified. + codepage = base::kCodepageLatin1; + } else { + // Otherwise trust the charset that was provided. + codepage = charset.c_str(); + } + + // We will be generous in the conversion -- if any characters lie + // outside of |charset| (i.e. invalid), then substitute them with + // U+FFFD rather than failing. + base::CodepageToUTF16(bytes, codepage, + base::OnStringConversionError::SUBSTITUTE, + utf16); +} + +} // namespace + +ProxyScriptFetcherImpl::ProxyScriptFetcherImpl( + URLRequestContext* url_request_context) + : weak_factory_(this), + url_request_context_(url_request_context), + buf_(new IOBuffer(kBufSize)), + next_id_(0), + cur_request_id_(0), + result_code_(OK), + result_text_(NULL), + max_response_bytes_(kDefaultMaxResponseBytes), + max_duration_(base::TimeDelta::FromMilliseconds(kDefaultMaxDurationMs)) { + DCHECK(url_request_context); +} + +ProxyScriptFetcherImpl::~ProxyScriptFetcherImpl() { + // The URLRequest's destructor will cancel the outstanding request, and + // ensure that the delegate (this) is not called again. +} + +base::TimeDelta ProxyScriptFetcherImpl::SetTimeoutConstraint( + base::TimeDelta timeout) { + base::TimeDelta prev = max_duration_; + max_duration_ = timeout; + return prev; +} + +size_t ProxyScriptFetcherImpl::SetSizeConstraint(size_t size_bytes) { + size_t prev = max_response_bytes_; + max_response_bytes_ = size_bytes; + return prev; +} + +void ProxyScriptFetcherImpl::OnResponseCompleted(URLRequest* request) { + DCHECK_EQ(request, cur_request_.get()); + + // Use |result_code_| as the request's error if we have already set it to + // something specific. + if (result_code_ == OK && !request->status().is_success()) + result_code_ = request->status().error(); + + FetchCompleted(); +} + +int ProxyScriptFetcherImpl::Fetch( + const GURL& url, base::string16* text, const CompletionCallback& callback) { + // It is invalid to call Fetch() while a request is already in progress. + DCHECK(!cur_request_.get()); + DCHECK(!callback.is_null()); + DCHECK(text); + + // Handle base-64 encoded data-urls that contain custom PAC scripts. + if (url.SchemeIs("data")) { + std::string mime_type; + std::string charset; + std::string data; + if (!DataURL::Parse(url, &mime_type, &charset, &data)) + return ERR_FAILED; + + ConvertResponseToUTF16(charset, data, text); + return OK; + } + + cur_request_.reset(url_request_context_->CreateRequest(url, this)); + cur_request_->set_method("GET"); + + // Make sure that the PAC script is downloaded using a direct connection, + // to avoid circular dependencies (fetching is a part of proxy resolution). + // Also disable the use of the disk cache. The cache is disabled so that if + // the user switches networks we don't potentially use the cached response + // from old network when we should in fact be re-fetching on the new network. + // If the PAC script is hosted on an HTTPS server we bypass revocation + // checking in order to avoid a circular dependency when attempting to fetch + // the OCSP response or CRL. We could make the revocation check go direct but + // the proxy might be the only way to the outside world. + cur_request_->set_load_flags(LOAD_BYPASS_PROXY | LOAD_DISABLE_CACHE | + LOAD_DISABLE_CERT_REVOCATION_CHECKING); + + // Save the caller's info for notification on completion. + callback_ = callback; + result_text_ = text; + + bytes_read_so_far_.clear(); + + // Post a task to timeout this request if it takes too long. + cur_request_id_ = ++next_id_; + base::MessageLoop::current()->PostDelayedTask( + FROM_HERE, + base::Bind(&ProxyScriptFetcherImpl::OnTimeout, + weak_factory_.GetWeakPtr(), + cur_request_id_), + max_duration_); + + // Start the request. + cur_request_->Start(); + return ERR_IO_PENDING; +} + +void ProxyScriptFetcherImpl::Cancel() { + // ResetCurRequestState will free the URLRequest, which will cause + // cancellation. + ResetCurRequestState(); +} + +URLRequestContext* ProxyScriptFetcherImpl::GetRequestContext() const { + return url_request_context_; +} + +void ProxyScriptFetcherImpl::OnAuthRequired(URLRequest* request, + AuthChallengeInfo* auth_info) { + DCHECK_EQ(request, cur_request_.get()); + // TODO(eroman): http://crbug.com/77366 + LOG(WARNING) << "Auth required to fetch PAC script, aborting."; + result_code_ = ERR_NOT_IMPLEMENTED; + request->CancelAuth(); +} + +void ProxyScriptFetcherImpl::OnSSLCertificateError(URLRequest* request, + const SSLInfo& ssl_info, + bool fatal) { + DCHECK_EQ(request, cur_request_.get()); + // Revocation check failures are not fatal. + if (IsCertStatusMinorError(ssl_info.cert_status)) { + request->ContinueDespiteLastError(); + return; + } + LOG(WARNING) << "SSL certificate error when fetching PAC script, aborting."; + // Certificate errors are in same space as net errors. + result_code_ = MapCertStatusToNetError(ssl_info.cert_status); + request->Cancel(); +} + +void ProxyScriptFetcherImpl::OnResponseStarted(URLRequest* request) { + DCHECK_EQ(request, cur_request_.get()); + + if (!request->status().is_success()) { + OnResponseCompleted(request); + return; + } + + // Require HTTP responses to have a success status code. + if (request->url().SchemeIs("http") || request->url().SchemeIs("https")) { + // NOTE about status codes: We are like Firefox 3 in this respect. + // {IE 7, Safari 3, Opera 9.5} do not care about the status code. + if (request->GetResponseCode() != 200) { + VLOG(1) << "Fetched PAC script had (bad) status line: " + << request->response_headers()->GetStatusLine(); + result_code_ = ERR_PAC_STATUS_NOT_OK; + request->Cancel(); + return; + } + + // NOTE about mime types: We do not enforce mime types on PAC files. + // This is for compatibility with {IE 7, Firefox 3, Opera 9.5}. We will + // however log mismatches to help with debugging. + std::string mime_type; + cur_request_->GetMimeType(&mime_type); + if (!IsPacMimeType(mime_type)) { + VLOG(1) << "Fetched PAC script does not have a proper mime type: " + << mime_type; + } + } + + ReadBody(request); +} + +void ProxyScriptFetcherImpl::OnReadCompleted(URLRequest* request, + int num_bytes) { + DCHECK_EQ(request, cur_request_.get()); + if (ConsumeBytesRead(request, num_bytes)) { + // Keep reading. + ReadBody(request); + } +} + +void ProxyScriptFetcherImpl::ReadBody(URLRequest* request) { + // Read as many bytes as are available synchronously. + while (true) { + int num_bytes; + if (!request->Read(buf_.get(), kBufSize, &num_bytes)) { + // Check whether the read failed synchronously. + if (!request->status().is_io_pending()) + OnResponseCompleted(request); + return; + } + if (!ConsumeBytesRead(request, num_bytes)) + return; + } +} + +bool ProxyScriptFetcherImpl::ConsumeBytesRead(URLRequest* request, + int num_bytes) { + if (num_bytes <= 0) { + // Error while reading, or EOF. + OnResponseCompleted(request); + return false; + } + + // Enforce maximum size bound. + if (num_bytes + bytes_read_so_far_.size() > + static_cast<size_t>(max_response_bytes_)) { + result_code_ = ERR_FILE_TOO_BIG; + request->Cancel(); + return false; + } + + bytes_read_so_far_.append(buf_->data(), num_bytes); + return true; +} + +void ProxyScriptFetcherImpl::FetchCompleted() { + if (result_code_ == OK) { + // The caller expects the response to be encoded as UTF16. + std::string charset; + cur_request_->GetCharset(&charset); + ConvertResponseToUTF16(charset, bytes_read_so_far_, result_text_); + } else { + // On error, the caller expects empty string for bytes. + result_text_->clear(); + } + + int result_code = result_code_; + CompletionCallback callback = callback_; + + ResetCurRequestState(); + + callback.Run(result_code); +} + +void ProxyScriptFetcherImpl::ResetCurRequestState() { + cur_request_.reset(); + cur_request_id_ = 0; + callback_.Reset(); + result_code_ = OK; + result_text_ = NULL; +} + +void ProxyScriptFetcherImpl::OnTimeout(int id) { + // Timeout tasks may outlive the URLRequest they reference. Make sure it + // is still applicable. + if (cur_request_id_ != id) + return; + + DCHECK(cur_request_.get()); + result_code_ = ERR_TIMED_OUT; + cur_request_->Cancel(); +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_script_fetcher_impl.h b/chromium/net/proxy/proxy_script_fetcher_impl.h new file mode 100644 index 00000000000..2da866901f5 --- /dev/null +++ b/chromium/net/proxy/proxy_script_fetcher_impl.h @@ -0,0 +1,127 @@ +// 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. + +#ifndef NET_PROXY_PROXY_SCRIPT_FETCHER_IMPL_H_ +#define NET_PROXY_PROXY_SCRIPT_FETCHER_IMPL_H_ + +#include <string> + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/strings/string16.h" +#include "base/time/time.h" +#include "net/proxy/proxy_script_fetcher.h" +#include "net/url_request/url_request.h" + +class GURL; + +namespace net { + +class URLRequestContext; + +// Implementation of ProxyScriptFetcher that downloads scripts using the +// specified request context. +class NET_EXPORT ProxyScriptFetcherImpl : public ProxyScriptFetcher, + public URLRequest::Delegate { + public: + // Creates a ProxyScriptFetcher that issues requests through + // |url_request_context|. |url_request_context| must remain valid for the + // lifetime of ProxyScriptFetcherImpl. + // Note that while a request is in progress, we will be holding a reference + // to |url_request_context|. Be careful not to create cycles between the + // fetcher and the context; you can break such cycles by calling Cancel(). + explicit ProxyScriptFetcherImpl(URLRequestContext* url_request_context); + + virtual ~ProxyScriptFetcherImpl(); + + // Used by unit-tests to modify the default limits. + base::TimeDelta SetTimeoutConstraint(base::TimeDelta timeout); + size_t SetSizeConstraint(size_t size_bytes); + + void OnResponseCompleted(URLRequest* request); + + // ProxyScriptFetcher methods: + virtual int Fetch(const GURL& url, base::string16* text, + const net::CompletionCallback& callback) OVERRIDE; + virtual void Cancel() OVERRIDE; + virtual URLRequestContext* GetRequestContext() const OVERRIDE; + + // URLRequest::Delegate methods: + virtual void OnAuthRequired(URLRequest* request, + AuthChallengeInfo* auth_info) OVERRIDE; + virtual void OnSSLCertificateError(URLRequest* request, + const SSLInfo& ssl_info, + bool is_hsts_ok) OVERRIDE; + virtual void OnResponseStarted(URLRequest* request) OVERRIDE; + virtual void OnReadCompleted(URLRequest* request, int num_bytes) OVERRIDE; + + private: + enum { kBufSize = 4096 }; + + // Read more bytes from the response. + void ReadBody(URLRequest* request); + + // Handles a response from Read(). Returns true if we should continue trying + // to read. |num_bytes| is 0 for EOF, and < 0 on errors. + bool ConsumeBytesRead(URLRequest* request, int num_bytes); + + // Called once the request has completed to notify the caller of + // |response_code_| and |response_text_|. + void FetchCompleted(); + + // Clear out the state for the current request. + void ResetCurRequestState(); + + // Callback for time-out task of request with id |id|. + void OnTimeout(int id); + + // Factory for creating the time-out task. This takes care of revoking + // outstanding tasks when |this| is deleted. + base::WeakPtrFactory<ProxyScriptFetcherImpl> weak_factory_; + + // The context used for making network requests. + URLRequestContext* const url_request_context_; + + // Buffer that URLRequest writes into. + scoped_refptr<IOBuffer> buf_; + + // The next ID to use for |cur_request_| (monotonically increasing). + int next_id_; + + // The current (in progress) request, or NULL. + scoped_ptr<URLRequest> cur_request_; + + // State for current request (only valid when |cur_request_| is not NULL): + + // Unique ID for the current request. + int cur_request_id_; + + // Callback to invoke on completion of the fetch. + net::CompletionCallback callback_; + + // Holds the error condition that was hit on the current request, or OK. + int result_code_; + + // Holds the bytes read so far. Will not exceed |max_response_bytes|. + std::string bytes_read_so_far_; + + // This buffer is owned by the owner of |callback|, and will be filled with + // UTF16 response on completion. + base::string16* result_text_; + + // The maximum number of bytes to allow in responses. + size_t max_response_bytes_; + + // The maximum amount of time to wait for download to complete. + base::TimeDelta max_duration_; + + DISALLOW_COPY_AND_ASSIGN(ProxyScriptFetcherImpl); +}; + +} // namespace net + +#endif // NET_PROXY_PROXY_SCRIPT_FETCHER_IMPL_H_ diff --git a/chromium/net/proxy/proxy_script_fetcher_impl_unittest.cc b/chromium/net/proxy/proxy_script_fetcher_impl_unittest.cc new file mode 100644 index 00000000000..8d42514a957 --- /dev/null +++ b/chromium/net/proxy/proxy_script_fetcher_impl_unittest.cc @@ -0,0 +1,481 @@ +// 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/proxy/proxy_script_fetcher_impl.h" + +#include <string> + +#include "base/compiler_specific.h" +#include "base/files/file_path.h" +#include "base/path_service.h" +#include "base/strings/utf_string_conversions.h" +#include "net/base/load_flags.h" +#include "net/base/net_util.h" +#include "net/base/test_completion_callback.h" +#include "net/cert/mock_cert_verifier.h" +#include "net/disk_cache/disk_cache.h" +#include "net/dns/mock_host_resolver.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/ssl/ssl_config_service_defaults.h" +#include "net/test/spawned_test_server/spawned_test_server.h" +#include "net/url_request/file_protocol_handler.h" +#include "net/url_request/url_request_context_storage.h" +#include "net/url_request/url_request_file_job.h" +#include "net/url_request/url_request_job_factory_impl.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" + +namespace net { + +// TODO(eroman): +// - Test canceling an outstanding request. +// - Test deleting ProxyScriptFetcher while a request is in progress. + +namespace { + +const base::FilePath::CharType kDocRoot[] = + FILE_PATH_LITERAL("net/data/proxy_script_fetcher_unittest"); + +struct FetchResult { + int code; + base::string16 text; +}; + +// A non-mock URL request which can access http:// and file:// urls. +class RequestContext : public URLRequestContext { + public: + RequestContext() : storage_(this) { + ProxyConfig no_proxy; + storage_.set_host_resolver(scoped_ptr<HostResolver>(new MockHostResolver)); + storage_.set_cert_verifier(new MockCertVerifier); + storage_.set_transport_security_state(new TransportSecurityState); + storage_.set_proxy_service(ProxyService::CreateFixed(no_proxy)); + storage_.set_ssl_config_service(new SSLConfigServiceDefaults); + storage_.set_http_server_properties( + scoped_ptr<HttpServerProperties>(new HttpServerPropertiesImpl())); + + HttpNetworkSession::Params params; + params.host_resolver = host_resolver(); + params.cert_verifier = cert_verifier(); + params.transport_security_state = transport_security_state(); + params.proxy_service = proxy_service(); + params.ssl_config_service = ssl_config_service(); + params.http_server_properties = http_server_properties(); + scoped_refptr<HttpNetworkSession> network_session( + new HttpNetworkSession(params)); + storage_.set_http_transaction_factory(new HttpCache( + network_session.get(), HttpCache::DefaultBackend::InMemory(0))); + URLRequestJobFactoryImpl* job_factory = new URLRequestJobFactoryImpl(); + job_factory->SetProtocolHandler("file", new FileProtocolHandler()); + storage_.set_job_factory(job_factory); + } + + virtual ~RequestContext() { + } + + private: + URLRequestContextStorage storage_; +}; + +// Get a file:// url relative to net/data/proxy/proxy_script_fetcher_unittest. +GURL GetTestFileUrl(const std::string& relpath) { + base::FilePath path; + PathService::Get(base::DIR_SOURCE_ROOT, &path); + path = path.AppendASCII("net"); + path = path.AppendASCII("data"); + path = path.AppendASCII("proxy_script_fetcher_unittest"); + GURL base_url = FilePathToFileURL(path); + return GURL(base_url.spec() + "/" + relpath); +} + +// Really simple NetworkDelegate so we can allow local file access on ChromeOS +// without introducing layering violations. Also causes a test failure if a +// request is seen that doesn't set a load flag to bypass revocation checking. + +class BasicNetworkDelegate : public NetworkDelegate { + public: + BasicNetworkDelegate() {} + virtual ~BasicNetworkDelegate() {} + + private: + virtual int OnBeforeURLRequest(URLRequest* request, + const CompletionCallback& callback, + GURL* new_url) OVERRIDE { + EXPECT_TRUE(request->load_flags() & LOAD_DISABLE_CERT_REVOCATION_CHECKING); + return OK; + } + + virtual int OnBeforeSendHeaders(URLRequest* request, + const CompletionCallback& callback, + HttpRequestHeaders* headers) OVERRIDE { + return OK; + } + + virtual void OnSendHeaders(URLRequest* request, + const HttpRequestHeaders& headers) OVERRIDE {} + + virtual int OnHeadersReceived( + URLRequest* request, + const CompletionCallback& callback, + const HttpResponseHeaders* original_response_headers, + scoped_refptr<HttpResponseHeaders>* override_response_headers) + OVERRIDE { + return OK; + } + + virtual void OnBeforeRedirect(URLRequest* request, + const GURL& new_location) OVERRIDE {} + + virtual void OnResponseStarted(URLRequest* request) OVERRIDE {} + + virtual void OnRawBytesRead(const URLRequest& request, + int bytes_read) OVERRIDE {} + + virtual void OnCompleted(URLRequest* request, bool started) OVERRIDE {} + + virtual void OnURLRequestDestroyed(URLRequest* request) OVERRIDE {} + + virtual void OnPACScriptError(int line_number, + const base::string16& error) OVERRIDE {} + + virtual NetworkDelegate::AuthRequiredResponse OnAuthRequired( + URLRequest* request, + const AuthChallengeInfo& auth_info, + const AuthCallback& callback, + AuthCredentials* credentials) OVERRIDE { + return NetworkDelegate::AUTH_REQUIRED_RESPONSE_NO_ACTION; + } + + virtual bool OnCanGetCookies(const URLRequest& request, + const CookieList& cookie_list) OVERRIDE { + return true; + } + + virtual bool OnCanSetCookie(const URLRequest& request, + const std::string& cookie_line, + CookieOptions* options) OVERRIDE { + return true; + } + + virtual bool OnCanAccessFile(const net::URLRequest& request, + const base::FilePath& path) const OVERRIDE { + return true; + } + virtual bool OnCanThrottleRequest(const URLRequest& request) const OVERRIDE { + return false; + } + + virtual int OnBeforeSocketStreamConnect( + SocketStream* stream, + const CompletionCallback& callback) OVERRIDE { + return OK; + } + + virtual void OnRequestWaitStateChange(const net::URLRequest& request, + RequestWaitState state) OVERRIDE { + } + + DISALLOW_COPY_AND_ASSIGN(BasicNetworkDelegate); +}; + +} // namespace + +class ProxyScriptFetcherImplTest : public PlatformTest { + public: + ProxyScriptFetcherImplTest() + : test_server_(SpawnedTestServer::TYPE_HTTP, + net::SpawnedTestServer::kLocalhost, + base::FilePath(kDocRoot)) { + context_.set_network_delegate(&network_delegate_); + } + + protected: + SpawnedTestServer test_server_; + BasicNetworkDelegate network_delegate_; + RequestContext context_; +}; + +TEST_F(ProxyScriptFetcherImplTest, FileUrl) { + ProxyScriptFetcherImpl pac_fetcher(&context_); + + { // Fetch a non-existent file. + base::string16 text; + TestCompletionCallback callback; + int result = pac_fetcher.Fetch(GetTestFileUrl("does-not-exist"), + &text, callback.callback()); + EXPECT_EQ(ERR_IO_PENDING, result); + EXPECT_EQ(ERR_FILE_NOT_FOUND, callback.WaitForResult()); + EXPECT_TRUE(text.empty()); + } + { // Fetch a file that exists. + base::string16 text; + TestCompletionCallback callback; + int result = pac_fetcher.Fetch(GetTestFileUrl("pac.txt"), + &text, callback.callback()); + EXPECT_EQ(ERR_IO_PENDING, result); + EXPECT_EQ(OK, callback.WaitForResult()); + EXPECT_EQ(ASCIIToUTF16("-pac.txt-\n"), text); + } +} + +// Note that all mime types are allowed for PAC file, to be consistent +// with other browsers. +TEST_F(ProxyScriptFetcherImplTest, HttpMimeType) { + ASSERT_TRUE(test_server_.Start()); + + ProxyScriptFetcherImpl pac_fetcher(&context_); + + { // Fetch a PAC with mime type "text/plain" + GURL url(test_server_.GetURL("files/pac.txt")); + base::string16 text; + TestCompletionCallback callback; + int result = pac_fetcher.Fetch(url, &text, callback.callback()); + EXPECT_EQ(ERR_IO_PENDING, result); + EXPECT_EQ(OK, callback.WaitForResult()); + EXPECT_EQ(ASCIIToUTF16("-pac.txt-\n"), text); + } + { // Fetch a PAC with mime type "text/html" + GURL url(test_server_.GetURL("files/pac.html")); + base::string16 text; + TestCompletionCallback callback; + int result = pac_fetcher.Fetch(url, &text, callback.callback()); + EXPECT_EQ(ERR_IO_PENDING, result); + EXPECT_EQ(OK, callback.WaitForResult()); + EXPECT_EQ(ASCIIToUTF16("-pac.html-\n"), text); + } + { // Fetch a PAC with mime type "application/x-ns-proxy-autoconfig" + GURL url(test_server_.GetURL("files/pac.nsproxy")); + base::string16 text; + TestCompletionCallback callback; + int result = pac_fetcher.Fetch(url, &text, callback.callback()); + EXPECT_EQ(ERR_IO_PENDING, result); + EXPECT_EQ(OK, callback.WaitForResult()); + EXPECT_EQ(ASCIIToUTF16("-pac.nsproxy-\n"), text); + } +} + +TEST_F(ProxyScriptFetcherImplTest, HttpStatusCode) { + ASSERT_TRUE(test_server_.Start()); + + ProxyScriptFetcherImpl pac_fetcher(&context_); + + { // Fetch a PAC which gives a 500 -- FAIL + GURL url(test_server_.GetURL("files/500.pac")); + base::string16 text; + TestCompletionCallback callback; + int result = pac_fetcher.Fetch(url, &text, callback.callback()); + EXPECT_EQ(ERR_IO_PENDING, result); + EXPECT_EQ(ERR_PAC_STATUS_NOT_OK, callback.WaitForResult()); + EXPECT_TRUE(text.empty()); + } + { // Fetch a PAC which gives a 404 -- FAIL + GURL url(test_server_.GetURL("files/404.pac")); + base::string16 text; + TestCompletionCallback callback; + int result = pac_fetcher.Fetch(url, &text, callback.callback()); + EXPECT_EQ(ERR_IO_PENDING, result); + EXPECT_EQ(ERR_PAC_STATUS_NOT_OK, callback.WaitForResult()); + EXPECT_TRUE(text.empty()); + } +} + +TEST_F(ProxyScriptFetcherImplTest, ContentDisposition) { + ASSERT_TRUE(test_server_.Start()); + + ProxyScriptFetcherImpl pac_fetcher(&context_); + + // Fetch PAC scripts via HTTP with a Content-Disposition header -- should + // have no effect. + GURL url(test_server_.GetURL("files/downloadable.pac")); + base::string16 text; + TestCompletionCallback callback; + int result = pac_fetcher.Fetch(url, &text, callback.callback()); + EXPECT_EQ(ERR_IO_PENDING, result); + EXPECT_EQ(OK, callback.WaitForResult()); + EXPECT_EQ(ASCIIToUTF16("-downloadable.pac-\n"), text); +} + +// Verifies that PAC scripts are not being cached. +TEST_F(ProxyScriptFetcherImplTest, NoCache) { + ASSERT_TRUE(test_server_.Start()); + + ProxyScriptFetcherImpl pac_fetcher(&context_); + + // Fetch a PAC script whose HTTP headers make it cacheable for 1 hour. + GURL url(test_server_.GetURL("files/cacheable_1hr.pac")); + { + base::string16 text; + TestCompletionCallback callback; + int result = pac_fetcher.Fetch(url, &text, callback.callback()); + EXPECT_EQ(ERR_IO_PENDING, result); + EXPECT_EQ(OK, callback.WaitForResult()); + EXPECT_EQ(ASCIIToUTF16("-cacheable_1hr.pac-\n"), text); + } + + // Kill the HTTP server. + ASSERT_TRUE(test_server_.Stop()); + + // Try to fetch the file again. Since the server is not running anymore, the + // call should fail, thus indicating that the file was not fetched from the + // local cache. + { + base::string16 text; + TestCompletionCallback callback; + int result = pac_fetcher.Fetch(url, &text, callback.callback()); + EXPECT_EQ(ERR_IO_PENDING, result); + + // Expect any error. The exact error varies by platform. + EXPECT_NE(OK, callback.WaitForResult()); + } +} + +TEST_F(ProxyScriptFetcherImplTest, TooLarge) { + ASSERT_TRUE(test_server_.Start()); + + ProxyScriptFetcherImpl pac_fetcher(&context_); + + // Set the maximum response size to 50 bytes. + int prev_size = pac_fetcher.SetSizeConstraint(50); + + // These two URLs are the same file, but are http:// vs file:// + GURL urls[] = { + test_server_.GetURL("files/large-pac.nsproxy"), + GetTestFileUrl("large-pac.nsproxy") + }; + + // Try fetching URLs that are 101 bytes large. We should abort the request + // after 50 bytes have been read, and fail with a too large error. + for (size_t i = 0; i < arraysize(urls); ++i) { + const GURL& url = urls[i]; + base::string16 text; + TestCompletionCallback callback; + int result = pac_fetcher.Fetch(url, &text, callback.callback()); + EXPECT_EQ(ERR_IO_PENDING, result); + EXPECT_EQ(ERR_FILE_TOO_BIG, callback.WaitForResult()); + EXPECT_TRUE(text.empty()); + } + + // Restore the original size bound. + pac_fetcher.SetSizeConstraint(prev_size); + + { // Make sure we can still fetch regular URLs. + GURL url(test_server_.GetURL("files/pac.nsproxy")); + base::string16 text; + TestCompletionCallback callback; + int result = pac_fetcher.Fetch(url, &text, callback.callback()); + EXPECT_EQ(ERR_IO_PENDING, result); + EXPECT_EQ(OK, callback.WaitForResult()); + EXPECT_EQ(ASCIIToUTF16("-pac.nsproxy-\n"), text); + } +} + +TEST_F(ProxyScriptFetcherImplTest, Hang) { + ASSERT_TRUE(test_server_.Start()); + + ProxyScriptFetcherImpl pac_fetcher(&context_); + + // Set the timeout period to 0.5 seconds. + base::TimeDelta prev_timeout = pac_fetcher.SetTimeoutConstraint( + base::TimeDelta::FromMilliseconds(500)); + + // Try fetching a URL which takes 1.2 seconds. We should abort the request + // after 500 ms, and fail with a timeout error. + { + GURL url(test_server_.GetURL("slow/proxy.pac?1.2")); + base::string16 text; + TestCompletionCallback callback; + int result = pac_fetcher.Fetch(url, &text, callback.callback()); + EXPECT_EQ(ERR_IO_PENDING, result); + EXPECT_EQ(ERR_TIMED_OUT, callback.WaitForResult()); + EXPECT_TRUE(text.empty()); + } + + // Restore the original timeout period. + pac_fetcher.SetTimeoutConstraint(prev_timeout); + + { // Make sure we can still fetch regular URLs. + GURL url(test_server_.GetURL("files/pac.nsproxy")); + base::string16 text; + TestCompletionCallback callback; + int result = pac_fetcher.Fetch(url, &text, callback.callback()); + EXPECT_EQ(ERR_IO_PENDING, result); + EXPECT_EQ(OK, callback.WaitForResult()); + EXPECT_EQ(ASCIIToUTF16("-pac.nsproxy-\n"), text); + } +} + +// The ProxyScriptFetcher should decode any content-codings +// (like gzip, bzip, etc.), and apply any charset conversions to yield +// UTF8. +TEST_F(ProxyScriptFetcherImplTest, Encodings) { + ASSERT_TRUE(test_server_.Start()); + + ProxyScriptFetcherImpl pac_fetcher(&context_); + + // Test a response that is gzip-encoded -- should get inflated. + { + GURL url(test_server_.GetURL("files/gzipped_pac")); + base::string16 text; + TestCompletionCallback callback; + int result = pac_fetcher.Fetch(url, &text, callback.callback()); + EXPECT_EQ(ERR_IO_PENDING, result); + EXPECT_EQ(OK, callback.WaitForResult()); + EXPECT_EQ(ASCIIToUTF16("This data was gzipped.\n"), text); + } + + // Test a response that was served as UTF-16 (BE). It should + // be converted to UTF8. + { + GURL url(test_server_.GetURL("files/utf16be_pac")); + base::string16 text; + TestCompletionCallback callback; + int result = pac_fetcher.Fetch(url, &text, callback.callback()); + EXPECT_EQ(ERR_IO_PENDING, result); + EXPECT_EQ(OK, callback.WaitForResult()); + EXPECT_EQ(ASCIIToUTF16("This was encoded as UTF-16BE.\n"), text); + } +} + +TEST_F(ProxyScriptFetcherImplTest, DataURLs) { + ProxyScriptFetcherImpl pac_fetcher(&context_); + + const char kEncodedUrl[] = + "data:application/x-ns-proxy-autoconfig;base64,ZnVuY3Rpb24gRmluZFByb3h5R" + "m9yVVJMKHVybCwgaG9zdCkgewogIGlmIChob3N0ID09ICdmb29iYXIuY29tJykKICAgIHJl" + "dHVybiAnUFJPWFkgYmxhY2tob2xlOjgwJzsKICByZXR1cm4gJ0RJUkVDVCc7Cn0="; + const char kPacScript[] = + "function FindProxyForURL(url, host) {\n" + " if (host == 'foobar.com')\n" + " return 'PROXY blackhole:80';\n" + " return 'DIRECT';\n" + "}"; + + // Test fetching a "data:"-url containing a base64 encoded PAC script. + { + GURL url(kEncodedUrl); + base::string16 text; + TestCompletionCallback callback; + int result = pac_fetcher.Fetch(url, &text, callback.callback()); + EXPECT_EQ(OK, result); + EXPECT_EQ(ASCIIToUTF16(kPacScript), text); + } + + const char kEncodedUrlBroken[] = + "data:application/x-ns-proxy-autoconfig;base64,ZnVuY3Rpb24gRmluZFByb3h5R"; + + // Test a broken "data:"-url containing a base64 encoded PAC script. + { + GURL url(kEncodedUrlBroken); + base::string16 text; + TestCompletionCallback callback; + int result = pac_fetcher.Fetch(url, &text, callback.callback()); + EXPECT_EQ(ERR_FAILED, result); + } +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_server.cc b/chromium/net/proxy/proxy_server.cc new file mode 100644 index 00000000000..6875b4a329b --- /dev/null +++ b/chromium/net/proxy/proxy_server.cc @@ -0,0 +1,243 @@ +// Copyright (c) 2010 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/proxy/proxy_server.h" + +#include <algorithm> + +#include "base/strings/string_util.h" +#include "net/base/net_util.h" +#include "net/http/http_util.h" + +namespace net { + +namespace { + +// Parses the proxy type from a PAC string, to a ProxyServer::Scheme. +// This mapping is case-insensitive. If no type could be matched +// returns SCHEME_INVALID. +ProxyServer::Scheme GetSchemeFromPacTypeInternal( + std::string::const_iterator begin, + std::string::const_iterator end) { + if (LowerCaseEqualsASCII(begin, end, "proxy")) + return ProxyServer::SCHEME_HTTP; + if (LowerCaseEqualsASCII(begin, end, "socks")) { + // Default to v4 for compatibility. This is because the SOCKS4 vs SOCKS5 + // notation didn't originally exist, so if a client returns SOCKS they + // really meant SOCKS4. + return ProxyServer::SCHEME_SOCKS4; + } + if (LowerCaseEqualsASCII(begin, end, "socks4")) + return ProxyServer::SCHEME_SOCKS4; + if (LowerCaseEqualsASCII(begin, end, "socks5")) + return ProxyServer::SCHEME_SOCKS5; + if (LowerCaseEqualsASCII(begin, end, "direct")) + return ProxyServer::SCHEME_DIRECT; + if (LowerCaseEqualsASCII(begin, end, "https")) + return ProxyServer::SCHEME_HTTPS; + + return ProxyServer::SCHEME_INVALID; +} + +// Parses the proxy scheme from a URL-like representation, to a +// ProxyServer::Scheme. This corresponds with the values used in +// ProxyServer::ToURI(). If no type could be matched, returns SCHEME_INVALID. +ProxyServer::Scheme GetSchemeFromURIInternal(std::string::const_iterator begin, + std::string::const_iterator end) { + if (LowerCaseEqualsASCII(begin, end, "http")) + return ProxyServer::SCHEME_HTTP; + if (LowerCaseEqualsASCII(begin, end, "socks4")) + return ProxyServer::SCHEME_SOCKS4; + if (LowerCaseEqualsASCII(begin, end, "socks")) + return ProxyServer::SCHEME_SOCKS5; + if (LowerCaseEqualsASCII(begin, end, "socks5")) + return ProxyServer::SCHEME_SOCKS5; + if (LowerCaseEqualsASCII(begin, end, "direct")) + return ProxyServer::SCHEME_DIRECT; + if (LowerCaseEqualsASCII(begin, end, "https")) + return ProxyServer::SCHEME_HTTPS; + return ProxyServer::SCHEME_INVALID; +} + +std::string HostNoBrackets(const std::string& host) { + // Remove brackets from an RFC 2732-style IPv6 literal address. + const std::string::size_type len = host.size(); + if (len >= 2 && host[0] == '[' && host[len - 1] == ']') + return host.substr(1, len - 2); + return host; +} + +} // namespace + +ProxyServer::ProxyServer(Scheme scheme, const HostPortPair& host_port_pair) + : scheme_(scheme), host_port_pair_(host_port_pair) { + if (scheme_ == SCHEME_DIRECT || scheme_ == SCHEME_INVALID) { + // |host_port_pair| isn't relevant for these special schemes, so none should + // have been specified. It is important for this to be consistent since we + // do raw field comparisons in the equality and comparison functions. + DCHECK(host_port_pair.Equals(HostPortPair())); + host_port_pair_ = HostPortPair(); + } +} + +const HostPortPair& ProxyServer::host_port_pair() const { + // Doesn't make sense to call this if the URI scheme doesn't + // have concept of a host. + DCHECK(is_valid() && !is_direct()); + return host_port_pair_; +} + +// static +ProxyServer ProxyServer::FromURI(const std::string& uri, + Scheme default_scheme) { + return FromURI(uri.begin(), uri.end(), default_scheme); +} + +// static +ProxyServer ProxyServer::FromURI(std::string::const_iterator begin, + std::string::const_iterator end, + Scheme default_scheme) { + // We will default to |default_scheme| if no scheme specifier was given. + Scheme scheme = default_scheme; + + // Trim the leading/trailing whitespace. + HttpUtil::TrimLWS(&begin, &end); + + // Check for [<scheme> "://"] + std::string::const_iterator colon = std::find(begin, end, ':'); + if (colon != end && + (end - colon) >= 3 && + *(colon + 1) == '/' && + *(colon + 2) == '/') { + scheme = GetSchemeFromURIInternal(begin, colon); + begin = colon + 3; // Skip past the "://" + } + + // Now parse the <host>[":"<port>]. + return FromSchemeHostAndPort(scheme, begin, end); +} + +std::string ProxyServer::ToURI() const { + switch (scheme_) { + case SCHEME_DIRECT: + return "direct://"; + case SCHEME_HTTP: + // Leave off "http://" since it is our default scheme. + return host_port_pair().ToString(); + case SCHEME_SOCKS4: + return std::string("socks4://") + host_port_pair().ToString(); + case SCHEME_SOCKS5: + return std::string("socks5://") + host_port_pair().ToString(); + case SCHEME_HTTPS: + return std::string("https://") + host_port_pair().ToString(); + default: + // Got called with an invalid scheme. + NOTREACHED(); + return std::string(); + } +} + +// static +ProxyServer ProxyServer::FromPacString(const std::string& pac_string) { + return FromPacString(pac_string.begin(), pac_string.end()); +} + +// static +ProxyServer ProxyServer::FromPacString(std::string::const_iterator begin, + std::string::const_iterator end) { + // Trim the leading/trailing whitespace. + HttpUtil::TrimLWS(&begin, &end); + + // Input should match: + // "DIRECT" | ( <type> 1*(LWS) <host-and-port> ) + + // Start by finding the first space (if any). + std::string::const_iterator space; + for (space = begin; space != end; ++space) { + if (HttpUtil::IsLWS(*space)) { + break; + } + } + + // Everything to the left of the space is the scheme. + Scheme scheme = GetSchemeFromPacTypeInternal(begin, space); + + // And everything to the right of the space is the + // <host>[":" <port>]. + return FromSchemeHostAndPort(scheme, space, end); +} + +std::string ProxyServer::ToPacString() const { + switch (scheme_) { + case SCHEME_DIRECT: + return "DIRECT"; + case SCHEME_HTTP: + return std::string("PROXY ") + host_port_pair().ToString(); + case SCHEME_SOCKS4: + // For compatibility send SOCKS instead of SOCKS4. + return std::string("SOCKS ") + host_port_pair().ToString(); + case SCHEME_SOCKS5: + return std::string("SOCKS5 ") + host_port_pair().ToString(); + case SCHEME_HTTPS: + return std::string("HTTPS ") + host_port_pair().ToString(); + default: + // Got called with an invalid scheme. + NOTREACHED(); + return std::string(); + } +} + +// static +int ProxyServer::GetDefaultPortForScheme(Scheme scheme) { + switch (scheme) { + case SCHEME_HTTP: + return 80; + case SCHEME_SOCKS4: + case SCHEME_SOCKS5: + return 1080; + case SCHEME_HTTPS: + return 443; + default: + return -1; + } +} + +// static +ProxyServer::Scheme ProxyServer::GetSchemeFromURI(const std::string& scheme) { + return GetSchemeFromURIInternal(scheme.begin(), scheme.end()); +} + +// static +ProxyServer ProxyServer::FromSchemeHostAndPort( + Scheme scheme, + std::string::const_iterator begin, + std::string::const_iterator end) { + + // Trim leading/trailing space. + HttpUtil::TrimLWS(&begin, &end); + + if (scheme == SCHEME_DIRECT && begin != end) + return ProxyServer(); // Invalid -- DIRECT cannot have a host/port. + + HostPortPair host_port_pair; + + if (scheme != SCHEME_INVALID && scheme != SCHEME_DIRECT) { + std::string host; + int port = -1; + // If the scheme has a host/port, parse it. + bool ok = net::ParseHostAndPort(begin, end, &host, &port); + if (!ok) + return ProxyServer(); // Invalid -- failed parsing <host>[":"<port>] + + // Choose a default port number if none was given. + if (port == -1) + port = GetDefaultPortForScheme(scheme); + + host_port_pair = HostPortPair(HostNoBrackets(host), port); + } + + return ProxyServer(scheme, host_port_pair); +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_server.h b/chromium/net/proxy/proxy_server.h new file mode 100644 index 00000000000..00cc9fddaf6 --- /dev/null +++ b/chromium/net/proxy/proxy_server.h @@ -0,0 +1,165 @@ +// 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. + +#ifndef NET_PROXY_PROXY_SERVER_H_ +#define NET_PROXY_PROXY_SERVER_H_ + +#include "build/build_config.h" + +#if defined(OS_MACOSX) +#include <CoreFoundation/CoreFoundation.h> +#endif + +#include <string> +#include "net/base/host_port_pair.h" +#include "net/base/net_export.h" + +namespace net { + +// ProxyServer encodes the {type, host, port} of a proxy server. +// ProxyServer is immutable. +class NET_EXPORT ProxyServer { + public: + // The type of proxy. These are defined as bit flags so they can be ORed + // together to pass as the |scheme_bit_field| argument to + // ProxyService::RemoveProxiesWithoutScheme(). + enum Scheme { + SCHEME_INVALID = 1 << 0, + SCHEME_DIRECT = 1 << 1, + SCHEME_HTTP = 1 << 2, + SCHEME_SOCKS4 = 1 << 3, + SCHEME_SOCKS5 = 1 << 4, + SCHEME_HTTPS = 1 << 5, + }; + + // Default copy-constructor and assignment operator are OK! + + // Constructs an invalid ProxyServer. + ProxyServer() : scheme_(SCHEME_INVALID) {} + + ProxyServer(Scheme scheme, const HostPortPair& host_port_pair); + + bool is_valid() const { return scheme_ != SCHEME_INVALID; } + + // Gets the proxy's scheme (i.e. SOCKS4, SOCKS5, HTTP) + Scheme scheme() const { return scheme_; } + + // Returns true if this ProxyServer is actually just a DIRECT connection. + bool is_direct() const { return scheme_ == SCHEME_DIRECT; } + + // Returns true if this ProxyServer is an HTTP proxy. + bool is_http() const { return scheme_ == SCHEME_HTTP; } + + // Returns true if this ProxyServer is an HTTPS proxy. + bool is_https() const { return scheme_ == SCHEME_HTTPS; } + + // Returns true if this ProxyServer is a SOCKS proxy. + bool is_socks() const { + return scheme_ == SCHEME_SOCKS4 || scheme_ == SCHEME_SOCKS5; + } + + const HostPortPair& host_port_pair() const; + + // Parses from an input with format: + // [<scheme>"://"]<server>[":"<port>] + // + // Both <scheme> and <port> are optional. If <scheme> is omitted, it will be + // assumed as |default_scheme|. If <port> is omitted, it will be assumed as + // the default port for the chosen scheme (80 for "http", 1080 for "socks"). + // + // If parsing fails the instance will be set to invalid. + // + // Examples (for |default_scheme| = SCHEME_HTTP ): + // "foopy" {scheme=HTTP, host="foopy", port=80} + // "socks://foopy" {scheme=SOCKS5, host="foopy", port=1080} + // "socks4://foopy" {scheme=SOCKS4, host="foopy", port=1080} + // "socks5://foopy" {scheme=SOCKS5, host="foopy", port=1080} + // "http://foopy:17" {scheme=HTTP, host="foopy", port=17} + // "https://foopy:17" {scheme=HTTPS, host="foopy", port=17} + // "direct://" {scheme=DIRECT} + // "foopy:X" INVALID -- bad port. + static ProxyServer FromURI(const std::string& uri, Scheme default_scheme); + static ProxyServer FromURI(std::string::const_iterator uri_begin, + std::string::const_iterator uri_end, + Scheme default_scheme); + + // Formats as a URI string. This does the reverse of FromURI. + std::string ToURI() const; + + // Parses from a PAC string result. + // + // If <port> is omitted, it will be assumed as the default port for the + // chosen scheme (80 for "http", 1080 for "socks"). + // + // If parsing fails the instance will be set to invalid. + // + // Examples: + // "PROXY foopy:19" {scheme=HTTP, host="foopy", port=19} + // "DIRECT" {scheme=DIRECT} + // "SOCKS5 foopy" {scheme=SOCKS5, host="foopy", port=1080} + // "HTTPS foopy:123" {scheme=HTTPS, host="foopy", port=123} + // "BLAH xxx:xx" INVALID + static ProxyServer FromPacString(const std::string& pac_string); + static ProxyServer FromPacString(std::string::const_iterator pac_string_begin, + std::string::const_iterator pac_string_end); + + // Returns a ProxyServer representing DIRECT connections. + static ProxyServer Direct() { + return ProxyServer(SCHEME_DIRECT, HostPortPair()); + } + +#if defined(OS_MACOSX) + // Utility function to pull out a host/port pair from a dictionary and return + // it as a ProxyServer object. Pass in a dictionary that has a value for the + // host key and optionally a value for the port key. In the error condition + // where the host value is especially malformed, returns an invalid + // ProxyServer. + static ProxyServer FromDictionary(Scheme scheme, + CFDictionaryRef dict, + CFStringRef host_key, + CFStringRef port_key); +#endif + + // Formats as a PAC result entry. This does the reverse of FromPacString(). + std::string ToPacString() const; + + // Returns the default port number for a proxy server with the specified + // scheme. Returns -1 if unknown. + static int GetDefaultPortForScheme(Scheme scheme); + + // Parses the proxy scheme from a URL-like representation, to a + // ProxyServer::Scheme. This corresponds with the values used in + // ProxyServer::ToURI(). If no type could be matched, returns SCHEME_INVALID. + // |scheme| can be one of http, https, socks, socks4, socks5, direct. + static Scheme GetSchemeFromURI(const std::string& scheme); + + bool operator==(const ProxyServer& other) const { + return scheme_ == other.scheme_ && + host_port_pair_.Equals(other.host_port_pair_); + } + + // Comparator function so this can be placed in a std::map. + bool operator<(const ProxyServer& other) const { + if (scheme_ != other.scheme_) + return scheme_ < other.scheme_; + return host_port_pair_ < other.host_port_pair_; + } + + private: + // Creates a ProxyServer given a scheme, and host/port string. If parsing the + // host/port string fails, the returned instance will be invalid. + static ProxyServer FromSchemeHostAndPort( + Scheme scheme, + std::string::const_iterator host_and_port_begin, + std::string::const_iterator host_and_port_end); + + Scheme scheme_; + HostPortPair host_port_pair_; +}; + +typedef std::pair<HostPortPair, ProxyServer> HostPortProxyPair; + +} // namespace net + +#endif // NET_PROXY_PROXY_SERVER_H_ diff --git a/chromium/net/proxy/proxy_server_mac.cc b/chromium/net/proxy/proxy_server_mac.cc new file mode 100644 index 00000000000..fe3faa93e52 --- /dev/null +++ b/chromium/net/proxy/proxy_server_mac.cc @@ -0,0 +1,49 @@ +// 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/proxy/proxy_server.h" + +#include <CoreFoundation/CoreFoundation.h> + +#include <string> + +#include "base/logging.h" +#include "base/mac/foundation_util.h" +#include "base/strings/sys_string_conversions.h" + +namespace net { + +// static +ProxyServer ProxyServer::FromDictionary(Scheme scheme, + CFDictionaryRef dict, + CFStringRef host_key, + CFStringRef port_key) { + if (scheme == SCHEME_INVALID || scheme == SCHEME_DIRECT) { + // No hostname port to extract; we are done. + return ProxyServer(scheme, HostPortPair()); + } + + CFStringRef host_ref = + base::mac::GetValueFromDictionary<CFStringRef>(dict, host_key); + if (!host_ref) { + LOG(WARNING) << "Could not find expected key " + << base::SysCFStringRefToUTF8(host_key) + << " in the proxy dictionary"; + return ProxyServer(); // Invalid. + } + std::string host = base::SysCFStringRefToUTF8(host_ref); + + CFNumberRef port_ref = + base::mac::GetValueFromDictionary<CFNumberRef>(dict, port_key); + int port; + if (port_ref) { + CFNumberGetValue(port_ref, kCFNumberIntType, &port); + } else { + port = GetDefaultPortForScheme(scheme); + } + + return ProxyServer(scheme, HostPortPair(host, port)); +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_server_unittest.cc b/chromium/net/proxy/proxy_server_unittest.cc new file mode 100644 index 00000000000..7646467538b --- /dev/null +++ b/chromium/net/proxy/proxy_server_unittest.cc @@ -0,0 +1,368 @@ +// Copyright (c) 2010 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 "base/basictypes.h" +#include "net/proxy/proxy_server.h" +#include "testing/gtest/include/gtest/gtest.h" + +// Test the creation of ProxyServer using ProxyServer::FromURI, which parses +// inputs of the form [<scheme>"://"]<host>[":"<port>]. Verify that each part +// was labelled correctly, and the accessors all give the right data. +TEST(ProxyServerTest, FromURI) { + const struct { + const char* input_uri; + const char* expected_uri; + net::ProxyServer::Scheme expected_scheme; + const char* expected_host; + int expected_port; + const char* expected_pac_string; + } tests[] = { + // HTTP proxy URIs: + { + "foopy:10", // No scheme. + "foopy:10", + net::ProxyServer::SCHEME_HTTP, + "foopy", + 10, + "PROXY foopy:10" + }, + { + "http://foopy", // No port. + "foopy:80", + net::ProxyServer::SCHEME_HTTP, + "foopy", + 80, + "PROXY foopy:80" + }, + { + "http://foopy:10", + "foopy:10", + net::ProxyServer::SCHEME_HTTP, + "foopy", + 10, + "PROXY foopy:10" + }, + + // IPv6 HTTP proxy URIs: + { + "[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:10", // No scheme. + "[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:10", + net::ProxyServer::SCHEME_HTTP, + "FEDC:BA98:7654:3210:FEDC:BA98:7654:3210", + 10, + "PROXY [FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:10" + }, + { + "http://[3ffe:2a00:100:7031::1]", // No port. + "[3ffe:2a00:100:7031::1]:80", + net::ProxyServer::SCHEME_HTTP, + "3ffe:2a00:100:7031::1", + 80, + "PROXY [3ffe:2a00:100:7031::1]:80" + }, + { + "http://[::192.9.5.5]", + "[::192.9.5.5]:80", + net::ProxyServer::SCHEME_HTTP, + "::192.9.5.5", + 80, + "PROXY [::192.9.5.5]:80" + }, + { + "http://[::FFFF:129.144.52.38]:80", + "[::FFFF:129.144.52.38]:80", + net::ProxyServer::SCHEME_HTTP, + "::FFFF:129.144.52.38", + 80, + "PROXY [::FFFF:129.144.52.38]:80" + }, + + // SOCKS4 proxy URIs: + { + "socks4://foopy", // No port. + "socks4://foopy:1080", + net::ProxyServer::SCHEME_SOCKS4, + "foopy", + 1080, + "SOCKS foopy:1080" + }, + { + "socks4://foopy:10", + "socks4://foopy:10", + net::ProxyServer::SCHEME_SOCKS4, + "foopy", + 10, + "SOCKS foopy:10" + }, + + // SOCKS5 proxy URIs + { + "socks5://foopy", // No port. + "socks5://foopy:1080", + net::ProxyServer::SCHEME_SOCKS5, + "foopy", + 1080, + "SOCKS5 foopy:1080" + }, + { + "socks5://foopy:10", + "socks5://foopy:10", + net::ProxyServer::SCHEME_SOCKS5, + "foopy", + 10, + "SOCKS5 foopy:10" + }, + + // SOCKS proxy URIs (should default to SOCKS5) + { + "socks://foopy", // No port. + "socks5://foopy:1080", + net::ProxyServer::SCHEME_SOCKS5, + "foopy", + 1080, + "SOCKS5 foopy:1080" + }, + { + "socks://foopy:10", + "socks5://foopy:10", + net::ProxyServer::SCHEME_SOCKS5, + "foopy", + 10, + "SOCKS5 foopy:10" + }, + + // HTTPS proxy URIs: + { + "https://foopy", // No port + "https://foopy:443", + net::ProxyServer::SCHEME_HTTPS, + "foopy", + 443, + "HTTPS foopy:443" + }, + { + "https://foopy:10", // Non-standard port + "https://foopy:10", + net::ProxyServer::SCHEME_HTTPS, + "foopy", + 10, + "HTTPS foopy:10" + }, + { + "https://1.2.3.4:10", // IP Address + "https://1.2.3.4:10", + net::ProxyServer::SCHEME_HTTPS, + "1.2.3.4", + 10, + "HTTPS 1.2.3.4:10" + }, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { + net::ProxyServer uri = + net::ProxyServer::FromURI(tests[i].input_uri, + net::ProxyServer::SCHEME_HTTP); + EXPECT_TRUE(uri.is_valid()); + EXPECT_FALSE(uri.is_direct()); + EXPECT_EQ(tests[i].expected_uri, uri.ToURI()); + EXPECT_EQ(tests[i].expected_scheme, uri.scheme()); + EXPECT_EQ(tests[i].expected_host, uri.host_port_pair().host()); + EXPECT_EQ(tests[i].expected_port, uri.host_port_pair().port()); + EXPECT_EQ(tests[i].expected_pac_string, uri.ToPacString()); + } +} + +TEST(ProxyServerTest, DefaultConstructor) { + net::ProxyServer proxy_server; + EXPECT_FALSE(proxy_server.is_valid()); +} + +// Test parsing of the special URI form "direct://". Analagous to the "DIRECT" +// entry in a PAC result. +TEST(ProxyServerTest, Direct) { + net::ProxyServer uri = + net::ProxyServer::FromURI("direct://", net::ProxyServer::SCHEME_HTTP); + EXPECT_TRUE(uri.is_valid()); + EXPECT_TRUE(uri.is_direct()); + EXPECT_EQ("direct://", uri.ToURI()); + EXPECT_EQ("DIRECT", uri.ToPacString()); +} + +// Test parsing some invalid inputs. +TEST(ProxyServerTest, Invalid) { + const char* tests[] = { + "", + " ", + "dddf:", // not a valid port + "dddd:d", // not a valid port + "http://", // not a valid host/port. + "direct://xyz", // direct is not allowed a host/port. + "http:/", // ambiguous, but will fail because of bad port. + "http:", // ambiguous, but will fail because of bad port. + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { + net::ProxyServer uri = + net::ProxyServer::FromURI(tests[i], net::ProxyServer::SCHEME_HTTP); + EXPECT_FALSE(uri.is_valid()); + EXPECT_FALSE(uri.is_direct()); + EXPECT_FALSE(uri.is_http()); + EXPECT_FALSE(uri.is_socks()); + } +} + +// Test that LWS (SP | HT) is disregarded from the ends. +TEST(ProxyServerTest, Whitespace) { + const char* tests[] = { + " foopy:80", + "foopy:80 \t", + " \tfoopy:80 ", + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { + net::ProxyServer uri = + net::ProxyServer::FromURI(tests[i], net::ProxyServer::SCHEME_HTTP); + EXPECT_EQ("foopy:80", uri.ToURI()); + } +} + +// Test parsing a ProxyServer from a PAC representation. +TEST(ProxyServerTest, FromPACString) { + const struct { + const char* input_pac; + const char* expected_uri; + } tests[] = { + { + "PROXY foopy:10", + "foopy:10", + }, + { + " PROXY foopy:10 ", + "foopy:10", + }, + { + "pRoXy foopy:10", + "foopy:10", + }, + { + "PROXY foopy", // No port. + "foopy:80", + }, + { + "socks foopy", + "socks4://foopy:1080", + }, + { + "socks4 foopy", + "socks4://foopy:1080", + }, + { + "socks5 foopy", + "socks5://foopy:1080", + }, + { + "socks5 foopy:11", + "socks5://foopy:11", + }, + { + " direct ", + "direct://", + }, + { + "https foopy", + "https://foopy:443", + }, + { + "https foopy:10", + "https://foopy:10", + }, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { + net::ProxyServer uri = net::ProxyServer::FromPacString(tests[i].input_pac); + EXPECT_TRUE(uri.is_valid()); + EXPECT_EQ(tests[i].expected_uri, uri.ToURI()); + } +} + +// Test parsing a ProxyServer from an invalid PAC representation. +TEST(ProxyServerTest, FromPACStringInvalid) { + const char* tests[] = { + "PROXY", // missing host/port. + "HTTPS", // missing host/port. + "SOCKS", // missing host/port. + "DIRECT foopy:10", // direct cannot have host/port. + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { + net::ProxyServer uri = net::ProxyServer::FromPacString(tests[i]); + EXPECT_FALSE(uri.is_valid()); + } +} + +TEST(ProxyServerTest, ComparatorAndEquality) { + struct { + // Inputs. + const char* server1; + const char* server2; + + // Expectation. + // -1 means server1 is less than server2 + // 0 means server1 equals server2 + // 1 means server1 is greater than server2 + int expected_comparison; + } tests[] = { + { // Equal. + "foo:11", + "http://foo:11", + 0 + }, + { // Port is different. + "foo:333", + "foo:444", + -1 + }, + { // Host is different. + "foo:33", + "bar:33", + 1 + }, + { // Scheme is different. + "socks4://foo:33", + "http://foo:33", + 1 + }, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { + // Parse the expected inputs to ProxyServer instances. + const net::ProxyServer server1 = + net::ProxyServer::FromURI( + tests[i].server1, net::ProxyServer::SCHEME_HTTP); + + const net::ProxyServer server2 = + net::ProxyServer::FromURI( + tests[i].server2, net::ProxyServer::SCHEME_HTTP); + + switch (tests[i].expected_comparison) { + case -1: + EXPECT_TRUE(server1 < server2); + EXPECT_FALSE(server2 < server1); + EXPECT_FALSE(server2 == server1); + break; + case 0: + EXPECT_FALSE(server1 < server2); + EXPECT_FALSE(server2 < server1); + EXPECT_TRUE(server2 == server1); + break; + case 1: + EXPECT_FALSE(server1 < server2); + EXPECT_TRUE(server2 < server1); + EXPECT_FALSE(server2 == server1); + break; + default: + FAIL() << "Invalid expectation. Can be only -1, 0, 1"; + } + } +} diff --git a/chromium/net/proxy/proxy_service.cc b/chromium/net/proxy/proxy_service.cc new file mode 100644 index 00000000000..33bbd490842 --- /dev/null +++ b/chromium/net/proxy/proxy_service.cc @@ -0,0 +1,1576 @@ +// 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/proxy/proxy_service.h" + +#include <algorithm> + +#include "base/bind.h" +#include "base/bind_helpers.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/message_loop/message_loop_proxy.h" +#include "base/strings/string_util.h" +#include "base/thread_task_runner_handle.h" +#include "base/values.h" +#include "net/base/completion_callback.h" +#include "net/base/net_errors.h" +#include "net/base/net_log.h" +#include "net/base/net_util.h" +#include "net/proxy/dhcp_proxy_script_fetcher.h" +#include "net/proxy/multi_threaded_proxy_resolver.h" +#include "net/proxy/network_delegate_error_observer.h" +#include "net/proxy/proxy_config_service_fixed.h" +#include "net/proxy/proxy_resolver.h" +#include "net/proxy/proxy_script_decider.h" +#include "net/proxy/proxy_script_fetcher.h" +#include "net/url_request/url_request_context.h" +#include "url/gurl.h" + +#if defined(OS_WIN) +#include "net/proxy/proxy_config_service_win.h" +#include "net/proxy/proxy_resolver_winhttp.h" +#elif defined(OS_IOS) +#include "net/proxy/proxy_config_service_ios.h" +#include "net/proxy/proxy_resolver_mac.h" +#elif defined(OS_MACOSX) +#include "net/proxy/proxy_config_service_mac.h" +#include "net/proxy/proxy_resolver_mac.h" +#elif defined(OS_LINUX) && !defined(OS_CHROMEOS) +#include "net/proxy/proxy_config_service_linux.h" +#elif defined(OS_ANDROID) +#include "net/proxy/proxy_config_service_android.h" +#endif + +using base::TimeDelta; +using base::TimeTicks; + +namespace net { + +namespace { + +const size_t kMaxNumNetLogEntries = 100; + +// When the IP address changes we don't immediately re-run proxy auto-config. +// Instead, we wait for |kDelayAfterNetworkChangesMs| before +// attempting to re-valuate proxy auto-config. +// +// During this time window, any resolve requests sent to the ProxyService will +// be queued. Once we have waited the required amount of them, the proxy +// auto-config step will be run, and the queued requests resumed. +// +// The reason we play this game is that our signal for detecting network +// changes (NetworkChangeNotifier) may fire *before* the system's networking +// dependencies are fully configured. This is a problem since it means if +// we were to run proxy auto-config right away, it could fail due to spurious +// DNS failures. (see http://crbug.com/50779 for more details.) +// +// By adding the wait window, we give things a better chance to get properly +// set up. Network failures can happen at any time though, so we additionally +// poll the PAC script for changes, which will allow us to recover from these +// sorts of problems. +const int64 kDelayAfterNetworkChangesMs = 2000; + +// This is the default policy for polling the PAC script. +// +// In response to a failure, the poll intervals are: +// 0: 8 seconds (scheduled on timer) +// 1: 32 seconds +// 2: 2 minutes +// 3+: 4 hours +// +// In response to a success, the poll intervals are: +// 0+: 12 hours +// +// Only the 8 second poll is scheduled on a timer, the rest happen in response +// to network activity (and hence will take longer than the written time). +// +// Explanation for these values: +// +// TODO(eroman): These values are somewhat arbitrary, and need to be tuned +// using some histograms data. Trying to be conservative so as not to break +// existing setups when deployed. A simple exponential retry scheme would be +// more elegant, but places more load on server. +// +// The motivation for trying quickly after failures (8 seconds) is to recover +// from spurious network failures, which are common after the IP address has +// just changed (like DNS failing to resolve). The next 32 second boundary is +// to try and catch other VPN weirdness which anecdotally I have seen take +// 10+ seconds for some users. +// +// The motivation for re-trying after a success is to check for possible +// content changes to the script, or to the WPAD auto-discovery results. We are +// not very aggressive with these checks so as to minimize the risk of +// overloading existing PAC setups. Moreover it is unlikely that PAC scripts +// change very frequently in existing setups. More research is needed to +// motivate what safe values are here, and what other user agents do. +// +// Comparison to other browsers: +// +// In Firefox the PAC URL is re-tried on failures according to +// network.proxy.autoconfig_retry_interval_min and +// network.proxy.autoconfig_retry_interval_max. The defaults are 5 seconds and +// 5 minutes respectively. It doubles the interval at each attempt. +// +// TODO(eroman): Figure out what Internet Explorer does. +class DefaultPollPolicy : public ProxyService::PacPollPolicy { + public: + DefaultPollPolicy() {} + + virtual Mode GetNextDelay(int initial_error, + TimeDelta current_delay, + TimeDelta* next_delay) const OVERRIDE { + if (initial_error != OK) { + // Re-try policy for failures. + const int kDelay1Seconds = 8; + const int kDelay2Seconds = 32; + const int kDelay3Seconds = 2 * 60; // 2 minutes + const int kDelay4Seconds = 4 * 60 * 60; // 4 Hours + + // Initial poll. + if (current_delay < TimeDelta()) { + *next_delay = TimeDelta::FromSeconds(kDelay1Seconds); + return MODE_USE_TIMER; + } + switch (current_delay.InSeconds()) { + case kDelay1Seconds: + *next_delay = TimeDelta::FromSeconds(kDelay2Seconds); + return MODE_START_AFTER_ACTIVITY; + case kDelay2Seconds: + *next_delay = TimeDelta::FromSeconds(kDelay3Seconds); + return MODE_START_AFTER_ACTIVITY; + default: + *next_delay = TimeDelta::FromSeconds(kDelay4Seconds); + return MODE_START_AFTER_ACTIVITY; + } + } else { + // Re-try policy for succeses. + *next_delay = TimeDelta::FromHours(12); + return MODE_START_AFTER_ACTIVITY; + } + } + + private: + DISALLOW_COPY_AND_ASSIGN(DefaultPollPolicy); +}; + +// Config getter that always returns direct settings. +class ProxyConfigServiceDirect : public ProxyConfigService { + public: + // ProxyConfigService implementation: + virtual void AddObserver(Observer* observer) OVERRIDE {} + virtual void RemoveObserver(Observer* observer) OVERRIDE {} + virtual ConfigAvailability GetLatestProxyConfig(ProxyConfig* config) + OVERRIDE { + *config = ProxyConfig::CreateDirect(); + config->set_source(PROXY_CONFIG_SOURCE_UNKNOWN); + return CONFIG_VALID; + } +}; + +// Proxy resolver that fails every time. +class ProxyResolverNull : public ProxyResolver { + public: + ProxyResolverNull() : ProxyResolver(false /*expects_pac_bytes*/) {} + + // ProxyResolver implementation. + virtual int GetProxyForURL(const GURL& url, + ProxyInfo* results, + const CompletionCallback& callback, + RequestHandle* request, + const BoundNetLog& net_log) OVERRIDE { + return ERR_NOT_IMPLEMENTED; + } + + virtual void CancelRequest(RequestHandle request) OVERRIDE { + NOTREACHED(); + } + + virtual LoadState GetLoadState(RequestHandle request) const OVERRIDE { + NOTREACHED(); + return LOAD_STATE_IDLE; + } + + virtual void CancelSetPacScript() OVERRIDE { + NOTREACHED(); + } + + virtual int SetPacScript( + const scoped_refptr<ProxyResolverScriptData>& /*script_data*/, + const CompletionCallback& /*callback*/) OVERRIDE { + return ERR_NOT_IMPLEMENTED; + } +}; + +// ProxyResolver that simulates a PAC script which returns +// |pac_string| for every single URL. +class ProxyResolverFromPacString : public ProxyResolver { + public: + explicit ProxyResolverFromPacString(const std::string& pac_string) + : ProxyResolver(false /*expects_pac_bytes*/), + pac_string_(pac_string) {} + + virtual int GetProxyForURL(const GURL& url, + ProxyInfo* results, + const CompletionCallback& callback, + RequestHandle* request, + const BoundNetLog& net_log) OVERRIDE { + results->UsePacString(pac_string_); + return OK; + } + + virtual void CancelRequest(RequestHandle request) OVERRIDE { + NOTREACHED(); + } + + virtual LoadState GetLoadState(RequestHandle request) const OVERRIDE { + NOTREACHED(); + return LOAD_STATE_IDLE; + } + + virtual void CancelSetPacScript() OVERRIDE { + NOTREACHED(); + } + + virtual int SetPacScript( + const scoped_refptr<ProxyResolverScriptData>& pac_script, + const CompletionCallback& callback) OVERRIDE { + return OK; + } + + private: + const std::string pac_string_; +}; + +// Creates ProxyResolvers using a platform-specific implementation. +class ProxyResolverFactoryForSystem : public ProxyResolverFactory { + public: + ProxyResolverFactoryForSystem() + : ProxyResolverFactory(false /*expects_pac_bytes*/) {} + + virtual ProxyResolver* CreateProxyResolver() OVERRIDE { + DCHECK(IsSupported()); +#if defined(OS_WIN) + return new ProxyResolverWinHttp(); +#elif defined(OS_MACOSX) + return new ProxyResolverMac(); +#else + NOTREACHED(); + return NULL; +#endif + } + + static bool IsSupported() { +#if defined(OS_WIN) || defined(OS_MACOSX) + return true; +#else + return false; +#endif + } +}; + +// Returns NetLog parameters describing a proxy configuration change. +base::Value* NetLogProxyConfigChangedCallback( + const ProxyConfig* old_config, + const ProxyConfig* new_config, + NetLog::LogLevel /* log_level */) { + base::DictionaryValue* dict = new base::DictionaryValue(); + // The "old_config" is optional -- the first notification will not have + // any "previous" configuration. + if (old_config->is_valid()) + dict->Set("old_config", old_config->ToValue()); + dict->Set("new_config", new_config->ToValue()); + return dict; +} + +base::Value* NetLogBadProxyListCallback(const ProxyRetryInfoMap* retry_info, + NetLog::LogLevel /* log_level */) { + base::DictionaryValue* dict = new base::DictionaryValue(); + base::ListValue* list = new base::ListValue(); + + for (ProxyRetryInfoMap::const_iterator iter = retry_info->begin(); + iter != retry_info->end(); ++iter) { + list->Append(new base::StringValue(iter->first)); + } + dict->Set("bad_proxy_list", list); + return dict; +} + +// Returns NetLog parameters on a successfuly proxy resolution. +base::Value* NetLogFinishedResolvingProxyCallback( + ProxyInfo* result, + NetLog::LogLevel /* log_level */) { + base::DictionaryValue* dict = new base::DictionaryValue(); + dict->SetString("pac_string", result->ToPacString()); + return dict; +} + +#if defined(OS_CHROMEOS) +class UnsetProxyConfigService : public ProxyConfigService { + public: + UnsetProxyConfigService() {} + virtual ~UnsetProxyConfigService() {} + + virtual void AddObserver(Observer* observer) OVERRIDE {} + virtual void RemoveObserver(Observer* observer) OVERRIDE {} + virtual ConfigAvailability GetLatestProxyConfig( + ProxyConfig* config) OVERRIDE { + return CONFIG_UNSET; + } +}; +#endif + +} // namespace + +// ProxyService::InitProxyResolver -------------------------------------------- + +// This glues together two asynchronous steps: +// (1) ProxyScriptDecider -- try to fetch/validate a sequence of PAC scripts +// to figure out what we should configure against. +// (2) Feed the fetched PAC script into the ProxyResolver. +// +// InitProxyResolver is a single-use class which encapsulates cancellation as +// part of its destructor. Start() or StartSkipDecider() should be called just +// once. The instance can be destroyed at any time, and the request will be +// cancelled. + +class ProxyService::InitProxyResolver { + public: + InitProxyResolver() + : proxy_resolver_(NULL), + next_state_(STATE_NONE) { + } + + ~InitProxyResolver() { + // Note that the destruction of ProxyScriptDecider will automatically cancel + // any outstanding work. + if (next_state_ == STATE_SET_PAC_SCRIPT_COMPLETE) { + proxy_resolver_->CancelSetPacScript(); + } + } + + // Begins initializing the proxy resolver; calls |callback| when done. + int Start(ProxyResolver* proxy_resolver, + ProxyScriptFetcher* proxy_script_fetcher, + DhcpProxyScriptFetcher* dhcp_proxy_script_fetcher, + NetLog* net_log, + const ProxyConfig& config, + TimeDelta wait_delay, + const CompletionCallback& callback) { + DCHECK_EQ(STATE_NONE, next_state_); + proxy_resolver_ = proxy_resolver; + + decider_.reset(new ProxyScriptDecider( + proxy_script_fetcher, dhcp_proxy_script_fetcher, net_log)); + config_ = config; + wait_delay_ = wait_delay; + callback_ = callback; + + next_state_ = STATE_DECIDE_PROXY_SCRIPT; + return DoLoop(OK); + } + + // Similar to Start(), however it skips the ProxyScriptDecider stage. Instead + // |effective_config|, |decider_result| and |script_data| will be used as the + // inputs for initializing the ProxyResolver. + int StartSkipDecider(ProxyResolver* proxy_resolver, + const ProxyConfig& effective_config, + int decider_result, + ProxyResolverScriptData* script_data, + const CompletionCallback& callback) { + DCHECK_EQ(STATE_NONE, next_state_); + proxy_resolver_ = proxy_resolver; + + effective_config_ = effective_config; + script_data_ = script_data; + callback_ = callback; + + if (decider_result != OK) + return decider_result; + + next_state_ = STATE_SET_PAC_SCRIPT; + return DoLoop(OK); + } + + // Returns the proxy configuration that was selected by ProxyScriptDecider. + // Should only be called upon completion of the initialization. + const ProxyConfig& effective_config() const { + DCHECK_EQ(STATE_NONE, next_state_); + return effective_config_; + } + + // Returns the PAC script data that was selected by ProxyScriptDecider. + // Should only be called upon completion of the initialization. + ProxyResolverScriptData* script_data() { + DCHECK_EQ(STATE_NONE, next_state_); + return script_data_.get(); + } + + LoadState GetLoadState() const { + if (next_state_ == STATE_DECIDE_PROXY_SCRIPT_COMPLETE) { + // In addition to downloading, this state may also include the stall time + // after network change events (kDelayAfterNetworkChangesMs). + return LOAD_STATE_DOWNLOADING_PROXY_SCRIPT; + } + return LOAD_STATE_RESOLVING_PROXY_FOR_URL; + } + + private: + enum State { + STATE_NONE, + STATE_DECIDE_PROXY_SCRIPT, + STATE_DECIDE_PROXY_SCRIPT_COMPLETE, + STATE_SET_PAC_SCRIPT, + STATE_SET_PAC_SCRIPT_COMPLETE, + }; + + int DoLoop(int result) { + DCHECK_NE(next_state_, STATE_NONE); + int rv = result; + do { + State state = next_state_; + next_state_ = STATE_NONE; + switch (state) { + case STATE_DECIDE_PROXY_SCRIPT: + DCHECK_EQ(OK, rv); + rv = DoDecideProxyScript(); + break; + case STATE_DECIDE_PROXY_SCRIPT_COMPLETE: + rv = DoDecideProxyScriptComplete(rv); + break; + case STATE_SET_PAC_SCRIPT: + DCHECK_EQ(OK, rv); + rv = DoSetPacScript(); + break; + case STATE_SET_PAC_SCRIPT_COMPLETE: + rv = DoSetPacScriptComplete(rv); + break; + default: + NOTREACHED() << "bad state: " << state; + rv = ERR_UNEXPECTED; + break; + } + } while (rv != ERR_IO_PENDING && next_state_ != STATE_NONE); + return rv; + } + + int DoDecideProxyScript() { + next_state_ = STATE_DECIDE_PROXY_SCRIPT_COMPLETE; + + return decider_->Start( + config_, wait_delay_, proxy_resolver_->expects_pac_bytes(), + base::Bind(&InitProxyResolver::OnIOCompletion, base::Unretained(this))); + } + + int DoDecideProxyScriptComplete(int result) { + if (result != OK) + return result; + + effective_config_ = decider_->effective_config(); + script_data_ = decider_->script_data(); + + next_state_ = STATE_SET_PAC_SCRIPT; + return OK; + } + + int DoSetPacScript() { + DCHECK(script_data_.get()); + // TODO(eroman): Should log this latency to the NetLog. + next_state_ = STATE_SET_PAC_SCRIPT_COMPLETE; + return proxy_resolver_->SetPacScript( + script_data_, + base::Bind(&InitProxyResolver::OnIOCompletion, base::Unretained(this))); + } + + int DoSetPacScriptComplete(int result) { + return result; + } + + void OnIOCompletion(int result) { + DCHECK_NE(STATE_NONE, next_state_); + int rv = DoLoop(result); + if (rv != ERR_IO_PENDING) + DoCallback(rv); + } + + void DoCallback(int result) { + DCHECK_NE(ERR_IO_PENDING, result); + callback_.Run(result); + } + + ProxyConfig config_; + ProxyConfig effective_config_; + scoped_refptr<ProxyResolverScriptData> script_data_; + TimeDelta wait_delay_; + scoped_ptr<ProxyScriptDecider> decider_; + ProxyResolver* proxy_resolver_; + CompletionCallback callback_; + State next_state_; + + DISALLOW_COPY_AND_ASSIGN(InitProxyResolver); +}; + +// ProxyService::ProxyScriptDeciderPoller ------------------------------------- + +// This helper class encapsulates the logic to schedule and run periodic +// background checks to see if the PAC script (or effective proxy configuration) +// has changed. If a change is detected, then the caller will be notified via +// the ChangeCallback. +class ProxyService::ProxyScriptDeciderPoller { + public: + typedef base::Callback<void(int, ProxyResolverScriptData*, + const ProxyConfig&)> ChangeCallback; + + // Builds a poller helper, and starts polling for updates. Whenever a change + // is observed, |callback| will be invoked with the details. + // + // |config| specifies the (unresolved) proxy configuration to poll. + // |proxy_resolver_expects_pac_bytes| the type of proxy resolver we expect + // to use the resulting script data with + // (so it can choose the right format). + // |proxy_script_fetcher| this pointer must remain alive throughout our + // lifetime. It is the dependency that will be used + // for downloading proxy scripts. + // |dhcp_proxy_script_fetcher| similar to |proxy_script_fetcher|, but for + // the DHCP dependency. + // |init_net_error| This is the initial network error (possibly success) + // encountered by the first PAC fetch attempt. We use it + // to schedule updates more aggressively if the initial + // fetch resulted in an error. + // |init_script_data| the initial script data from the PAC fetch attempt. + // This is the baseline used to determine when the + // script's contents have changed. + // |net_log| the NetLog to log progress into. + ProxyScriptDeciderPoller(ChangeCallback callback, + const ProxyConfig& config, + bool proxy_resolver_expects_pac_bytes, + ProxyScriptFetcher* proxy_script_fetcher, + DhcpProxyScriptFetcher* dhcp_proxy_script_fetcher, + int init_net_error, + ProxyResolverScriptData* init_script_data, + NetLog* net_log) + : weak_factory_(this), + change_callback_(callback), + config_(config), + proxy_resolver_expects_pac_bytes_(proxy_resolver_expects_pac_bytes), + proxy_script_fetcher_(proxy_script_fetcher), + dhcp_proxy_script_fetcher_(dhcp_proxy_script_fetcher), + last_error_(init_net_error), + last_script_data_(init_script_data), + last_poll_time_(TimeTicks::Now()) { + // Set the initial poll delay. + next_poll_mode_ = poll_policy()->GetNextDelay( + last_error_, TimeDelta::FromSeconds(-1), &next_poll_delay_); + TryToStartNextPoll(false); + } + + void OnLazyPoll() { + // We have just been notified of network activity. Use this opportunity to + // see if we can start our next poll. + TryToStartNextPoll(true); + } + + static const PacPollPolicy* set_policy(const PacPollPolicy* policy) { + const PacPollPolicy* prev = poll_policy_; + poll_policy_ = policy; + return prev; + } + + private: + // Returns the effective poll policy (the one injected by unit-tests, or the + // default). + const PacPollPolicy* poll_policy() { + if (poll_policy_) + return poll_policy_; + return &default_poll_policy_; + } + + void StartPollTimer() { + DCHECK(!decider_.get()); + + base::MessageLoop::current()->PostDelayedTask( + FROM_HERE, + base::Bind(&ProxyScriptDeciderPoller::DoPoll, + weak_factory_.GetWeakPtr()), + next_poll_delay_); + } + + void TryToStartNextPoll(bool triggered_by_activity) { + switch (next_poll_mode_) { + case PacPollPolicy::MODE_USE_TIMER: + if (!triggered_by_activity) + StartPollTimer(); + break; + + case PacPollPolicy::MODE_START_AFTER_ACTIVITY: + if (triggered_by_activity && !decider_.get()) { + TimeDelta elapsed_time = TimeTicks::Now() - last_poll_time_; + if (elapsed_time >= next_poll_delay_) + DoPoll(); + } + break; + } + } + + void DoPoll() { + last_poll_time_ = TimeTicks::Now(); + + // Start the proxy script decider to see if anything has changed. + // TODO(eroman): Pass a proper NetLog rather than NULL. + decider_.reset(new ProxyScriptDecider( + proxy_script_fetcher_, dhcp_proxy_script_fetcher_, NULL)); + int result = decider_->Start( + config_, TimeDelta(), proxy_resolver_expects_pac_bytes_, + base::Bind(&ProxyScriptDeciderPoller::OnProxyScriptDeciderCompleted, + base::Unretained(this))); + + if (result != ERR_IO_PENDING) + OnProxyScriptDeciderCompleted(result); + } + + void OnProxyScriptDeciderCompleted(int result) { + if (HasScriptDataChanged(result, decider_->script_data())) { + // Something has changed, we must notify the ProxyService so it can + // re-initialize its ProxyResolver. Note that we post a notification task + // rather than calling it directly -- this is done to avoid an ugly + // destruction sequence, since |this| might be destroyed as a result of + // the notification. + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(&ProxyScriptDeciderPoller::NotifyProxyServiceOfChange, + weak_factory_.GetWeakPtr(), + result, + make_scoped_refptr(decider_->script_data()), + decider_->effective_config())); + return; + } + + decider_.reset(); + + // Decide when the next poll should take place, and possibly start the + // next timer. + next_poll_mode_ = poll_policy()->GetNextDelay( + last_error_, next_poll_delay_, &next_poll_delay_); + TryToStartNextPoll(false); + } + + bool HasScriptDataChanged(int result, ProxyResolverScriptData* script_data) { + if (result != last_error_) { + // Something changed -- it was failing before and now it succeeded, or + // conversely it succeeded before and now it failed. Or it failed in + // both cases, however the specific failure error codes differ. + return true; + } + + if (result != OK) { + // If it failed last time and failed again with the same error code this + // time, then nothing has actually changed. + return false; + } + + // Otherwise if it succeeded both this time and last time, we need to look + // closer and see if we ended up downloading different content for the PAC + // script. + return !script_data->Equals(last_script_data_.get()); + } + + void NotifyProxyServiceOfChange( + int result, + const scoped_refptr<ProxyResolverScriptData>& script_data, + const ProxyConfig& effective_config) { + // Note that |this| may be deleted after calling into the ProxyService. + change_callback_.Run(result, script_data.get(), effective_config); + } + + base::WeakPtrFactory<ProxyScriptDeciderPoller> weak_factory_; + + ChangeCallback change_callback_; + ProxyConfig config_; + bool proxy_resolver_expects_pac_bytes_; + ProxyScriptFetcher* proxy_script_fetcher_; + DhcpProxyScriptFetcher* dhcp_proxy_script_fetcher_; + + int last_error_; + scoped_refptr<ProxyResolverScriptData> last_script_data_; + + scoped_ptr<ProxyScriptDecider> decider_; + TimeDelta next_poll_delay_; + PacPollPolicy::Mode next_poll_mode_; + + TimeTicks last_poll_time_; + + // Polling policy injected by unit-tests. Otherwise this is NULL and the + // default policy will be used. + static const PacPollPolicy* poll_policy_; + + const DefaultPollPolicy default_poll_policy_; + + DISALLOW_COPY_AND_ASSIGN(ProxyScriptDeciderPoller); +}; + +// static +const ProxyService::PacPollPolicy* + ProxyService::ProxyScriptDeciderPoller::poll_policy_ = NULL; + +// ProxyService::PacRequest --------------------------------------------------- + +class ProxyService::PacRequest + : public base::RefCounted<ProxyService::PacRequest> { + public: + PacRequest(ProxyService* service, + const GURL& url, + ProxyInfo* results, + const net::CompletionCallback& user_callback, + const BoundNetLog& net_log) + : service_(service), + user_callback_(user_callback), + results_(results), + url_(url), + resolve_job_(NULL), + config_id_(ProxyConfig::kInvalidConfigID), + config_source_(PROXY_CONFIG_SOURCE_UNKNOWN), + net_log_(net_log) { + DCHECK(!user_callback.is_null()); + } + + // Starts the resolve proxy request. + int Start() { + DCHECK(!was_cancelled()); + DCHECK(!is_started()); + + DCHECK(service_->config_.is_valid()); + + config_id_ = service_->config_.id(); + config_source_ = service_->config_.source(); + proxy_resolve_start_time_ = TimeTicks::Now(); + + return resolver()->GetProxyForURL( + url_, results_, + base::Bind(&PacRequest::QueryComplete, base::Unretained(this)), + &resolve_job_, net_log_); + } + + bool is_started() const { + // Note that !! casts to bool. (VS gives a warning otherwise). + return !!resolve_job_; + } + + void StartAndCompleteCheckingForSynchronous() { + int rv = service_->TryToCompleteSynchronously(url_, results_); + if (rv == ERR_IO_PENDING) + rv = Start(); + if (rv != ERR_IO_PENDING) + QueryComplete(rv); + } + + void CancelResolveJob() { + DCHECK(is_started()); + // The request may already be running in the resolver. + resolver()->CancelRequest(resolve_job_); + resolve_job_ = NULL; + DCHECK(!is_started()); + } + + void Cancel() { + net_log_.AddEvent(NetLog::TYPE_CANCELLED); + + if (is_started()) + CancelResolveJob(); + + // Mark as cancelled, to prevent accessing this again later. + service_ = NULL; + user_callback_.Reset(); + results_ = NULL; + + net_log_.EndEvent(NetLog::TYPE_PROXY_SERVICE); + } + + // Returns true if Cancel() has been called. + bool was_cancelled() const { + return user_callback_.is_null(); + } + + // Helper to call after ProxyResolver completion (both synchronous and + // asynchronous). Fixes up the result that is to be returned to user. + int QueryDidComplete(int result_code) { + DCHECK(!was_cancelled()); + + // Note that DidFinishResolvingProxy might modify |results_|. + int rv = service_->DidFinishResolvingProxy(results_, result_code, net_log_); + + // Make a note in the results which configuration was in use at the + // time of the resolve. + results_->config_id_ = config_id_; + results_->config_source_ = config_source_; + results_->did_use_pac_script_ = true; + results_->proxy_resolve_start_time_ = proxy_resolve_start_time_; + results_->proxy_resolve_end_time_ = TimeTicks::Now(); + + // Reset the state associated with in-progress-resolve. + resolve_job_ = NULL; + config_id_ = ProxyConfig::kInvalidConfigID; + config_source_ = PROXY_CONFIG_SOURCE_UNKNOWN; + + return rv; + } + + BoundNetLog* net_log() { return &net_log_; } + + LoadState GetLoadState() const { + if (is_started()) + return resolver()->GetLoadState(resolve_job_); + return LOAD_STATE_RESOLVING_PROXY_FOR_URL; + } + + private: + friend class base::RefCounted<ProxyService::PacRequest>; + + ~PacRequest() {} + + // Callback for when the ProxyResolver request has completed. + void QueryComplete(int result_code) { + result_code = QueryDidComplete(result_code); + + // Remove this completed PacRequest from the service's pending list. + /// (which will probably cause deletion of |this|). + if (!user_callback_.is_null()) { + net::CompletionCallback callback = user_callback_; + service_->RemovePendingRequest(this); + callback.Run(result_code); + } + } + + ProxyResolver* resolver() const { return service_->resolver_.get(); } + + // Note that we don't hold a reference to the ProxyService. Outstanding + // requests are cancelled during ~ProxyService, so this is guaranteed + // to be valid throughout our lifetime. + ProxyService* service_; + net::CompletionCallback user_callback_; + ProxyInfo* results_; + GURL url_; + ProxyResolver::RequestHandle resolve_job_; + ProxyConfig::ID config_id_; // The config id when the resolve was started. + ProxyConfigSource config_source_; // The source of proxy settings. + BoundNetLog net_log_; + // Time when the PAC is started. Cached here since resetting ProxyInfo also + // clears the proxy times. + TimeTicks proxy_resolve_start_time_; +}; + +// ProxyService --------------------------------------------------------------- + +ProxyService::ProxyService(ProxyConfigService* config_service, + ProxyResolver* resolver, + NetLog* net_log) + : resolver_(resolver), + next_config_id_(1), + current_state_(STATE_NONE) , + net_log_(net_log), + stall_proxy_auto_config_delay_(TimeDelta::FromMilliseconds( + kDelayAfterNetworkChangesMs)) { + NetworkChangeNotifier::AddIPAddressObserver(this); + NetworkChangeNotifier::AddDNSObserver(this); + ResetConfigService(config_service); +} + +// static +ProxyService* ProxyService::CreateUsingSystemProxyResolver( + ProxyConfigService* proxy_config_service, + size_t num_pac_threads, + NetLog* net_log) { + DCHECK(proxy_config_service); + + if (!ProxyResolverFactoryForSystem::IsSupported()) { + LOG(WARNING) << "PAC support disabled because there is no " + "system implementation"; + return CreateWithoutProxyResolver(proxy_config_service, net_log); + } + + if (num_pac_threads == 0) + num_pac_threads = kDefaultNumPacThreads; + + ProxyResolver* proxy_resolver = new MultiThreadedProxyResolver( + new ProxyResolverFactoryForSystem(), num_pac_threads); + + return new ProxyService(proxy_config_service, proxy_resolver, net_log); +} + +// static +ProxyService* ProxyService::CreateWithoutProxyResolver( + ProxyConfigService* proxy_config_service, + NetLog* net_log) { + return new ProxyService(proxy_config_service, + new ProxyResolverNull(), + net_log); +} + +// static +ProxyService* ProxyService::CreateFixed(const ProxyConfig& pc) { + // TODO(eroman): This isn't quite right, won't work if |pc| specifies + // a PAC script. + return CreateUsingSystemProxyResolver(new ProxyConfigServiceFixed(pc), + 0, NULL); +} + +// static +ProxyService* ProxyService::CreateFixed(const std::string& proxy) { + net::ProxyConfig proxy_config; + proxy_config.proxy_rules().ParseFromString(proxy); + return ProxyService::CreateFixed(proxy_config); +} + +// static +ProxyService* ProxyService::CreateDirect() { + return CreateDirectWithNetLog(NULL); +} + +ProxyService* ProxyService::CreateDirectWithNetLog(NetLog* net_log) { + // Use direct connections. + return new ProxyService(new ProxyConfigServiceDirect, new ProxyResolverNull, + net_log); +} + +// static +ProxyService* ProxyService::CreateFixedFromPacResult( + const std::string& pac_string) { + + // We need the settings to contain an "automatic" setting, otherwise the + // ProxyResolver dependency we give it will never be used. + scoped_ptr<ProxyConfigService> proxy_config_service( + new ProxyConfigServiceFixed(ProxyConfig::CreateAutoDetect())); + + scoped_ptr<ProxyResolver> proxy_resolver( + new ProxyResolverFromPacString(pac_string)); + + return new ProxyService(proxy_config_service.release(), + proxy_resolver.release(), + NULL); +} + +int ProxyService::ResolveProxy(const GURL& raw_url, + ProxyInfo* result, + const net::CompletionCallback& callback, + PacRequest** pac_request, + const BoundNetLog& net_log) { + DCHECK(CalledOnValidThread()); + DCHECK(!callback.is_null()); + + net_log.BeginEvent(NetLog::TYPE_PROXY_SERVICE); + + // Notify our polling-based dependencies that a resolve is taking place. + // This way they can schedule their polls in response to network activity. + config_service_->OnLazyPoll(); + if (script_poller_.get()) + script_poller_->OnLazyPoll(); + + if (current_state_ == STATE_NONE) + ApplyProxyConfigIfAvailable(); + + // Strip away any reference fragments and the username/password, as they + // are not relevant to proxy resolution. + GURL url = SimplifyUrlForRequest(raw_url); + + // Check if the request can be completed right away. (This is the case when + // using a direct connection for example). + int rv = TryToCompleteSynchronously(url, result); + if (rv != ERR_IO_PENDING) + return DidFinishResolvingProxy(result, rv, net_log); + + scoped_refptr<PacRequest> req( + new PacRequest(this, url, result, callback, net_log)); + + if (current_state_ == STATE_READY) { + // Start the resolve request. + rv = req->Start(); + if (rv != ERR_IO_PENDING) + return req->QueryDidComplete(rv); + } else { + req->net_log()->BeginEvent(NetLog::TYPE_PROXY_SERVICE_WAITING_FOR_INIT_PAC); + } + + DCHECK_EQ(ERR_IO_PENDING, rv); + DCHECK(!ContainsPendingRequest(req.get())); + pending_requests_.push_back(req); + + // Completion will be notified through |callback|, unless the caller cancels + // the request using |pac_request|. + if (pac_request) + *pac_request = req.get(); + return rv; // ERR_IO_PENDING +} + +int ProxyService::TryToCompleteSynchronously(const GURL& url, + ProxyInfo* result) { + DCHECK_NE(STATE_NONE, current_state_); + + if (current_state_ != STATE_READY) + return ERR_IO_PENDING; // Still initializing. + + DCHECK_NE(config_.id(), ProxyConfig::kInvalidConfigID); + + // If it was impossible to fetch or parse the PAC script, we cannot complete + // the request here and bail out. + if (permanent_error_ != OK) + return permanent_error_; + + if (config_.HasAutomaticSettings()) + return ERR_IO_PENDING; // Must submit the request to the proxy resolver. + + // Use the manual proxy settings. + config_.proxy_rules().Apply(url, result); + result->config_source_ = config_.source(); + result->config_id_ = config_.id(); + return OK; +} + +ProxyService::~ProxyService() { + NetworkChangeNotifier::RemoveIPAddressObserver(this); + NetworkChangeNotifier::RemoveDNSObserver(this); + config_service_->RemoveObserver(this); + + // Cancel any inprogress requests. + for (PendingRequests::iterator it = pending_requests_.begin(); + it != pending_requests_.end(); + ++it) { + (*it)->Cancel(); + } +} + +void ProxyService::SuspendAllPendingRequests() { + for (PendingRequests::iterator it = pending_requests_.begin(); + it != pending_requests_.end(); + ++it) { + PacRequest* req = it->get(); + if (req->is_started()) { + req->CancelResolveJob(); + + req->net_log()->BeginEvent( + NetLog::TYPE_PROXY_SERVICE_WAITING_FOR_INIT_PAC); + } + } +} + +void ProxyService::SetReady() { + DCHECK(!init_proxy_resolver_.get()); + current_state_ = STATE_READY; + + // Make a copy in case |this| is deleted during the synchronous completion + // of one of the requests. If |this| is deleted then all of the PacRequest + // instances will be Cancel()-ed. + PendingRequests pending_copy = pending_requests_; + + for (PendingRequests::iterator it = pending_copy.begin(); + it != pending_copy.end(); + ++it) { + PacRequest* req = it->get(); + if (!req->is_started() && !req->was_cancelled()) { + req->net_log()->EndEvent(NetLog::TYPE_PROXY_SERVICE_WAITING_FOR_INIT_PAC); + + // Note that we re-check for synchronous completion, in case we are + // no longer using a ProxyResolver (can happen if we fell-back to manual). + req->StartAndCompleteCheckingForSynchronous(); + } + } +} + +void ProxyService::ApplyProxyConfigIfAvailable() { + DCHECK_EQ(STATE_NONE, current_state_); + + config_service_->OnLazyPoll(); + + // If we have already fetched the configuration, start applying it. + if (fetched_config_.is_valid()) { + InitializeUsingLastFetchedConfig(); + return; + } + + // Otherwise we need to first fetch the configuration. + current_state_ = STATE_WAITING_FOR_PROXY_CONFIG; + + // Retrieve the current proxy configuration from the ProxyConfigService. + // If a configuration is not available yet, we will get called back later + // by our ProxyConfigService::Observer once it changes. + ProxyConfig config; + ProxyConfigService::ConfigAvailability availability = + config_service_->GetLatestProxyConfig(&config); + if (availability != ProxyConfigService::CONFIG_PENDING) + OnProxyConfigChanged(config, availability); +} + +void ProxyService::OnInitProxyResolverComplete(int result) { + DCHECK_EQ(STATE_WAITING_FOR_INIT_PROXY_RESOLVER, current_state_); + DCHECK(init_proxy_resolver_.get()); + DCHECK(fetched_config_.HasAutomaticSettings()); + config_ = init_proxy_resolver_->effective_config(); + + // At this point we have decided which proxy settings to use (i.e. which PAC + // script if any). We start up a background poller to periodically revisit + // this decision. If the contents of the PAC script change, or if the + // result of proxy auto-discovery changes, this poller will notice it and + // will trigger a re-initialization using the newly discovered PAC. + script_poller_.reset(new ProxyScriptDeciderPoller( + base::Bind(&ProxyService::InitializeUsingDecidedConfig, + base::Unretained(this)), + fetched_config_, + resolver_->expects_pac_bytes(), + proxy_script_fetcher_.get(), + dhcp_proxy_script_fetcher_.get(), + result, + init_proxy_resolver_->script_data(), + NULL)); + + init_proxy_resolver_.reset(); + + if (result != OK) { + if (fetched_config_.pac_mandatory()) { + VLOG(1) << "Failed configuring with mandatory PAC script, blocking all " + "traffic."; + config_ = fetched_config_; + result = ERR_MANDATORY_PROXY_CONFIGURATION_FAILED; + } else { + VLOG(1) << "Failed configuring with PAC script, falling-back to manual " + "proxy servers."; + config_ = fetched_config_; + config_.ClearAutomaticSettings(); + result = OK; + } + } + permanent_error_ = result; + + // TODO(eroman): Make this ID unique in the case where configuration changed + // due to ProxyScriptDeciderPoller. + config_.set_id(fetched_config_.id()); + config_.set_source(fetched_config_.source()); + + // Resume any requests which we had to defer until the PAC script was + // downloaded. + SetReady(); +} + +int ProxyService::ReconsiderProxyAfterError(const GURL& url, + ProxyInfo* result, + const CompletionCallback& callback, + PacRequest** pac_request, + const BoundNetLog& net_log) { + DCHECK(CalledOnValidThread()); + + // Check to see if we have a new config since ResolveProxy was called. We + // want to re-run ResolveProxy in two cases: 1) we have a new config, or 2) a + // direct connection failed and we never tried the current config. + + bool re_resolve = result->config_id_ != config_.id(); + + if (re_resolve) { + // If we have a new config or the config was never tried, we delete the + // list of bad proxies and we try again. + proxy_retry_info_.clear(); + return ResolveProxy(url, result, callback, pac_request, net_log); + } + + // We don't have new proxy settings to try, try to fallback to the next proxy + // in the list. + bool did_fallback = result->Fallback(net_log); + + // Return synchronous failure if there is nothing left to fall-back to. + // TODO(eroman): This is a yucky API, clean it up. + return did_fallback ? OK : ERR_FAILED; +} + +bool ProxyService::MarkProxyAsBad(const ProxyInfo& result, + const BoundNetLog& net_log) { + result.proxy_list_.UpdateRetryInfoOnFallback(&proxy_retry_info_, net_log); + return result.proxy_list_.HasUntriedProxies(proxy_retry_info_); +} + +void ProxyService::ReportSuccess(const ProxyInfo& result) { + DCHECK(CalledOnValidThread()); + + const ProxyRetryInfoMap& new_retry_info = result.proxy_retry_info(); + if (new_retry_info.empty()) + return; + + for (ProxyRetryInfoMap::const_iterator iter = new_retry_info.begin(); + iter != new_retry_info.end(); ++iter) { + ProxyRetryInfoMap::iterator existing = proxy_retry_info_.find(iter->first); + if (existing == proxy_retry_info_.end()) + proxy_retry_info_[iter->first] = iter->second; + else if (existing->second.bad_until < iter->second.bad_until) + existing->second.bad_until = iter->second.bad_until; + } + if (net_log_) { + net_log_->AddGlobalEntry( + NetLog::TYPE_BAD_PROXY_LIST_REPORTED, + base::Bind(&NetLogBadProxyListCallback, &new_retry_info)); + } +} + +void ProxyService::CancelPacRequest(PacRequest* req) { + DCHECK(CalledOnValidThread()); + DCHECK(req); + req->Cancel(); + RemovePendingRequest(req); +} + +LoadState ProxyService::GetLoadState(const PacRequest* req) const { + CHECK(req); + if (current_state_ == STATE_WAITING_FOR_INIT_PROXY_RESOLVER) + return init_proxy_resolver_->GetLoadState(); + return req->GetLoadState(); +} + +bool ProxyService::ContainsPendingRequest(PacRequest* req) { + PendingRequests::iterator it = std::find( + pending_requests_.begin(), pending_requests_.end(), req); + return pending_requests_.end() != it; +} + +void ProxyService::RemovePendingRequest(PacRequest* req) { + DCHECK(ContainsPendingRequest(req)); + PendingRequests::iterator it = std::find( + pending_requests_.begin(), pending_requests_.end(), req); + pending_requests_.erase(it); +} + +int ProxyService::DidFinishResolvingProxy(ProxyInfo* result, + int result_code, + const BoundNetLog& net_log) { + // Log the result of the proxy resolution. + if (result_code == OK) { + // When logging all events is enabled, dump the proxy list. + if (net_log.IsLoggingAllEvents()) { + net_log.AddEvent( + NetLog::TYPE_PROXY_SERVICE_RESOLVED_PROXY_LIST, + base::Bind(&NetLogFinishedResolvingProxyCallback, result)); + } + result->DeprioritizeBadProxies(proxy_retry_info_); + } else { + net_log.AddEventWithNetErrorCode( + NetLog::TYPE_PROXY_SERVICE_RESOLVED_PROXY_LIST, result_code); + + if (!config_.pac_mandatory()) { + // Fall-back to direct when the proxy resolver fails. This corresponds + // with a javascript runtime error in the PAC script. + // + // This implicit fall-back to direct matches Firefox 3.5 and + // Internet Explorer 8. For more information, see: + // + // http://www.chromium.org/developers/design-documents/proxy-settings-fallback + result->UseDirect(); + result_code = OK; + } else { + result_code = ERR_MANDATORY_PROXY_CONFIGURATION_FAILED; + } + } + + net_log.EndEvent(NetLog::TYPE_PROXY_SERVICE); + return result_code; +} + +void ProxyService::SetProxyScriptFetchers( + ProxyScriptFetcher* proxy_script_fetcher, + DhcpProxyScriptFetcher* dhcp_proxy_script_fetcher) { + DCHECK(CalledOnValidThread()); + State previous_state = ResetProxyConfig(false); + proxy_script_fetcher_.reset(proxy_script_fetcher); + dhcp_proxy_script_fetcher_.reset(dhcp_proxy_script_fetcher); + if (previous_state != STATE_NONE) + ApplyProxyConfigIfAvailable(); +} + +ProxyScriptFetcher* ProxyService::GetProxyScriptFetcher() const { + DCHECK(CalledOnValidThread()); + return proxy_script_fetcher_.get(); +} + +ProxyService::State ProxyService::ResetProxyConfig(bool reset_fetched_config) { + DCHECK(CalledOnValidThread()); + State previous_state = current_state_; + + permanent_error_ = OK; + proxy_retry_info_.clear(); + script_poller_.reset(); + init_proxy_resolver_.reset(); + SuspendAllPendingRequests(); + config_ = ProxyConfig(); + if (reset_fetched_config) + fetched_config_ = ProxyConfig(); + current_state_ = STATE_NONE; + + return previous_state; +} + +void ProxyService::ResetConfigService( + ProxyConfigService* new_proxy_config_service) { + DCHECK(CalledOnValidThread()); + State previous_state = ResetProxyConfig(true); + + // Release the old configuration service. + if (config_service_.get()) + config_service_->RemoveObserver(this); + + // Set the new configuration service. + config_service_.reset(new_proxy_config_service); + config_service_->AddObserver(this); + + if (previous_state != STATE_NONE) + ApplyProxyConfigIfAvailable(); +} + +void ProxyService::PurgeMemory() { + DCHECK(CalledOnValidThread()); + if (resolver_.get()) + resolver_->PurgeMemory(); +} + +void ProxyService::ForceReloadProxyConfig() { + DCHECK(CalledOnValidThread()); + ResetProxyConfig(false); + ApplyProxyConfigIfAvailable(); +} + +// static +ProxyConfigService* ProxyService::CreateSystemProxyConfigService( + base::SingleThreadTaskRunner* io_thread_task_runner, + base::MessageLoop* file_loop) { +#if defined(OS_WIN) + return new ProxyConfigServiceWin(); +#elif defined(OS_IOS) + return new ProxyConfigServiceIOS(); +#elif defined(OS_MACOSX) + return new ProxyConfigServiceMac(io_thread_task_runner); +#elif defined(OS_CHROMEOS) + LOG(ERROR) << "ProxyConfigService for ChromeOS should be created in " + << "profile_io_data.cc::CreateProxyConfigService and this should " + << "be used only for examples."; + return new UnsetProxyConfigService; +#elif defined(OS_LINUX) + ProxyConfigServiceLinux* linux_config_service = + new ProxyConfigServiceLinux(); + + // Assume we got called on the thread that runs the default glib + // main loop, so the current thread is where we should be running + // gconf calls from. + scoped_refptr<base::SingleThreadTaskRunner> glib_thread_task_runner = + base::ThreadTaskRunnerHandle::Get(); + + // The file loop should be a MessageLoopForIO on Linux. + DCHECK_EQ(base::MessageLoop::TYPE_IO, file_loop->type()); + + // Synchronously fetch the current proxy config (since we are + // running on glib_default_loop). Additionally register for + // notifications (delivered in either |glib_default_loop| or + // |file_loop|) to keep us updated when the proxy config changes. + linux_config_service->SetupAndFetchInitialConfig( + glib_thread_task_runner.get(), + io_thread_task_runner, + static_cast<base::MessageLoopForIO*>(file_loop)); + + return linux_config_service; +#elif defined(OS_ANDROID) + return new ProxyConfigServiceAndroid( + io_thread_task_runner, + base::MessageLoop::current()->message_loop_proxy()); +#else + LOG(WARNING) << "Failed to choose a system proxy settings fetcher " + "for this platform."; + return new ProxyConfigServiceDirect(); +#endif +} + +// static +const ProxyService::PacPollPolicy* ProxyService::set_pac_script_poll_policy( + const PacPollPolicy* policy) { + return ProxyScriptDeciderPoller::set_policy(policy); +} + +// static +scoped_ptr<ProxyService::PacPollPolicy> + ProxyService::CreateDefaultPacPollPolicy() { + return scoped_ptr<PacPollPolicy>(new DefaultPollPolicy()); +} + +void ProxyService::OnProxyConfigChanged( + const ProxyConfig& config, + ProxyConfigService::ConfigAvailability availability) { + // Retrieve the current proxy configuration from the ProxyConfigService. + // If a configuration is not available yet, we will get called back later + // by our ProxyConfigService::Observer once it changes. + ProxyConfig effective_config; + switch (availability) { + case ProxyConfigService::CONFIG_PENDING: + // ProxyConfigService implementors should never pass CONFIG_PENDING. + NOTREACHED() << "Proxy config change with CONFIG_PENDING availability!"; + return; + case ProxyConfigService::CONFIG_VALID: + effective_config = config; + break; + case ProxyConfigService::CONFIG_UNSET: + effective_config = ProxyConfig::CreateDirect(); + break; + } + + // Emit the proxy settings change to the NetLog stream. + if (net_log_) { + net_log_->AddGlobalEntry( + net::NetLog::TYPE_PROXY_CONFIG_CHANGED, + base::Bind(&NetLogProxyConfigChangedCallback, + &fetched_config_, &effective_config)); + } + + // Set the new configuration as the most recently fetched one. + fetched_config_ = effective_config; + fetched_config_.set_id(1); // Needed for a later DCHECK of is_valid(). + + InitializeUsingLastFetchedConfig(); +} + +void ProxyService::InitializeUsingLastFetchedConfig() { + ResetProxyConfig(false); + + DCHECK(fetched_config_.is_valid()); + + // Increment the ID to reflect that the config has changed. + fetched_config_.set_id(next_config_id_++); + + if (!fetched_config_.HasAutomaticSettings()) { + config_ = fetched_config_; + SetReady(); + return; + } + + // Start downloading + testing the PAC scripts for this new configuration. + current_state_ = STATE_WAITING_FOR_INIT_PROXY_RESOLVER; + + // If we changed networks recently, we should delay running proxy auto-config. + TimeDelta wait_delay = + stall_proxy_autoconfig_until_ - TimeTicks::Now(); + + init_proxy_resolver_.reset(new InitProxyResolver()); + int rv = init_proxy_resolver_->Start( + resolver_.get(), + proxy_script_fetcher_.get(), + dhcp_proxy_script_fetcher_.get(), + net_log_, + fetched_config_, + wait_delay, + base::Bind(&ProxyService::OnInitProxyResolverComplete, + base::Unretained(this))); + + if (rv != ERR_IO_PENDING) + OnInitProxyResolverComplete(rv); +} + +void ProxyService::InitializeUsingDecidedConfig( + int decider_result, + ProxyResolverScriptData* script_data, + const ProxyConfig& effective_config) { + DCHECK(fetched_config_.is_valid()); + DCHECK(fetched_config_.HasAutomaticSettings()); + + ResetProxyConfig(false); + + current_state_ = STATE_WAITING_FOR_INIT_PROXY_RESOLVER; + + init_proxy_resolver_.reset(new InitProxyResolver()); + int rv = init_proxy_resolver_->StartSkipDecider( + resolver_.get(), + effective_config, + decider_result, + script_data, + base::Bind(&ProxyService::OnInitProxyResolverComplete, + base::Unretained(this))); + + if (rv != ERR_IO_PENDING) + OnInitProxyResolverComplete(rv); +} + +void ProxyService::OnIPAddressChanged() { + // See the comment block by |kDelayAfterNetworkChangesMs| for info. + stall_proxy_autoconfig_until_ = + TimeTicks::Now() + stall_proxy_auto_config_delay_; + + State previous_state = ResetProxyConfig(false); + if (previous_state != STATE_NONE) + ApplyProxyConfigIfAvailable(); +} + +void ProxyService::OnDNSChanged() { + OnIPAddressChanged(); +} + +SyncProxyServiceHelper::SyncProxyServiceHelper( + base::MessageLoop* io_message_loop, + ProxyService* proxy_service) + : io_message_loop_(io_message_loop), + proxy_service_(proxy_service), + event_(false, false), + callback_(base::Bind(&SyncProxyServiceHelper::OnCompletion, + base::Unretained(this))) { + DCHECK(io_message_loop_ != base::MessageLoop::current()); +} + +int SyncProxyServiceHelper::ResolveProxy(const GURL& url, + ProxyInfo* proxy_info, + const BoundNetLog& net_log) { + DCHECK(io_message_loop_ != base::MessageLoop::current()); + + io_message_loop_->PostTask( + FROM_HERE, + base::Bind(&SyncProxyServiceHelper::StartAsyncResolve, this, url, + net_log)); + + event_.Wait(); + + if (result_ == net::OK) { + *proxy_info = proxy_info_; + } + return result_; +} + +int SyncProxyServiceHelper::ReconsiderProxyAfterError( + const GURL& url, ProxyInfo* proxy_info, const BoundNetLog& net_log) { + DCHECK(io_message_loop_ != base::MessageLoop::current()); + + io_message_loop_->PostTask( + FROM_HERE, + base::Bind(&SyncProxyServiceHelper::StartAsyncReconsider, this, url, + net_log)); + + event_.Wait(); + + if (result_ == net::OK) { + *proxy_info = proxy_info_; + } + return result_; +} + +SyncProxyServiceHelper::~SyncProxyServiceHelper() {} + +void SyncProxyServiceHelper::StartAsyncResolve(const GURL& url, + const BoundNetLog& net_log) { + result_ = proxy_service_->ResolveProxy( + url, &proxy_info_, callback_, NULL, net_log); + if (result_ != net::ERR_IO_PENDING) { + OnCompletion(result_); + } +} + +void SyncProxyServiceHelper::StartAsyncReconsider(const GURL& url, + const BoundNetLog& net_log) { + result_ = proxy_service_->ReconsiderProxyAfterError( + url, &proxy_info_, callback_, NULL, net_log); + if (result_ != net::ERR_IO_PENDING) { + OnCompletion(result_); + } +} + +void SyncProxyServiceHelper::OnCompletion(int rv) { + result_ = rv; + event_.Signal(); +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_service.h b/chromium/net/proxy/proxy_service.h new file mode 100644 index 00000000000..663991133a0 --- /dev/null +++ b/chromium/net/proxy/proxy_service.h @@ -0,0 +1,441 @@ +// 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. + +#ifndef NET_PROXY_PROXY_SERVICE_H_ +#define NET_PROXY_PROXY_SERVICE_H_ + +#include <string> +#include <vector> + +#include "base/gtest_prod_util.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/synchronization/waitable_event.h" +#include "base/threading/non_thread_safe.h" +#include "net/base/completion_callback.h" +#include "net/base/load_states.h" +#include "net/base/net_export.h" +#include "net/base/net_log.h" +#include "net/base/network_change_notifier.h" +#include "net/proxy/proxy_config_service.h" +#include "net/proxy/proxy_info.h" +#include "net/proxy/proxy_server.h" + +class GURL; + +namespace base { +class MessageLoop; +class SingleThreadTaskRunner; +} // namespace base + +namespace net { + +class DhcpProxyScriptFetcher; +class HostResolver; +class NetworkDelegate; +class ProxyResolver; +class ProxyResolverScriptData; +class ProxyScriptDecider; +class ProxyScriptFetcher; + +// This class can be used to resolve the proxy server to use when loading a +// HTTP(S) URL. It uses the given ProxyResolver to handle the actual proxy +// resolution. See ProxyResolverV8 for example. +class NET_EXPORT ProxyService : public NetworkChangeNotifier::IPAddressObserver, + public NetworkChangeNotifier::DNSObserver, + public ProxyConfigService::Observer, + NON_EXPORTED_BASE(public base::NonThreadSafe) { + public: + static const size_t kDefaultNumPacThreads = 4; + + // This interface defines the set of policies for when to poll the PAC + // script for changes. + // + // The polling policy decides what the next poll delay should be in + // milliseconds. It also decides how to wait for this delay -- either + // by starting a timer to do the poll at exactly |next_delay_ms| + // (MODE_USE_TIMER) or by waiting for the first network request issued after + // |next_delay_ms| (MODE_START_AFTER_ACTIVITY). + // + // The timer method is more precise and guarantees that polling happens when + // it was requested. However it has the disadvantage of causing spurious CPU + // and network activity. It is a reasonable choice to use for short poll + // intervals which only happen a couple times. + // + // However for repeated timers this will prevent the browser from going + // idle. MODE_START_AFTER_ACTIVITY solves this problem by only polling in + // direct response to network activity. The drawback to + // MODE_START_AFTER_ACTIVITY is since the poll is initiated only after the + // request is received, the first couple requests initiated after a long + // period of inactivity will likely see a stale version of the PAC script + // until the background polling gets a chance to update things. + class NET_EXPORT_PRIVATE PacPollPolicy { + public: + enum Mode { + MODE_USE_TIMER, + MODE_START_AFTER_ACTIVITY, + }; + + virtual ~PacPollPolicy() {} + + // Decides the next poll delay. |current_delay| is the delay used + // by the preceding poll, or a negative TimeDelta value if determining + // the delay for the initial poll. |initial_error| is the network error + // code that the last PAC fetch (or WPAD initialization) failed with, + // or OK if it completed successfully. Implementations must set + // |next_delay| to a non-negative value. + virtual Mode GetNextDelay(int initial_error, + base::TimeDelta current_delay, + base::TimeDelta* next_delay) const = 0; + }; + + // The instance takes ownership of |config_service| and |resolver|. + // |net_log| is a possibly NULL destination to send log events to. It must + // remain alive for the lifetime of this ProxyService. + ProxyService(ProxyConfigService* config_service, + ProxyResolver* resolver, + NetLog* net_log); + + virtual ~ProxyService(); + + // Used internally to handle PAC queries. + // TODO(eroman): consider naming this simply "Request". + class PacRequest; + + // Returns ERR_IO_PENDING if the proxy information could not be provided + // synchronously, to indicate that the result will be available when the + // callback is run. The callback is run on the thread that calls + // ResolveProxy. + // + // The caller is responsible for ensuring that |results| and |callback| + // remain valid until the callback is run or until |pac_request| is cancelled + // via CancelPacRequest. |pac_request| is only valid while the completion + // callback is still pending. NULL can be passed for |pac_request| if + // the caller will not need to cancel the request. + // + // We use the three possible proxy access types in the following order, + // doing fallback if one doesn't work. See "pac_script_decider.h" + // for the specifics. + // 1. WPAD auto-detection + // 2. PAC URL + // 3. named proxy + // + // Profiling information for the request is saved to |net_log| if non-NULL. + int ResolveProxy(const GURL& url, + ProxyInfo* results, + const net::CompletionCallback& callback, + PacRequest** pac_request, + const BoundNetLog& net_log); + + // This method is called after a failure to connect or resolve a host name. + // It gives the proxy service an opportunity to reconsider the proxy to use. + // The |results| parameter contains the results returned by an earlier call + // to ResolveProxy. The semantics of this call are otherwise similar to + // ResolveProxy. + // + // NULL can be passed for |pac_request| if the caller will not need to + // cancel the request. + // + // Returns ERR_FAILED if there is not another proxy config to try. + // + // Profiling information for the request is saved to |net_log| if non-NULL. + int ReconsiderProxyAfterError(const GURL& url, + ProxyInfo* results, + const CompletionCallback& callback, + PacRequest** pac_request, + const BoundNetLog& net_log); + + // Explicitly trigger proxy fallback for the given |results| by updating our + // list of bad proxies to include the first entry of |results|. Returns true + // if there will be at least one proxy remaining in the list after fallback + // and false otherwise. + bool MarkProxyAsBad(const ProxyInfo& results, const BoundNetLog& net_log); + + // Called to report that the last proxy connection succeeded. If |proxy_info| + // has a non empty proxy_retry_info map, the proxies that have been tried (and + // failed) for this request will be marked as bad. + void ReportSuccess(const ProxyInfo& proxy_info); + + // Call this method with a non-null |pac_request| to cancel the PAC request. + void CancelPacRequest(PacRequest* pac_request); + + // Returns the LoadState for this |pac_request| which must be non-NULL. + LoadState GetLoadState(const PacRequest* pac_request) const; + + // Sets the ProxyScriptFetcher and DhcpProxyScriptFetcher dependencies. This + // is needed if the ProxyResolver is of type ProxyResolverWithoutFetch. + // ProxyService takes ownership of both objects. + void SetProxyScriptFetchers( + ProxyScriptFetcher* proxy_script_fetcher, + DhcpProxyScriptFetcher* dhcp_proxy_script_fetcher); + ProxyScriptFetcher* GetProxyScriptFetcher() const; + + // Tells this ProxyService to start using a new ProxyConfigService to + // retrieve its ProxyConfig from. The new ProxyConfigService will immediately + // be queried for new config info which will be used for all subsequent + // ResolveProxy calls. ProxyService takes ownership of + // |new_proxy_config_service|. + void ResetConfigService(ProxyConfigService* new_proxy_config_service); + + // Tells the resolver to purge any memory it does not need. + void PurgeMemory(); + + + // Returns the last configuration fetched from ProxyConfigService. + const ProxyConfig& fetched_config() { + return fetched_config_; + } + + // Returns the current configuration being used by ProxyConfigService. + const ProxyConfig& config() { + return config_; + } + + // Returns the map of proxies which have been marked as "bad". + const ProxyRetryInfoMap& proxy_retry_info() const { + return proxy_retry_info_; + } + + // Clears the list of bad proxy servers that has been cached. + void ClearBadProxiesCache() { + proxy_retry_info_.clear(); + } + + // Forces refetching the proxy configuration, and applying it. + // This re-does everything from fetching the system configuration, + // to downloading and testing the PAC files. + void ForceReloadProxyConfig(); + + // Same as CreateProxyServiceUsingV8ProxyResolver, except it uses system + // libraries for evaluating the PAC script if available, otherwise skips + // proxy autoconfig. + static ProxyService* CreateUsingSystemProxyResolver( + ProxyConfigService* proxy_config_service, + size_t num_pac_threads, + NetLog* net_log); + + // Creates a ProxyService without support for proxy autoconfig. + static ProxyService* CreateWithoutProxyResolver( + ProxyConfigService* proxy_config_service, + NetLog* net_log); + + // Convenience methods that creates a proxy service using the + // specified fixed settings. + static ProxyService* CreateFixed(const ProxyConfig& pc); + static ProxyService* CreateFixed(const std::string& proxy); + + // Creates a proxy service that uses a DIRECT connection for all requests. + static ProxyService* CreateDirect(); + // |net_log|'s lifetime must exceed ProxyService. + static ProxyService* CreateDirectWithNetLog(NetLog* net_log); + + // This method is used by tests to create a ProxyService that returns a + // hardcoded proxy fallback list (|pac_string|) for every URL. + // + // |pac_string| is a list of proxy servers, in the format that a PAC script + // would return it. For example, "PROXY foobar:99; SOCKS fml:2; DIRECT" + static ProxyService* CreateFixedFromPacResult(const std::string& pac_string); + + // Creates a config service appropriate for this platform that fetches the + // system proxy settings. + static ProxyConfigService* CreateSystemProxyConfigService( + base::SingleThreadTaskRunner* io_thread_task_runner, + base::MessageLoop* file_loop); + + // This method should only be used by unit tests. + void set_stall_proxy_auto_config_delay(base::TimeDelta delay) { + stall_proxy_auto_config_delay_ = delay; + } + + // This method should only be used by unit tests. Returns the previously + // active policy. + static const PacPollPolicy* set_pac_script_poll_policy( + const PacPollPolicy* policy); + + // This method should only be used by unit tests. Creates an instance + // of the default internal PacPollPolicy used by ProxyService. + static scoped_ptr<PacPollPolicy> CreateDefaultPacPollPolicy(); + + private: + FRIEND_TEST_ALL_PREFIXES(ProxyServiceTest, UpdateConfigAfterFailedAutodetect); + FRIEND_TEST_ALL_PREFIXES(ProxyServiceTest, UpdateConfigFromPACToDirect); + friend class PacRequest; + class InitProxyResolver; + class ProxyScriptDeciderPoller; + + // TODO(eroman): change this to a std::set. Note that this requires updating + // some tests in proxy_service_unittest.cc such as: + // ProxyServiceTest.InitialPACScriptDownload + // which expects requests to finish in the order they were added. + typedef std::vector<scoped_refptr<PacRequest> > PendingRequests; + + enum State { + STATE_NONE, + STATE_WAITING_FOR_PROXY_CONFIG, + STATE_WAITING_FOR_INIT_PROXY_RESOLVER, + STATE_READY, + }; + + // Resets all the variables associated with the current proxy configuration, + // and rewinds the current state to |STATE_NONE|. Returns the previous value + // of |current_state_|. If |reset_fetched_config| is true then + // |fetched_config_| will also be reset, otherwise it will be left as-is. + // Resetting it means that we will have to re-fetch the configuration from + // the ProxyConfigService later. + State ResetProxyConfig(bool reset_fetched_config); + + // Retrieves the current proxy configuration from the ProxyConfigService, and + // starts initializing for it. + void ApplyProxyConfigIfAvailable(); + + // Callback for when the proxy resolver has been initialized with a + // PAC script. + void OnInitProxyResolverComplete(int result); + + // Returns ERR_IO_PENDING if the request cannot be completed synchronously. + // Otherwise it fills |result| with the proxy information for |url|. + // Completing synchronously means we don't need to query ProxyResolver. + int TryToCompleteSynchronously(const GURL& url, ProxyInfo* result); + + // Cancels all of the requests sent to the ProxyResolver. These will be + // restarted when calling SetReady(). + void SuspendAllPendingRequests(); + + // Advances the current state to |STATE_READY|, and resumes any pending + // requests which had been stalled waiting for initialization to complete. + void SetReady(); + + // Returns true if |pending_requests_| contains |req|. + bool ContainsPendingRequest(PacRequest* req); + + // Removes |req| from the list of pending requests. + void RemovePendingRequest(PacRequest* req); + + // Called when proxy resolution has completed (either synchronously or + // asynchronously). Handles logging the result, and cleaning out + // bad entries from the results list. + int DidFinishResolvingProxy(ProxyInfo* result, + int result_code, + const BoundNetLog& net_log); + + // Start initialization using |fetched_config_|. + void InitializeUsingLastFetchedConfig(); + + // Start the initialization skipping past the "decision" phase. + void InitializeUsingDecidedConfig( + int decider_result, + ProxyResolverScriptData* script_data, + const ProxyConfig& effective_config); + + // NetworkChangeNotifier::IPAddressObserver + // When this is called, we re-fetch PAC scripts and re-run WPAD. + virtual void OnIPAddressChanged() OVERRIDE; + + // NetworkChangeNotifier::DNSObserver + // We respond as above. + virtual void OnDNSChanged() OVERRIDE; + + // ProxyConfigService::Observer + virtual void OnProxyConfigChanged( + const ProxyConfig& config, + ProxyConfigService::ConfigAvailability availability) OVERRIDE; + + scoped_ptr<ProxyConfigService> config_service_; + scoped_ptr<ProxyResolver> resolver_; + + // We store the proxy configuration that was last fetched from the + // ProxyConfigService, as well as the resulting "effective" configuration. + // The effective configuration is what we condense the original fetched + // settings to after testing the various automatic settings (auto-detect + // and custom PAC url). + ProxyConfig fetched_config_; + ProxyConfig config_; + + // Increasing ID to give to the next ProxyConfig that we set. + int next_config_id_; + + // The time when the proxy configuration was last read from the system. + base::TimeTicks config_last_update_time_; + + // Map of the known bad proxies and the information about the retry time. + ProxyRetryInfoMap proxy_retry_info_; + + // Set of pending/inprogress requests. + PendingRequests pending_requests_; + + // The fetcher to use when downloading PAC scripts for the ProxyResolver. + // This dependency can be NULL if our ProxyResolver has no need for + // external PAC script fetching. + scoped_ptr<ProxyScriptFetcher> proxy_script_fetcher_; + + // The fetcher to use when attempting to download the most appropriate PAC + // script configured in DHCP, if any. Can be NULL if the ProxyResolver has + // no need for DHCP PAC script fetching. + scoped_ptr<DhcpProxyScriptFetcher> dhcp_proxy_script_fetcher_; + + // Helper to download the PAC script (wpad + custom) and apply fallback rules. + // + // Note that the declaration is important here: |proxy_script_fetcher_| and + // |proxy_resolver_| must outlive |init_proxy_resolver_|. + scoped_ptr<InitProxyResolver> init_proxy_resolver_; + + // Helper to poll the PAC script for changes. + scoped_ptr<ProxyScriptDeciderPoller> script_poller_; + + State current_state_; + + // Either OK or an ERR_* value indicating that a permanent error (e.g. + // failed to fetch the PAC script) prevents proxy resolution. + int permanent_error_; + + // This is the log where any events generated by |init_proxy_resolver_| are + // sent to. + NetLog* net_log_; + + // The earliest time at which we should run any proxy auto-config. (Used to + // stall re-configuration following an IP address change). + base::TimeTicks stall_proxy_autoconfig_until_; + + // The amount of time to stall requests following IP address changes. + base::TimeDelta stall_proxy_auto_config_delay_; + + DISALLOW_COPY_AND_ASSIGN(ProxyService); +}; + +// Wrapper for invoking methods on a ProxyService synchronously. +class NET_EXPORT SyncProxyServiceHelper + : public base::RefCountedThreadSafe<SyncProxyServiceHelper> { + public: + SyncProxyServiceHelper(base::MessageLoop* io_message_loop, + ProxyService* proxy_service); + + int ResolveProxy(const GURL& url, + ProxyInfo* proxy_info, + const BoundNetLog& net_log); + int ReconsiderProxyAfterError(const GURL& url, + ProxyInfo* proxy_info, + const BoundNetLog& net_log); + + private: + friend class base::RefCountedThreadSafe<SyncProxyServiceHelper>; + + virtual ~SyncProxyServiceHelper(); + + void StartAsyncResolve(const GURL& url, const BoundNetLog& net_log); + void StartAsyncReconsider(const GURL& url, const BoundNetLog& net_log); + + void OnCompletion(int result); + + base::MessageLoop* io_message_loop_; + ProxyService* proxy_service_; + + base::WaitableEvent event_; + CompletionCallback callback_; + ProxyInfo proxy_info_; + int result_; +}; + +} // namespace net + +#endif // NET_PROXY_PROXY_SERVICE_H_ diff --git a/chromium/net/proxy/proxy_service_unittest.cc b/chromium/net/proxy/proxy_service_unittest.cc new file mode 100644 index 00000000000..c8551b05e84 --- /dev/null +++ b/chromium/net/proxy/proxy_service_unittest.cc @@ -0,0 +1,2791 @@ +// 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/proxy/proxy_service.h" + +#include <vector> + +#include "base/format_macros.h" +#include "base/logging.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "net/base/net_errors.h" +#include "net/base/net_log.h" +#include "net/base/net_log_unittest.h" +#include "net/base/test_completion_callback.h" +#include "net/proxy/dhcp_proxy_script_fetcher.h" +#include "net/proxy/mock_proxy_resolver.h" +#include "net/proxy/mock_proxy_script_fetcher.h" +#include "net/proxy/proxy_config_service.h" +#include "net/proxy/proxy_resolver.h" +#include "net/proxy/proxy_script_fetcher.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +// TODO(eroman): Write a test which exercises +// ProxyService::SuspendAllPendingRequests(). +namespace net { +namespace { + +// This polling policy will decide to poll every 1 ms. +class ImmediatePollPolicy : public ProxyService::PacPollPolicy { + public: + ImmediatePollPolicy() {} + + virtual Mode GetNextDelay(int error, base::TimeDelta current_delay, + base::TimeDelta* next_delay) const OVERRIDE { + *next_delay = base::TimeDelta::FromMilliseconds(1); + return MODE_USE_TIMER; + } + + private: + DISALLOW_COPY_AND_ASSIGN(ImmediatePollPolicy); +}; + +// This polling policy chooses a fantastically large delay. In other words, it +// will never trigger a poll +class NeverPollPolicy : public ProxyService::PacPollPolicy { + public: + NeverPollPolicy() {} + + virtual Mode GetNextDelay(int error, base::TimeDelta current_delay, + base::TimeDelta* next_delay) const OVERRIDE { + *next_delay = base::TimeDelta::FromDays(60); + return MODE_USE_TIMER; + } + + private: + DISALLOW_COPY_AND_ASSIGN(NeverPollPolicy); +}; + +// This polling policy starts a poll immediately after network activity. +class ImmediateAfterActivityPollPolicy : public ProxyService::PacPollPolicy { + public: + ImmediateAfterActivityPollPolicy() {} + + virtual Mode GetNextDelay(int error, base::TimeDelta current_delay, + base::TimeDelta* next_delay) const OVERRIDE { + *next_delay = base::TimeDelta(); + return MODE_START_AFTER_ACTIVITY; + } + + private: + DISALLOW_COPY_AND_ASSIGN(ImmediateAfterActivityPollPolicy); +}; + +// This test fixture is used to partially disable the background polling done by +// the ProxyService (which it uses to detect whenever its PAC script contents or +// WPAD results have changed). +// +// We disable the feature by setting the poll interval to something really +// large, so it will never actually be reached even on the slowest bots that run +// these tests. +// +// We disable the polling in order to avoid any timing dependencies in the +// tests. If the bot were to run the tests very slowly and we hadn't disabled +// polling, then it might start a background re-try in the middle of our test +// and confuse our expectations leading to flaky failures. +// +// The tests which verify the polling code re-enable the polling behavior but +// are careful to avoid timing problems. +class ProxyServiceTest : public testing::Test { + protected: + virtual void SetUp() OVERRIDE { + testing::Test::SetUp(); + previous_policy_ = + ProxyService::set_pac_script_poll_policy(&never_poll_policy_); + } + + virtual void TearDown() OVERRIDE { + // Restore the original policy. + ProxyService::set_pac_script_poll_policy(previous_policy_); + testing::Test::TearDown(); + } + + private: + NeverPollPolicy never_poll_policy_; + const ProxyService::PacPollPolicy* previous_policy_; +}; + +const char kValidPacScript1[] = "pac-script-v1-FindProxyForURL"; +const char kValidPacScript2[] = "pac-script-v2-FindProxyForURL"; + +class MockProxyConfigService: public ProxyConfigService { + public: + explicit MockProxyConfigService(const ProxyConfig& config) + : availability_(CONFIG_VALID), + config_(config) { + } + + explicit MockProxyConfigService(const std::string& pac_url) + : availability_(CONFIG_VALID), + config_(ProxyConfig::CreateFromCustomPacURL(GURL(pac_url))) { + } + + virtual void AddObserver(Observer* observer) OVERRIDE { + observers_.AddObserver(observer); + } + + virtual void RemoveObserver(Observer* observer) OVERRIDE { + observers_.RemoveObserver(observer); + } + + virtual ConfigAvailability GetLatestProxyConfig(ProxyConfig* results) + OVERRIDE { + if (availability_ == CONFIG_VALID) + *results = config_; + return availability_; + } + + void SetConfig(const ProxyConfig& config) { + availability_ = CONFIG_VALID; + config_ = config; + FOR_EACH_OBSERVER(Observer, observers_, + OnProxyConfigChanged(config_, availability_)); + } + + private: + ConfigAvailability availability_; + ProxyConfig config_; + ObserverList<Observer, true> observers_; +}; + +} // namespace + +TEST_F(ProxyServiceTest, Direct) { + MockAsyncProxyResolver* resolver = new MockAsyncProxyResolver; + ProxyService service(new MockProxyConfigService( + ProxyConfig::CreateDirect()), resolver, NULL); + + GURL url("http://www.google.com/"); + + ProxyInfo info; + TestCompletionCallback callback; + CapturingBoundNetLog log; + int rv = service.ResolveProxy( + url, &info, callback.callback(), NULL, log.bound()); + EXPECT_EQ(OK, rv); + EXPECT_TRUE(resolver->pending_requests().empty()); + + EXPECT_TRUE(info.is_direct()); + EXPECT_TRUE(info.proxy_resolve_start_time().is_null()); + EXPECT_TRUE(info.proxy_resolve_end_time().is_null()); + + // Check the NetLog was filled correctly. + CapturingNetLog::CapturedEntryList entries; + log.GetEntries(&entries); + + EXPECT_EQ(3u, entries.size()); + EXPECT_TRUE(LogContainsBeginEvent( + entries, 0, NetLog::TYPE_PROXY_SERVICE)); + EXPECT_TRUE(LogContainsEvent( + entries, 1, NetLog::TYPE_PROXY_SERVICE_RESOLVED_PROXY_LIST, + NetLog::PHASE_NONE)); + EXPECT_TRUE(LogContainsEndEvent( + entries, 2, NetLog::TYPE_PROXY_SERVICE)); +} + +TEST_F(ProxyServiceTest, PAC) { + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + + MockAsyncProxyResolver* resolver = new MockAsyncProxyResolver; + + ProxyService service(config_service, resolver, NULL); + + GURL url("http://www.google.com/"); + + ProxyInfo info; + TestCompletionCallback callback; + ProxyService::PacRequest* request; + CapturingBoundNetLog log; + + int rv = service.ResolveProxy( + url, &info, callback.callback(), &request, log.bound()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + EXPECT_EQ(LOAD_STATE_RESOLVING_PROXY_FOR_URL, service.GetLoadState(request)); + + EXPECT_EQ(GURL("http://foopy/proxy.pac"), + resolver->pending_set_pac_script_request()->script_data()->url()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(url, resolver->pending_requests()[0]->url()); + + // Set the result in proxy resolver. + resolver->pending_requests()[0]->results()->UseNamedProxy("foopy"); + resolver->pending_requests()[0]->CompleteNow(OK); + + EXPECT_EQ(OK, callback.WaitForResult()); + EXPECT_FALSE(info.is_direct()); + EXPECT_EQ("foopy:80", info.proxy_server().ToURI()); + EXPECT_TRUE(info.did_use_pac_script()); + + EXPECT_FALSE(info.proxy_resolve_start_time().is_null()); + EXPECT_FALSE(info.proxy_resolve_end_time().is_null()); + EXPECT_LE(info.proxy_resolve_start_time(), info.proxy_resolve_end_time()); + + // Check the NetLog was filled correctly. + CapturingNetLog::CapturedEntryList entries; + log.GetEntries(&entries); + + EXPECT_EQ(5u, entries.size()); + EXPECT_TRUE(LogContainsBeginEvent( + entries, 0, NetLog::TYPE_PROXY_SERVICE)); + EXPECT_TRUE(LogContainsBeginEvent( + entries, 1, NetLog::TYPE_PROXY_SERVICE_WAITING_FOR_INIT_PAC)); + EXPECT_TRUE(LogContainsEndEvent( + entries, 2, NetLog::TYPE_PROXY_SERVICE_WAITING_FOR_INIT_PAC)); + EXPECT_TRUE(LogContainsEndEvent( + entries, 4, NetLog::TYPE_PROXY_SERVICE)); +} + +// Test that the proxy resolver does not see the URL's username/password +// or its reference section. +TEST_F(ProxyServiceTest, PAC_NoIdentityOrHash) { + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + + MockAsyncProxyResolver* resolver = new MockAsyncProxyResolver; + + ProxyService service(config_service, resolver, NULL); + + GURL url("http://username:password@www.google.com/?ref#hash#hash"); + + ProxyInfo info; + TestCompletionCallback callback; + int rv = service.ResolveProxy( + url, &info, callback.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + EXPECT_EQ(GURL("http://foopy/proxy.pac"), + resolver->pending_set_pac_script_request()->script_data()->url()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + // The URL should have been simplified, stripping the username/password/hash. + EXPECT_EQ(GURL("http://www.google.com/?ref"), + resolver->pending_requests()[0]->url()); + + // We end here without ever completing the request -- destruction of + // ProxyService will cancel the outstanding request. +} + +TEST_F(ProxyServiceTest, PAC_FailoverWithoutDirect) { + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + MockAsyncProxyResolver* resolver = new MockAsyncProxyResolver; + + ProxyService service(config_service, resolver, NULL); + + GURL url("http://www.google.com/"); + + ProxyInfo info; + TestCompletionCallback callback1; + int rv = service.ResolveProxy( + url, &info, callback1.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + EXPECT_EQ(GURL("http://foopy/proxy.pac"), + resolver->pending_set_pac_script_request()->script_data()->url()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(url, resolver->pending_requests()[0]->url()); + + // Set the result in proxy resolver. + resolver->pending_requests()[0]->results()->UseNamedProxy("foopy:8080"); + resolver->pending_requests()[0]->CompleteNow(OK); + + EXPECT_EQ(OK, callback1.WaitForResult()); + EXPECT_FALSE(info.is_direct()); + EXPECT_EQ("foopy:8080", info.proxy_server().ToURI()); + EXPECT_TRUE(info.did_use_pac_script()); + + EXPECT_FALSE(info.proxy_resolve_start_time().is_null()); + EXPECT_FALSE(info.proxy_resolve_end_time().is_null()); + EXPECT_LE(info.proxy_resolve_start_time(), info.proxy_resolve_end_time()); + + // Now, imagine that connecting to foopy:8080 fails: there is nothing + // left to fallback to, since our proxy list was NOT terminated by + // DIRECT. + TestCompletionCallback callback2; + rv = service.ReconsiderProxyAfterError( + url, &info, callback2.callback(), NULL, BoundNetLog()); + // ReconsiderProxyAfterError returns error indicating nothing left. + EXPECT_EQ(ERR_FAILED, rv); + EXPECT_TRUE(info.is_empty()); +} + +// Test that if the execution of the PAC script fails (i.e. javascript runtime +// error), and the PAC settings are non-mandatory, that we fall-back to direct. +TEST_F(ProxyServiceTest, PAC_RuntimeError) { + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + MockAsyncProxyResolver* resolver = new MockAsyncProxyResolver; + + ProxyService service(config_service, resolver, NULL); + + GURL url("http://this-causes-js-error/"); + + ProxyInfo info; + TestCompletionCallback callback1; + int rv = service.ResolveProxy( + url, &info, callback1.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + EXPECT_EQ(GURL("http://foopy/proxy.pac"), + resolver->pending_set_pac_script_request()->script_data()->url()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(url, resolver->pending_requests()[0]->url()); + + // Simulate a failure in the PAC executor. + resolver->pending_requests()[0]->CompleteNow(ERR_PAC_SCRIPT_FAILED); + + EXPECT_EQ(OK, callback1.WaitForResult()); + + // Since the PAC script was non-mandatory, we should have fallen-back to + // DIRECT. + EXPECT_TRUE(info.is_direct()); + EXPECT_TRUE(info.did_use_pac_script()); + EXPECT_EQ(1, info.config_id()); + + EXPECT_FALSE(info.proxy_resolve_start_time().is_null()); + EXPECT_FALSE(info.proxy_resolve_end_time().is_null()); + EXPECT_LE(info.proxy_resolve_start_time(), info.proxy_resolve_end_time()); +} + +// The proxy list could potentially contain the DIRECT fallback choice +// in a location other than the very end of the list, and could even +// specify it multiple times. +// +// This is not a typical usage, but we will obey it. +// (If we wanted to disallow this type of input, the right place to +// enforce it would be in parsing the PAC result string). +// +// This test will use the PAC result string: +// +// "DIRECT ; PROXY foobar:10 ; DIRECT ; PROXY foobar:20" +// +// For which we expect it to try DIRECT, then foobar:10, then DIRECT again, +// then foobar:20, and then give up and error. +// +// The important check of this test is to make sure that DIRECT is not somehow +// cached as being a bad proxy. +TEST_F(ProxyServiceTest, PAC_FailoverAfterDirect) { + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + MockAsyncProxyResolver* resolver = new MockAsyncProxyResolver; + + ProxyService service(config_service, resolver, NULL); + + GURL url("http://www.google.com/"); + + ProxyInfo info; + TestCompletionCallback callback1; + int rv = service.ResolveProxy( + url, &info, callback1.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + EXPECT_EQ(GURL("http://foopy/proxy.pac"), + resolver->pending_set_pac_script_request()->script_data()->url()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(url, resolver->pending_requests()[0]->url()); + + // Set the result in proxy resolver. + resolver->pending_requests()[0]->results()->UsePacString( + "DIRECT ; PROXY foobar:10 ; DIRECT ; PROXY foobar:20"); + resolver->pending_requests()[0]->CompleteNow(OK); + + EXPECT_EQ(OK, callback1.WaitForResult()); + EXPECT_TRUE(info.is_direct()); + + // Fallback 1. + TestCompletionCallback callback2; + rv = service.ReconsiderProxyAfterError(url, &info, callback2.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(OK, rv); + EXPECT_FALSE(info.is_direct()); + EXPECT_EQ("foobar:10", info.proxy_server().ToURI()); + + // Fallback 2. + TestCompletionCallback callback3; + rv = service.ReconsiderProxyAfterError(url, &info, callback3.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(OK, rv); + EXPECT_TRUE(info.is_direct()); + + // Fallback 3. + TestCompletionCallback callback4; + rv = service.ReconsiderProxyAfterError(url, &info, callback4.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(OK, rv); + EXPECT_FALSE(info.is_direct()); + EXPECT_EQ("foobar:20", info.proxy_server().ToURI()); + + // Fallback 4 -- Nothing to fall back to! + TestCompletionCallback callback5; + rv = service.ReconsiderProxyAfterError(url, &info, callback5.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(ERR_FAILED, rv); + EXPECT_TRUE(info.is_empty()); +} + +TEST_F(ProxyServiceTest, PAC_ConfigSourcePropagates) { + // Test whether the ProxyConfigSource set by the ProxyConfigService is applied + // to ProxyInfo after the proxy is resolved via a PAC script. + ProxyConfig config = + ProxyConfig::CreateFromCustomPacURL(GURL("http://foopy/proxy.pac")); + config.set_source(PROXY_CONFIG_SOURCE_TEST); + + MockProxyConfigService* config_service = new MockProxyConfigService(config); + MockAsyncProxyResolver* resolver = new MockAsyncProxyResolver; + ProxyService service(config_service, resolver, NULL); + + // Resolve something. + GURL url("http://www.google.com/"); + ProxyInfo info; + TestCompletionCallback callback; + int rv = service.ResolveProxy( + url, &info, callback.callback(), NULL, BoundNetLog()); + ASSERT_EQ(ERR_IO_PENDING, rv); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + ASSERT_EQ(1u, resolver->pending_requests().size()); + + // Set the result in proxy resolver. + resolver->pending_requests()[0]->results()->UseNamedProxy("foopy"); + resolver->pending_requests()[0]->CompleteNow(OK); + + EXPECT_EQ(OK, callback.WaitForResult()); + EXPECT_EQ(PROXY_CONFIG_SOURCE_TEST, info.config_source()); + EXPECT_TRUE(info.did_use_pac_script()); + + EXPECT_FALSE(info.proxy_resolve_start_time().is_null()); + EXPECT_FALSE(info.proxy_resolve_end_time().is_null()); + EXPECT_LE(info.proxy_resolve_start_time(), info.proxy_resolve_end_time()); +} + +TEST_F(ProxyServiceTest, ProxyResolverFails) { + // Test what happens when the ProxyResolver fails. The download and setting + // of the PAC script have already succeeded, so this corresponds with a + // javascript runtime error while calling FindProxyForURL(). + + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + + MockAsyncProxyResolver* resolver = new MockAsyncProxyResolver; + + ProxyService service(config_service, resolver, NULL); + + // Start first resolve request. + GURL url("http://www.google.com/"); + ProxyInfo info; + TestCompletionCallback callback1; + int rv = service.ResolveProxy( + url, &info, callback1.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + EXPECT_EQ(GURL("http://foopy/proxy.pac"), + resolver->pending_set_pac_script_request()->script_data()->url()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(url, resolver->pending_requests()[0]->url()); + + // Fail the first resolve request in MockAsyncProxyResolver. + resolver->pending_requests()[0]->CompleteNow(ERR_FAILED); + + // Although the proxy resolver failed the request, ProxyService implicitly + // falls-back to DIRECT. + EXPECT_EQ(OK, callback1.WaitForResult()); + EXPECT_TRUE(info.is_direct()); + + // Failed PAC executions still have proxy resolution times. + EXPECT_FALSE(info.proxy_resolve_start_time().is_null()); + EXPECT_FALSE(info.proxy_resolve_end_time().is_null()); + EXPECT_LE(info.proxy_resolve_start_time(), info.proxy_resolve_end_time()); + + // The second resolve request will try to run through the proxy resolver, + // regardless of whether the first request failed in it. + TestCompletionCallback callback2; + rv = service.ResolveProxy( + url, &info, callback2.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(url, resolver->pending_requests()[0]->url()); + + // This time we will have the resolver succeed (perhaps the PAC script has + // a dependency on the current time). + resolver->pending_requests()[0]->results()->UseNamedProxy("foopy_valid:8080"); + resolver->pending_requests()[0]->CompleteNow(OK); + + EXPECT_EQ(OK, callback2.WaitForResult()); + EXPECT_FALSE(info.is_direct()); + EXPECT_EQ("foopy_valid:8080", info.proxy_server().ToURI()); +} + +TEST_F(ProxyServiceTest, ProxyScriptFetcherFailsDownloadingMandatoryPac) { + // Test what happens when the ProxyScriptResolver fails to download a + // mandatory PAC script. + + ProxyConfig config( + ProxyConfig::CreateFromCustomPacURL(GURL("http://foopy/proxy.pac"))); + config.set_pac_mandatory(true); + + MockProxyConfigService* config_service = new MockProxyConfigService(config); + + MockAsyncProxyResolver* resolver = new MockAsyncProxyResolver; + + ProxyService service(config_service, resolver, NULL); + + // Start first resolve request. + GURL url("http://www.google.com/"); + ProxyInfo info; + TestCompletionCallback callback1; + int rv = service.ResolveProxy( + url, &info, callback1.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + EXPECT_EQ(GURL("http://foopy/proxy.pac"), + resolver->pending_set_pac_script_request()->script_data()->url()); + resolver->pending_set_pac_script_request()->CompleteNow(ERR_FAILED); + + ASSERT_EQ(0u, resolver->pending_requests().size()); + + // As the proxy resolver failed the request and is configured for a mandatory + // PAC script, ProxyService must not implicitly fall-back to DIRECT. + EXPECT_EQ(ERR_MANDATORY_PROXY_CONFIGURATION_FAILED, + callback1.WaitForResult()); + EXPECT_FALSE(info.is_direct()); + + // As the proxy resolver failed the request and is configured for a mandatory + // PAC script, ProxyService must not implicitly fall-back to DIRECT. + TestCompletionCallback callback2; + rv = service.ResolveProxy( + url, &info, callback2.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_MANDATORY_PROXY_CONFIGURATION_FAILED, rv); + EXPECT_FALSE(info.is_direct()); +} + +TEST_F(ProxyServiceTest, ProxyResolverFailsParsingJavaScriptMandatoryPac) { + // Test what happens when the ProxyResolver fails that is configured to use a + // mandatory PAC script. The download of the PAC script has already + // succeeded but the PAC script contains no valid javascript. + + ProxyConfig config( + ProxyConfig::CreateFromCustomPacURL(GURL("http://foopy/proxy.pac"))); + config.set_pac_mandatory(true); + + MockProxyConfigService* config_service = new MockProxyConfigService(config); + + MockAsyncProxyResolverExpectsBytes* resolver = + new MockAsyncProxyResolverExpectsBytes; + + ProxyService service(config_service, resolver, NULL); + + MockProxyScriptFetcher* fetcher = new MockProxyScriptFetcher; + DhcpProxyScriptFetcher* dhcp_fetcher = new DoNothingDhcpProxyScriptFetcher(); + service.SetProxyScriptFetchers(fetcher, dhcp_fetcher); + + // Start resolve request. + GURL url("http://www.google.com/"); + ProxyInfo info; + TestCompletionCallback callback; + int rv = service.ResolveProxy( + url, &info, callback.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Check that nothing has been sent to the proxy resolver yet. + ASSERT_EQ(0u, resolver->pending_requests().size()); + + // Downloading the PAC script succeeds. + EXPECT_TRUE(fetcher->has_pending_request()); + EXPECT_EQ(GURL("http://foopy/proxy.pac"), fetcher->pending_request_url()); + fetcher->NotifyFetchCompletion(OK, "invalid-script-contents"); + + EXPECT_FALSE(fetcher->has_pending_request()); + ASSERT_EQ(0u, resolver->pending_requests().size()); + + // Since ProxyScriptDecider failed to identify a valid PAC and PAC was + // mandatory for this configuration, the ProxyService must not implicitly + // fall-back to DIRECT. + EXPECT_EQ(ERR_MANDATORY_PROXY_CONFIGURATION_FAILED, + callback.WaitForResult()); + EXPECT_FALSE(info.is_direct()); +} + +TEST_F(ProxyServiceTest, ProxyResolverFailsInJavaScriptMandatoryPac) { + // Test what happens when the ProxyResolver fails that is configured to use a + // mandatory PAC script. The download and setting of the PAC script have + // already succeeded, so this corresponds with a javascript runtime error + // while calling FindProxyForURL(). + + ProxyConfig config( + ProxyConfig::CreateFromCustomPacURL(GURL("http://foopy/proxy.pac"))); + config.set_pac_mandatory(true); + + MockProxyConfigService* config_service = new MockProxyConfigService(config); + + MockAsyncProxyResolver* resolver = new MockAsyncProxyResolver; + + ProxyService service(config_service, resolver, NULL); + + // Start first resolve request. + GURL url("http://www.google.com/"); + ProxyInfo info; + TestCompletionCallback callback1; + int rv = service.ResolveProxy( + url, &info, callback1.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + EXPECT_EQ(GURL("http://foopy/proxy.pac"), + resolver->pending_set_pac_script_request()->script_data()->url()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(url, resolver->pending_requests()[0]->url()); + + // Fail the first resolve request in MockAsyncProxyResolver. + resolver->pending_requests()[0]->CompleteNow(ERR_FAILED); + + // As the proxy resolver failed the request and is configured for a mandatory + // PAC script, ProxyService must not implicitly fall-back to DIRECT. + EXPECT_EQ(ERR_MANDATORY_PROXY_CONFIGURATION_FAILED, + callback1.WaitForResult()); + EXPECT_FALSE(info.is_direct()); + + // The second resolve request will try to run through the proxy resolver, + // regardless of whether the first request failed in it. + TestCompletionCallback callback2; + rv = service.ResolveProxy( + url, &info, callback2.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(url, resolver->pending_requests()[0]->url()); + + // This time we will have the resolver succeed (perhaps the PAC script has + // a dependency on the current time). + resolver->pending_requests()[0]->results()->UseNamedProxy("foopy_valid:8080"); + resolver->pending_requests()[0]->CompleteNow(OK); + + EXPECT_EQ(OK, callback2.WaitForResult()); + EXPECT_FALSE(info.is_direct()); + EXPECT_EQ("foopy_valid:8080", info.proxy_server().ToURI()); +} + +TEST_F(ProxyServiceTest, ProxyFallback) { + // Test what happens when we specify multiple proxy servers and some of them + // are bad. + + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + + MockAsyncProxyResolver* resolver = new MockAsyncProxyResolver; + + ProxyService service(config_service, resolver, NULL); + + GURL url("http://www.google.com/"); + + // Get the proxy information. + ProxyInfo info; + TestCompletionCallback callback1; + int rv = service.ResolveProxy( + url, &info, callback1.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + EXPECT_EQ(GURL("http://foopy/proxy.pac"), + resolver->pending_set_pac_script_request()->script_data()->url()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(url, resolver->pending_requests()[0]->url()); + + // Set the result in proxy resolver. + resolver->pending_requests()[0]->results()->UseNamedProxy( + "foopy1:8080;foopy2:9090"); + resolver->pending_requests()[0]->CompleteNow(OK); + + // The first item is valid. + EXPECT_EQ(OK, callback1.WaitForResult()); + EXPECT_FALSE(info.is_direct()); + EXPECT_EQ("foopy1:8080", info.proxy_server().ToURI()); + + EXPECT_FALSE(info.proxy_resolve_start_time().is_null()); + EXPECT_FALSE(info.proxy_resolve_end_time().is_null()); + EXPECT_LE(info.proxy_resolve_start_time(), info.proxy_resolve_end_time()); + base::TimeTicks proxy_resolve_start_time = info.proxy_resolve_start_time(); + base::TimeTicks proxy_resolve_end_time = info.proxy_resolve_end_time(); + + // Fake an error on the proxy. + TestCompletionCallback callback2; + rv = service.ReconsiderProxyAfterError(url, &info, callback2.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(OK, rv); + + // Proxy times should not have been modified by fallback. + EXPECT_EQ(proxy_resolve_start_time, info.proxy_resolve_start_time()); + EXPECT_EQ(proxy_resolve_end_time, info.proxy_resolve_end_time()); + + // The second proxy should be specified. + EXPECT_EQ("foopy2:9090", info.proxy_server().ToURI()); + // Report back that the second proxy worked. This will globally mark the + // first proxy as bad. + service.ReportSuccess(info); + + TestCompletionCallback callback3; + rv = service.ResolveProxy( + url, &info, callback3.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(url, resolver->pending_requests()[0]->url()); + + // Set the result in proxy resolver -- the second result is already known + // to be bad, so we will not try to use it initially. + resolver->pending_requests()[0]->results()->UseNamedProxy( + "foopy3:7070;foopy1:8080;foopy2:9090"); + resolver->pending_requests()[0]->CompleteNow(OK); + + EXPECT_EQ(OK, callback3.WaitForResult()); + EXPECT_FALSE(info.is_direct()); + EXPECT_EQ("foopy3:7070", info.proxy_server().ToURI()); + + // Proxy times should have been updated, so get them again. + EXPECT_LE(proxy_resolve_end_time, info.proxy_resolve_start_time()); + EXPECT_FALSE(info.proxy_resolve_start_time().is_null()); + EXPECT_FALSE(info.proxy_resolve_end_time().is_null()); + EXPECT_LE(info.proxy_resolve_start_time(), info.proxy_resolve_end_time()); + proxy_resolve_start_time = info.proxy_resolve_start_time(); + proxy_resolve_end_time = info.proxy_resolve_end_time(); + + // We fake another error. It should now try the third one. + TestCompletionCallback callback4; + rv = service.ReconsiderProxyAfterError(url, &info, callback4.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(OK, rv); + EXPECT_EQ("foopy2:9090", info.proxy_server().ToURI()); + + // We fake another error. At this point we have tried all of the + // proxy servers we thought were valid; next we try the proxy server + // that was in our bad proxies map (foopy1:8080). + TestCompletionCallback callback5; + rv = service.ReconsiderProxyAfterError(url, &info, callback5.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(OK, rv); + EXPECT_EQ("foopy1:8080", info.proxy_server().ToURI()); + + // Fake another error, the last proxy is gone, the list should now be empty, + // so there is nothing left to try. + TestCompletionCallback callback6; + rv = service.ReconsiderProxyAfterError(url, &info, callback6.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(ERR_FAILED, rv); + EXPECT_FALSE(info.is_direct()); + EXPECT_TRUE(info.is_empty()); + + // Proxy times should not have been modified by fallback. + EXPECT_EQ(proxy_resolve_start_time, info.proxy_resolve_start_time()); + EXPECT_EQ(proxy_resolve_end_time, info.proxy_resolve_end_time()); + + // Look up proxies again + TestCompletionCallback callback7; + rv = service.ResolveProxy(url, &info, callback7.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(url, resolver->pending_requests()[0]->url()); + + // This time, the first 3 results have been found to be bad, but only the + // first proxy has been confirmed ... + resolver->pending_requests()[0]->results()->UseNamedProxy( + "foopy1:8080;foopy3:7070;foopy2:9090;foopy4:9091"); + resolver->pending_requests()[0]->CompleteNow(OK); + + // ... therefore, we should see the second proxy first. + EXPECT_EQ(OK, callback7.WaitForResult()); + EXPECT_FALSE(info.is_direct()); + EXPECT_EQ("foopy3:7070", info.proxy_server().ToURI()); + + EXPECT_LE(proxy_resolve_end_time, info.proxy_resolve_start_time()); + EXPECT_FALSE(info.proxy_resolve_start_time().is_null()); + EXPECT_FALSE(info.proxy_resolve_end_time().is_null()); + // TODO(nsylvain): Test that the proxy can be retried after the delay. +} + +// This test is similar to ProxyFallback, but this time we have an explicit +// fallback choice to DIRECT. +TEST_F(ProxyServiceTest, ProxyFallbackToDirect) { + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + + MockAsyncProxyResolver* resolver = new MockAsyncProxyResolver; + + ProxyService service(config_service, resolver, NULL); + + GURL url("http://www.google.com/"); + + // Get the proxy information. + ProxyInfo info; + TestCompletionCallback callback1; + int rv = service.ResolveProxy( + url, &info, callback1.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + EXPECT_EQ(GURL("http://foopy/proxy.pac"), + resolver->pending_set_pac_script_request()->script_data()->url()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(url, resolver->pending_requests()[0]->url()); + + // Set the result in proxy resolver. + resolver->pending_requests()[0]->results()->UsePacString( + "PROXY foopy1:8080; PROXY foopy2:9090; DIRECT"); + resolver->pending_requests()[0]->CompleteNow(OK); + + // Get the first result. + EXPECT_EQ(OK, callback1.WaitForResult()); + EXPECT_FALSE(info.is_direct()); + EXPECT_EQ("foopy1:8080", info.proxy_server().ToURI()); + + // Fake an error on the proxy. + TestCompletionCallback callback2; + rv = service.ReconsiderProxyAfterError(url, &info, callback2.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(OK, rv); + + // Now we get back the second proxy. + EXPECT_EQ("foopy2:9090", info.proxy_server().ToURI()); + + // Fake an error on this proxy as well. + TestCompletionCallback callback3; + rv = service.ReconsiderProxyAfterError(url, &info, callback3.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(OK, rv); + + // Finally, we get back DIRECT. + EXPECT_TRUE(info.is_direct()); + + EXPECT_FALSE(info.proxy_resolve_start_time().is_null()); + EXPECT_FALSE(info.proxy_resolve_end_time().is_null()); + EXPECT_LE(info.proxy_resolve_start_time(), info.proxy_resolve_end_time()); + + // Now we tell the proxy service that even DIRECT failed. + TestCompletionCallback callback4; + rv = service.ReconsiderProxyAfterError(url, &info, callback4.callback(), NULL, + BoundNetLog()); + // There was nothing left to try after DIRECT, so we are out of + // choices. + EXPECT_EQ(ERR_FAILED, rv); +} + +TEST_F(ProxyServiceTest, ProxyFallback_NewSettings) { + // Test proxy failover when new settings are available. + + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + + MockAsyncProxyResolver* resolver = new MockAsyncProxyResolver; + + ProxyService service(config_service, resolver, NULL); + + GURL url("http://www.google.com/"); + + // Get the proxy information. + ProxyInfo info; + TestCompletionCallback callback1; + int rv = service.ResolveProxy( + url, &info, callback1.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + EXPECT_EQ(GURL("http://foopy/proxy.pac"), + resolver->pending_set_pac_script_request()->script_data()->url()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(url, resolver->pending_requests()[0]->url()); + + // Set the result in proxy resolver. + resolver->pending_requests()[0]->results()->UseNamedProxy( + "foopy1:8080;foopy2:9090"); + resolver->pending_requests()[0]->CompleteNow(OK); + + // The first item is valid. + EXPECT_EQ(OK, callback1.WaitForResult()); + EXPECT_FALSE(info.is_direct()); + EXPECT_EQ("foopy1:8080", info.proxy_server().ToURI()); + + // Fake an error on the proxy, and also a new configuration on the proxy. + config_service->SetConfig( + ProxyConfig::CreateFromCustomPacURL(GURL("http://foopy-new/proxy.pac"))); + + TestCompletionCallback callback2; + rv = service.ReconsiderProxyAfterError(url, &info, callback2.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + EXPECT_EQ(GURL("http://foopy-new/proxy.pac"), + resolver->pending_set_pac_script_request()->script_data()->url()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(url, resolver->pending_requests()[0]->url()); + + resolver->pending_requests()[0]->results()->UseNamedProxy( + "foopy1:8080;foopy2:9090"); + resolver->pending_requests()[0]->CompleteNow(OK); + + // The first proxy is still there since the configuration changed. + EXPECT_EQ(OK, callback2.WaitForResult()); + EXPECT_EQ("foopy1:8080", info.proxy_server().ToURI()); + + // We fake another error. It should now ignore the first one. + TestCompletionCallback callback3; + rv = service.ReconsiderProxyAfterError(url, &info, callback3.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(OK, rv); + EXPECT_EQ("foopy2:9090", info.proxy_server().ToURI()); + + // We simulate a new configuration. + config_service->SetConfig( + ProxyConfig::CreateFromCustomPacURL( + GURL("http://foopy-new2/proxy.pac"))); + + // We fake another error. It should go back to the first proxy. + TestCompletionCallback callback4; + rv = service.ReconsiderProxyAfterError(url, &info, callback4.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + EXPECT_EQ(GURL("http://foopy-new2/proxy.pac"), + resolver->pending_set_pac_script_request()->script_data()->url()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(url, resolver->pending_requests()[0]->url()); + + resolver->pending_requests()[0]->results()->UseNamedProxy( + "foopy1:8080;foopy2:9090"); + resolver->pending_requests()[0]->CompleteNow(OK); + + EXPECT_EQ(OK, callback4.WaitForResult()); + EXPECT_EQ("foopy1:8080", info.proxy_server().ToURI()); + + EXPECT_FALSE(info.proxy_resolve_start_time().is_null()); + EXPECT_FALSE(info.proxy_resolve_end_time().is_null()); + EXPECT_LE(info.proxy_resolve_start_time(), info.proxy_resolve_end_time()); +} + +TEST_F(ProxyServiceTest, ProxyFallback_BadConfig) { + // Test proxy failover when the configuration is bad. + + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + + MockAsyncProxyResolver* resolver = new MockAsyncProxyResolver; + + ProxyService service(config_service, resolver, NULL); + + GURL url("http://www.google.com/"); + + // Get the proxy information. + ProxyInfo info; + TestCompletionCallback callback1; + int rv = service.ResolveProxy( + url, &info, callback1.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + EXPECT_EQ(GURL("http://foopy/proxy.pac"), + resolver->pending_set_pac_script_request()->script_data()->url()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(url, resolver->pending_requests()[0]->url()); + + resolver->pending_requests()[0]->results()->UseNamedProxy( + "foopy1:8080;foopy2:9090"); + resolver->pending_requests()[0]->CompleteNow(OK); + + // The first item is valid. + EXPECT_EQ(OK, callback1.WaitForResult()); + EXPECT_FALSE(info.is_direct()); + EXPECT_EQ("foopy1:8080", info.proxy_server().ToURI()); + + // Fake a proxy error. + TestCompletionCallback callback2; + rv = service.ReconsiderProxyAfterError(url, &info, callback2.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(OK, rv); + + // The first proxy is ignored, and the second one is selected. + EXPECT_FALSE(info.is_direct()); + EXPECT_EQ("foopy2:9090", info.proxy_server().ToURI()); + + // Fake a PAC failure. + ProxyInfo info2; + TestCompletionCallback callback3; + rv = service.ResolveProxy( + url, &info2, callback3.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(url, resolver->pending_requests()[0]->url()); + + // This simulates a javascript runtime error in the PAC script. + resolver->pending_requests()[0]->CompleteNow(ERR_FAILED); + + // Although the resolver failed, the ProxyService will implicitly fall-back + // to a DIRECT connection. + EXPECT_EQ(OK, callback3.WaitForResult()); + EXPECT_TRUE(info2.is_direct()); + EXPECT_FALSE(info2.is_empty()); + + // The PAC script will work properly next time and successfully return a + // proxy list. Since we have not marked the configuration as bad, it should + // "just work" the next time we call it. + ProxyInfo info3; + TestCompletionCallback callback4; + rv = service.ReconsiderProxyAfterError(url, &info3, callback4.callback(), + NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(url, resolver->pending_requests()[0]->url()); + + resolver->pending_requests()[0]->results()->UseNamedProxy( + "foopy1:8080;foopy2:9090"); + resolver->pending_requests()[0]->CompleteNow(OK); + + // The first proxy is not there since the it was added to the bad proxies + // list by the earlier ReconsiderProxyAfterError(). + EXPECT_EQ(OK, callback4.WaitForResult()); + EXPECT_FALSE(info3.is_direct()); + EXPECT_EQ("foopy1:8080", info3.proxy_server().ToURI()); + + EXPECT_FALSE(info.proxy_resolve_start_time().is_null()); + EXPECT_FALSE(info.proxy_resolve_end_time().is_null()); + EXPECT_LE(info.proxy_resolve_start_time(), info.proxy_resolve_end_time()); +} + +TEST_F(ProxyServiceTest, ProxyFallback_BadConfigMandatory) { + // Test proxy failover when the configuration is bad. + + ProxyConfig config( + ProxyConfig::CreateFromCustomPacURL(GURL("http://foopy/proxy.pac"))); + + config.set_pac_mandatory(true); + MockProxyConfigService* config_service = new MockProxyConfigService(config); + + MockAsyncProxyResolver* resolver = new MockAsyncProxyResolver; + + ProxyService service(config_service, resolver, NULL); + + GURL url("http://www.google.com/"); + + // Get the proxy information. + ProxyInfo info; + TestCompletionCallback callback1; + int rv = service.ResolveProxy( + url, &info, callback1.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + EXPECT_EQ(GURL("http://foopy/proxy.pac"), + resolver->pending_set_pac_script_request()->script_data()->url()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(url, resolver->pending_requests()[0]->url()); + + resolver->pending_requests()[0]->results()->UseNamedProxy( + "foopy1:8080;foopy2:9090"); + resolver->pending_requests()[0]->CompleteNow(OK); + + // The first item is valid. + EXPECT_EQ(OK, callback1.WaitForResult()); + EXPECT_FALSE(info.is_direct()); + EXPECT_EQ("foopy1:8080", info.proxy_server().ToURI()); + + // Fake a proxy error. + TestCompletionCallback callback2; + rv = service.ReconsiderProxyAfterError(url, &info, callback2.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(OK, rv); + + // The first proxy is ignored, and the second one is selected. + EXPECT_FALSE(info.is_direct()); + EXPECT_EQ("foopy2:9090", info.proxy_server().ToURI()); + + // Fake a PAC failure. + ProxyInfo info2; + TestCompletionCallback callback3; + rv = service.ResolveProxy( + url, &info2, callback3.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(url, resolver->pending_requests()[0]->url()); + + // This simulates a javascript runtime error in the PAC script. + resolver->pending_requests()[0]->CompleteNow(ERR_FAILED); + + // Although the resolver failed, the ProxyService will NOT fall-back + // to a DIRECT connection as it is configured as mandatory. + EXPECT_EQ(ERR_MANDATORY_PROXY_CONFIGURATION_FAILED, + callback3.WaitForResult()); + EXPECT_FALSE(info2.is_direct()); + EXPECT_TRUE(info2.is_empty()); + + // The PAC script will work properly next time and successfully return a + // proxy list. Since we have not marked the configuration as bad, it should + // "just work" the next time we call it. + ProxyInfo info3; + TestCompletionCallback callback4; + rv = service.ReconsiderProxyAfterError(url, &info3, callback4.callback(), + NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(url, resolver->pending_requests()[0]->url()); + + resolver->pending_requests()[0]->results()->UseNamedProxy( + "foopy1:8080;foopy2:9090"); + resolver->pending_requests()[0]->CompleteNow(OK); + + // The first proxy is not there since the it was added to the bad proxies + // list by the earlier ReconsiderProxyAfterError(). + EXPECT_EQ(OK, callback4.WaitForResult()); + EXPECT_FALSE(info3.is_direct()); + EXPECT_EQ("foopy1:8080", info3.proxy_server().ToURI()); +} + +TEST_F(ProxyServiceTest, ProxyBypassList) { + // Test that the proxy bypass rules are consulted. + + TestCompletionCallback callback[2]; + ProxyInfo info[2]; + ProxyConfig config; + config.proxy_rules().ParseFromString("foopy1:8080;foopy2:9090"); + config.set_auto_detect(false); + config.proxy_rules().bypass_rules.ParseFromString("*.org"); + + ProxyService service( + new MockProxyConfigService(config), new MockAsyncProxyResolver, NULL); + + int rv; + GURL url1("http://www.webkit.org"); + GURL url2("http://www.webkit.com"); + + // Request for a .org domain should bypass proxy. + rv = service.ResolveProxy( + url1, &info[0], callback[0].callback(), NULL, BoundNetLog()); + EXPECT_EQ(OK, rv); + EXPECT_TRUE(info[0].is_direct()); + + // Request for a .com domain hits the proxy. + rv = service.ResolveProxy( + url2, &info[1], callback[1].callback(), NULL, BoundNetLog()); + EXPECT_EQ(OK, rv); + EXPECT_EQ("foopy1:8080", info[1].proxy_server().ToURI()); +} + + +TEST_F(ProxyServiceTest, PerProtocolProxyTests) { + ProxyConfig config; + config.proxy_rules().ParseFromString("http=foopy1:8080;https=foopy2:8080"); + config.set_auto_detect(false); + { + ProxyService service( + new MockProxyConfigService(config), new MockAsyncProxyResolver, NULL); + GURL test_url("http://www.msn.com"); + ProxyInfo info; + TestCompletionCallback callback; + int rv = service.ResolveProxy(test_url, &info, callback.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(OK, rv); + EXPECT_FALSE(info.is_direct()); + EXPECT_EQ("foopy1:8080", info.proxy_server().ToURI()); + } + { + ProxyService service( + new MockProxyConfigService(config), new MockAsyncProxyResolver, NULL); + GURL test_url("ftp://ftp.google.com"); + ProxyInfo info; + TestCompletionCallback callback; + int rv = service.ResolveProxy(test_url, &info, callback.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(OK, rv); + EXPECT_TRUE(info.is_direct()); + EXPECT_EQ("direct://", info.proxy_server().ToURI()); + } + { + ProxyService service( + new MockProxyConfigService(config), new MockAsyncProxyResolver, NULL); + GURL test_url("https://webbranch.techcu.com"); + ProxyInfo info; + TestCompletionCallback callback; + int rv = service.ResolveProxy(test_url, &info, callback.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(OK, rv); + EXPECT_FALSE(info.is_direct()); + EXPECT_EQ("foopy2:8080", info.proxy_server().ToURI()); + } + { + config.proxy_rules().ParseFromString("foopy1:8080"); + ProxyService service( + new MockProxyConfigService(config), new MockAsyncProxyResolver, NULL); + GURL test_url("http://www.microsoft.com"); + ProxyInfo info; + TestCompletionCallback callback; + int rv = service.ResolveProxy(test_url, &info, callback.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(OK, rv); + EXPECT_FALSE(info.is_direct()); + EXPECT_EQ("foopy1:8080", info.proxy_server().ToURI()); + } +} + +TEST_F(ProxyServiceTest, ProxyConfigSourcePropagates) { + // Test that the proxy config source is set correctly when resolving proxies + // using manual proxy rules. Namely, the config source should only be set if + // any of the rules were applied. + { + ProxyConfig config; + config.set_source(PROXY_CONFIG_SOURCE_TEST); + config.proxy_rules().ParseFromString("https=foopy2:8080"); + ProxyService service( + new MockProxyConfigService(config), new MockAsyncProxyResolver, NULL); + GURL test_url("http://www.google.com"); + ProxyInfo info; + TestCompletionCallback callback; + int rv = service.ResolveProxy(test_url, &info, callback.callback(), NULL, + BoundNetLog()); + ASSERT_EQ(OK, rv); + // Should be SOURCE_TEST, even if there are no HTTP proxies configured. + EXPECT_EQ(PROXY_CONFIG_SOURCE_TEST, info.config_source()); + } + { + ProxyConfig config; + config.set_source(PROXY_CONFIG_SOURCE_TEST); + config.proxy_rules().ParseFromString("https=foopy2:8080"); + ProxyService service( + new MockProxyConfigService(config), new MockAsyncProxyResolver, NULL); + GURL test_url("https://www.google.com"); + ProxyInfo info; + TestCompletionCallback callback; + int rv = service.ResolveProxy(test_url, &info, callback.callback(), NULL, + BoundNetLog()); + ASSERT_EQ(OK, rv); + // Used the HTTPS proxy. So source should be TEST. + EXPECT_EQ(PROXY_CONFIG_SOURCE_TEST, info.config_source()); + } + { + ProxyConfig config; + config.set_source(PROXY_CONFIG_SOURCE_TEST); + ProxyService service( + new MockProxyConfigService(config), new MockAsyncProxyResolver, NULL); + GURL test_url("http://www.google.com"); + ProxyInfo info; + TestCompletionCallback callback; + int rv = service.ResolveProxy(test_url, &info, callback.callback(), NULL, + BoundNetLog()); + ASSERT_EQ(OK, rv); + // ProxyConfig is empty. Source should still be TEST. + EXPECT_EQ(PROXY_CONFIG_SOURCE_TEST, info.config_source()); + } +} + +// If only HTTP and a SOCKS proxy are specified, check if ftp/https queries +// fall back to the SOCKS proxy. +TEST_F(ProxyServiceTest, DefaultProxyFallbackToSOCKS) { + ProxyConfig config; + config.proxy_rules().ParseFromString("http=foopy1:8080;socks=foopy2:1080"); + config.set_auto_detect(false); + EXPECT_EQ(ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME, + config.proxy_rules().type); + + { + ProxyService service( + new MockProxyConfigService(config), new MockAsyncProxyResolver, NULL); + GURL test_url("http://www.msn.com"); + ProxyInfo info; + TestCompletionCallback callback; + int rv = service.ResolveProxy(test_url, &info, callback.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(OK, rv); + EXPECT_FALSE(info.is_direct()); + EXPECT_EQ("foopy1:8080", info.proxy_server().ToURI()); + } + { + ProxyService service( + new MockProxyConfigService(config), new MockAsyncProxyResolver, NULL); + GURL test_url("ftp://ftp.google.com"); + ProxyInfo info; + TestCompletionCallback callback; + int rv = service.ResolveProxy(test_url, &info, callback.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(OK, rv); + EXPECT_FALSE(info.is_direct()); + EXPECT_EQ("socks4://foopy2:1080", info.proxy_server().ToURI()); + } + { + ProxyService service( + new MockProxyConfigService(config), new MockAsyncProxyResolver, NULL); + GURL test_url("https://webbranch.techcu.com"); + ProxyInfo info; + TestCompletionCallback callback; + int rv = service.ResolveProxy(test_url, &info, callback.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(OK, rv); + EXPECT_FALSE(info.is_direct()); + EXPECT_EQ("socks4://foopy2:1080", info.proxy_server().ToURI()); + } + { + ProxyService service( + new MockProxyConfigService(config), new MockAsyncProxyResolver, NULL); + GURL test_url("unknown://www.microsoft.com"); + ProxyInfo info; + TestCompletionCallback callback; + int rv = service.ResolveProxy(test_url, &info, callback.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(OK, rv); + EXPECT_FALSE(info.is_direct()); + EXPECT_EQ("socks4://foopy2:1080", info.proxy_server().ToURI()); + } +} + +// Test cancellation of an in-progress request. +TEST_F(ProxyServiceTest, CancelInProgressRequest) { + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + + MockAsyncProxyResolver* resolver = new MockAsyncProxyResolver; + + ProxyService service(config_service, resolver, NULL); + + // Start 3 requests. + + ProxyInfo info1; + TestCompletionCallback callback1; + int rv = service.ResolveProxy(GURL("http://request1"), &info1, + callback1.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Nothing has been sent to the proxy resolver yet, since the proxy + // resolver has not been configured yet. + ASSERT_EQ(0u, resolver->pending_requests().size()); + + // Successfully initialize the PAC script. + EXPECT_EQ(GURL("http://foopy/proxy.pac"), + resolver->pending_set_pac_script_request()->script_data()->url()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(GURL("http://request1"), resolver->pending_requests()[0]->url()); + + ProxyInfo info2; + TestCompletionCallback callback2; + ProxyService::PacRequest* request2; + rv = service.ResolveProxy(GURL("http://request2"), &info2, + callback2.callback(), &request2, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + ASSERT_EQ(2u, resolver->pending_requests().size()); + EXPECT_EQ(GURL("http://request2"), resolver->pending_requests()[1]->url()); + + ProxyInfo info3; + TestCompletionCallback callback3; + rv = service.ResolveProxy(GURL("http://request3"), &info3, + callback3.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + ASSERT_EQ(3u, resolver->pending_requests().size()); + EXPECT_EQ(GURL("http://request3"), resolver->pending_requests()[2]->url()); + + // Cancel the second request + service.CancelPacRequest(request2); + + ASSERT_EQ(2u, resolver->pending_requests().size()); + EXPECT_EQ(GURL("http://request1"), resolver->pending_requests()[0]->url()); + EXPECT_EQ(GURL("http://request3"), resolver->pending_requests()[1]->url()); + + // Complete the two un-cancelled requests. + // We complete the last one first, just to mix it up a bit. + resolver->pending_requests()[1]->results()->UseNamedProxy("request3:80"); + resolver->pending_requests()[1]->CompleteNow(OK); + + resolver->pending_requests()[0]->results()->UseNamedProxy("request1:80"); + resolver->pending_requests()[0]->CompleteNow(OK); + + // Complete and verify that requests ran as expected. + EXPECT_EQ(OK, callback1.WaitForResult()); + EXPECT_EQ("request1:80", info1.proxy_server().ToURI()); + + EXPECT_FALSE(callback2.have_result()); // Cancelled. + ASSERT_EQ(1u, resolver->cancelled_requests().size()); + EXPECT_EQ(GURL("http://request2"), resolver->cancelled_requests()[0]->url()); + + EXPECT_EQ(OK, callback3.WaitForResult()); + EXPECT_EQ("request3:80", info3.proxy_server().ToURI()); +} + +// Test the initial PAC download for resolver that expects bytes. +TEST_F(ProxyServiceTest, InitialPACScriptDownload) { + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + + MockAsyncProxyResolverExpectsBytes* resolver = + new MockAsyncProxyResolverExpectsBytes; + + ProxyService service(config_service, resolver, NULL); + + MockProxyScriptFetcher* fetcher = new MockProxyScriptFetcher; + service.SetProxyScriptFetchers(fetcher, + new DoNothingDhcpProxyScriptFetcher()); + + // Start 3 requests. + + ProxyInfo info1; + TestCompletionCallback callback1; + ProxyService::PacRequest* request1; + int rv = service.ResolveProxy(GURL("http://request1"), &info1, + callback1.callback(), &request1, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // The first request should have triggered download of PAC script. + EXPECT_TRUE(fetcher->has_pending_request()); + EXPECT_EQ(GURL("http://foopy/proxy.pac"), fetcher->pending_request_url()); + + ProxyInfo info2; + TestCompletionCallback callback2; + ProxyService::PacRequest* request2; + rv = service.ResolveProxy(GURL("http://request2"), &info2, + callback2.callback(), &request2, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + ProxyInfo info3; + TestCompletionCallback callback3; + ProxyService::PacRequest* request3; + rv = service.ResolveProxy(GURL("http://request3"), &info3, + callback3.callback(), &request3, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Nothing has been sent to the resolver yet. + EXPECT_TRUE(resolver->pending_requests().empty()); + + EXPECT_EQ(LOAD_STATE_DOWNLOADING_PROXY_SCRIPT, + service.GetLoadState(request1)); + EXPECT_EQ(LOAD_STATE_DOWNLOADING_PROXY_SCRIPT, + service.GetLoadState(request2)); + EXPECT_EQ(LOAD_STATE_DOWNLOADING_PROXY_SCRIPT, + service.GetLoadState(request3)); + + // At this point the ProxyService should be waiting for the + // ProxyScriptFetcher to invoke its completion callback, notifying it of + // PAC script download completion. + fetcher->NotifyFetchCompletion(OK, kValidPacScript1); + + // Now that the PAC script is downloaded, it will have been sent to the proxy + // resolver. + EXPECT_EQ(ASCIIToUTF16(kValidPacScript1), + resolver->pending_set_pac_script_request()->script_data()->utf16()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + ASSERT_EQ(3u, resolver->pending_requests().size()); + EXPECT_EQ(GURL("http://request1"), resolver->pending_requests()[0]->url()); + EXPECT_EQ(GURL("http://request2"), resolver->pending_requests()[1]->url()); + EXPECT_EQ(GURL("http://request3"), resolver->pending_requests()[2]->url()); + + EXPECT_EQ(LOAD_STATE_RESOLVING_PROXY_FOR_URL, service.GetLoadState(request1)); + EXPECT_EQ(LOAD_STATE_RESOLVING_PROXY_FOR_URL, service.GetLoadState(request2)); + EXPECT_EQ(LOAD_STATE_RESOLVING_PROXY_FOR_URL, service.GetLoadState(request3)); + + // Complete all the requests (in some order). + // Note that as we complete requests, they shift up in |pending_requests()|. + + resolver->pending_requests()[2]->results()->UseNamedProxy("request3:80"); + resolver->pending_requests()[2]->CompleteNow(OK); + + resolver->pending_requests()[0]->results()->UseNamedProxy("request1:80"); + resolver->pending_requests()[0]->CompleteNow(OK); + + resolver->pending_requests()[0]->results()->UseNamedProxy("request2:80"); + resolver->pending_requests()[0]->CompleteNow(OK); + + // Complete and verify that requests ran as expected. + EXPECT_EQ(OK, callback1.WaitForResult()); + EXPECT_EQ("request1:80", info1.proxy_server().ToURI()); + EXPECT_FALSE(info1.proxy_resolve_start_time().is_null()); + EXPECT_FALSE(info1.proxy_resolve_end_time().is_null()); + EXPECT_LE(info1.proxy_resolve_start_time(), info1.proxy_resolve_end_time()); + + EXPECT_EQ(OK, callback2.WaitForResult()); + EXPECT_EQ("request2:80", info2.proxy_server().ToURI()); + EXPECT_FALSE(info2.proxy_resolve_start_time().is_null()); + EXPECT_FALSE(info2.proxy_resolve_end_time().is_null()); + EXPECT_LE(info2.proxy_resolve_start_time(), info2.proxy_resolve_end_time()); + + EXPECT_EQ(OK, callback3.WaitForResult()); + EXPECT_EQ("request3:80", info3.proxy_server().ToURI()); + EXPECT_FALSE(info3.proxy_resolve_start_time().is_null()); + EXPECT_FALSE(info3.proxy_resolve_end_time().is_null()); + EXPECT_LE(info3.proxy_resolve_start_time(), info3.proxy_resolve_end_time()); +} + +// Test changing the ProxyScriptFetcher while PAC download is in progress. +TEST_F(ProxyServiceTest, ChangeScriptFetcherWhilePACDownloadInProgress) { + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + + MockAsyncProxyResolverExpectsBytes* resolver = + new MockAsyncProxyResolverExpectsBytes; + + ProxyService service(config_service, resolver, NULL); + + MockProxyScriptFetcher* fetcher = new MockProxyScriptFetcher; + service.SetProxyScriptFetchers(fetcher, + new DoNothingDhcpProxyScriptFetcher()); + + // Start 2 requests. + + ProxyInfo info1; + TestCompletionCallback callback1; + int rv = service.ResolveProxy(GURL("http://request1"), &info1, + callback1.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // The first request should have triggered download of PAC script. + EXPECT_TRUE(fetcher->has_pending_request()); + EXPECT_EQ(GURL("http://foopy/proxy.pac"), fetcher->pending_request_url()); + + ProxyInfo info2; + TestCompletionCallback callback2; + rv = service.ResolveProxy(GURL("http://request2"), &info2, + callback2.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // At this point the ProxyService should be waiting for the + // ProxyScriptFetcher to invoke its completion callback, notifying it of + // PAC script download completion. + + // We now change out the ProxyService's script fetcher. We should restart + // the initialization with the new fetcher. + + fetcher = new MockProxyScriptFetcher; + service.SetProxyScriptFetchers(fetcher, + new DoNothingDhcpProxyScriptFetcher()); + + // Nothing has been sent to the resolver yet. + EXPECT_TRUE(resolver->pending_requests().empty()); + + fetcher->NotifyFetchCompletion(OK, kValidPacScript1); + + // Now that the PAC script is downloaded, it will have been sent to the proxy + // resolver. + EXPECT_EQ(ASCIIToUTF16(kValidPacScript1), + resolver->pending_set_pac_script_request()->script_data()->utf16()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + ASSERT_EQ(2u, resolver->pending_requests().size()); + EXPECT_EQ(GURL("http://request1"), resolver->pending_requests()[0]->url()); + EXPECT_EQ(GURL("http://request2"), resolver->pending_requests()[1]->url()); +} + +// Test cancellation of a request, while the PAC script is being fetched. +TEST_F(ProxyServiceTest, CancelWhilePACFetching) { + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + + MockAsyncProxyResolverExpectsBytes* resolver = + new MockAsyncProxyResolverExpectsBytes; + + ProxyService service(config_service, resolver, NULL); + + MockProxyScriptFetcher* fetcher = new MockProxyScriptFetcher; + service.SetProxyScriptFetchers(fetcher, + new DoNothingDhcpProxyScriptFetcher()); + + // Start 3 requests. + ProxyInfo info1; + TestCompletionCallback callback1; + ProxyService::PacRequest* request1; + CapturingBoundNetLog log1; + int rv = service.ResolveProxy(GURL("http://request1"), &info1, + callback1.callback(), &request1, log1.bound()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // The first request should have triggered download of PAC script. + EXPECT_TRUE(fetcher->has_pending_request()); + EXPECT_EQ(GURL("http://foopy/proxy.pac"), fetcher->pending_request_url()); + + ProxyInfo info2; + TestCompletionCallback callback2; + ProxyService::PacRequest* request2; + rv = service.ResolveProxy(GURL("http://request2"), &info2, + callback2.callback(), &request2, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + ProxyInfo info3; + TestCompletionCallback callback3; + rv = service.ResolveProxy(GURL("http://request3"), &info3, + callback3.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Nothing has been sent to the resolver yet. + EXPECT_TRUE(resolver->pending_requests().empty()); + + // Cancel the first 2 requests. + service.CancelPacRequest(request1); + service.CancelPacRequest(request2); + + // At this point the ProxyService should be waiting for the + // ProxyScriptFetcher to invoke its completion callback, notifying it of + // PAC script download completion. + fetcher->NotifyFetchCompletion(OK, kValidPacScript1); + + // Now that the PAC script is downloaded, it will have been sent to the + // proxy resolver. + EXPECT_EQ(ASCIIToUTF16(kValidPacScript1), + resolver->pending_set_pac_script_request()->script_data()->utf16()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(GURL("http://request3"), resolver->pending_requests()[0]->url()); + + // Complete all the requests. + resolver->pending_requests()[0]->results()->UseNamedProxy("request3:80"); + resolver->pending_requests()[0]->CompleteNow(OK); + + EXPECT_EQ(OK, callback3.WaitForResult()); + EXPECT_EQ("request3:80", info3.proxy_server().ToURI()); + + EXPECT_TRUE(resolver->cancelled_requests().empty()); + + EXPECT_FALSE(callback1.have_result()); // Cancelled. + EXPECT_FALSE(callback2.have_result()); // Cancelled. + + CapturingNetLog::CapturedEntryList entries1; + log1.GetEntries(&entries1); + + // Check the NetLog for request 1 (which was cancelled) got filled properly. + EXPECT_EQ(4u, entries1.size()); + EXPECT_TRUE(LogContainsBeginEvent( + entries1, 0, NetLog::TYPE_PROXY_SERVICE)); + EXPECT_TRUE(LogContainsBeginEvent( + entries1, 1, NetLog::TYPE_PROXY_SERVICE_WAITING_FOR_INIT_PAC)); + // Note that TYPE_PROXY_SERVICE_WAITING_FOR_INIT_PAC is never completed before + // the cancellation occured. + EXPECT_TRUE(LogContainsEvent( + entries1, 2, NetLog::TYPE_CANCELLED, NetLog::PHASE_NONE)); + EXPECT_TRUE(LogContainsEndEvent( + entries1, 3, NetLog::TYPE_PROXY_SERVICE)); +} + +// Test that if auto-detect fails, we fall-back to the custom pac. +TEST_F(ProxyServiceTest, FallbackFromAutodetectToCustomPac) { + ProxyConfig config; + config.set_auto_detect(true); + config.set_pac_url(GURL("http://foopy/proxy.pac")); + config.proxy_rules().ParseFromString("http=foopy:80"); // Won't be used. + + MockProxyConfigService* config_service = new MockProxyConfigService(config); + MockAsyncProxyResolverExpectsBytes* resolver = + new MockAsyncProxyResolverExpectsBytes; + ProxyService service(config_service, resolver, NULL); + + MockProxyScriptFetcher* fetcher = new MockProxyScriptFetcher; + service.SetProxyScriptFetchers(fetcher, + new DoNothingDhcpProxyScriptFetcher()); + + // Start 2 requests. + + ProxyInfo info1; + TestCompletionCallback callback1; + int rv = service.ResolveProxy(GURL("http://request1"), &info1, + callback1.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + ProxyInfo info2; + TestCompletionCallback callback2; + ProxyService::PacRequest* request2; + rv = service.ResolveProxy(GURL("http://request2"), &info2, + callback2.callback(), &request2, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Check that nothing has been sent to the proxy resolver yet. + ASSERT_EQ(0u, resolver->pending_requests().size()); + + // It should be trying to auto-detect first -- FAIL the autodetect during + // the script download. + EXPECT_TRUE(fetcher->has_pending_request()); + EXPECT_EQ(GURL("http://wpad/wpad.dat"), fetcher->pending_request_url()); + fetcher->NotifyFetchCompletion(ERR_FAILED, std::string()); + + // Next it should be trying the custom PAC url. + EXPECT_TRUE(fetcher->has_pending_request()); + EXPECT_EQ(GURL("http://foopy/proxy.pac"), fetcher->pending_request_url()); + fetcher->NotifyFetchCompletion(OK, kValidPacScript1); + + EXPECT_EQ(ASCIIToUTF16(kValidPacScript1), + resolver->pending_set_pac_script_request()->script_data()->utf16()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + // Now finally, the pending requests should have been sent to the resolver + // (which was initialized with custom PAC script). + + ASSERT_EQ(2u, resolver->pending_requests().size()); + EXPECT_EQ(GURL("http://request1"), resolver->pending_requests()[0]->url()); + EXPECT_EQ(GURL("http://request2"), resolver->pending_requests()[1]->url()); + + // Complete the pending requests. + resolver->pending_requests()[1]->results()->UseNamedProxy("request2:80"); + resolver->pending_requests()[1]->CompleteNow(OK); + resolver->pending_requests()[0]->results()->UseNamedProxy("request1:80"); + resolver->pending_requests()[0]->CompleteNow(OK); + + // Verify that requests ran as expected. + EXPECT_EQ(OK, callback1.WaitForResult()); + EXPECT_EQ("request1:80", info1.proxy_server().ToURI()); + EXPECT_FALSE(info1.proxy_resolve_start_time().is_null()); + EXPECT_FALSE(info1.proxy_resolve_end_time().is_null()); + EXPECT_LE(info1.proxy_resolve_start_time(), info1.proxy_resolve_end_time()); + + EXPECT_EQ(OK, callback2.WaitForResult()); + EXPECT_EQ("request2:80", info2.proxy_server().ToURI()); + EXPECT_FALSE(info2.proxy_resolve_start_time().is_null()); + EXPECT_FALSE(info2.proxy_resolve_end_time().is_null()); + EXPECT_LE(info2.proxy_resolve_start_time(), info2.proxy_resolve_end_time()); +} + +// This is the same test as FallbackFromAutodetectToCustomPac, except +// the auto-detect script fails parsing rather than downloading. +TEST_F(ProxyServiceTest, FallbackFromAutodetectToCustomPac2) { + ProxyConfig config; + config.set_auto_detect(true); + config.set_pac_url(GURL("http://foopy/proxy.pac")); + config.proxy_rules().ParseFromString("http=foopy:80"); // Won't be used. + + MockProxyConfigService* config_service = new MockProxyConfigService(config); + MockAsyncProxyResolverExpectsBytes* resolver = + new MockAsyncProxyResolverExpectsBytes; + ProxyService service(config_service, resolver, NULL); + + MockProxyScriptFetcher* fetcher = new MockProxyScriptFetcher; + service.SetProxyScriptFetchers(fetcher, + new DoNothingDhcpProxyScriptFetcher()); + + // Start 2 requests. + + ProxyInfo info1; + TestCompletionCallback callback1; + int rv = service.ResolveProxy(GURL("http://request1"), &info1, + callback1.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + ProxyInfo info2; + TestCompletionCallback callback2; + ProxyService::PacRequest* request2; + rv = service.ResolveProxy(GURL("http://request2"), &info2, + callback2.callback(), &request2, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Check that nothing has been sent to the proxy resolver yet. + ASSERT_EQ(0u, resolver->pending_requests().size()); + + // It should be trying to auto-detect first -- succeed the download. + EXPECT_TRUE(fetcher->has_pending_request()); + EXPECT_EQ(GURL("http://wpad/wpad.dat"), fetcher->pending_request_url()); + fetcher->NotifyFetchCompletion(OK, "invalid-script-contents"); + + // The script contents passed failed basic verification step (since didn't + // contain token FindProxyForURL), so it was never passed to the resolver. + + // Next it should be trying the custom PAC url. + EXPECT_TRUE(fetcher->has_pending_request()); + EXPECT_EQ(GURL("http://foopy/proxy.pac"), fetcher->pending_request_url()); + fetcher->NotifyFetchCompletion(OK, kValidPacScript1); + + EXPECT_EQ(ASCIIToUTF16(kValidPacScript1), + resolver->pending_set_pac_script_request()->script_data()->utf16()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + // Now finally, the pending requests should have been sent to the resolver + // (which was initialized with custom PAC script). + + ASSERT_EQ(2u, resolver->pending_requests().size()); + EXPECT_EQ(GURL("http://request1"), resolver->pending_requests()[0]->url()); + EXPECT_EQ(GURL("http://request2"), resolver->pending_requests()[1]->url()); + + // Complete the pending requests. + resolver->pending_requests()[1]->results()->UseNamedProxy("request2:80"); + resolver->pending_requests()[1]->CompleteNow(OK); + resolver->pending_requests()[0]->results()->UseNamedProxy("request1:80"); + resolver->pending_requests()[0]->CompleteNow(OK); + + // Verify that requests ran as expected. + EXPECT_EQ(OK, callback1.WaitForResult()); + EXPECT_EQ("request1:80", info1.proxy_server().ToURI()); + + EXPECT_EQ(OK, callback2.WaitForResult()); + EXPECT_EQ("request2:80", info2.proxy_server().ToURI()); +} + +// Test that if all of auto-detect, a custom PAC script, and manual settings +// are given, then we will try them in that order. +TEST_F(ProxyServiceTest, FallbackFromAutodetectToCustomToManual) { + ProxyConfig config; + config.set_auto_detect(true); + config.set_pac_url(GURL("http://foopy/proxy.pac")); + config.proxy_rules().ParseFromString("http=foopy:80"); + + MockProxyConfigService* config_service = new MockProxyConfigService(config); + MockAsyncProxyResolverExpectsBytes* resolver = + new MockAsyncProxyResolverExpectsBytes; + ProxyService service(config_service, resolver, NULL); + + MockProxyScriptFetcher* fetcher = new MockProxyScriptFetcher; + service.SetProxyScriptFetchers(fetcher, + new DoNothingDhcpProxyScriptFetcher()); + + // Start 2 requests. + + ProxyInfo info1; + TestCompletionCallback callback1; + int rv = service.ResolveProxy(GURL("http://request1"), &info1, + callback1.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + ProxyInfo info2; + TestCompletionCallback callback2; + ProxyService::PacRequest* request2; + rv = service.ResolveProxy(GURL("http://request2"), &info2, + callback2.callback(), &request2, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Check that nothing has been sent to the proxy resolver yet. + ASSERT_EQ(0u, resolver->pending_requests().size()); + + // It should be trying to auto-detect first -- fail the download. + EXPECT_TRUE(fetcher->has_pending_request()); + EXPECT_EQ(GURL("http://wpad/wpad.dat"), fetcher->pending_request_url()); + fetcher->NotifyFetchCompletion(ERR_FAILED, std::string()); + + // Next it should be trying the custom PAC url -- fail the download. + EXPECT_TRUE(fetcher->has_pending_request()); + EXPECT_EQ(GURL("http://foopy/proxy.pac"), fetcher->pending_request_url()); + fetcher->NotifyFetchCompletion(ERR_FAILED, std::string()); + + // Since we never managed to initialize a ProxyResolver, nothing should have + // been sent to it. + ASSERT_EQ(0u, resolver->pending_requests().size()); + + // Verify that requests ran as expected -- they should have fallen back to + // the manual proxy configuration for HTTP urls. + EXPECT_EQ(OK, callback1.WaitForResult()); + EXPECT_EQ("foopy:80", info1.proxy_server().ToURI()); + + EXPECT_EQ(OK, callback2.WaitForResult()); + EXPECT_EQ("foopy:80", info2.proxy_server().ToURI()); +} + +// Test that the bypass rules are NOT applied when using autodetect. +TEST_F(ProxyServiceTest, BypassDoesntApplyToPac) { + ProxyConfig config; + config.set_auto_detect(true); + config.set_pac_url(GURL("http://foopy/proxy.pac")); + config.proxy_rules().ParseFromString("http=foopy:80"); // Not used. + config.proxy_rules().bypass_rules.ParseFromString("www.google.com"); + + MockProxyConfigService* config_service = new MockProxyConfigService(config); + MockAsyncProxyResolverExpectsBytes* resolver = + new MockAsyncProxyResolverExpectsBytes; + ProxyService service(config_service, resolver, NULL); + + MockProxyScriptFetcher* fetcher = new MockProxyScriptFetcher; + service.SetProxyScriptFetchers(fetcher, + new DoNothingDhcpProxyScriptFetcher()); + + // Start 1 requests. + + ProxyInfo info1; + TestCompletionCallback callback1; + int rv = service.ResolveProxy( + GURL("http://www.google.com"), &info1, callback1.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Check that nothing has been sent to the proxy resolver yet. + ASSERT_EQ(0u, resolver->pending_requests().size()); + + // It should be trying to auto-detect first -- succeed the download. + EXPECT_TRUE(fetcher->has_pending_request()); + EXPECT_EQ(GURL("http://wpad/wpad.dat"), fetcher->pending_request_url()); + fetcher->NotifyFetchCompletion(OK, kValidPacScript1); + + EXPECT_EQ(ASCIIToUTF16(kValidPacScript1), + resolver->pending_set_pac_script_request()->script_data()->utf16()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(GURL("http://www.google.com"), + resolver->pending_requests()[0]->url()); + + // Complete the pending request. + resolver->pending_requests()[0]->results()->UseNamedProxy("request1:80"); + resolver->pending_requests()[0]->CompleteNow(OK); + + // Verify that request ran as expected. + EXPECT_EQ(OK, callback1.WaitForResult()); + EXPECT_EQ("request1:80", info1.proxy_server().ToURI()); + + // Start another request, it should pickup the bypass item. + ProxyInfo info2; + TestCompletionCallback callback2; + rv = service.ResolveProxy(GURL("http://www.google.com"), &info2, + callback2.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(GURL("http://www.google.com"), + resolver->pending_requests()[0]->url()); + + // Complete the pending request. + resolver->pending_requests()[0]->results()->UseNamedProxy("request2:80"); + resolver->pending_requests()[0]->CompleteNow(OK); + + EXPECT_EQ(OK, callback2.WaitForResult()); + EXPECT_EQ("request2:80", info2.proxy_server().ToURI()); +} + +// Delete the ProxyService while InitProxyResolver has an outstanding +// request to the script fetcher. When run under valgrind, should not +// have any memory errors (used to be that the ProxyScriptFetcher was +// being deleted prior to the InitProxyResolver). +TEST_F(ProxyServiceTest, DeleteWhileInitProxyResolverHasOutstandingFetch) { + ProxyConfig config = + ProxyConfig::CreateFromCustomPacURL(GURL("http://foopy/proxy.pac")); + + MockProxyConfigService* config_service = new MockProxyConfigService(config); + MockAsyncProxyResolverExpectsBytes* resolver = + new MockAsyncProxyResolverExpectsBytes; + ProxyService service(config_service, resolver, NULL); + + MockProxyScriptFetcher* fetcher = new MockProxyScriptFetcher; + service.SetProxyScriptFetchers(fetcher, + new DoNothingDhcpProxyScriptFetcher()); + + // Start 1 request. + + ProxyInfo info1; + TestCompletionCallback callback1; + int rv = service.ResolveProxy(GURL("http://www.google.com"), &info1, + callback1.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Check that nothing has been sent to the proxy resolver yet. + ASSERT_EQ(0u, resolver->pending_requests().size()); + + // InitProxyResolver should have issued a request to the ProxyScriptFetcher + // and be waiting on that to complete. + EXPECT_TRUE(fetcher->has_pending_request()); + EXPECT_EQ(GURL("http://foopy/proxy.pac"), fetcher->pending_request_url()); +} + +// Delete the ProxyService while InitProxyResolver has an outstanding +// request to the proxy resolver. When run under valgrind, should not +// have any memory errors (used to be that the ProxyResolver was +// being deleted prior to the InitProxyResolver). +TEST_F(ProxyServiceTest, DeleteWhileInitProxyResolverHasOutstandingSet) { + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + + MockAsyncProxyResolver* resolver = new MockAsyncProxyResolver; + + ProxyService service(config_service, resolver, NULL); + + GURL url("http://www.google.com/"); + + ProxyInfo info; + TestCompletionCallback callback; + int rv = service.ResolveProxy( + url, &info, callback.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + EXPECT_EQ(GURL("http://foopy/proxy.pac"), + resolver->pending_set_pac_script_request()->script_data()->url()); +} + +TEST_F(ProxyServiceTest, ResetProxyConfigService) { + ProxyConfig config1; + config1.proxy_rules().ParseFromString("foopy1:8080"); + config1.set_auto_detect(false); + ProxyService service( + new MockProxyConfigService(config1), + new MockAsyncProxyResolverExpectsBytes, NULL); + + ProxyInfo info; + TestCompletionCallback callback1; + int rv = service.ResolveProxy(GURL("http://request1"), &info, + callback1.callback(), NULL, BoundNetLog()); + EXPECT_EQ(OK, rv); + EXPECT_EQ("foopy1:8080", info.proxy_server().ToURI()); + + ProxyConfig config2; + config2.proxy_rules().ParseFromString("foopy2:8080"); + config2.set_auto_detect(false); + service.ResetConfigService(new MockProxyConfigService(config2)); + TestCompletionCallback callback2; + rv = service.ResolveProxy(GURL("http://request2"), &info, + callback2.callback(), NULL, BoundNetLog()); + EXPECT_EQ(OK, rv); + EXPECT_EQ("foopy2:8080", info.proxy_server().ToURI()); +} + +// Test that when going from a configuration that required PAC to one +// that does NOT, we unset the variable |should_use_proxy_resolver_|. +TEST_F(ProxyServiceTest, UpdateConfigFromPACToDirect) { + ProxyConfig config = ProxyConfig::CreateAutoDetect(); + + MockProxyConfigService* config_service = new MockProxyConfigService(config); + MockAsyncProxyResolver* resolver = new MockAsyncProxyResolver; + ProxyService service(config_service, resolver, NULL); + + // Start 1 request. + + ProxyInfo info1; + TestCompletionCallback callback1; + int rv = service.ResolveProxy(GURL("http://www.google.com"), &info1, + callback1.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Check that nothing has been sent to the proxy resolver yet. + ASSERT_EQ(0u, resolver->pending_requests().size()); + + // Successfully set the autodetect script. + EXPECT_EQ(ProxyResolverScriptData::TYPE_AUTO_DETECT, + resolver->pending_set_pac_script_request()->script_data()->type()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + // Complete the pending request. + ASSERT_EQ(1u, resolver->pending_requests().size()); + resolver->pending_requests()[0]->results()->UseNamedProxy("request1:80"); + resolver->pending_requests()[0]->CompleteNow(OK); + + // Verify that request ran as expected. + EXPECT_EQ(OK, callback1.WaitForResult()); + EXPECT_EQ("request1:80", info1.proxy_server().ToURI()); + + // Force the ProxyService to pull down a new proxy configuration. + // (Even though the configuration isn't old/bad). + // + // This new configuration no longer has auto_detect set, so + // requests should complete synchronously now as direct-connect. + config_service->SetConfig(ProxyConfig::CreateDirect()); + + // Start another request -- the effective configuration has changed. + ProxyInfo info2; + TestCompletionCallback callback2; + rv = service.ResolveProxy(GURL("http://www.google.com"), &info2, + callback2.callback(), NULL, BoundNetLog()); + EXPECT_EQ(OK, rv); + + EXPECT_TRUE(info2.is_direct()); +} + +TEST_F(ProxyServiceTest, NetworkChangeTriggersPacRefetch) { + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + + MockAsyncProxyResolverExpectsBytes* resolver = + new MockAsyncProxyResolverExpectsBytes; + + CapturingNetLog log; + + ProxyService service(config_service, resolver, &log); + + MockProxyScriptFetcher* fetcher = new MockProxyScriptFetcher; + service.SetProxyScriptFetchers(fetcher, + new DoNothingDhcpProxyScriptFetcher()); + + // Disable the "wait after IP address changes" hack, so this unit-test can + // complete quickly. + service.set_stall_proxy_auto_config_delay(base::TimeDelta()); + + // Start 1 request. + + ProxyInfo info1; + TestCompletionCallback callback1; + int rv = service.ResolveProxy(GURL("http://request1"), &info1, + callback1.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // The first request should have triggered initial download of PAC script. + EXPECT_TRUE(fetcher->has_pending_request()); + EXPECT_EQ(GURL("http://foopy/proxy.pac"), fetcher->pending_request_url()); + + // Nothing has been sent to the resolver yet. + EXPECT_TRUE(resolver->pending_requests().empty()); + + // At this point the ProxyService should be waiting for the + // ProxyScriptFetcher to invoke its completion callback, notifying it of + // PAC script download completion. + fetcher->NotifyFetchCompletion(OK, kValidPacScript1); + + // Now that the PAC script is downloaded, the request will have been sent to + // the proxy resolver. + EXPECT_EQ(ASCIIToUTF16(kValidPacScript1), + resolver->pending_set_pac_script_request()->script_data()->utf16()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(GURL("http://request1"), resolver->pending_requests()[0]->url()); + + // Complete the pending request. + resolver->pending_requests()[0]->results()->UseNamedProxy("request1:80"); + resolver->pending_requests()[0]->CompleteNow(OK); + + // Wait for completion callback, and verify that the request ran as expected. + EXPECT_EQ(OK, callback1.WaitForResult()); + EXPECT_EQ("request1:80", info1.proxy_server().ToURI()); + + // Now simluate a change in the network. The ProxyConfigService is still + // going to return the same PAC URL as before, but this URL needs to be + // refetched on the new network. + NetworkChangeNotifier::NotifyObserversOfIPAddressChangeForTests(); + base::MessageLoop::current()->RunUntilIdle(); // Notification happens async. + + // Start a second request. + ProxyInfo info2; + TestCompletionCallback callback2; + rv = service.ResolveProxy(GURL("http://request2"), &info2, + callback2.callback(), NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // This second request should have triggered the re-download of the PAC + // script (since we marked the network as having changed). + EXPECT_TRUE(fetcher->has_pending_request()); + EXPECT_EQ(GURL("http://foopy/proxy.pac"), fetcher->pending_request_url()); + + // Nothing has been sent to the resolver yet. + EXPECT_TRUE(resolver->pending_requests().empty()); + + // Simulate the PAC script fetch as having completed (this time with + // different data). + fetcher->NotifyFetchCompletion(OK, kValidPacScript2); + + // Now that the PAC script is downloaded, the second request will have been + // sent to the proxy resolver. + EXPECT_EQ(ASCIIToUTF16(kValidPacScript2), + resolver->pending_set_pac_script_request()->script_data()->utf16()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(GURL("http://request2"), resolver->pending_requests()[0]->url()); + + // Complete the pending second request. + resolver->pending_requests()[0]->results()->UseNamedProxy("request2:80"); + resolver->pending_requests()[0]->CompleteNow(OK); + + // Wait for completion callback, and verify that the request ran as expected. + EXPECT_EQ(OK, callback2.WaitForResult()); + EXPECT_EQ("request2:80", info2.proxy_server().ToURI()); + + // Check that the expected events were output to the log stream. In particular + // PROXY_CONFIG_CHANGED should have only been emitted once (for the initial + // setup), and NOT a second time when the IP address changed. + CapturingNetLog::CapturedEntryList entries; + log.GetEntries(&entries); + + EXPECT_TRUE(LogContainsEntryWithType(entries, 0, + NetLog::TYPE_PROXY_CONFIG_CHANGED)); + ASSERT_EQ(9u, entries.size()); + for (size_t i = 1; i < entries.size(); ++i) + EXPECT_NE(NetLog::TYPE_PROXY_CONFIG_CHANGED, entries[i].type); +} + +// This test verifies that the PAC script specified by the settings is +// periodically polled for changes. Specifically, if the initial fetch fails due +// to a network error, we will eventually re-configure the service to use the +// script once it becomes available. +TEST_F(ProxyServiceTest, PACScriptRefetchAfterFailure) { + // Change the retry policy to wait a mere 1 ms before retrying, so the test + // runs quickly. + ImmediatePollPolicy poll_policy; + ProxyService::set_pac_script_poll_policy(&poll_policy); + + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + + MockAsyncProxyResolverExpectsBytes* resolver = + new MockAsyncProxyResolverExpectsBytes; + + ProxyService service(config_service, resolver, NULL); + + MockProxyScriptFetcher* fetcher = new MockProxyScriptFetcher; + service.SetProxyScriptFetchers(fetcher, + new DoNothingDhcpProxyScriptFetcher()); + + // Start 1 request. + + ProxyInfo info1; + TestCompletionCallback callback1; + int rv = service.ResolveProxy( + GURL("http://request1"), &info1, callback1.callback(), + NULL, BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // The first request should have triggered initial download of PAC script. + EXPECT_TRUE(fetcher->has_pending_request()); + EXPECT_EQ(GURL("http://foopy/proxy.pac"), fetcher->pending_request_url()); + + // Nothing has been sent to the resolver yet. + EXPECT_TRUE(resolver->pending_requests().empty()); + + // At this point the ProxyService should be waiting for the + // ProxyScriptFetcher to invoke its completion callback, notifying it of + // PAC script download completion. + // + // We simulate a failed download attempt, the proxy service should now + // fall-back to DIRECT connections. + fetcher->NotifyFetchCompletion(ERR_FAILED, std::string()); + + ASSERT_TRUE(resolver->pending_requests().empty()); + + // Wait for completion callback, and verify it used DIRECT. + EXPECT_EQ(OK, callback1.WaitForResult()); + EXPECT_TRUE(info1.is_direct()); + + // At this point we have initialized the proxy service using a PAC script, + // however it failed and fell-back to DIRECT. + // + // A background task to periodically re-check the PAC script for validity will + // have been started. We will now wait for the next download attempt to start. + // + // Note that we shouldn't have to wait long here, since our test enables a + // special unit-test mode. + fetcher->WaitUntilFetch(); + + ASSERT_TRUE(resolver->pending_requests().empty()); + + // Make sure that our background checker is trying to download the expected + // PAC script (same one as before). This time we will simulate a successful + // download of the script. + EXPECT_TRUE(fetcher->has_pending_request()); + EXPECT_EQ(GURL("http://foopy/proxy.pac"), fetcher->pending_request_url()); + fetcher->NotifyFetchCompletion(OK, kValidPacScript1); + + base::MessageLoop::current()->RunUntilIdle(); + + // Now that the PAC script is downloaded, it should be used to initialize the + // ProxyResolver. Simulate a successful parse. + EXPECT_EQ(ASCIIToUTF16(kValidPacScript1), + resolver->pending_set_pac_script_request()->script_data()->utf16()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + // At this point the ProxyService should have re-configured itself to use the + // PAC script (thereby recovering from the initial fetch failure). We will + // verify that the next Resolve request uses the resolver rather than + // DIRECT. + + // Start a second request. + ProxyInfo info2; + TestCompletionCallback callback2; + rv = service.ResolveProxy( + GURL("http://request2"), &info2, callback2.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Check that it was sent to the resolver. + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(GURL("http://request2"), resolver->pending_requests()[0]->url()); + + // Complete the pending second request. + resolver->pending_requests()[0]->results()->UseNamedProxy("request2:80"); + resolver->pending_requests()[0]->CompleteNow(OK); + + // Wait for completion callback, and verify that the request ran as expected. + EXPECT_EQ(OK, callback2.WaitForResult()); + EXPECT_EQ("request2:80", info2.proxy_server().ToURI()); +} + +// This test verifies that the PAC script specified by the settings is +// periodically polled for changes. Specifically, if the initial fetch succeeds, +// however at a later time its *contents* change, we will eventually +// re-configure the service to use the new script. +TEST_F(ProxyServiceTest, PACScriptRefetchAfterContentChange) { + // Change the retry policy to wait a mere 1 ms before retrying, so the test + // runs quickly. + ImmediatePollPolicy poll_policy; + ProxyService::set_pac_script_poll_policy(&poll_policy); + + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + + MockAsyncProxyResolverExpectsBytes* resolver = + new MockAsyncProxyResolverExpectsBytes; + + ProxyService service(config_service, resolver, NULL); + + MockProxyScriptFetcher* fetcher = new MockProxyScriptFetcher; + service.SetProxyScriptFetchers(fetcher, + new DoNothingDhcpProxyScriptFetcher()); + + // Start 1 request. + + ProxyInfo info1; + TestCompletionCallback callback1; + int rv = service.ResolveProxy( + GURL("http://request1"), &info1, callback1.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // The first request should have triggered initial download of PAC script. + EXPECT_TRUE(fetcher->has_pending_request()); + EXPECT_EQ(GURL("http://foopy/proxy.pac"), fetcher->pending_request_url()); + + // Nothing has been sent to the resolver yet. + EXPECT_TRUE(resolver->pending_requests().empty()); + + // At this point the ProxyService should be waiting for the + // ProxyScriptFetcher to invoke its completion callback, notifying it of + // PAC script download completion. + fetcher->NotifyFetchCompletion(OK, kValidPacScript1); + + // Now that the PAC script is downloaded, the request will have been sent to + // the proxy resolver. + EXPECT_EQ(ASCIIToUTF16(kValidPacScript1), + resolver->pending_set_pac_script_request()->script_data()->utf16()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(GURL("http://request1"), resolver->pending_requests()[0]->url()); + + // Complete the pending request. + resolver->pending_requests()[0]->results()->UseNamedProxy("request1:80"); + resolver->pending_requests()[0]->CompleteNow(OK); + + // Wait for completion callback, and verify that the request ran as expected. + EXPECT_EQ(OK, callback1.WaitForResult()); + EXPECT_EQ("request1:80", info1.proxy_server().ToURI()); + + // At this point we have initialized the proxy service using a PAC script. + // + // A background task to periodically re-check the PAC script for validity will + // have been started. We will now wait for the next download attempt to start. + // + // Note that we shouldn't have to wait long here, since our test enables a + // special unit-test mode. + fetcher->WaitUntilFetch(); + + ASSERT_TRUE(resolver->pending_requests().empty()); + + // Make sure that our background checker is trying to download the expected + // PAC script (same one as before). This time we will simulate a successful + // download of a DIFFERENT script. + EXPECT_TRUE(fetcher->has_pending_request()); + EXPECT_EQ(GURL("http://foopy/proxy.pac"), fetcher->pending_request_url()); + fetcher->NotifyFetchCompletion(OK, kValidPacScript2); + + base::MessageLoop::current()->RunUntilIdle(); + + // Now that the PAC script is downloaded, it should be used to initialize the + // ProxyResolver. Simulate a successful parse. + EXPECT_EQ(ASCIIToUTF16(kValidPacScript2), + resolver->pending_set_pac_script_request()->script_data()->utf16()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + // At this point the ProxyService should have re-configured itself to use the + // new PAC script. + + // Start a second request. + ProxyInfo info2; + TestCompletionCallback callback2; + rv = service.ResolveProxy( + GURL("http://request2"), &info2, callback2.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Check that it was sent to the resolver. + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(GURL("http://request2"), resolver->pending_requests()[0]->url()); + + // Complete the pending second request. + resolver->pending_requests()[0]->results()->UseNamedProxy("request2:80"); + resolver->pending_requests()[0]->CompleteNow(OK); + + // Wait for completion callback, and verify that the request ran as expected. + EXPECT_EQ(OK, callback2.WaitForResult()); + EXPECT_EQ("request2:80", info2.proxy_server().ToURI()); +} + +// This test verifies that the PAC script specified by the settings is +// periodically polled for changes. Specifically, if the initial fetch succeeds +// and so does the next poll, however the contents of the downloaded script +// have NOT changed, then we do not bother to re-initialize the proxy resolver. +TEST_F(ProxyServiceTest, PACScriptRefetchAfterContentUnchanged) { + // Change the retry policy to wait a mere 1 ms before retrying, so the test + // runs quickly. + ImmediatePollPolicy poll_policy; + ProxyService::set_pac_script_poll_policy(&poll_policy); + + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + + MockAsyncProxyResolverExpectsBytes* resolver = + new MockAsyncProxyResolverExpectsBytes; + + ProxyService service(config_service, resolver, NULL); + + MockProxyScriptFetcher* fetcher = new MockProxyScriptFetcher; + service.SetProxyScriptFetchers(fetcher, + new DoNothingDhcpProxyScriptFetcher()); + + // Start 1 request. + + ProxyInfo info1; + TestCompletionCallback callback1; + int rv = service.ResolveProxy( + GURL("http://request1"), &info1, callback1.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // The first request should have triggered initial download of PAC script. + EXPECT_TRUE(fetcher->has_pending_request()); + EXPECT_EQ(GURL("http://foopy/proxy.pac"), fetcher->pending_request_url()); + + // Nothing has been sent to the resolver yet. + EXPECT_TRUE(resolver->pending_requests().empty()); + + // At this point the ProxyService should be waiting for the + // ProxyScriptFetcher to invoke its completion callback, notifying it of + // PAC script download completion. + fetcher->NotifyFetchCompletion(OK, kValidPacScript1); + + // Now that the PAC script is downloaded, the request will have been sent to + // the proxy resolver. + EXPECT_EQ(ASCIIToUTF16(kValidPacScript1), + resolver->pending_set_pac_script_request()->script_data()->utf16()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(GURL("http://request1"), resolver->pending_requests()[0]->url()); + + // Complete the pending request. + resolver->pending_requests()[0]->results()->UseNamedProxy("request1:80"); + resolver->pending_requests()[0]->CompleteNow(OK); + + // Wait for completion callback, and verify that the request ran as expected. + EXPECT_EQ(OK, callback1.WaitForResult()); + EXPECT_EQ("request1:80", info1.proxy_server().ToURI()); + + // At this point we have initialized the proxy service using a PAC script. + // + // A background task to periodically re-check the PAC script for validity will + // have been started. We will now wait for the next download attempt to start. + // + // Note that we shouldn't have to wait long here, since our test enables a + // special unit-test mode. + fetcher->WaitUntilFetch(); + + ASSERT_TRUE(resolver->pending_requests().empty()); + + // Make sure that our background checker is trying to download the expected + // PAC script (same one as before). We will simulate the same response as + // last time (i.e. the script is unchanged). + EXPECT_TRUE(fetcher->has_pending_request()); + EXPECT_EQ(GURL("http://foopy/proxy.pac"), fetcher->pending_request_url()); + fetcher->NotifyFetchCompletion(OK, kValidPacScript1); + + base::MessageLoop::current()->RunUntilIdle(); + + ASSERT_FALSE(resolver->has_pending_set_pac_script_request()); + + // At this point the ProxyService is still running the same PAC script as + // before. + + // Start a second request. + ProxyInfo info2; + TestCompletionCallback callback2; + rv = service.ResolveProxy( + GURL("http://request2"), &info2, callback2.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // Check that it was sent to the resolver. + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(GURL("http://request2"), resolver->pending_requests()[0]->url()); + + // Complete the pending second request. + resolver->pending_requests()[0]->results()->UseNamedProxy("request2:80"); + resolver->pending_requests()[0]->CompleteNow(OK); + + // Wait for completion callback, and verify that the request ran as expected. + EXPECT_EQ(OK, callback2.WaitForResult()); + EXPECT_EQ("request2:80", info2.proxy_server().ToURI()); +} + +// This test verifies that the PAC script specified by the settings is +// periodically polled for changes. Specifically, if the initial fetch succeeds, +// however at a later time it starts to fail, we should re-configure the +// ProxyService to stop using that PAC script. +TEST_F(ProxyServiceTest, PACScriptRefetchAfterSuccess) { + // Change the retry policy to wait a mere 1 ms before retrying, so the test + // runs quickly. + ImmediatePollPolicy poll_policy; + ProxyService::set_pac_script_poll_policy(&poll_policy); + + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + + MockAsyncProxyResolverExpectsBytes* resolver = + new MockAsyncProxyResolverExpectsBytes; + + ProxyService service(config_service, resolver, NULL); + + MockProxyScriptFetcher* fetcher = new MockProxyScriptFetcher; + service.SetProxyScriptFetchers(fetcher, + new DoNothingDhcpProxyScriptFetcher()); + + // Start 1 request. + + ProxyInfo info1; + TestCompletionCallback callback1; + int rv = service.ResolveProxy( + GURL("http://request1"), &info1, callback1.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // The first request should have triggered initial download of PAC script. + EXPECT_TRUE(fetcher->has_pending_request()); + EXPECT_EQ(GURL("http://foopy/proxy.pac"), fetcher->pending_request_url()); + + // Nothing has been sent to the resolver yet. + EXPECT_TRUE(resolver->pending_requests().empty()); + + // At this point the ProxyService should be waiting for the + // ProxyScriptFetcher to invoke its completion callback, notifying it of + // PAC script download completion. + fetcher->NotifyFetchCompletion(OK, kValidPacScript1); + + // Now that the PAC script is downloaded, the request will have been sent to + // the proxy resolver. + EXPECT_EQ(ASCIIToUTF16(kValidPacScript1), + resolver->pending_set_pac_script_request()->script_data()->utf16()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(GURL("http://request1"), resolver->pending_requests()[0]->url()); + + // Complete the pending request. + resolver->pending_requests()[0]->results()->UseNamedProxy("request1:80"); + resolver->pending_requests()[0]->CompleteNow(OK); + + // Wait for completion callback, and verify that the request ran as expected. + EXPECT_EQ(OK, callback1.WaitForResult()); + EXPECT_EQ("request1:80", info1.proxy_server().ToURI()); + + // At this point we have initialized the proxy service using a PAC script. + // + // A background task to periodically re-check the PAC script for validity will + // have been started. We will now wait for the next download attempt to start. + // + // Note that we shouldn't have to wait long here, since our test enables a + // special unit-test mode. + fetcher->WaitUntilFetch(); + + ASSERT_TRUE(resolver->pending_requests().empty()); + + // Make sure that our background checker is trying to download the expected + // PAC script (same one as before). This time we will simulate a failure + // to download the script. + EXPECT_TRUE(fetcher->has_pending_request()); + EXPECT_EQ(GURL("http://foopy/proxy.pac"), fetcher->pending_request_url()); + fetcher->NotifyFetchCompletion(ERR_FAILED, std::string()); + + base::MessageLoop::current()->RunUntilIdle(); + + // At this point the ProxyService should have re-configured itself to use + // DIRECT connections rather than the given proxy resolver. + + // Start a second request. + ProxyInfo info2; + TestCompletionCallback callback2; + rv = service.ResolveProxy( + GURL("http://request2"), &info2, callback2.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(OK, rv); + EXPECT_TRUE(info2.is_direct()); +} + +// Tests that the code which decides at what times to poll the PAC +// script follows the expected policy. +TEST_F(ProxyServiceTest, PACScriptPollingPolicy) { + // Retrieve the internal polling policy implementation used by ProxyService. + scoped_ptr<ProxyService::PacPollPolicy> policy = + ProxyService::CreateDefaultPacPollPolicy(); + + int error; + ProxyService::PacPollPolicy::Mode mode; + const base::TimeDelta initial_delay = base::TimeDelta::FromMilliseconds(-1); + base::TimeDelta delay = initial_delay; + + // -------------------------------------------------- + // Test the poll sequence in response to a failure. + // -------------------------------------------------- + error = ERR_NAME_NOT_RESOLVED; + + // Poll #0 + mode = policy->GetNextDelay(error, initial_delay, &delay); + EXPECT_EQ(8, delay.InSeconds()); + EXPECT_EQ(ProxyService::PacPollPolicy::MODE_USE_TIMER, mode); + + // Poll #1 + mode = policy->GetNextDelay(error, delay, &delay); + EXPECT_EQ(32, delay.InSeconds()); + EXPECT_EQ(ProxyService::PacPollPolicy::MODE_START_AFTER_ACTIVITY, mode); + + // Poll #2 + mode = policy->GetNextDelay(error, delay, &delay); + EXPECT_EQ(120, delay.InSeconds()); + EXPECT_EQ(ProxyService::PacPollPolicy::MODE_START_AFTER_ACTIVITY, mode); + + // Poll #3 + mode = policy->GetNextDelay(error, delay, &delay); + EXPECT_EQ(14400, delay.InSeconds()); + EXPECT_EQ(ProxyService::PacPollPolicy::MODE_START_AFTER_ACTIVITY, mode); + + // Poll #4 + mode = policy->GetNextDelay(error, delay, &delay); + EXPECT_EQ(14400, delay.InSeconds()); + EXPECT_EQ(ProxyService::PacPollPolicy::MODE_START_AFTER_ACTIVITY, mode); + + // -------------------------------------------------- + // Test the poll sequence in response to a success. + // -------------------------------------------------- + error = OK; + + // Poll #0 + mode = policy->GetNextDelay(error, initial_delay, &delay); + EXPECT_EQ(43200, delay.InSeconds()); + EXPECT_EQ(ProxyService::PacPollPolicy::MODE_START_AFTER_ACTIVITY, mode); + + // Poll #1 + mode = policy->GetNextDelay(error, delay, &delay); + EXPECT_EQ(43200, delay.InSeconds()); + EXPECT_EQ(ProxyService::PacPollPolicy::MODE_START_AFTER_ACTIVITY, mode); + + // Poll #2 + mode = policy->GetNextDelay(error, delay, &delay); + EXPECT_EQ(43200, delay.InSeconds()); + EXPECT_EQ(ProxyService::PacPollPolicy::MODE_START_AFTER_ACTIVITY, mode); +} + +// This tests the polling of the PAC script. Specifically, it tests that +// polling occurs in response to user activity. +TEST_F(ProxyServiceTest, PACScriptRefetchAfterActivity) { + ImmediateAfterActivityPollPolicy poll_policy; + ProxyService::set_pac_script_poll_policy(&poll_policy); + + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + + MockAsyncProxyResolverExpectsBytes* resolver = + new MockAsyncProxyResolverExpectsBytes; + + ProxyService service(config_service, resolver, NULL); + + MockProxyScriptFetcher* fetcher = new MockProxyScriptFetcher; + service.SetProxyScriptFetchers(fetcher, + new DoNothingDhcpProxyScriptFetcher()); + + // Start 1 request. + + ProxyInfo info1; + TestCompletionCallback callback1; + int rv = service.ResolveProxy( + GURL("http://request1"), &info1, callback1.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // The first request should have triggered initial download of PAC script. + EXPECT_TRUE(fetcher->has_pending_request()); + EXPECT_EQ(GURL("http://foopy/proxy.pac"), fetcher->pending_request_url()); + + // Nothing has been sent to the resolver yet. + EXPECT_TRUE(resolver->pending_requests().empty()); + + // At this point the ProxyService should be waiting for the + // ProxyScriptFetcher to invoke its completion callback, notifying it of + // PAC script download completion. + fetcher->NotifyFetchCompletion(OK, kValidPacScript1); + + // Now that the PAC script is downloaded, the request will have been sent to + // the proxy resolver. + EXPECT_EQ(ASCIIToUTF16(kValidPacScript1), + resolver->pending_set_pac_script_request()->script_data()->utf16()); + resolver->pending_set_pac_script_request()->CompleteNow(OK); + + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(GURL("http://request1"), resolver->pending_requests()[0]->url()); + + // Complete the pending request. + resolver->pending_requests()[0]->results()->UseNamedProxy("request1:80"); + resolver->pending_requests()[0]->CompleteNow(OK); + + // Wait for completion callback, and verify that the request ran as expected. + EXPECT_EQ(OK, callback1.WaitForResult()); + EXPECT_EQ("request1:80", info1.proxy_server().ToURI()); + + // At this point we have initialized the proxy service using a PAC script. + // Our PAC poller is set to update ONLY in response to network activity, + // (i.e. another call to ResolveProxy()). + + ASSERT_FALSE(fetcher->has_pending_request()); + ASSERT_TRUE(resolver->pending_requests().empty()); + + // Start a second request. + ProxyInfo info2; + TestCompletionCallback callback2; + rv = service.ResolveProxy( + GURL("http://request2"), &info2, callback2.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(ERR_IO_PENDING, rv); + + // This request should have sent work to the resolver; complete it. + ASSERT_EQ(1u, resolver->pending_requests().size()); + EXPECT_EQ(GURL("http://request2"), resolver->pending_requests()[0]->url()); + resolver->pending_requests()[0]->results()->UseNamedProxy("request2:80"); + resolver->pending_requests()[0]->CompleteNow(OK); + + EXPECT_EQ(OK, callback2.WaitForResult()); + EXPECT_EQ("request2:80", info2.proxy_server().ToURI()); + + // In response to getting that resolve request, the poller should have + // started the next poll, and made it as far as to request the download. + + EXPECT_TRUE(fetcher->has_pending_request()); + EXPECT_EQ(GURL("http://foopy/proxy.pac"), fetcher->pending_request_url()); + + // This time we will fail the download, to simulate a PAC script change. + fetcher->NotifyFetchCompletion(ERR_FAILED, std::string()); + + // Drain the message loop, so ProxyService is notified of the change + // and has a chance to re-configure itself. + base::MessageLoop::current()->RunUntilIdle(); + + // Start a third request -- this time we expect to get a direct connection + // since the PAC script poller experienced a failure. + ProxyInfo info3; + TestCompletionCallback callback3; + rv = service.ResolveProxy( + GURL("http://request3"), &info3, callback3.callback(), NULL, + BoundNetLog()); + EXPECT_EQ(OK, rv); + EXPECT_TRUE(info3.is_direct()); +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_service_v8.cc b/chromium/net/proxy/proxy_service_v8.cc new file mode 100644 index 00000000000..945719a1647 --- /dev/null +++ b/chromium/net/proxy/proxy_service_v8.cc @@ -0,0 +1,44 @@ +// 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/proxy/proxy_service_v8.h" + +#include "base/logging.h" +#include "net/proxy/network_delegate_error_observer.h" +#include "net/proxy/proxy_resolver.h" +#include "net/proxy/proxy_resolver_v8_tracing.h" +#include "net/proxy/proxy_service.h" + +namespace net { + +// static +ProxyService* CreateProxyServiceUsingV8ProxyResolver( + ProxyConfigService* proxy_config_service, + ProxyScriptFetcher* proxy_script_fetcher, + DhcpProxyScriptFetcher* dhcp_proxy_script_fetcher, + HostResolver* host_resolver, + NetLog* net_log, + NetworkDelegate* network_delegate) { + DCHECK(proxy_config_service); + DCHECK(proxy_script_fetcher); + DCHECK(dhcp_proxy_script_fetcher); + DCHECK(host_resolver); + + ProxyResolverErrorObserver* error_observer = new NetworkDelegateErrorObserver( + network_delegate, base::MessageLoopProxy::current().get()); + + ProxyResolver* proxy_resolver = + new ProxyResolverV8Tracing(host_resolver, error_observer, net_log); + + ProxyService* proxy_service = + new ProxyService(proxy_config_service, proxy_resolver, net_log); + + // Configure fetchers to use for PAC script downloads and auto-detect. + proxy_service->SetProxyScriptFetchers(proxy_script_fetcher, + dhcp_proxy_script_fetcher); + + return proxy_service; +} + +} // namespace net diff --git a/chromium/net/proxy/proxy_service_v8.h b/chromium/net/proxy/proxy_service_v8.h new file mode 100644 index 00000000000..0e339ebf3e7 --- /dev/null +++ b/chromium/net/proxy/proxy_service_v8.h @@ -0,0 +1,50 @@ +// 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. + +#ifndef NET_PROXY_PROXY_SERVICE_V8_H_ +#define NET_PROXY_PROXY_SERVICE_V8_H_ + +#include "base/basictypes.h" +#include "net/base/net_export.h" + +namespace net { + +class DhcpProxyScriptFetcher; +class HostResolver; +class NetLog; +class NetworkDelegate; +class ProxyConfigService; +class ProxyScriptFetcher; +class ProxyService; + +// Creates a proxy service that polls |proxy_config_service| to notice when +// the proxy settings change. We take ownership of |proxy_config_service|. +// +// |proxy_script_fetcher| specifies the dependency to use for downloading +// any PAC scripts. The resulting ProxyService will take ownership of it. +// +// |dhcp_proxy_script_fetcher| specifies the dependency to use for attempting +// to retrieve the most appropriate PAC script configured in DHCP. The +// resulting ProxyService will take ownership of it. +// +// |host_resolver| points to the host resolving dependency the PAC script +// should use for any DNS queries. It must remain valid throughout the +// lifetime of the ProxyService. +// +// ########################################################################## +// # See the warnings in net/proxy/proxy_resolver_v8.h describing the +// # multi-threading model. In order for this to be safe to use, *ALL* the +// # other V8's running in the process must use v8::Locker. +// ########################################################################## +NET_EXPORT ProxyService* CreateProxyServiceUsingV8ProxyResolver( + ProxyConfigService* proxy_config_service, + ProxyScriptFetcher* proxy_script_fetcher, + DhcpProxyScriptFetcher* dhcp_proxy_script_fetcher, + HostResolver* host_resolver, + NetLog* net_log, + NetworkDelegate* network_delegate); + +} // namespace net + +#endif // NET_PROXY_PROXY_SERVICE_V8_H_ |