blob: a302415a6ab3a773734b793f2a8baf90a8087414 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/proxy_resolver_win/windows_system_proxy_resolver_impl.h"
#include <cwchar>
#include <utility>
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "net/base/host_port_pair.h"
#include "net/base/proxy_server.h"
#include "net/proxy_resolution/proxy_list.h"
#include "services/proxy_resolver_win/winhttp_api_wrapper_impl.h"
#include "url/gurl.h"
#include "url/url_canon.h"
namespace proxy_resolver_win {
namespace {
bool GetProxyChainFromWinHttpResultEntry(
const WINHTTP_PROXY_RESULT_ENTRY& result_entry,
net::ProxyChain* out_proxy_chain) {
// TODO(https://crbug.com/1032820): Include net logs for proxy bypass
if (!result_entry.fProxy) {
*out_proxy_chain = net::ProxyChain::Direct();
return true;
}
net::ProxyServer::Scheme scheme = net::ProxyServer::Scheme::SCHEME_INVALID;
switch (result_entry.ProxyScheme) {
case (INTERNET_SCHEME_HTTP):
scheme = net::ProxyServer::Scheme::SCHEME_HTTP;
break;
case (INTERNET_SCHEME_HTTPS):
scheme = net::ProxyServer::Scheme::SCHEME_HTTPS;
break;
case (INTERNET_SCHEME_SOCKS):
scheme = net::ProxyServer::Scheme::SCHEME_SOCKS4;
break;
default:
LOG(WARNING) << "Of the possible proxy schemes returned by WinHttp, "
"Chrome supports HTTP(S) and SOCKS4. The ProxyScheme "
"that triggered this message is: "
<< result_entry.ProxyScheme;
break;
}
if (scheme == net::ProxyServer::Scheme::SCHEME_INVALID)
return false;
// Chrome expects a specific port from WinHttp. The WinHttp documentation on
// MSDN makes it unclear whether or not a specific port is guaranteed.
if (result_entry.ProxyPort == INTERNET_DEFAULT_PORT) {
LOG(WARNING) << "WinHttpGetProxyForUrlEx() returned a proxy with "
"INTERNET_PORT_DEFAULT!";
return false;
}
// Since there is a proxy in the result (i.e. `fProxy` is TRUE), the
// `pwszProxy` is guaranteed to be non-null and non-empty.
DCHECK(!!result_entry.pwszProxy);
DCHECK(wcslen(result_entry.pwszProxy) > 0);
std::wstring host_wide(result_entry.pwszProxy,
wcslen(result_entry.pwszProxy));
if (!base::IsStringASCII(host_wide)) {
const int kInitialBufferSize = 256;
url::RawCanonOutputT<char16_t, kInitialBufferSize> punycode_output;
if (!url::IDNToASCII(base::AsStringPiece16(host_wide), &punycode_output)) {
return false;
}
host_wide = base::AsWString(punycode_output.view());
}
// At this point the string in `host_wide` is ASCII.
std::string host;
if (!base::WideToUTF8(host_wide.data(), host_wide.length(), &host))
return false;
net::HostPortPair host_and_port(host, result_entry.ProxyPort);
*out_proxy_chain = net::ProxyChain(scheme, host_and_port);
return true;
}
} // namespace
// Encapsulates an in-flight WinHttp proxy resolution request. This also owns
// the GetProxyForUrlCallback and will attempt to supply that callback with the
// proxy result once WinHttp returns.
class WindowsSystemProxyResolverImpl::Request {
public:
Request(WindowsSystemProxyResolverImpl* parent,
GetProxyForUrlCallback callback);
Request(const Request&) = delete;
Request& operator=(const Request&) = delete;
~Request();
// Sets up and kicks off proxy resolution using WinHttp.
bool Start(const GURL& url);
base::SequencedTaskRunner* sequenced_task_runner() {
return sequenced_task_runner_.get();
}
// Called from WinHttpStatusCallback on the right sequence. This will make
// decisions about what to do from the results of the proxy resolution call.
void DoWinHttpStatusCallback(HINTERNET resolver_handle,
DWORD status,
int windows_error);
private:
// On a successful call to WinHttpGetProxyForUrlEx(), this translates WinHttp
// results into Chromium-friendly structures and reports the result.
void GetProxyResultForCallback();
// Notifies `callback_` of the proxy result.
void ReportResult(const net::ProxyList& proxy_list,
net::WinHttpStatus winhttp_status,
int windows_error);
WinHttpAPIWrapper* winhttp_api_wrapper() {
return parent_->winhttp_api_wrapper_.get();
}
// The WindowsSystemProxyResolverImpl manages the lifetime of this object. The
// Request cannot outlive the WindowsSystemProxyResolverImpl. Thus, it is safe
// to hold on to a raw pointer.
const raw_ptr<WindowsSystemProxyResolverImpl> parent_;
GetProxyForUrlCallback callback_;
HINTERNET resolver_handle_;
scoped_refptr<base::SequencedTaskRunner> sequenced_task_runner_;
SEQUENCE_CHECKER(sequence_checker_);
};
WindowsSystemProxyResolverImpl::Request::Request(
WindowsSystemProxyResolverImpl* parent,
GetProxyForUrlCallback callback)
: parent_(parent),
callback_(std::move(callback)),
resolver_handle_(nullptr),
sequenced_task_runner_(base::SequencedTaskRunner::GetCurrentDefault()) {}
WindowsSystemProxyResolverImpl::Request::~Request() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// If the `resolver_handle_` is not null, then there may be an in-flight
// WinHttp call. This could conceivably happen in some shutdown scenarios. In
// this case, close the handle to cancel the operation.
if (resolver_handle_) {
winhttp_api_wrapper()->CallWinHttpCloseHandle(resolver_handle_);
resolver_handle_ = nullptr;
}
}
bool WindowsSystemProxyResolverImpl::Request::Start(const GURL& url) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(https://crbug.com/1032820): Use better/distinct net errors.
net::WinHttpStatus winhttp_status = parent_->EnsureInitialized();
if (winhttp_status != net::WinHttpStatus::kOk) {
const int error = GetLastError();
ReportResult(net::ProxyList(), winhttp_status, error);
return false;
}
// Fetch the current system proxy settings. These are per current network
// interface and per current user.
ScopedIEConfig scoped_ie_config;
if (!winhttp_api_wrapper()->CallWinHttpGetIEProxyConfigForCurrentUser(
scoped_ie_config.config())) {
const int error = GetLastError();
ReportResult(
net::ProxyList(),
net::WinHttpStatus::kWinHttpGetIEProxyConfigForCurrentUserFailed,
error);
return false;
}
// This will create a handle specifically for WinHttpGetProxyForUrlEx().
if (!winhttp_api_wrapper()->CallWinHttpCreateProxyResolver(
&resolver_handle_)) {
const int error = GetLastError();
ReportResult(net::ProxyList(),
net::WinHttpStatus::kWinHttpCreateProxyResolverFailed, error);
return false;
}
// WinHttp will do all necessary proxy resolution fallback for this request.
// If automatic settings aren't configured or fail, it'll use any manually
// configured proxies on the machine. The WINHTTP_AUTOPROXY_ALLOW_STATIC flag
// tells the APIs to pick up manually configured proxies.
//
// Separately, Windows allows different proxy settings for different network
// interfaces. The WINHTTP_AUTOPROXY_ALLOW_CM flag tells WinHttp to
// differentiate between these settings and to get the proxy that's most
// specific to the current interface.
WINHTTP_AUTOPROXY_OPTIONS autoproxy_options = {0};
autoproxy_options.dwFlags =
WINHTTP_AUTOPROXY_ALLOW_STATIC | WINHTTP_AUTOPROXY_ALLOW_CM;
// The fAutoLogonIfChallenged option has been deprecated and should always be
// set to FALSE throughout Windows 10. Even in earlier versions of the OS,
// this feature did not work particularly well.
// https://support.microsoft.com/en-us/help/3161949/ms16-077-description-of-the-security-update-for-wpad-june-14-2016
autoproxy_options.fAutoLogonIfChallenged = FALSE;
// Sets a specific PAC URL if there was one in the IE configs.
if (scoped_ie_config.config()->lpszAutoConfigUrl) {
autoproxy_options.dwFlags |= WINHTTP_AUTOPROXY_CONFIG_URL;
autoproxy_options.lpszAutoConfigUrl =
scoped_ie_config.config()->lpszAutoConfigUrl;
}
// Similarly, allow WPAD if it was enabled in the IE configs.
if (!!scoped_ie_config.config()->fAutoDetect) {
autoproxy_options.dwFlags |= WINHTTP_AUTOPROXY_AUTO_DETECT;
// Enable WPAD using both DNS and DHCP, since that is what idiomatic Windows
// applications do.
autoproxy_options.dwAutoDetectFlags |= WINHTTP_AUTO_DETECT_TYPE_DNS_A;
autoproxy_options.dwAutoDetectFlags |= WINHTTP_AUTO_DETECT_TYPE_DHCP;
}
// Now that everything is set-up, ask WinHTTP to get the actual proxy list.
const DWORD_PTR context = reinterpret_cast<DWORD_PTR>(this);
if (!winhttp_api_wrapper()->CallWinHttpGetProxyForUrlEx(
resolver_handle_, url.spec(), &autoproxy_options, context)) {
const int error = GetLastError();
winhttp_api_wrapper()->CallWinHttpCloseHandle(resolver_handle_);
resolver_handle_ = nullptr;
ReportResult(net::ProxyList(),
net::WinHttpStatus::kWinHttpGetProxyForURLExFailed, error);
return false;
}
return true;
}
void WindowsSystemProxyResolverImpl::Request::DoWinHttpStatusCallback(
HINTERNET resolver_handle,
DWORD status,
int windows_error) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// The `resolver_handle` should correspond to `resolver_handle_`.
DCHECK_EQ(resolver_handle_, resolver_handle);
// There is no work to do if this request has been cancelled.
if (callback_) {
switch (status) {
case WINHTTP_CALLBACK_STATUS_GETPROXYFORURL_COMPLETE:
GetProxyResultForCallback();
break;
case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR:
// TODO(https://crbug.com/1032820): Use a better/distinct net error.
ReportResult(net::ProxyList(),
net::WinHttpStatus::kStatusCallbackFailed, windows_error);
break;
default:
LOG(WARNING) << "DoWinHttpStatusCallback() expects only callbacks for "
"WINHTTP_CALLBACK_STATUS_GETPROXYFORURL_COMPLETE and "
"WINHTTP_CALLBACK_STATUS_REQUEST_ERROR, not: "
<< status;
ReportResult(net::ProxyList(),
net::WinHttpStatus::kStatusCallbackFailed, status);
break;
}
}
// The HINTERNET `resolver_handle_` for this attempt at getting a proxy is no
// longer needed.
winhttp_api_wrapper()->CallWinHttpCloseHandle(resolver_handle_);
resolver_handle_ = nullptr;
// At this point, the mojo callback has definitely been called.
DCHECK(callback_.is_null());
// Now, it's finally safe to delete this object.
auto it = parent_->requests_.find(this);
DCHECK(it != parent_->requests_.end());
parent_->requests_.erase(it);
// DO NOT ADD ANYTHING BELOW THIS LINE, THE OBJECT HAS NOW BEEN DESTROYED.
}
void WindowsSystemProxyResolverImpl::Request::GetProxyResultForCallback() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(https://crbug.com/1032820): Use better/distinct net errors.
WINHTTP_PROXY_RESULT proxy_result = {0};
if (!winhttp_api_wrapper()->CallWinHttpGetProxyResult(resolver_handle_,
&proxy_result)) {
const int error = GetLastError();
ReportResult(net::ProxyList(),
net::WinHttpStatus::kWinHttpGetProxyResultFailed, error);
return;
}
// Translate the results for ProxyInfo.
net::ProxyList proxy_list;
for (DWORD i = 0u; i < proxy_result.cEntries; ++i) {
net::ProxyChain proxy_chain;
if (GetProxyChainFromWinHttpResultEntry(proxy_result.pEntries[i],
&proxy_chain)) {
proxy_list.AddProxyChain(proxy_chain);
}
}
// The `proxy_result` must be freed.
winhttp_api_wrapper()->CallWinHttpFreeProxyResult(&proxy_result);
// The consumer of this proxy resolution may not understand an empty proxy
// list. Thus, this case is considered an error.
net::WinHttpStatus winhttp_status = proxy_list.IsEmpty()
? net::WinHttpStatus::kEmptyProxyList
: net::WinHttpStatus::kOk;
ReportResult(proxy_list, winhttp_status, 0);
}
void WindowsSystemProxyResolverImpl::Request::ReportResult(
const net::ProxyList& proxy_list,
net::WinHttpStatus winhttp_status,
int windows_error) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!callback_)
return;
std::move(callback_).Run(proxy_list, winhttp_status, windows_error);
callback_.Reset();
// Even though there are no more mojo calls to make, it is not safe to delete
// this object yet. To be absolutely sure that WinHttpStatusCallback() does
// not attempt to dereference a freed object, the only times we delete this
// object are when we fail to call into WinHttp APIs completely (i.e. we do
// not expect a callback) and when the WinHttp callback completes in
// DoWinHttpStatusCallback().
}
WindowsSystemProxyResolverImpl::WindowsSystemProxyResolverImpl(
mojo::PendingReceiver<mojom::WindowsSystemProxyResolver> receiver)
: receiver_(this, std::move(receiver)) {}
WindowsSystemProxyResolverImpl::~WindowsSystemProxyResolverImpl() {
// The WindowsSystemProxyResolverImpl must outlive every Request it owns.
CHECK(requests_.empty());
}
void WindowsSystemProxyResolverImpl::SetCreateWinHttpAPIWrapperForTesting(
std::unique_ptr<WinHttpAPIWrapper> winhttp_api_wrapper_for_testing) {
winhttp_api_wrapper_ = std::move(winhttp_api_wrapper_for_testing);
}
void WindowsSystemProxyResolverImpl::GetProxyForUrl(
const GURL& url,
GetProxyForUrlCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::unique_ptr<Request> request =
std::make_unique<Request>(this, std::move(callback));
// If the request fails to start, it will internally report that to
// `callback`. After that, it's safe to delete this `request`.
if (request->Start(url))
requests_.insert(std::move(request));
}
net::WinHttpStatus WindowsSystemProxyResolverImpl::EnsureInitialized() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (initialized_)
return net::WinHttpStatus::kOk;
// TODO(https://crbug.com/1032820): Limit the number of times this can
// fail to initialize.
// The `winhttp_api_wrapper_` is intended to only get set when initialization
// is successful. However, it may have been pre-populated via
// SetCreateWinHttpAPIWrapperForTesting(). In those cases, use that object
// instead of creating a new one.
std::unique_ptr<WinHttpAPIWrapper> uninitialized_winhttp_api_wrapper;
if (winhttp_api_wrapper_) {
uninitialized_winhttp_api_wrapper = std::move(winhttp_api_wrapper_);
winhttp_api_wrapper_.reset();
} else {
uninitialized_winhttp_api_wrapper =
std::make_unique<WinHttpAPIWrapperImpl>();
}
if (!uninitialized_winhttp_api_wrapper->CallWinHttpOpen())
return net::WinHttpStatus::kWinHttpOpenFailed;
// 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.
if (!uninitialized_winhttp_api_wrapper->CallWinHttpSetTimeouts(10000, 10000,
5000, 5000)) {
return net::WinHttpStatus::kWinHttpSetTimeoutsFailed;
}
// This sets the entry point for every callback in the WinHttp session created
// above.
if (!uninitialized_winhttp_api_wrapper->CallWinHttpSetStatusCallback(
&WindowsSystemProxyResolverImpl::WinHttpStatusCallback)) {
return net::WinHttpStatus::kWinHttpSetStatusCallbackFailed;
}
initialized_ = true;
winhttp_api_wrapper_ = std::move(uninitialized_winhttp_api_wrapper);
return net::WinHttpStatus::kOk;
}
// static
void CALLBACK
WindowsSystemProxyResolverImpl::WinHttpStatusCallback(HINTERNET resolver_handle,
DWORD_PTR context,
DWORD status,
void* info,
DWORD info_len) {
DCHECK(resolver_handle);
DCHECK(context);
Request* request = reinterpret_cast<Request*>(context);
// Make a copy of any error information in `info` so it can be accessed from
// the subsequently posted task. The `info` pointer's lifetime is managed by
// WinHTTP and hence is not valid once this frame returns.
int windows_error = S_OK;
if (info && status == WINHTTP_CALLBACK_STATUS_REQUEST_ERROR) {
WINHTTP_ASYNC_RESULT* result = static_cast<WINHTTP_ASYNC_RESULT*>(info);
windows_error = result->dwError;
}
request->sequenced_task_runner()->PostTask(
FROM_HERE, base::BindOnce(&Request::DoWinHttpStatusCallback,
base::Unretained(request), resolver_handle,
status, windows_error));
}
} // namespace proxy_resolver_win