Files
wxWidgets/src/msw/webrequest_winhttp.cpp
Vadim Zeitlin e5bd5a926c Move backend-specific wxWebRequest headers to private subdirs
There is no need to make these headers public and keeping them private
will allow making backwards-incompatible changes to them in the future.
2020-12-26 17:00:07 +01:00

539 lines
15 KiB
C++

///////////////////////////////////////////////////////////////////////////////
// Name: wx/msw/webrequest_winhttp.h
// Purpose: wxWebRequest WinHTTP implementation
// Author: Tobias Taschner
// Created: 2018-10-17
// Copyright: (c) 2018 wxWidgets development team
// Licence: wxWindows licence
///////////////////////////////////////////////////////////////////////////////
// For compilers that support precompilation, includes "wx.h".
#include "wx/wxprec.h"
#include "wx/webrequest.h"
#if wxUSE_WEBREQUEST_WINHTTP
#include "wx/mstream.h"
#include "wx/uri.h"
#include "wx/msw/private/webrequest_winhttp.h"
#ifndef WX_PRECOMP
#include "wx/log.h"
#include "wx/utils.h"
#include "wx/translation.h"
#endif
// For MSVC we can link in the required library explicitly, for the other
// compilers (e.g. MinGW) this needs to be done at makefiles level.
#ifdef __VISUALC__
#pragma comment(lib, "winhttp")
#endif
// Define constants potentially missing in old SDKs
#ifndef WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY
#define WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY 4
#endif
#ifndef WINHTTP_PROTOCOL_FLAG_HTTP2
#define WINHTTP_PROTOCOL_FLAG_HTTP2 0x1
#endif
#ifndef WINHTTP_OPTION_ENABLE_HTTP_PROTOCOL
#define WINHTTP_OPTION_ENABLE_HTTP_PROTOCOL 133
#endif
#ifndef WINHTTP_DECOMPRESSION_FLAG_ALL
#define WINHTTP_DECOMPRESSION_FLAG_GZIP 0x00000001
#define WINHTTP_DECOMPRESSION_FLAG_DEFLATE 0x00000002
#define WINHTTP_DECOMPRESSION_FLAG_ALL ( \
WINHTTP_DECOMPRESSION_FLAG_GZIP | \
WINHTTP_DECOMPRESSION_FLAG_DEFLATE)
#endif
#ifndef WINHTTP_OPTION_DECOMPRESSION
#define WINHTTP_OPTION_DECOMPRESSION 118
#endif
#ifndef WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1
#define WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1 0x00000200
#endif
#ifndef WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2
#define WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2 0x00000800
#endif
// Helper functions
static wxString wxWinHTTPErrorToString(DWORD errorCode)
{
wxString errorString;
LPVOID msgBuf;
if ( FormatMessageW(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS |
FORMAT_MESSAGE_FROM_HMODULE,
GetModuleHandle(TEXT("WINHTTP")),
errorCode,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPWSTR)&msgBuf,
0, NULL) )
{
errorString.assign((LPWSTR)msgBuf);
LocalFree(msgBuf);
// Truncate trailing \n\r
if ( errorString.size() > 2 )
errorString.resize(errorString.size());
}
return errorString;
}
static wxString wxWinHTTPQueryHeaderString(HINTERNET hRequest, DWORD dwInfoLevel,
LPCWSTR pwszName = WINHTTP_HEADER_NAME_BY_INDEX)
{
wxString result;
DWORD bufferLen = 0;
::WinHttpQueryHeaders(hRequest, dwInfoLevel, pwszName, NULL, &bufferLen,
WINHTTP_NO_HEADER_INDEX);
if ( ::GetLastError() == ERROR_INSUFFICIENT_BUFFER )
{
wxWCharBuffer resBuf(bufferLen);
if ( ::WinHttpQueryHeaders(hRequest, dwInfoLevel, pwszName,
resBuf.data(), &bufferLen,
WINHTTP_NO_HEADER_INDEX) )
{
result.assign(resBuf);
}
}
return result;
}
static wxString wxWinHTTPQueryOptionString(HINTERNET hInternet, DWORD dwOption)
{
wxString result;
DWORD bufferLen = 0;
::WinHttpQueryOption(hInternet, dwOption, NULL, &bufferLen);
if ( ::GetLastError() == ERROR_INSUFFICIENT_BUFFER )
{
wxWCharBuffer resBuf(bufferLen);
if ( ::WinHttpQueryOption(hInternet, dwOption, resBuf.data(), &bufferLen) )
result.assign(resBuf);
}
return result;
}
static void CALLBACK wxRequestStatusCallback(
HINTERNET WXUNUSED(hInternet),
DWORD_PTR dwContext,
DWORD dwInternetStatus,
LPVOID lpvStatusInformation,
DWORD dwStatusInformationLength
)
{
if ( dwContext )
{
wxWebRequestWinHTTP* request =
reinterpret_cast<wxWebRequestWinHTTP*>(dwContext);
request->HandleCallback(dwInternetStatus, lpvStatusInformation,
dwStatusInformationLength);
}
}
//
// wxWebRequestWinHTTP
//
wxWebRequestWinHTTP::wxWebRequestWinHTTP(int id, wxWebSessionWinHTTP& session, const wxString& url):
wxWebRequest(session, id),
m_url(url),
m_connect(NULL),
m_request(NULL),
m_dataWritten(0)
{
}
wxWebRequestWinHTTP::~wxWebRequestWinHTTP()
{
if ( m_request )
::WinHttpCloseHandle(m_request);
if ( m_connect )
::WinHttpCloseHandle(m_connect);
}
void wxWebRequestWinHTTP::HandleCallback(DWORD dwInternetStatus,
LPVOID lpvStatusInformation, DWORD dwStatusInformationLength)
{
switch ( dwInternetStatus )
{
case WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE:
if ( m_dataSize )
WriteData();
else
CreateResponse();
break;
case WINHTTP_CALLBACK_STATUS_READ_COMPLETE:
if ( dwStatusInformationLength > 0 )
{
if ( !m_response->ReportAvailableData(dwStatusInformationLength) &&
GetState() != State_Cancelled )
SetFailedWithLastError();
}
else
SetState(State_Completed);
break;
case WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE:
WriteData();
break;
case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR:
{
LPWINHTTP_ASYNC_RESULT asyncResult = reinterpret_cast<LPWINHTTP_ASYNC_RESULT>(lpvStatusInformation);
SetState(State_Failed, wxWinHTTPErrorToString(asyncResult->dwError));
break;
}
}
}
void wxWebRequestWinHTTP::WriteData()
{
int dataWriteSize = 8 * 1024;
if ( m_dataWritten + dataWriteSize > m_dataSize )
dataWriteSize = m_dataSize - m_dataWritten;
if ( dataWriteSize )
{
m_dataWriteBuffer.Clear();
m_dataWriteBuffer.GetWriteBuf(dataWriteSize);
m_dataStream->Read(m_dataWriteBuffer.GetData(), dataWriteSize);
if ( !::WinHttpWriteData(m_request, m_dataWriteBuffer.GetData(), dataWriteSize, NULL) )
SetFailedWithLastError();
m_dataWritten += dataWriteSize;
}
else
CreateResponse();
}
void wxWebRequestWinHTTP::CreateResponse()
{
if ( ::WinHttpReceiveResponse(m_request, NULL) )
{
m_response.reset(new wxWebResponseWinHTTP(*this));
// wxWebResponseWinHTTP ctor could have changed the state if its
// initialization failed, so check for this.
if ( GetState() == State_Failed )
return;
int status = m_response->GetStatus();
if ( status == 401 || status == 407)
{
m_authChallenge.reset(new wxWebAuthChallengeWinHTTP(
(status == 407) ? wxWebAuthChallenge::Source_Proxy : wxWebAuthChallenge::Source_Server, *this));
if ( m_authChallenge->Init() )
SetState(State_Unauthorized, m_response->GetStatusText());
else
SetFailedWithLastError();
}
else if ( CheckServerStatus() )
{
// Start reading the response
if ( !m_response->ReadData() )
SetFailedWithLastError();
}
}
else
SetFailedWithLastError();
}
void wxWebRequestWinHTTP::SetFailedWithLastError()
{
wxString failMessage = wxWinHTTPErrorToString(::GetLastError());
SetState(State_Failed, failMessage);
}
void wxWebRequestWinHTTP::Start()
{
if ( GetState() != State_Idle ) // Completed requests can not be restarted
return;
// Parse the URL
wxURI uri(m_url);
bool isSecure = uri.GetScheme().IsSameAs("HTTPS", false);
int port;
if ( !uri.HasPort() )
port = isSecure ? 443 : 80;
else
port = wxAtoi(uri.GetPort());
// Open a connection
m_connect = ::WinHttpConnect(
static_cast<wxWebSessionWinHTTP&>(GetSession()).GetHandle(),
uri.GetServer().wc_str(), port, 0);
if ( m_connect == NULL )
{
SetFailedWithLastError();
return;
}
wxString method;
if ( !m_method.empty() )
method = m_method;
else if ( m_dataSize )
method = "POST";
else
method = "GET";
wxString objectName = uri.GetPath();
if ( uri.HasQuery() )
objectName += "?" + uri.GetQuery();
// Open a request
m_request = ::WinHttpOpenRequest(m_connect,
method.wc_str(), objectName.wc_str(),
NULL, WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
(isSecure) ? WINHTTP_FLAG_SECURE : 0);
if ( m_request == NULL )
{
SetFailedWithLastError();
return;
}
// Register callback
if ( ::WinHttpSetStatusCallback(m_request,
(WINHTTP_STATUS_CALLBACK)wxRequestStatusCallback,
WINHTTP_CALLBACK_FLAG_READ_COMPLETE |
WINHTTP_CALLBACK_FLAG_WRITE_COMPLETE |
WINHTTP_CALLBACK_FLAG_SENDREQUEST_COMPLETE |
WINHTTP_CALLBACK_FLAG_REQUEST_ERROR,
0) == WINHTTP_INVALID_STATUS_CALLBACK )
{
SetFailedWithLastError();
return;
}
SendRequest();
}
void wxWebRequestWinHTTP::SendRequest()
{
// Combine all headers to a string
wxString allHeaders;
for ( wxWebRequestHeaderMap::const_iterator header = m_headers.begin(); header != m_headers.end(); ++header )
allHeaders.append(wxString::Format("%s: %s\n", header->first, header->second));
if ( m_dataSize )
m_dataWritten = 0;
// Send request
if ( ::WinHttpSendRequest(m_request,
allHeaders.wc_str(), allHeaders.length(),
NULL, 0, m_dataSize,
(DWORD_PTR)this) )
{
SetState(State_Active);
}
else
SetFailedWithLastError();
}
void wxWebRequestWinHTTP::Cancel()
{
SetState(State_Cancelled);
if ( m_request != NULL )
{
::WinHttpCloseHandle(m_request);
m_request = NULL;
}
}
wxWebResponse* wxWebRequestWinHTTP::GetResponse() const
{
return m_response.get();
}
//
// wxWebResponseWinHTTP
//
wxWebResponseWinHTTP::wxWebResponseWinHTTP(wxWebRequestWinHTTP& request):
wxWebResponse(request),
m_requestHandle(request.GetHandle())
{
wxString contentLengthStr = wxWinHTTPQueryHeaderString(m_requestHandle,
WINHTTP_QUERY_CONTENT_LENGTH);
if ( contentLengthStr.empty() ||
!contentLengthStr.ToLongLong(&m_contentLength) )
m_contentLength = -1;
Init();
}
wxString wxWebResponseWinHTTP::GetURL() const
{
return wxWinHTTPQueryOptionString(m_requestHandle, WINHTTP_OPTION_URL);
}
wxString wxWebResponseWinHTTP::GetHeader(const wxString& name) const
{
return wxWinHTTPQueryHeaderString(m_requestHandle,
WINHTTP_QUERY_CUSTOM, name.wc_str());
}
int wxWebResponseWinHTTP::GetStatus() const
{
DWORD status = 0;
DWORD statusSize = sizeof(status);
if ( !::WinHttpQueryHeaders(m_requestHandle,
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX, &status, &statusSize, 0) )
{
status = 0;
wxLogLastError("WinHttpQueryHeaders/status");
}
return status;
}
wxString wxWebResponseWinHTTP::GetStatusText() const
{
return wxWinHTTPQueryHeaderString(m_requestHandle, WINHTTP_QUERY_STATUS_TEXT);
}
bool wxWebResponseWinHTTP::ReadData()
{
if ( ::WinHttpReadData(m_requestHandle,
GetDataBuffer(m_readSize), m_readSize, NULL) )
return true;
else
return false;
}
bool wxWebResponseWinHTTP::ReportAvailableData(DWORD dataLen)
{
ReportDataReceived(dataLen);
return ReadData();
}
//
// wxWebAuthChallengeWinHTTP
//
wxWebAuthChallengeWinHTTP::wxWebAuthChallengeWinHTTP(Source source, wxWebRequestWinHTTP & request):
wxWebAuthChallenge(source),
m_request(request),
m_target(0),
m_selectedScheme(0)
{
}
bool wxWebAuthChallengeWinHTTP::Init()
{
DWORD supportedSchemes;
DWORD firstScheme;
if ( ::WinHttpQueryAuthSchemes(m_request.GetHandle(),
&supportedSchemes, &firstScheme, &m_target) )
{
if ( supportedSchemes & WINHTTP_AUTH_SCHEME_NEGOTIATE )
m_selectedScheme = WINHTTP_AUTH_SCHEME_NEGOTIATE;
else if ( supportedSchemes & WINHTTP_AUTH_SCHEME_NTLM )
m_selectedScheme = WINHTTP_AUTH_SCHEME_NTLM;
else if ( supportedSchemes & WINHTTP_AUTH_SCHEME_PASSPORT )
m_selectedScheme = WINHTTP_AUTH_SCHEME_PASSPORT;
else if ( supportedSchemes & WINHTTP_AUTH_SCHEME_DIGEST )
m_selectedScheme = WINHTTP_AUTH_SCHEME_DIGEST;
else if ( supportedSchemes & WINHTTP_AUTH_SCHEME_BASIC )
m_selectedScheme = WINHTTP_AUTH_SCHEME_BASIC;
else
m_selectedScheme = 0;
if ( m_selectedScheme )
return true;
}
return false;
}
void wxWebAuthChallengeWinHTTP::SetCredentials(const wxString& user,
const wxString& password)
{
if ( ::WinHttpSetCredentials(m_request.GetHandle(), m_target, m_selectedScheme,
user.wc_str(), password.wc_str(), NULL) )
m_request.SendRequest();
else
m_request.SetFailedWithLastError();
}
//
// wxWebSessionWinHTTP
//
wxWebSessionWinHTTP::wxWebSessionWinHTTP():
m_initialized(false),
m_handle(NULL)
{
}
wxWebSessionWinHTTP::~wxWebSessionWinHTTP()
{
if ( m_handle != INVALID_HANDLE_VALUE )
::WinHttpCloseHandle(m_handle);
}
void wxWebSessionWinHTTP::Init()
{
DWORD accessType;
if ( wxCheckOsVersion(6, 3) )
accessType = WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY;
else
accessType = WINHTTP_ACCESS_TYPE_DEFAULT_PROXY;
m_handle = ::WinHttpOpen(GetHeaders().find("User-Agent")->second.wc_str(), accessType,
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS,
WINHTTP_FLAG_ASYNC);
if ( m_handle != NULL )
{
// Try to enable HTTP/2 (available since Win 10 1607)
DWORD protFlags = WINHTTP_PROTOCOL_FLAG_HTTP2;
::WinHttpSetOption(m_handle, WINHTTP_OPTION_ENABLE_HTTP_PROTOCOL,
&protFlags, sizeof(protFlags));
// Try to enable GZIP and DEFLATE (available since Win 8.1)
DWORD decompressFlags = WINHTTP_DECOMPRESSION_FLAG_ALL;
::WinHttpSetOption(m_handle, WINHTTP_OPTION_DECOMPRESSION,
&decompressFlags, sizeof(decompressFlags));
// Try to enable modern TLS for older Windows versions
if ( !wxCheckOsVersion(6, 3) )
{
DWORD securityFlags = WINHTTP_FLAG_SECURE_PROTOCOL_SSL3 |
WINHTTP_FLAG_SECURE_PROTOCOL_TLS1 |
WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1 |
WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2;
::WinHttpSetOption(m_handle, WINHTTP_OPTION_SECURE_PROTOCOLS,
&securityFlags, sizeof(securityFlags));
}
}
else
wxLogLastError("WinHttpOpen");
m_initialized = true;
}
wxWebRequest* wxWebSessionWinHTTP::CreateRequest(const wxString& url, int id)
{
if ( !m_initialized )
Init();
return new wxWebRequestWinHTTP(id, *this, url);
}
wxVersionInfo wxWebSessionWinHTTP::GetLibraryVersionInfo()
{
int verMaj, verMin, verMicro;
wxGetOsVersion(&verMaj, &verMin, &verMicro);
return wxVersionInfo("WinHTTP", verMaj, verMin, verMicro);
}
#endif // wxUSE_WEBREQUEST_WINHTTP