blob: f80f9ef1450b8704eb74b5adfd25da8472e00679 [file] [log] [blame]
// 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_resolution/dhcp_pac_file_fetcher_win.h"
#include <memory>
#include <vector>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/containers/queue.h"
#include "base/memory/free_deleter.h"
#include "base/synchronization/lock.h"
#include "base/task_runner.h"
#include "base/task_scheduler/post_task.h"
#include "base/threading/scoped_blocking_call.h"
#include "net/base/net_errors.h"
#include "net/proxy_resolution/dhcp_pac_file_adapter_fetcher_win.h"
#include <winsock2.h>
#include <iphlpapi.h>
namespace {
// Maximum number of DHCP lookup tasks running concurrently. This is chosen
// based on the following UMA data:
// - When OnWaitTimer fires, ~99.8% of users have 6 or fewer network
// adapters enabled for DHCP in total.
// - At the same measurement point, ~99.7% of users have 3 or fewer pending
// DHCP adapter lookups.
// - There is however a very long and thin tail of users who have
// systems reporting up to 100+ adapters (this must be some very weird
// OS bug (?), probably the cause of http://crbug.com/240034).
//
// Th value is chosen such that DHCP lookup tasks don't prevent other tasks from
// running even on systems that report a huge number of network adapters, while
// giving a good chance of getting back results for any responsive adapters.
constexpr int kMaxConcurrentDhcpLookupTasks = 12;
// 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.
constexpr base::TimeDelta kMaxWaitAfterFirstResult =
base::TimeDelta::FromMilliseconds(400);
// A TaskRunner that never schedules more than |kMaxConcurrentDhcpLookupTasks|
// tasks concurrently.
class TaskRunnerWithCap : public base::TaskRunner {
public:
TaskRunnerWithCap() = default;
bool PostDelayedTask(const base::Location& from_here,
base::OnceClosure task,
base::TimeDelta delay) override {
// Delayed tasks are not supported.
DCHECK(delay.is_zero());
// Wrap the task in a callback that runs |task|, then tries to schedule a
// task from |pending_tasks_|.
base::OnceClosure wrapped_task =
base::BindOnce(&TaskRunnerWithCap::RunTaskAndSchedulePendingTask, this,
std::move(task));
{
base::AutoLock auto_lock(lock_);
// If |kMaxConcurrentDhcpLookupTasks| tasks are scheduled, move the task
// to |pending_tasks_|.
DCHECK_LE(num_scheduled_tasks_, kMaxConcurrentDhcpLookupTasks);
if (num_scheduled_tasks_ == kMaxConcurrentDhcpLookupTasks) {
pending_tasks_.emplace(from_here, std::move(wrapped_task));
return true;
}
// If less than |kMaxConcurrentDhcpLookupTasks| tasks are scheduled,
// increment |num_scheduled_tasks_| and schedule the task.
++num_scheduled_tasks_;
}
task_runner_->PostTask(from_here, std::move(wrapped_task));
return true;
}
bool RunsTasksInCurrentSequence() const override {
return task_runner_->RunsTasksInCurrentSequence();
}
private:
struct LocationAndTask {
LocationAndTask() = default;
LocationAndTask(const base::Location& from_here, base::OnceClosure task)
: from_here(from_here), task(std::move(task)) {}
base::Location from_here;
base::OnceClosure task;
};
~TaskRunnerWithCap() override = default;
void RunTaskAndSchedulePendingTask(base::OnceClosure task) {
// Run |task|.
std::move(task).Run();
// If |pending_tasks_| is non-empty, schedule a task from it. Otherwise,
// decrement |num_scheduled_tasks_|.
LocationAndTask task_to_schedule;
{
base::AutoLock auto_lock(lock_);
DCHECK_GT(num_scheduled_tasks_, 0);
if (pending_tasks_.empty()) {
--num_scheduled_tasks_;
return;
}
task_to_schedule = std::move(pending_tasks_.front());
pending_tasks_.pop();
}
DCHECK(task_to_schedule.task);
task_runner_->PostTask(task_to_schedule.from_here,
std::move(task_to_schedule.task));
}
const scoped_refptr<base::TaskRunner> task_runner_ =
base::CreateTaskRunnerWithTraits(
{base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN,
base::TaskPriority::USER_VISIBLE});
// Synchronizes access to members below.
base::Lock lock_;
// Number of tasks that are currently scheduled.
int num_scheduled_tasks_ = 0;
// Tasks that are waiting to be scheduled.
base::queue<LocationAndTask> pending_tasks_;
DISALLOW_COPY_AND_ASSIGN(TaskRunnerWithCap);
};
} // 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),
task_runner_(base::MakeRefCounted<TaskRunnerWithCap>()) {
DCHECK(url_request_context_);
}
DhcpProxyScriptFetcherWin::~DhcpProxyScriptFetcherWin() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
// Count as user-initiated if we are not yet in STATE_DONE.
Cancel();
}
int DhcpProxyScriptFetcherWin::Fetch(base::string16* utf16_text,
const CompletionCallback& callback) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
if (state_ != STATE_START && state_ != STATE_DONE) {
NOTREACHED();
return ERR_UNEXPECTED;
}
if (!url_request_context_)
return ERR_CONTEXT_SHUT_DOWN;
state_ = STATE_WAIT_ADAPTERS;
callback_ = callback;
destination_string_ = utf16_text;
last_query_ = ImplCreateAdapterQuery();
task_runner_->PostTaskAndReply(
FROM_HERE,
base::Bind(
&DhcpProxyScriptFetcherWin::AdapterQuery::GetCandidateAdapterNames,
last_query_.get()),
base::Bind(&DhcpProxyScriptFetcherWin::OnGetCandidateAdapterNamesDone,
AsWeakPtr(), last_query_));
return ERR_IO_PENDING;
}
void DhcpProxyScriptFetcherWin::Cancel() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
CancelImpl();
}
void DhcpProxyScriptFetcherWin::OnShutdown() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
// Back up callback, if there is one, as CancelImpl() will destroy it.
net::CompletionCallback callback = std::move(callback_);
// Cancel current request, if there is one.
CancelImpl();
// Prevent future network requests.
url_request_context_ = nullptr;
// Invoke callback with error, if present.
if (callback)
callback.Run(ERR_CONTEXT_SHUT_DOWN);
}
void DhcpProxyScriptFetcherWin::CancelImpl() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
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_CALLED_ON_VALID_THREAD(thread_checker_);
// 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) {
std::unique_ptr<DhcpProxyScriptAdapterFetcher> fetcher(
ImplCreateAdapterFetcher());
fetcher->Fetch(
*it, base::Bind(&DhcpProxyScriptFetcherWin::OnFetcherDone,
base::Unretained(this)));
fetchers_.push_back(std::move(fetcher));
}
num_pending_fetchers_ = fetchers_.size();
}
std::string DhcpProxyScriptFetcherWin::GetFetcherName() const {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
return "win";
}
const GURL& DhcpProxyScriptFetcherWin::GetPacURL() const {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
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);
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.
// 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_;
}
scoped_refptr<base::TaskRunner> DhcpProxyScriptFetcherWin::GetTaskRunner() {
return task_runner_;
}
DhcpProxyScriptAdapterFetcher*
DhcpProxyScriptFetcherWin::ImplCreateAdapterFetcher() {
return new DhcpProxyScriptAdapterFetcher(url_request_context_, task_runner_);
}
DhcpProxyScriptFetcherWin::AdapterQuery*
DhcpProxyScriptFetcherWin::ImplCreateAdapterQuery() {
return new AdapterQuery();
}
base::TimeDelta DhcpProxyScriptFetcherWin::ImplGetMaxWait() {
return kMaxWaitAfterFirstResult;
}
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;
std::unique_ptr<IP_ADAPTER_ADDRESSES, base::FreeDeleter> adapters;
ULONG error = ERROR_SUCCESS;
int num_tries = 0;
do {
adapters.reset(static_cast<IP_ADAPTER_ADDRESSES*>(malloc(adapters_size)));
// Return only unicast addresses, and skip information we do not need.
base::ScopedBlockingCall scoped_blocking_call(
base::BlockingType::MAY_BLOCK);
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);
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() {
}
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);
}
DhcpProxyScriptFetcherWin::AdapterQuery::~AdapterQuery() {
}
} // namespace net