diff options
author | ericroman@google.com <ericroman@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-02-23 19:04:40 +0000 |
---|---|---|
committer | ericroman@google.com <ericroman@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-02-23 19:04:40 +0000 |
commit | 45bdf86341670d95573acbc699f2d2998a25cf1d (patch) | |
tree | 7aa8e9afee81bb43b076110760a09abcc9df046c /net | |
parent | 7d1fbb6f3cab062e4c2d819aad3e8a11118db730 (diff) | |
download | chromium_src-45bdf86341670d95573acbc699f2d2998a25cf1d.zip chromium_src-45bdf86341670d95573acbc699f2d2998a25cf1d.tar.gz chromium_src-45bdf86341670d95573acbc699f2d2998a25cf1d.tar.bz2 |
Add support to ProxyService for downloading a PAC script on behalf of the ProxyResolver. A ProxyResolver can select this new behavior by subclassing ProxyResolver with |does_fetch = false|. A consequence of this change is that proxy resolve requests are maintained in a queue by ProxyService (rather than implicitly in a queue on the PAC thread's message loop). This simplifies cancellation.This solves issue 7461, and is work-in-progress towards {2764, 74}BUG=7461
Review URL: http://codereview.chromium.org/21328
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@10197 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'net')
-rw-r--r-- | net/proxy/proxy_resolver_mac.h | 2 | ||||
-rw-r--r-- | net/proxy/proxy_resolver_winhttp.cc | 2 | ||||
-rw-r--r-- | net/proxy/proxy_service.cc | 284 | ||||
-rw-r--r-- | net/proxy/proxy_service.h | 87 | ||||
-rw-r--r-- | net/proxy/proxy_service_unittest.cc | 453 |
5 files changed, 776 insertions, 52 deletions
diff --git a/net/proxy/proxy_resolver_mac.h b/net/proxy/proxy_resolver_mac.h index f207e1b..8796d45 100644 --- a/net/proxy/proxy_resolver_mac.h +++ b/net/proxy/proxy_resolver_mac.h @@ -13,6 +13,8 @@ namespace net { // proxies. class ProxyResolverMac : public ProxyResolver { public: + ProxyResolverMac() : ProxyResolver(true) {} + // ProxyResolver methods: virtual int GetProxyForURL(const GURL& query_url, const GURL& pac_url, diff --git a/net/proxy/proxy_resolver_winhttp.cc b/net/proxy/proxy_resolver_winhttp.cc index 61a1d2f..aa7ab83 100644 --- a/net/proxy/proxy_resolver_winhttp.cc +++ b/net/proxy/proxy_resolver_winhttp.cc @@ -42,7 +42,7 @@ static void FreeInfo(WINHTTP_PROXY_INFO* info) { } ProxyResolverWinHttp::ProxyResolverWinHttp() - : session_handle_(NULL) { + : ProxyResolver(true), session_handle_(NULL) { } ProxyResolverWinHttp::~ProxyResolverWinHttp() { diff --git a/net/proxy/proxy_service.cc b/net/proxy/proxy_service.cc index b97c010..daf0913 100644 --- a/net/proxy/proxy_service.cc +++ b/net/proxy/proxy_service.cc @@ -38,6 +38,34 @@ class ProxyConfigServiceNull : public ProxyConfigService { } }; +// Strip away any reference fragments and the username/password, as they +// are not relevant to proxy resolution. +static GURL SanitizeURLForProxyResolver(const GURL& url) { + // TODO(eroman): The following duplicates logic from + // HttpUtil::SpecForRequest. Should probably live in net_util.h + GURL::Replacements replacements; + replacements.ClearUsername(); + replacements.ClearPassword(); + replacements.ClearRef(); + return url.ReplaceComponents(replacements); +} + +// Runs on the PAC thread to notify the proxy resolver of the fetched PAC +// script contents. This task shouldn't outlive ProxyService, since +// |resolver| is owned by ProxyService. +class NotifyFetchCompletionTask : public Task { + public: + NotifyFetchCompletionTask(ProxyResolver* resolver, const std::string& bytes) + : resolver_(resolver), bytes_(bytes) {} + + virtual void Run() { + resolver_->SetPacScript(bytes_); + } + + private: + ProxyResolver* resolver_; + std::string bytes_; +}; // ProxyConfig ---------------------------------------------------------------- @@ -194,25 +222,42 @@ std::string ProxyInfo::ToPacString() { class ProxyService::PacRequest : public base::RefCountedThreadSafe<ProxyService::PacRequest> { public: + // |service| -- the ProxyService that owns this request. + // |url| -- the url of the query. + // |results| -- the structure to fill with proxy resolve results. PacRequest(ProxyService* service, - const GURL& pac_url, + const GURL& url, + ProxyInfo* results, CompletionCallback* callback) : service_(service), callback_(callback), - results_(NULL), - config_id_(service->config_id()), - pac_url_(pac_url), + results_(results), + url_(url), + is_started_(false), origin_loop_(MessageLoop::current()) { DCHECK(callback); } - void Query(const GURL& url, ProxyInfo* results) { - results_ = results; - // Execute Query asynchronously + // Start the resolve proxy request on the PAC thread. + void Query() { + is_started_ = true; AddRef(); // balanced in QueryComplete + + GURL query_url = SanitizeURLForProxyResolver(url_); + const GURL& pac_url = service_->config_.pac_url; + results_->config_id_ = service_->config_.id(); + service_->pac_thread()->message_loop()->PostTask(FROM_HERE, NewRunnableMethod(this, &ProxyService::PacRequest::DoQuery, - service_->resolver(), url, pac_url_)); + service_->resolver(), query_url, pac_url)); + } + + // Run the request's callback on the current message loop. + void PostCallback(int result_code) { + AddRef(); // balanced in DoCallback + MessageLoop::current()->PostTask(FROM_HERE, + NewRunnableMethod(this, &ProxyService::PacRequest::DoCallback, + result_code)); } void Cancel() { @@ -223,7 +268,12 @@ class ProxyService::PacRequest : results_ = NULL; } + // Returns true if Cancel() has been called. + bool was_cancelled() const { return callback_ == NULL; } + private: + friend class ProxyService; + // Runs on the PAC thread. void DoQuery(ProxyResolver* resolver, const GURL& query_url, @@ -235,33 +285,43 @@ class ProxyService::PacRequest : // Runs the completion callback on the origin thread. void QueryComplete(int result_code) { - // The PacRequest may have been cancelled after it was started. If it was - // cancelled then |callback_|, |service_|, and |results_| will be NULL. - bool was_cancelled = callback_ == NULL; - - if (!was_cancelled) { - service_->DidCompletePacRequest(config_id_, result_code); + // The PacRequest may have been cancelled after it was started. + if (!was_cancelled()) { + service_->DidCompletePacRequest(results_->config_id_, result_code); if (result_code == OK) { results_->Use(results_buf_); results_->RemoveBadProxies(service_->proxy_retry_info_); } callback_->Run(result_code); + + // We check for cancellation once again, in case the callback deleted + // the owning ProxyService (whose destructor will in turn cancel us). + if (!was_cancelled()) + service_->RemoveFrontOfRequestQueue(this); } Release(); // balances the AddRef in Query. we may get deleted after // we return. } + // Runs the completion callback on the origin thread. + void DoCallback(int result_code) { + if (!was_cancelled()) { + callback_->Run(result_code); + } + Release(); // balances the AddRef in PostCallback. + } + // Must only be used on the "origin" thread. ProxyService* service_; CompletionCallback* callback_; ProxyInfo* results_; - ProxyConfig::ID config_id_; + GURL url_; + bool is_started_; // Usable from within DoQuery on the PAC thread. ProxyInfo results_buf_; - GURL pac_url_; MessageLoop* origin_loop_; }; @@ -272,7 +332,12 @@ ProxyService::ProxyService(ProxyConfigService* config_service, : config_service_(config_service), resolver_(resolver), config_is_bad_(false), - config_has_been_updated_(false) { + config_has_been_updated_(false), + ALLOW_THIS_IN_INITIALIZER_LIST(proxy_script_fetcher_callback_( + this, &ProxyService::OnScriptFetchCompletion)), + fetched_pac_config_id_(ProxyConfig::INVALID_ID), + fetched_pac_error_(OK), + in_progress_fetch_config_id_(ProxyConfig::INVALID_ID) { } // static @@ -309,15 +374,32 @@ int ProxyService::ResolveProxy(const GURL& url, ProxyInfo* result, CompletionCallback* callback, PacRequest** pac_request) { DCHECK(callback); - // The overhead of calling ProxyConfigService::GetProxyConfig is very low. - const TimeDelta kProxyConfigMaxAge = TimeDelta::FromSeconds(5); - // Periodically check for a new config. - if (!config_has_been_updated_ || - (TimeTicks::Now() - config_last_update_time_) > kProxyConfigMaxAge) - UpdateConfig(); + // Check if the request can be completed right away. This is the case when + // using a direct connection, or when the config is bad. + UpdateConfigIfOld(); + int rv = TryToCompleteSynchronously(url, result); + if (rv != ERR_IO_PENDING) + return rv; + + // Otherwise, push the request into the work queue. + scoped_refptr<PacRequest> req = new PacRequest(this, url, result, callback); + pending_requests_.push_back(req); + ProcessPendingRequests(req.get()); + + // Completion will be notifed 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) { result->config_id_ = config_.id(); + DCHECK(config_.id() != ProxyConfig::INVALID_ID); + // Fallback to a "direct" (no proxy) connection if the current configuration // is known to be bad. if (config_is_bad_) { @@ -369,38 +451,110 @@ int ProxyService::ResolveProxy(const GURL& url, ProxyInfo* result, } if (config_.pac_url.is_valid() || config_.auto_detect) { - if (!pac_thread_.get()) { - pac_thread_.reset(new base::Thread("pac-thread")); - pac_thread_->Start(); + // If we failed to download the PAC script, return the network error + // from the failed download. This is only going to happen for the first + // request after the failed download -- after that |config_is_bad_| will + // be set to true, so we short-cuircuit sooner. + if (fetched_pac_error_ != OK && !IsFetchingPacScript()) { + DidCompletePacRequest(fetched_pac_config_id_, fetched_pac_error_); + return fetched_pac_error_; } + return ERR_IO_PENDING; + } + } - scoped_refptr<PacRequest> req = - new PacRequest(this, config_.pac_url, callback); - - // Strip away any reference fragments and the username/password, as they - // are not relevant to proxy resolution. - GURL sanitized_url; - { // TODO(eroman): The following duplicates logic from - // HttpUtil::SpecForRequest. Should probably live in net_util.h - GURL::Replacements replacements; - replacements.ClearUsername(); - replacements.ClearPassword(); - replacements.ClearRef(); - sanitized_url = url.ReplaceComponents(replacements); - } + // otherwise, we have no proxy config + result->UseDirect(); + return OK; +} - req->Query(sanitized_url, result); +void ProxyService::InitPacThread() { + if (!pac_thread_.get()) { + pac_thread_.reset(new base::Thread("pac-thread")); + pac_thread_->Start(); + } +} - if (pac_request) - *pac_request = req; +ProxyService::~ProxyService() { + // Cancel the inprogress request (if any), and free the rest. + for (PendingRequestsQueue::iterator it = pending_requests_.begin(); + it != pending_requests_.end(); + ++it) { + (*it)->Cancel(); + } +} + +void ProxyService::ProcessPendingRequests(PacRequest* recent_req) { + if (pending_requests_.empty()) + return; + + // While the PAC script is being downloaded, requests are blocked. + if (IsFetchingPacScript()) + return; + + // Get the next request to process (FIFO). + PacRequest* req = pending_requests_.front().get(); + if (req->is_started_) + return; - return ERR_IO_PENDING; // Wait for callback. + // The configuration may have changed since |req| was added to the + // queue. It could be this request now completes synchronously. + if (req != recent_req) { + UpdateConfigIfOld(); + int rv = TryToCompleteSynchronously(req->url_, req->results_); + if (rv != ERR_IO_PENDING) { + req->PostCallback(rv); + RemoveFrontOfRequestQueue(req); + return; } } - // otherwise, we have no proxy config - result->UseDirect(); - return OK; + // Check if a new PAC script needs to be downloaded. + DCHECK(config_.id() != ProxyConfig::INVALID_ID); + if (!resolver_->does_fetch() && config_.id() != fetched_pac_config_id_) { + // For auto-detect we use the well known WPAD url. + GURL pac_url = config_.auto_detect ? + GURL("http://wpad/wpad.dat") : config_.pac_url; + + in_progress_fetch_config_id_ = config_.id(); + + proxy_script_fetcher_->Fetch( + pac_url, &in_progress_fetch_bytes_, &proxy_script_fetcher_callback_); + return; + } + + // The only choice left now is to actually run the ProxyResolver on + // the PAC thread. + InitPacThread(); + req->Query(); +} + +void ProxyService::RemoveFrontOfRequestQueue(PacRequest* expected_req) { + DCHECK(pending_requests_.front().get() == expected_req); + pending_requests_.pop_front(); + + // Start next work item. + ProcessPendingRequests(NULL); +} + +void ProxyService::OnScriptFetchCompletion(int result) { + DCHECK(IsFetchingPacScript()); + DCHECK(!resolver_->does_fetch()); + + // Notify the ProxyResolver of the new script data (will be empty string if + // result != OK). + InitPacThread(); + pac_thread()->message_loop()->PostTask(FROM_HERE, + new NotifyFetchCompletionTask( + resolver_.get(), in_progress_fetch_bytes_)); + + fetched_pac_config_id_ = in_progress_fetch_config_id_; + fetched_pac_error_ = result; + in_progress_fetch_config_id_ = ProxyConfig::INVALID_ID; + in_progress_fetch_bytes_.clear(); + + // Start a pending request if any. + ProcessPendingRequests(NULL); } int ProxyService::ReconsiderProxyAfterError(const GURL& url, @@ -453,8 +607,30 @@ int ProxyService::ReconsiderProxyAfterError(const GURL& url, return OK; } -void ProxyService::CancelPacRequest(PacRequest* pac_request) { - pac_request->Cancel(); +// There are four states of the request we need to handle: +// (1) Not started (just sitting in the queue). +// (2) Executing PacRequest::DoQuery in the PAC thread. +// (3) Waiting for PacRequest::QueryComplete to be run on the origin thread. +// (4) Waiting for PacRequest::DoCallback to be run on the origin thread. +void ProxyService::CancelPacRequest(PacRequest* req) { + DCHECK(req); + + bool is_active_request = req->is_started_ && !pending_requests_.empty() && + pending_requests_.front().get() == req; + + req->Cancel(); + + if (is_active_request) { + RemoveFrontOfRequestQueue(req); + return; + } + + // Otherwise just delete the request from the queue. + PendingRequestsQueue::iterator it = std::find( + pending_requests_.begin(), pending_requests_.end(), req); + if (it != pending_requests_.end()) { + pending_requests_.erase(it); + } } void ProxyService::DidCompletePacRequest(int config_id, int result_code) { @@ -487,6 +663,16 @@ void ProxyService::UpdateConfig() { proxy_retry_info_.clear(); } +void ProxyService::UpdateConfigIfOld() { + // The overhead of calling ProxyConfigService::GetProxyConfig is very low. + const TimeDelta kProxyConfigMaxAge = TimeDelta::FromSeconds(5); + + // Periodically check for a new config. + if (!config_has_been_updated_ || + (TimeTicks::Now() - config_last_update_time_) > kProxyConfigMaxAge) + UpdateConfig(); +} + bool ProxyService::ShouldBypassProxyForURL(const GURL& url) { std::string url_domain = url.scheme(); if (!url_domain.empty()) diff --git a/net/proxy/proxy_service.h b/net/proxy/proxy_service.h index d14cc3e..3c29195 100644 --- a/net/proxy/proxy_service.h +++ b/net/proxy/proxy_service.h @@ -5,6 +5,7 @@ #ifndef NET_PROXY_PROXY_SERVICE_H_ #define NET_PROXY_PROXY_SERVICE_H_ +#include <deque> #include <map> #include <string> #include <vector> @@ -17,6 +18,7 @@ #include "base/waitable_event.h" #include "googleurl/src/gurl.h" #include "net/base/completion_callback.h" +#include "net/proxy/proxy_script_fetcher.h" #include "net/proxy/proxy_server.h" class GURL; @@ -102,6 +104,8 @@ class ProxyService { ProxyService(ProxyConfigService* config_service, ProxyResolver* resolver); + ~ProxyService(); + // Used internally to handle PAC queries. class PacRequest; @@ -156,6 +160,13 @@ class ProxyService { // so it falls back to direct connect. static ProxyService* CreateNull(); + // Set the ProxyScriptFetcher dependency. This is needed if the ProxyResolver + // is of type ProxyResolverWithoutFetch. ProxyService takes ownership of + // |proxy_script_fetcher|. + void SetProxyScriptFetcher(ProxyScriptFetcher* proxy_script_fetcher) { + proxy_script_fetcher_.reset(proxy_script_fetcher); + } + private: friend class PacRequest; @@ -169,6 +180,37 @@ class ProxyService { // to reference the new configuration. void UpdateConfig(); + // Tries to update the configuration if it hasn't been checked in a while. + void UpdateConfigIfOld(); + + // Returns true if this ProxyService is downloading a PAC script on behalf + // of ProxyResolverWithoutFetch. Resolve requests will be frozen until + // the fetch has completed. + bool IsFetchingPacScript() const { + return in_progress_fetch_config_id_ != ProxyConfig::INVALID_ID; + } + + // Callback for when the PAC script has finished downloading. + void OnScriptFetchCompletion(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. + // (ProxyResolver runs on PAC thread.) + int TryToCompleteSynchronously(const GURL& url, ProxyInfo* result); + + // Starts the PAC thread if it isn't already running. + void InitPacThread(); + + // Starts the next request from |pending_requests_| is possible. + // |recent_req| is the request that just got added, or NULL. + void ProcessPendingRequests(PacRequest* recent_req); + + // Removes the front entry of the requests queue. |expected_req| is our + // expectation of what the front of the request queue is; it is only used by + // DCHECK for verification purposes. + void RemoveFrontOfRequestQueue(PacRequest* expected_req); + // Called to indicate that a PacRequest completed. The |config_id| parameter // indicates the proxy configuration that was queried. |result_code| is OK // if the PAC file could be downloaded and executed. Otherwise, it is an @@ -201,6 +243,32 @@ class ProxyService { // Map of the known bad proxies and the information about the retry time. ProxyRetryInfoMap proxy_retry_info_; + // FIFO queue of pending/inprogress requests. + typedef std::deque<scoped_refptr<PacRequest> > PendingRequestsQueue; + PendingRequestsQueue 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_; + + // Callback for when |proxy_script_fetcher_| is done. + CompletionCallbackImpl<ProxyService> proxy_script_fetcher_callback_; + + // The ID of the configuration for which we last downloaded a PAC script. + // If no PAC script has been fetched yet, will be ProxyConfig::INVALID_ID. + ProxyConfig::ID fetched_pac_config_id_; + + // The error corresponding with |fetched_pac_config_id_|, or OK. + int fetched_pac_error_; + + // The ID of the configuration for which we are currently downloading the + // PAC script. If no fetch is in progress, will be ProxyConfig::INVALID_ID. + ProxyConfig::ID in_progress_fetch_config_id_; + + // The results of the current in progress fetch, or empty string. + std::string in_progress_fetch_bytes_; + DISALLOW_COPY_AND_ASSIGN(ProxyService); }; @@ -324,6 +392,13 @@ class ProxyConfigService { // PAC Thread. class ProxyResolver { public: + + // If a subclass sets |does_fetch| to false, then the owning ProxyResolver + // will download PAC scripts on our behalf, and notify changes with + // SetPacScript(). Otherwise the subclass is expected to fetch the + // PAC script internally, and SetPacScript() will go unused. + ProxyResolver(bool does_fetch) : does_fetch_(does_fetch) {} + virtual ~ProxyResolver() {} // Query the proxy auto-config file (specified by |pac_url|) for the proxy to @@ -332,6 +407,18 @@ class ProxyResolver { virtual int GetProxyForURL(const GURL& query_url, const GURL& pac_url, ProxyInfo* results) = 0; + + // Called whenever the PAC script has changed, with the contents of the + // PAC script. |bytes| may be empty string if there was a fetch error. + virtual void SetPacScript(const std::string& bytes) { + // Must override SetPacScript() if |does_fetch_ = true|. + NOTREACHED(); + } + + bool does_fetch() const { return does_fetch_; } + + protected: + bool does_fetch_; }; // Wrapper for invoking methods on a ProxyService synchronously. diff --git a/net/proxy/proxy_service_unittest.cc b/net/proxy/proxy_service_unittest.cc index 35df20f..84a9395 100644 --- a/net/proxy/proxy_service_unittest.cc +++ b/net/proxy/proxy_service_unittest.cc @@ -2,6 +2,7 @@ // 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 "googleurl/src/gurl.h" #include "net/base/net_errors.h" #include "net/proxy/proxy_service.h" @@ -27,7 +28,8 @@ class MockProxyConfigService: public net::ProxyConfigService { class MockProxyResolver : public net::ProxyResolver { public: - MockProxyResolver() : fail_get_proxy_for_url(false) { + MockProxyResolver() : net::ProxyResolver(true), + fail_get_proxy_for_url(false) { } virtual int GetProxyForURL(const GURL& query_url, @@ -80,8 +82,272 @@ class SyncProxyService { scoped_refptr<net::SyncProxyServiceHelper> sync_proxy_service_; }; +// ResultFuture is a handle to get at the result from +// ProxyService::ResolveProxyForURL() that ran on another thread. +class ResultFuture : public base::RefCountedThreadSafe<ResultFuture> { + public: + // |service| is the ProxyService to issue requests on, and |io_message_loop| + // is the message loop where ProxyService lives. + ResultFuture(MessageLoop* io_message_loop, + net::ProxyService* service) + : io_message_loop_(io_message_loop), + service_(service), + request_(NULL), + ALLOW_THIS_IN_INITIALIZER_LIST( + callback_(this, &ResultFuture::OnCompletion)), + completion_(true, false), + cancelled_(false, false), + started_(false, false), + did_complete_(false) { + } + + // Block until the request has completed, then return the result. + int GetResultCode() { + DCHECK(MessageLoop::current() != io_message_loop_); + WaitUntilCompleted(); + return result_code_; + } + + // Block until the request has completed, then return the result. + const net::ProxyInfo& GetProxyInfo() { + DCHECK(MessageLoop::current() != io_message_loop_); + WaitUntilCompleted(); + return proxy_info_; + } + + // Cancel this request (wait until the cancel has been issued before + // returning). + void Cancel() { + DCHECK(MessageLoop::current() != io_message_loop_); + io_message_loop_->PostTask(FROM_HERE, NewRunnableMethod( + this, &ResultFuture::DoCancel)); + cancelled_.Wait(); + } + + // Return true if the request has already completed. + bool IsCompleted() { + DCHECK(MessageLoop::current() != io_message_loop_); + return did_complete_; + } + + // Wait until the ProxyService completes this request. + void WaitUntilCompleted() { + DCHECK(MessageLoop::current() != io_message_loop_); + completion_.Wait(); + DCHECK(did_complete_); + } + + private: + friend class ProxyServiceWithFutures; + + // Start the request. Return once ProxyService::GetProxyForURL() returns. + void StartResolve(const GURL& url) { + DCHECK(MessageLoop::current() != io_message_loop_); + io_message_loop_->PostTask(FROM_HERE, NewRunnableMethod( + this, &ResultFuture::DoStartResolve, url)); + started_.Wait(); + } + + // Called on |io_message_loop_|. + void DoStartResolve(const GURL& url) { + DCHECK(MessageLoop::current() == io_message_loop_); + int rv = service_->ResolveProxy(url, &proxy_info_, &callback_, &request_); + if (rv != net::ERR_IO_PENDING) { + // Completed synchronously. + OnCompletion(rv); + } + started_.Signal(); + } + + // Called on |io_message_loop_|. + void DoCancel() { + DCHECK(MessageLoop::current() == io_message_loop_); + if (!did_complete_) + service_->CancelPacRequest(request_); + cancelled_.Signal(); + } + + // Called on |io_message_loop_|. + void OnCompletion(int result) { + DCHECK(MessageLoop::current() == io_message_loop_); + DCHECK(!did_complete_); + did_complete_ = true; + result_code_ = result; + request_ = NULL; + completion_.Signal(); + } + + // The message loop where the ProxyService lives. + MessageLoop* io_message_loop_; + + // The proxy service that started this request. + net::ProxyService* service_; + + // The in-progress request. + net::ProxyService::PacRequest* request_; + + net::CompletionCallbackImpl<ResultFuture> callback_; + base::WaitableEvent completion_; + base::WaitableEvent cancelled_; + base::WaitableEvent started_; + bool did_complete_; + + // Results from the request. + int result_code_; + net::ProxyInfo proxy_info_; +}; + +// Wraps a ProxyService running on its own IO thread. +class ProxyServiceWithFutures { + public: + ProxyServiceWithFutures(net::ProxyConfigService* config_service, + net::ProxyResolver* resolver) + : io_thread_("IO_Thread"), + service_(config_service, resolver) { + base::Thread::Options options; + options.message_loop_type = MessageLoop::TYPE_IO; + io_thread_.StartWithOptions(options); + } + + // Start the request on |io_thread_|, and return a handle that can be + // used to access the results. The caller is responsible for freeing + // the ResultFuture. + void ResolveProxy(scoped_refptr<ResultFuture>* result, const GURL& url) { + (*result) = new ResultFuture(io_thread_.message_loop(), &service_); + (*result)->StartResolve(url); + } + + void SetProxyScriptFetcher(net::ProxyScriptFetcher* proxy_script_fetcher) { + service_.SetProxyScriptFetcher(proxy_script_fetcher); + } + + private: + base::Thread io_thread_; + net::ProxyService service_; +}; + +// A ProxyResolver which can be set to block upon reaching GetProxyForURL. +class BlockableProxyResolver : public net::ProxyResolver { + public: + BlockableProxyResolver() : net::ProxyResolver(true), + 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(); + } + + // net::ProxyResolver implementation: + virtual int GetProxyForURL(const GURL& query_url, + const GURL& pac_url, + net::ProxyInfo* results) { + if (should_block_) { + blocked_.Signal(); + unblocked_.Wait(); + } + + results->UseNamedProxy(query_url.host()); + return net::OK; + } + + private: + bool should_block_; + base::WaitableEvent unblocked_; + base::WaitableEvent blocked_; +}; + +// A mock ProxyResolverWithoutFetch which concatenates the query's host with +// the last download PAC contents. This way the result describes what the last +// downloaded PAC script's contents were, in addition to the query url itself. +class MockProxyResolverWithoutFetch : public net::ProxyResolver { + public: + MockProxyResolverWithoutFetch() : net::ProxyResolver(false), + last_pac_contents_("NONE") {} + + // net::ProxyResolver implementation: + virtual int GetProxyForURL(const GURL& query_url, + const GURL& pac_url, + net::ProxyInfo* results) { + results->UseNamedProxy(last_pac_contents_ + "." + query_url.host()); + return net::OK; + } + + virtual void SetPacScript(const std::string& bytes) { + last_pac_contents_ = bytes; + } + + private: + std::string last_pac_contents_; +}; + } // namespace +// A mock ProxyScriptFetcher. No result will be returned to the fetch client +// until we call NotifyFetchCompletion() to set the results. +class MockProxyScriptFetcher : public net::ProxyScriptFetcher { + public: + MockProxyScriptFetcher() : pending_request_loop_(NULL), + pending_request_callback_(NULL), pending_request_bytes_(NULL) {} + + // net::ProxyScriptFetcher implementation. + virtual void Fetch(const GURL& url, std::string* bytes, + net::CompletionCallback* callback) { + DCHECK(!HasPendingRequest()); + + // Save the caller's information, and have them wait. + pending_request_loop_ = MessageLoop::current(); + pending_request_url_ = url; + pending_request_callback_ = callback; + pending_request_bytes_ = bytes; + } + + void NotifyFetchCompletion(int result, const std::string& bytes) { + DCHECK(HasPendingRequest()); + pending_request_loop_->PostTask(FROM_HERE, NewRunnableMethod( + this, &MockProxyScriptFetcher::DoNotifyFetchCompletion, result, bytes)); + } + + virtual void Cancel() {} + + private: + // Runs on |pending_request_loop_|. + void DoNotifyFetchCompletion(int result, const std::string& bytes) { + DCHECK(HasPendingRequest()); + *pending_request_bytes_ = bytes; + pending_request_callback_->Run(result); + } + + bool HasPendingRequest() const { + return pending_request_loop_ != NULL; + } + + MessageLoop* pending_request_loop_; + GURL pending_request_url_; + net::CompletionCallback* pending_request_callback_; + std::string* pending_request_bytes_; +}; + +// Template specialization so MockProxyScriptFetcher does not have to be refcounted. +template<> +void RunnableMethodTraits<MockProxyScriptFetcher>::RetainCallee( + MockProxyScriptFetcher* remover) {} +template<> +void RunnableMethodTraits<MockProxyScriptFetcher>::ReleaseCallee( + MockProxyScriptFetcher* remover) {} + // Test parsing from a PAC string. TEST(ProxyListTest, SetFromPacString) { const struct { @@ -419,7 +685,7 @@ TEST(ProxyServiceTest, ProxyBypassList) { config.proxy_rules = "foopy1:8080;foopy2:9090"; config.auto_detect = false; config.proxy_bypass_local_names = true; - + SyncProxyService service(new MockProxyConfigService(config), new MockProxyResolver()); GURL url("http://www.google.com/"); @@ -537,3 +803,186 @@ TEST(ProxyServiceTest, PerProtocolProxyTests) { EXPECT_EQ("foopy1:8080", info4.proxy_server().ToURI()); } +// Test cancellation of a queued request. +TEST(ProxyServiceTest, CancelQueuedRequest) { + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + + BlockableProxyResolver* resolver = new BlockableProxyResolver; + + ProxyServiceWithFutures service(config_service, resolver); + + // Cause requests to pile up, by having them block in the PAC thread. + resolver->Block(); + + // Start 3 requests. + scoped_refptr<ResultFuture> result1; + service.ResolveProxy(&result1, GURL("http://request1")); + + scoped_refptr<ResultFuture> result2; + service.ResolveProxy(&result2, GURL("http://request2")); + + scoped_refptr<ResultFuture> result3; + service.ResolveProxy(&result3, GURL("http://request3")); + + // Wait until the first request has become blocked in the PAC thread. + resolver->WaitUntilBlocked(); + + // Cancel the second request + result2->Cancel(); + + // Unblock the PAC thread. + resolver->Unblock(); + + // Wait for the final request to complete. + result3->WaitUntilCompleted(); + + // Verify that requests ran as expected. + + EXPECT_TRUE(result1->IsCompleted()); + EXPECT_EQ(net::OK, result1->GetResultCode()); + EXPECT_EQ("request1:80", result1->GetProxyInfo().proxy_server().ToURI()); + + EXPECT_FALSE(result2->IsCompleted()); // Cancelled. + + EXPECT_TRUE(result3->IsCompleted()); + EXPECT_EQ(net::OK, result3->GetResultCode()); + EXPECT_EQ("request3:80", result3->GetProxyInfo().proxy_server().ToURI()); +} + +// Test cancellation of an in-progress request. +TEST(ProxyServiceTest, CancelInprogressRequest) { + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + + BlockableProxyResolver* resolver = new BlockableProxyResolver; + + ProxyServiceWithFutures service(config_service, resolver); + + // Cause requests to pile up, by having them block in the PAC thread. + resolver->Block(); + + // Start 3 requests. + scoped_refptr<ResultFuture> result1; + service.ResolveProxy(&result1, GURL("http://request1")); + + scoped_refptr<ResultFuture> result2; + service.ResolveProxy(&result2, GURL("http://request2")); + + scoped_refptr<ResultFuture> result3; + service.ResolveProxy(&result3, GURL("http://request3")); + + // Wait until the first request has become blocked in the PAC thread. + resolver->WaitUntilBlocked(); + + // Cancel the first request + result1->Cancel(); + + // Unblock the PAC thread. + resolver->Unblock(); + + // Wait for the final request to complete. + result3->WaitUntilCompleted(); + + // Verify that requests ran as expected. + + EXPECT_FALSE(result1->IsCompleted()); // Cancelled. + + EXPECT_TRUE(result2->IsCompleted()); + EXPECT_EQ(net::OK, result2->GetResultCode()); + EXPECT_EQ("request2:80", result2->GetProxyInfo().proxy_server().ToURI()); + + EXPECT_TRUE(result3->IsCompleted()); + EXPECT_EQ(net::OK, result3->GetResultCode()); + EXPECT_EQ("request3:80", result3->GetProxyInfo().proxy_server().ToURI()); +} + +// Test the initial PAC download for ProxyResolverWithoutFetch. +TEST(ProxyServiceTest, InitialPACScriptDownload) { + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + + MockProxyResolverWithoutFetch* resolver = new MockProxyResolverWithoutFetch; + + ProxyServiceWithFutures service(config_service, resolver); + + MockProxyScriptFetcher* fetcher = new MockProxyScriptFetcher; + service.SetProxyScriptFetcher(fetcher); + + // Start 3 requests. + scoped_refptr<ResultFuture> result1; + service.ResolveProxy(&result1, GURL("http://request1")); + + scoped_refptr<ResultFuture> result2; + service.ResolveProxy(&result2, GURL("http://request2")); + + scoped_refptr<ResultFuture> result3; + service.ResolveProxy(&result3, GURL("http://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(net::OK, "pac-v1"); + + // Complete all the requests. + result3->WaitUntilCompleted(); + + EXPECT_TRUE(result1->IsCompleted()); + EXPECT_EQ(net::OK, result1->GetResultCode()); + EXPECT_EQ("pac-v1.request1:80", + result1->GetProxyInfo().proxy_server().ToURI()); + + EXPECT_TRUE(result2->IsCompleted()); + EXPECT_EQ(net::OK, result2->GetResultCode()); + EXPECT_EQ("pac-v1.request2:80", + result2->GetProxyInfo().proxy_server().ToURI()); + + EXPECT_TRUE(result3->IsCompleted()); + EXPECT_EQ(net::OK, result3->GetResultCode()); + EXPECT_EQ("pac-v1.request3:80", + result3->GetProxyInfo().proxy_server().ToURI()); +} + +// Test cancellation of a request, while the PAC script is being fetched. +TEST(ProxyServiceTest, CancelWhilePACFetching) { + MockProxyConfigService* config_service = + new MockProxyConfigService("http://foopy/proxy.pac"); + + MockProxyResolverWithoutFetch* resolver = new MockProxyResolverWithoutFetch; + + ProxyServiceWithFutures service(config_service, resolver); + + MockProxyScriptFetcher* fetcher = new MockProxyScriptFetcher; + service.SetProxyScriptFetcher(fetcher); + + // Start 3 requests. + scoped_refptr<ResultFuture> result1; + service.ResolveProxy(&result1, GURL("http://request1")); + + scoped_refptr<ResultFuture> result2; + service.ResolveProxy(&result2, GURL("http://request2")); + + scoped_refptr<ResultFuture> result3; + service.ResolveProxy(&result3, GURL("http://request3")); + + // Cancel the first 2 requests. + result1->Cancel(); + result2->Cancel(); + + // 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(net::OK, "pac-v1"); + + // Complete all the requests. + result3->WaitUntilCompleted(); + + EXPECT_FALSE(result1->IsCompleted()); // Cancelled. + EXPECT_FALSE(result2->IsCompleted()); // Cancelled. + + EXPECT_TRUE(result3->IsCompleted()); + EXPECT_EQ(net::OK, result3->GetResultCode()); + EXPECT_EQ("pac-v1.request3:80", + result3->GetProxyInfo().proxy_server().ToURI()); +} + |