/////////////////////////////////////////////////////////////////////////////// // Name: src/common/webrequest_curl.h // Purpose: wxWebRequest implementation using libcurl // Author: Tobias Taschner // Created: 2018-10-25 // 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_CURL #include "wx/private/webrequest_curl.h" #ifndef WX_PRECOMP #include "wx/log.h" #include "wx/translation.h" #include "wx/utils.h" #endif #include "wx/uri.h" #include "wx/socket.h" #include "wx/evtloop.h" #ifdef __WINDOWS__ #include "wx/hashset.h" #include "wx/msw/wrapwin.h" #else #include "wx/evtloopsrc.h" #include "wx/evtloop.h" #endif // Define symbols that might be missing from older libcurl headers #ifndef CURL_AT_LEAST_VERSION #define CURL_VERSION_BITS(x,y,z) ((x)<<16|(y)<<8|z) #define CURL_AT_LEAST_VERSION(x,y,z) \ (LIBCURL_VERSION_NUM >= CURL_VERSION_BITS(x, y, z)) #endif // The new name was introduced in curl 7.21.6. #ifndef CURLOPT_ACCEPT_ENCODING #define CURLOPT_ACCEPT_ENCODING CURLOPT_ENCODING #endif // // wxWebResponseCURL // static size_t wxCURLWriteData(void* buffer, size_t size, size_t nmemb, void* userdata) { wxCHECK_MSG( userdata, 0, "invalid curl write callback data" ); return static_cast(userdata)->CURLOnWrite(buffer, size * nmemb); } static size_t wxCURLHeader(char *buffer, size_t size, size_t nitems, void *userdata) { wxCHECK_MSG( userdata, 0, "invalid curl header callback data" ); return static_cast(userdata)->CURLOnHeader(buffer, size * nitems); } int wxCURLXferInfo(void* clientp, curl_off_t dltotal, curl_off_t WXUNUSED(dlnow), curl_off_t WXUNUSED(ultotal), curl_off_t WXUNUSED(ulnow)) { wxCHECK_MSG( clientp, 0, "invalid curl progress callback data" ); wxWebResponseCURL* response = reinterpret_cast(clientp); return response->CURLOnProgress(dltotal); } int wxCURLProgress(void* clientp, double dltotal, double dlnow, double ultotal, double ulnow) { return wxCURLXferInfo(clientp, static_cast(dltotal), static_cast(dlnow), static_cast(ultotal), static_cast(ulnow)); } wxWebResponseCURL::wxWebResponseCURL(wxWebRequestCURL& request) : wxWebResponseImpl(request) { m_knownDownloadSize = 0; curl_easy_setopt(GetHandle(), CURLOPT_WRITEDATA, static_cast(this)); curl_easy_setopt(GetHandle(), CURLOPT_HEADERDATA, static_cast(this)); // Set the progress callback. #if CURL_AT_LEAST_VERSION(7, 32, 0) if ( wxWebSessionCURL::CurlRuntimeAtLeastVersion(7, 32, 0) ) { curl_easy_setopt(GetHandle(), CURLOPT_XFERINFOFUNCTION, wxCURLXferInfo); curl_easy_setopt(GetHandle(), CURLOPT_XFERINFODATA, static_cast(this)); } else #endif { curl_easy_setopt(GetHandle(), CURLOPT_PROGRESSFUNCTION, wxCURLProgress); curl_easy_setopt(GetHandle(), CURLOPT_PROGRESSDATA, static_cast(this)); } // Have curl call the progress callback. curl_easy_setopt(GetHandle(), CURLOPT_NOPROGRESS, 0L); Init(); } size_t wxWebResponseCURL::CURLOnWrite(void* buffer, size_t size) { void* buf = GetDataBuffer(size); memcpy(buf, buffer, size); ReportDataReceived(size); return size; } size_t wxWebResponseCURL::CURLOnHeader(const char * buffer, size_t size) { // HTTP headers are supposed to only contain ASCII data, so any encoding // should work here, but use Latin-1 for compatibility with some servers // that send it directly and to at least avoid losing data entirely when // the current encoding is UTF-8 but the input doesn't decode correctly. wxString hdr = wxString::From8BitData(buffer, size); hdr.Trim(); if ( hdr.StartsWith("HTTP/") ) { // First line of the headers contains status text after // version and status m_statusText = hdr.AfterFirst(' ').AfterFirst(' '); m_headers.clear(); } else if ( !hdr.empty() ) { wxString hdrValue; wxString hdrName = hdr.BeforeFirst(':', &hdrValue).Strip(wxString::trailing); hdrName.MakeUpper(); m_headers[hdrName] = hdrValue.Strip(wxString::leading); } return size; } int wxWebResponseCURL::CURLOnProgress(curl_off_t total) { if ( m_knownDownloadSize != total ) { if ( m_request.GetStorage() == wxWebRequest::Storage_Memory ) { PreAllocBuffer(static_cast(total)); } m_knownDownloadSize = total; } return 0; } wxFileOffset wxWebResponseCURL::GetContentLength() const { #if CURL_AT_LEAST_VERSION(7, 55, 0) curl_off_t len = 0; curl_easy_getinfo(GetHandle(), CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &len); return len; #else double len = 0; curl_easy_getinfo(GetHandle(), CURLINFO_CONTENT_LENGTH_DOWNLOAD, &len); return (wxFileOffset)len; #endif } wxString wxWebResponseCURL::GetURL() const { char* urlp = NULL; curl_easy_getinfo(GetHandle(), CURLINFO_EFFECTIVE_URL, &urlp); // While URLs should contain ASCII characters only as per // https://tools.ietf.org/html/rfc3986#section-2 we still want to avoid // losing data if they somehow contain something else but are not in UTF-8 // by interpreting it as Latin-1. return wxString::From8BitData(urlp); } wxString wxWebResponseCURL::GetHeader(const wxString& name) const { wxWebRequestHeaderMap::const_iterator it = m_headers.find(name.Upper()); if ( it != m_headers.end() ) return it->second; else return wxString(); } int wxWebResponseCURL::GetStatus() const { long status = 0; curl_easy_getinfo(GetHandle(), CURLINFO_RESPONSE_CODE, &status); return status; } // // wxWebRequestCURL // static size_t wxCURLRead(char *buffer, size_t size, size_t nitems, void *userdata) { wxCHECK_MSG( userdata, 0, "invalid curl read callback data" ); return static_cast(userdata)->CURLOnRead(buffer, size * nitems); } wxWebRequestCURL::wxWebRequestCURL(wxWebSession & session, wxWebSessionCURL& sessionImpl, wxEvtHandler* handler, const wxString & url, int id): wxWebRequestImpl(session, sessionImpl, handler, id), m_sessionImpl(sessionImpl) { m_headerList = NULL; m_handle = curl_easy_init(); if ( !m_handle ) { wxStrlcpy(m_errorBuffer, "libcurl initialization failed", CURL_ERROR_SIZE); return; } // Set error buffer to get more detailed CURL status m_errorBuffer[0] = '\0'; curl_easy_setopt(m_handle, CURLOPT_ERRORBUFFER, m_errorBuffer); // Set URL to handle: note that we must use wxURI to escape characters not // allowed in the URLs correctly (URL API is only available in libcurl // since the relatively recent v7.62.0, so we don't want to rely on it). curl_easy_setopt(m_handle, CURLOPT_URL, wxURI(url).BuildURI().utf8_str().data()); // Set callback functions curl_easy_setopt(m_handle, CURLOPT_WRITEFUNCTION, wxCURLWriteData); curl_easy_setopt(m_handle, CURLOPT_HEADERFUNCTION, wxCURLHeader); curl_easy_setopt(m_handle, CURLOPT_READFUNCTION, wxCURLRead); curl_easy_setopt(m_handle, CURLOPT_READDATA, static_cast(this)); // Enable gzip, etc decompression curl_easy_setopt(m_handle, CURLOPT_ACCEPT_ENCODING, ""); // Enable redirection handling curl_easy_setopt(m_handle, CURLOPT_FOLLOWLOCATION, 1L); // Limit redirect to HTTP curl_easy_setopt(m_handle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); // Enable all supported authentication methods curl_easy_setopt(m_handle, CURLOPT_HTTPAUTH, CURLAUTH_ANY); curl_easy_setopt(m_handle, CURLOPT_PROXYAUTH, CURLAUTH_ANY); } wxWebRequestCURL::~wxWebRequestCURL() { DestroyHeaderList(); m_sessionImpl.RequestHasTerminated(this); } void wxWebRequestCURL::Start() { m_response.reset(new wxWebResponseCURL(*this)); if ( m_dataSize ) { if ( m_method.empty() || m_method.CmpNoCase("POST") == 0 ) { curl_easy_setopt(m_handle, CURLOPT_POSTFIELDSIZE_LARGE, static_cast(m_dataSize)); curl_easy_setopt(m_handle, CURLOPT_POST, 1L); } else if ( m_method.CmpNoCase("PUT") == 0 ) { curl_easy_setopt(m_handle, CURLOPT_UPLOAD, 1L); curl_easy_setopt(m_handle, CURLOPT_INFILESIZE_LARGE, static_cast(m_dataSize)); } else { wxFAIL_MSG(wxString::Format( "Supplied data is ignored when using method %s", m_method )); } } if ( m_method.CmpNoCase("HEAD") == 0 ) { curl_easy_setopt(m_handle, CURLOPT_NOBODY, 1L); } else if ( !m_method.empty() ) { curl_easy_setopt(m_handle, CURLOPT_CUSTOMREQUEST, static_cast(m_method.mb_str())); } for ( wxWebRequestHeaderMap::const_iterator it = m_headers.begin(); it != m_headers.end(); ++it ) { // TODO: We need to implement RFC 2047 encoding here instead of blindly // sending UTF-8 which is against the standard. wxString hdrStr = wxString::Format("%s: %s", it->first, it->second); m_headerList = curl_slist_append(m_headerList, hdrStr.utf8_str()); } curl_easy_setopt(m_handle, CURLOPT_HTTPHEADER, m_headerList); if ( IsPeerVerifyDisabled() ) curl_easy_setopt(m_handle, CURLOPT_SSL_VERIFYPEER, 0); StartRequest(); } bool wxWebRequestCURL::StartRequest() { m_bytesSent = 0; if ( !m_sessionImpl.StartRequest(*this) ) { SetState(wxWebRequest::State_Failed); return false; } return true; } void wxWebRequestCURL::DoCancel() { m_sessionImpl.CancelRequest(this); } void wxWebRequestCURL::HandleCompletion() { int status = m_response ? m_response->GetStatus() : 0; if ( status == 0 ) { SetState(wxWebRequest::State_Failed, GetError()); } else if ( status == 401 || status == 407 ) { m_authChallenge.reset(new wxWebAuthChallengeCURL( (status == 407) ? wxWebAuthChallenge::Source_Proxy : wxWebAuthChallenge::Source_Server, *this)); SetState(wxWebRequest::State_Unauthorized, m_response->GetStatusText()); } else { SetFinalStateFromStatus(); } } wxString wxWebRequestCURL::GetError() const { // We don't know what encoding is used for libcurl errors, so do whatever // is needed in order to interpret this data at least somehow. return wxString(m_errorBuffer, wxConvWhateverWorks); } size_t wxWebRequestCURL::CURLOnRead(char* buffer, size_t size) { if ( m_dataStream ) { m_dataStream->Read(buffer, size); size_t readSize = m_dataStream->LastRead(); m_bytesSent += readSize; return readSize; } else return 0; } void wxWebRequestCURL::DestroyHeaderList() { if ( m_headerList ) { curl_slist_free_all(m_headerList); m_headerList = NULL; } } wxFileOffset wxWebRequestCURL::GetBytesSent() const { return m_bytesSent; } wxFileOffset wxWebRequestCURL::GetBytesExpectedToSend() const { return m_dataSize; } // // wxWebAuthChallengeCURL // wxWebAuthChallengeCURL::wxWebAuthChallengeCURL(wxWebAuthChallenge::Source source, wxWebRequestCURL& request) : wxWebAuthChallengeImpl(source), m_request(request) { } void wxWebAuthChallengeCURL::SetCredentials(const wxWebCredentials& cred) { const wxSecretString authStr = wxString::Format ( "%s:%s", cred.GetUser(), static_cast(wxSecretString(cred.GetPassword())) ); curl_easy_setopt(m_request.GetHandle(), (GetSource() == wxWebAuthChallenge::Source_Proxy) ? CURLOPT_PROXYUSERPWD : CURLOPT_USERPWD, authStr.utf8_str().data()); m_request.StartRequest(); } // // SocketPoller - a helper class for wxWebSessionCURL // wxDECLARE_EVENT(wxEVT_SOCKET_POLLER_RESULT, wxThreadEvent); class SocketPollerImpl; class SocketPoller { public: enum PollAction { INVALID_ACTION = 0x00, POLL_FOR_READ = 0x01, POLL_FOR_WRITE = 0x02 }; enum Result { INVALID_RESULT = 0x00, READY_FOR_READ = 0x01, READY_FOR_WRITE = 0x02, HAS_ERROR = 0x04 }; SocketPoller(wxEvtHandler*); ~SocketPoller(); bool StartPolling(wxSOCKET_T, int); void StopPolling(wxSOCKET_T); void ResumePolling(wxSOCKET_T); private: SocketPollerImpl* m_impl; }; wxDEFINE_EVENT(wxEVT_SOCKET_POLLER_RESULT, wxThreadEvent); class SocketPollerImpl { public: virtual ~SocketPollerImpl(){}; virtual bool StartPolling(wxSOCKET_T, int) = 0; virtual void StopPolling(wxSOCKET_T) = 0; virtual void ResumePolling(wxSOCKET_T) = 0; static SocketPollerImpl* Create(wxEvtHandler*); }; SocketPoller::SocketPoller(wxEvtHandler* hndlr) { m_impl = SocketPollerImpl::Create(hndlr); } SocketPoller::~SocketPoller() { delete m_impl; } bool SocketPoller::StartPolling(wxSOCKET_T sock, int pollAction) { return m_impl->StartPolling(sock, pollAction); } void SocketPoller::StopPolling(wxSOCKET_T sock) { m_impl->StopPolling(sock); } void SocketPoller::ResumePolling(wxSOCKET_T sock) { m_impl->ResumePolling(sock); } #ifdef __WINDOWS__ class WinSock1SocketPoller: public SocketPollerImpl { public: WinSock1SocketPoller(wxEvtHandler*); virtual ~WinSock1SocketPoller(); virtual bool StartPolling(wxSOCKET_T, int) wxOVERRIDE; virtual void StopPolling(wxSOCKET_T) wxOVERRIDE; virtual void ResumePolling(wxSOCKET_T) wxOVERRIDE; private: static LRESULT CALLBACK MsgProc(HWND hwnd, WXUINT uMsg, WXWPARAM wParam, WXLPARAM lParam); static const WXUINT SOCKET_MESSAGE; WX_DECLARE_HASH_SET(wxSOCKET_T, wxIntegerHash, wxIntegerEqual, SocketSet); SocketSet m_polledSockets; WXHWND m_hwnd; }; const WXUINT WinSock1SocketPoller::SOCKET_MESSAGE = WM_USER + 1; WinSock1SocketPoller::WinSock1SocketPoller(wxEvtHandler* hndlr) { // Initialize winsock in case it's not already done. WORD wVersionRequested = MAKEWORD(1,1); WSADATA wsaData; WSAStartup(wVersionRequested, &wsaData); // Create a dummy message only window. m_hwnd = CreateWindowEx( 0, //DWORD dwExStyle, TEXT("STATIC"), //LPCSTR lpClassName, NULL, //LPCSTR lpWindowName, 0, //DWORD dwStyle, 0, //int X, 0, //int Y, 0, //int nWidth, 0, //int nHeight, HWND_MESSAGE, //HWND hWndParent, NULL, //HMENU hMenu, NULL, //HINSTANCE hInstance, NULL //LPVOID lpParam ); if ( m_hwnd == NULL ) { wxLogError("Unable to create message window for WinSock1SocketPoller"); return; } // Set the event handler to be the message window's user data. Also set the // message window to use our MsgProc to process messages it receives. SetWindowLongPtr(m_hwnd, GWLP_USERDATA, reinterpret_cast(hndlr)); SetWindowLongPtr(m_hwnd, GWLP_WNDPROC, reinterpret_cast(WinSock1SocketPoller::MsgProc)); } WinSock1SocketPoller::~WinSock1SocketPoller() { // Stop monitoring any leftover sockets. for ( SocketSet::iterator it = m_polledSockets.begin() ; it != m_polledSockets.end() ; ++it ) { WSAAsyncSelect(*it, m_hwnd, 0, 0); } // Close the message window. if ( m_hwnd ) { CloseWindow(m_hwnd); } // Cleanup winsock. WSACleanup(); } bool WinSock1SocketPoller::StartPolling(wxSOCKET_T sock, int pollAction) { StopPolling(sock); // Convert pollAction to a flag that can be used by winsock. int winActions = 0; if ( pollAction & SocketPoller::POLL_FOR_READ ) { winActions |= FD_READ; } if ( pollAction & SocketPoller::POLL_FOR_WRITE ) { winActions |= FD_WRITE; } // Have winsock send a message to our window whenever activity is // detected on the socket. WSAAsyncSelect(sock, m_hwnd, SOCKET_MESSAGE, winActions); m_polledSockets.insert(sock); return true; } void WinSock1SocketPoller::StopPolling(wxSOCKET_T sock) { SocketSet::iterator it = m_polledSockets.find(sock); if ( it != m_polledSockets.end() ) { // Stop sending messages when there is activity on the socket. WSAAsyncSelect(sock, m_hwnd, 0, 0); m_polledSockets.erase(it); } } void WinSock1SocketPoller::ResumePolling(wxSOCKET_T WXUNUSED(sock)) { } LRESULT CALLBACK WinSock1SocketPoller::MsgProc(WXHWND hwnd, WXUINT uMsg, WXWPARAM wParam, WXLPARAM lParam) { // We only handle 1 message - the message we told winsock to send when // it notices activity on sockets we are monitoring. if ( uMsg == SOCKET_MESSAGE ) { // Extract the result any any errors from lParam. int winResult = LOWORD(lParam); int error = HIWORD(lParam); // Convert the result/errors to a SocketPoller::Result flag. int pollResult = 0; if ( winResult & FD_READ ) { pollResult |= SocketPoller::READY_FOR_READ; } if ( winResult & FD_WRITE ) { pollResult |= SocketPoller::READY_FOR_WRITE; } if ( error != 0 ) { pollResult |= SocketPoller::HAS_ERROR; } // If there is a significant result, send an event. if ( pollResult != 0 ) { // The event handler is stored in the window's user data and the // socket with activity is given by wParam. LONG_PTR userData = GetWindowLongPtr(hwnd, GWLP_USERDATA); wxEvtHandler* hndlr = reinterpret_cast(userData); wxSOCKET_T sock = wParam; wxThreadEvent* event = new wxThreadEvent(wxEVT_SOCKET_POLLER_RESULT); event->SetPayload(sock); event->SetInt(pollResult); if ( wxThread::IsMain() ) { hndlr->ProcessEvent(*event); delete event; } else { wxQueueEvent(hndlr, event); } } return 0; } else { return DefWindowProc(hwnd, uMsg, wParam, lParam); } } SocketPollerImpl* SocketPollerImpl::Create(wxEvtHandler* hndlr) { return new WinSock1SocketPoller(hndlr); } #else // SocketPollerSourceHandler - a source handler used by the SocketPoller class. class SocketPollerSourceHandler: public wxEventLoopSourceHandler { public: SocketPollerSourceHandler(wxSOCKET_T, wxEvtHandler*); void OnReadWaiting() wxOVERRIDE; void OnWriteWaiting() wxOVERRIDE; void OnExceptionWaiting() wxOVERRIDE; ~SocketPollerSourceHandler(){} private: void SendEvent(int); wxSOCKET_T m_socket; wxEvtHandler* m_handler; }; SocketPollerSourceHandler::SocketPollerSourceHandler(wxSOCKET_T sock, wxEvtHandler* hndlr) { m_socket = sock; m_handler = hndlr; } void SocketPollerSourceHandler::OnReadWaiting() { SendEvent(SocketPoller::READY_FOR_READ); } void SocketPollerSourceHandler::OnWriteWaiting() { SendEvent(SocketPoller::READY_FOR_WRITE); } void SocketPollerSourceHandler::OnExceptionWaiting() { SendEvent(SocketPoller::HAS_ERROR); } void SocketPollerSourceHandler::SendEvent(int result) { wxThreadEvent event(wxEVT_SOCKET_POLLER_RESULT); event.SetPayload(m_socket); event.SetInt(result); m_handler->ProcessEvent(event); } // SourceSocketPoller - a SocketPollerImpl based on event loop sources. class SourceSocketPoller: public SocketPollerImpl { public: SourceSocketPoller(wxEvtHandler*); ~SourceSocketPoller(); bool StartPolling(wxSOCKET_T, int) wxOVERRIDE; void StopPolling(wxSOCKET_T) wxOVERRIDE; void ResumePolling(wxSOCKET_T) wxOVERRIDE; private: WX_DECLARE_HASH_MAP(wxSOCKET_T, wxEventLoopSource*, wxIntegerHash,\ wxIntegerEqual, SocketDataMap); void CleanUpSocketSource(wxEventLoopSource*); SocketDataMap m_socketData; wxEvtHandler* m_handler; }; SourceSocketPoller::SourceSocketPoller(wxEvtHandler* hndlr) { m_handler = hndlr; } SourceSocketPoller::~SourceSocketPoller() { // Clean up any leftover socket data. for ( SocketDataMap::iterator it = m_socketData.begin() ; it != m_socketData.end() ; ++it ) { CleanUpSocketSource(it->second); } } static int SocketPoller2EventSource(int pollAction) { // Convert the SocketPoller::PollAction value to a flag that can be used // by wxEventLoopSource. // Always check for errors. int eventSourceFlag = wxEVENT_SOURCE_EXCEPTION; if ( pollAction & SocketPoller::POLL_FOR_READ ) { eventSourceFlag |= wxEVENT_SOURCE_INPUT; } if ( pollAction & SocketPoller::POLL_FOR_WRITE ) { eventSourceFlag |= wxEVENT_SOURCE_OUTPUT; } return eventSourceFlag; } bool SourceSocketPoller::StartPolling(wxSOCKET_T sock, int pollAction) { SocketDataMap::iterator it = m_socketData.find(sock); wxEventLoopSourceHandler* srcHandler = NULL; if ( it != m_socketData.end() ) { // If this socket is already being polled, reuse the old handler. Also // delete the old source object to stop the old polling operations. wxEventLoopSource* oldSrc = it->second; srcHandler = oldSrc->GetHandler(); delete oldSrc; } else { // Otherwise create a new source handler. srcHandler = new SocketPollerSourceHandler(sock, m_handler); } // Get a new source object for these polling checks. bool socketIsPolled = true; int eventSourceFlag = SocketPoller2EventSource(pollAction); wxEventLoopSource* newSrc = wxEventLoopBase::AddSourceForFD(sock, srcHandler, eventSourceFlag); if ( newSrc == NULL ) { // We were not able to add a source for this socket. wxLogDebug(wxString::Format( "Unable to create event loop source for %d", static_cast(sock))); delete srcHandler; socketIsPolled = false; if ( it != m_socketData.end() ) { m_socketData.erase(it); } } else { m_socketData[sock] = newSrc; } return socketIsPolled; } void SourceSocketPoller::StopPolling(wxSOCKET_T sock) { SocketDataMap::iterator it = m_socketData.find(sock); if ( it != m_socketData.end() ) { CleanUpSocketSource(it->second); m_socketData.erase(it); } } void SourceSocketPoller::ResumePolling(wxSOCKET_T WXUNUSED(sock)) { } void SourceSocketPoller::CleanUpSocketSource(wxEventLoopSource* source) { wxEventLoopSourceHandler* srcHandler = source->GetHandler(); delete source; delete srcHandler; } SocketPollerImpl* SocketPollerImpl::Create(wxEvtHandler* hndlr) { return new SourceSocketPoller(hndlr); } #endif // // wxWebSessionCURL // int wxWebSessionCURL::ms_activeSessions = 0; unsigned int wxWebSessionCURL::ms_runtimeVersion = 0; wxWebSessionCURL::wxWebSessionCURL() : m_handle(NULL) { // Initialize CURL globally if no sessions are active if ( ms_activeSessions == 0 ) { if ( curl_global_init(CURL_GLOBAL_ALL) ) { wxLogError(_("libcurl could not be initialized")); } else { curl_version_info_data* data = curl_version_info(CURLVERSION_NOW); ms_runtimeVersion = data->version_num; } } ms_activeSessions++; m_socketPoller = new SocketPoller(this); m_timeoutTimer.SetOwner(this); Bind(wxEVT_TIMER, &wxWebSessionCURL::TimeoutNotification, this); Bind(wxEVT_SOCKET_POLLER_RESULT, &wxWebSessionCURL::ProcessSocketPollerResult, this); } wxWebSessionCURL::~wxWebSessionCURL() { delete m_socketPoller; if ( m_handle ) curl_multi_cleanup(m_handle); // Global CURL cleanup if this is the last session --ms_activeSessions; if ( ms_activeSessions == 0 ) curl_global_cleanup(); } wxWebRequestImplPtr wxWebSessionCURL::CreateRequest(wxWebSession& session, wxEvtHandler* handler, const wxString& url, int id) { // Allocate our handle on demand. if ( !m_handle ) { m_handle = curl_multi_init(); if ( !m_handle ) { wxLogDebug("curl_multi_init() failed"); return wxWebRequestImplPtr(); } else { curl_multi_setopt(m_handle, CURLMOPT_SOCKETDATA, this); curl_multi_setopt(m_handle, CURLMOPT_SOCKETFUNCTION, SocketCallback); curl_multi_setopt(m_handle, CURLMOPT_TIMERDATA, this); curl_multi_setopt(m_handle, CURLMOPT_TIMERFUNCTION, TimerCallback); } } return wxWebRequestImplPtr(new wxWebRequestCURL(session, *this, handler, url, id)); } bool wxWebSessionCURL::StartRequest(wxWebRequestCURL & request) { // Add request easy handle to multi handle CURL* curl = request.GetHandle(); int code = curl_multi_add_handle(m_handle, curl); if ( code == CURLM_OK ) { request.SetState(wxWebRequest::State_Active); m_activeTransfers[curl] = &request; // Report a timeout to curl to initiate this transfer. int runningHandles; curl_multi_socket_action(m_handle, CURL_SOCKET_TIMEOUT, 0, &runningHandles); return true; } else { return false; } } void wxWebSessionCURL::CancelRequest(wxWebRequestCURL* request) { CURL* curl = request->GetHandle(); TransferSet::iterator it = m_activeTransfers.find(curl); if ( it != m_activeTransfers.end() ) { m_activeTransfers.erase(it); } curl_multi_remove_handle(m_handle, curl); StopTransfer(curl); request->SetState(wxWebRequest::State_Cancelled); } void wxWebSessionCURL::RequestHasTerminated(wxWebRequestCURL* request) { CURL* curl = request->GetHandle(); TransferSet::iterator it = m_activeTransfers.find(curl); if ( it != m_activeTransfers.end() ) { // The transfer the CURL handle is performing is still in progress, but // the web request object it belongs to is being deleted. Since the // next step will call curl_easy_cleanup and any calls on the CURL // handle after cleanup are illegal, remove it from the CURLM // multihandle now. curl_multi_remove_handle(m_handle, curl); m_activeTransfers.erase(it); } curl_easy_cleanup(curl); } wxVersionInfo wxWebSessionCURL::GetLibraryVersionInfo() { const curl_version_info_data* vi = curl_version_info(CURLVERSION_NOW); wxString desc = wxString::Format("libcurl/%s", vi->version); if (vi->ssl_version[0]) desc += " " + wxString(vi->ssl_version); return wxVersionInfo("libcurl", vi->version_num >> 16 & 0xff, vi->version_num >> 8 & 0xff, vi->version_num & 0xff, desc); } bool wxWebSessionCURL::CurlRuntimeAtLeastVersion(unsigned int major, unsigned int minor, unsigned int patch) { return (ms_runtimeVersion >= CURL_VERSION_BITS(major, minor, patch)); } // curl interacts with the wxWebSessionCURL class through 2 callback functions // 1) TimerCallback is called whenever curl wants us to start or stop a timer. // 2) SocketCallback is called when curl wants us to start monitoring a socket // for activity. // // curl accomplishes the network transfers by calling the // curl_multi_socket_action function to move pieces of the transfer to or from // the system's network services. Consequently we call this function when a // timeout occurs or when activity is detected on a socket. int wxWebSessionCURL::TimerCallback(CURLM* WXUNUSED(multi), long timeoutms, void *userp) { wxWebSessionCURL* session = reinterpret_cast(userp); session->ProcessTimerCallback(timeoutms); return 0; } int wxWebSessionCURL::SocketCallback(CURL* curl, curl_socket_t sock, int what, void* userp, void* WXUNUSED(sp)) { wxWebSessionCURL* session = reinterpret_cast(userp); session->ProcessSocketCallback(curl, sock, what); return CURLM_OK; }; void wxWebSessionCURL::ProcessTimerCallback(long timeoutms) { // When this callback is called, curl wants us to start or stop a timer. // If timeoutms = -1, we should stop the timer. If timeoutms > 0, we should // start a oneshot timer and when that timer expires, we should call // curl_multi_socket_action(m_handle, CURL_SOCKET_TIMEOUT,... // // In the special case that timeoutms = 0, we should signal a timeout as // soon as possible (as above by calling curl_multi_socket_action). But // according to the curl documentation, we can't do that from this callback // or we might cause an infinite loop. So use CallAfter to report the // timeout at a slightly later time. if ( timeoutms > 0) { m_timeoutTimer.StartOnce(timeoutms); } else if ( timeoutms < 0 ) { m_timeoutTimer.Stop(); } else // timeoutms == 0 { CallAfter(&wxWebSessionCURL::ProcessTimeoutNotification); } } void wxWebSessionCURL::TimeoutNotification(wxTimerEvent& WXUNUSED(event)) { ProcessTimeoutNotification(); } void wxWebSessionCURL::ProcessTimeoutNotification() { int runningHandles; curl_multi_socket_action(m_handle, CURL_SOCKET_TIMEOUT, 0, &runningHandles); CheckForCompletedTransfers(); } static int CurlPoll2SocketPoller(int what) { int pollAction = SocketPoller::INVALID_ACTION; if ( what == CURL_POLL_IN ) { pollAction = SocketPoller::POLL_FOR_READ ; } else if ( what == CURL_POLL_OUT ) { pollAction = SocketPoller::POLL_FOR_WRITE; } else if ( what == CURL_POLL_INOUT ) { pollAction = SocketPoller::POLL_FOR_READ | SocketPoller::POLL_FOR_WRITE; } return pollAction; } void wxWebSessionCURL::ProcessSocketCallback(CURL* curl, curl_socket_t s, int what) { // Have the socket poller start or stop monitoring a socket depending of // the value of what. switch ( what ) { case CURL_POLL_IN: wxFALLTHROUGH; case CURL_POLL_OUT: wxFALLTHROUGH; case CURL_POLL_INOUT: { int pollAction = CurlPoll2SocketPoller(what); bool socketIsMonitored = m_socketPoller->StartPolling(s, pollAction); if ( !socketIsMonitored ) { TransferSet::iterator it = m_activeTransfers.find(curl); if ( it != m_activeTransfers.end() ) { FailRequest(curl, "wxWebSession failed to monitor a socket for this " "transfer"); } } } break; case CURL_POLL_REMOVE: m_socketPoller->StopPolling(s); break; default: wxLogDebug("Unknown socket action in ProcessSocketCallback"); break; } } static int SocketPollerResult2CurlSelect(int socketEventFlag) { int curlSelect = 0; if ( socketEventFlag & SocketPoller::READY_FOR_READ ) { curlSelect |= CURL_CSELECT_IN; } if ( socketEventFlag & SocketPoller::READY_FOR_WRITE ) { curlSelect |= CURL_CSELECT_OUT; } if ( socketEventFlag & SocketPoller::HAS_ERROR ) { curlSelect |= CURL_CSELECT_ERR; } return curlSelect; } void wxWebSessionCURL::ProcessSocketPollerResult(wxThreadEvent& event) { // Convert the socket poller result to an action flag needed by curl. // Then call curl_multi_socket_action. curl_socket_t sock = event.GetPayload(); int action = SocketPollerResult2CurlSelect(event.GetInt()); int runningHandles; curl_multi_socket_action(m_handle, sock, action, &runningHandles); CheckForCompletedTransfers(); m_socketPoller->ResumePolling(sock); } void wxWebSessionCURL::CheckForCompletedTransfers() { // Process CURL message queue int msgQueueCount; while ( CURLMsg* msg = curl_multi_info_read(m_handle, &msgQueueCount) ) { if ( msg->msg == CURLMSG_DONE ) { CURL* curl = msg->easy_handle; TransferSet::iterator it = m_activeTransfers.find(curl); if ( it != m_activeTransfers.end() ) { wxWebRequestCURL* request = it->second; curl_multi_remove_handle(m_handle, curl); request->HandleCompletion(); m_activeTransfers.erase(it); } } } } void wxWebSessionCURL::FailRequest(CURL* curl,const wxString& msg) { TransferSet::iterator it = m_activeTransfers.find(curl); if ( it != m_activeTransfers.end() ) { wxWebRequestCURL* request = it->second; m_activeTransfers.erase(it); curl_multi_remove_handle(m_handle, curl); StopTransfer(curl); request->SetState(wxWebRequest::State_Failed, msg); } } void wxWebSessionCURL::StopTransfer(CURL* curl) { curl_socket_t activeSocket; bool closeActiveSocket = true; bool useLastSocket = false; #if CURL_AT_LEAST_VERSION(7, 45, 0) if ( CurlRuntimeAtLeastVersion(7, 45, 0) ) { CURLcode code = curl_easy_getinfo(curl, CURLINFO_ACTIVESOCKET, &activeSocket); if ( code != CURLE_OK || activeSocket == CURL_SOCKET_BAD ) { closeActiveSocket = false; } } else { useLastSocket = true; } #else useLastSocket = true; #endif //CURL_AT_LEAST_VERSION(7, 45, 0) // CURLINFO_ACTIVESOCKET is not available either at compile time or run // time. So we must use the older CURLINFO_LASTSOCKET instead. if ( useLastSocket ) { #ifdef __WIN64__ // CURLINFO_LASTSOCKET won't work on 64 bit windows because it // uses a long to retrive the socket. However sockets will be 64 // bit values. In this case there is nothing we can do. closeActiveSocket = false; #endif //__WIN64__ if ( closeActiveSocket ) { long longSocket; CURLcode code = curl_easy_getinfo(curl, CURLINFO_LASTSOCKET, &longSocket); if ( code == CURLE_OK && longSocket!= -1 ) { activeSocket = static_cast(longSocket); } else { closeActiveSocket = false; } } } if ( closeActiveSocket ) { #ifdef __WINDOWS__ closesocket(activeSocket); #else close(activeSocket); #endif } } #endif // wxUSE_WEBREQUEST_CURL