/////////////////////////////////////////////////////////////////////////////// // 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(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(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(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