diff options
Diffstat (limited to 'xbmc/filesystem/CurlFile.cpp')
-rw-r--r-- | xbmc/filesystem/CurlFile.cpp | 2141 |
1 files changed, 2141 insertions, 0 deletions
diff --git a/xbmc/filesystem/CurlFile.cpp b/xbmc/filesystem/CurlFile.cpp new file mode 100644 index 0000000..11a5272 --- /dev/null +++ b/xbmc/filesystem/CurlFile.cpp @@ -0,0 +1,2141 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "CurlFile.h" + +#include "File.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "filesystem/SpecialProtocol.h" +#include "network/DNSNameCache.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "threads/SystemClock.h" +#include "utils/Base64.h" +#include "utils/XTimeUtils.h" + +#include <algorithm> +#include <cassert> +#include <climits> +#include <vector> + +#ifdef TARGET_POSIX +#include <errno.h> +#include <inttypes.h> +#include "platform/posix/ConvUtils.h" +#endif + +#include "DllLibCurl.h" +#include "ShoutcastFile.h" +#include "utils/CharsetConverter.h" +#include "utils/log.h" +#include "utils/StringUtils.h" + +using namespace XFILE; +using namespace XCURL; + +using namespace std::chrono_literals; + +#define FITS_INT(a) (((a) <= INT_MAX) && ((a) >= INT_MIN)) + +curl_proxytype proxyType2CUrlProxyType[] = { + CURLPROXY_HTTP, CURLPROXY_SOCKS4, CURLPROXY_SOCKS4A, + CURLPROXY_SOCKS5, CURLPROXY_SOCKS5_HOSTNAME, CURLPROXY_HTTPS, +}; + +#define FILLBUFFER_OK 0 +#define FILLBUFFER_NO_DATA 1 +#define FILLBUFFER_FAIL 2 + +// curl calls this routine to debug +extern "C" int debug_callback(CURL_HANDLE *handle, curl_infotype info, char *output, size_t size, void *data) +{ + if (info == CURLINFO_DATA_IN || info == CURLINFO_DATA_OUT) + return 0; + + if (!CServiceBroker::GetLogging().CanLogComponent(LOGCURL)) + return 0; + + std::string strLine; + strLine.append(output, size); + std::vector<std::string> vecLines; + StringUtils::Tokenize(strLine, vecLines, "\r\n"); + std::vector<std::string>::const_iterator it = vecLines.begin(); + + const char *infotype; + switch(info) + { + case CURLINFO_TEXT : infotype = "TEXT: "; break; + case CURLINFO_HEADER_IN : infotype = "HEADER_IN: "; break; + case CURLINFO_HEADER_OUT : infotype = "HEADER_OUT: "; break; + case CURLINFO_SSL_DATA_IN : infotype = "SSL_DATA_IN: "; break; + case CURLINFO_SSL_DATA_OUT : infotype = "SSL_DATA_OUT: "; break; + case CURLINFO_END : infotype = "END: "; break; + default : infotype = ""; break; + } + + while (it != vecLines.end()) + { + CLog::Log(LOGDEBUG, "Curl::Debug - {}{}", infotype, (*it)); + ++it; + } + return 0; +} + +/* curl calls this routine to get more data */ +extern "C" size_t write_callback(char *buffer, + size_t size, + size_t nitems, + void *userp) +{ + if(userp == NULL) return 0; + + CCurlFile::CReadState *state = (CCurlFile::CReadState *)userp; + return state->WriteCallback(buffer, size, nitems); +} + +extern "C" size_t read_callback(char *buffer, + size_t size, + size_t nitems, + void *userp) +{ + if(userp == NULL) return 0; + + CCurlFile::CReadState *state = (CCurlFile::CReadState *)userp; + return state->ReadCallback(buffer, size, nitems); +} + +extern "C" size_t header_callback(void *ptr, size_t size, size_t nmemb, void *stream) +{ + CCurlFile::CReadState *state = (CCurlFile::CReadState *)stream; + return state->HeaderCallback(ptr, size, nmemb); +} + +/* used only by CCurlFile::Stat to bail out of unwanted transfers */ +extern "C" int transfer_abort_callback(void *clientp, + curl_off_t dltotal, + curl_off_t dlnow, + curl_off_t ultotal, + curl_off_t ulnow) +{ + if(dlnow > 0) + return 1; + else + return 0; +} + +/* fix for silly behavior of realloc */ +static inline void* realloc_simple(void *ptr, size_t size) +{ + void *ptr2 = realloc(ptr, size); + if(ptr && !ptr2 && size > 0) + { + free(ptr); + return NULL; + } + else + return ptr2; +} + +static constexpr int CURL_OFF = 0L; +static constexpr int CURL_ON = 1L; + +size_t CCurlFile::CReadState::HeaderCallback(void *ptr, size_t size, size_t nmemb) +{ + std::string inString; + // libcurl doc says that this info is not always \0 terminated + const char* strBuf = (const char*)ptr; + const size_t iSize = size * nmemb; + if (strBuf[iSize - 1] == 0) + inString.assign(strBuf, iSize - 1); // skip last char if it's zero + else + inString.append(strBuf, iSize); + + m_httpheader.Parse(inString); + + return iSize; +} + +size_t CCurlFile::CReadState::ReadCallback(char *buffer, size_t size, size_t nitems) +{ + if (m_fileSize == 0) + return 0; + + if (m_filePos >= m_fileSize) + { + m_isPaused = true; + return CURL_READFUNC_PAUSE; + } + + int64_t retSize = std::min(m_fileSize - m_filePos, int64_t(nitems * size)); + memcpy(buffer, m_readBuffer + m_filePos, retSize); + m_filePos += retSize; + + return retSize; +} + +size_t CCurlFile::CReadState::WriteCallback(char *buffer, size_t size, size_t nitems) +{ + unsigned int amount = size * nitems; + if (m_overflowSize) + { + // we have our overflow buffer - first get rid of as much as we can + unsigned int maxWriteable = std::min(m_buffer.getMaxWriteSize(), m_overflowSize); + if (maxWriteable) + { + if (!m_buffer.WriteData(m_overflowBuffer, maxWriteable)) + { + CLog::Log(LOGERROR, "CCurlFile::CReadState::{} - ({}) Unable to write to buffer", + __FUNCTION__, fmt::ptr(this)); + return 0; + } + + if (maxWriteable < m_overflowSize) + { + // still have some more - copy it down + memmove(m_overflowBuffer, m_overflowBuffer + maxWriteable, m_overflowSize - maxWriteable); + } + m_overflowSize -= maxWriteable; + + // Shrink memory: + m_overflowBuffer = (char*)realloc_simple(m_overflowBuffer, m_overflowSize); + } + } + // ok, now copy the data into our ring buffer + unsigned int maxWriteable = std::min(m_buffer.getMaxWriteSize(), amount); + if (maxWriteable) + { + if (!m_buffer.WriteData(buffer, maxWriteable)) + { + CLog::Log(LOGERROR, + "CCurlFile::CReadState::{} - ({}) Unable to write to buffer with {} bytes", + __FUNCTION__, fmt::ptr(this), maxWriteable); + return 0; + } + else + { + amount -= maxWriteable; + buffer += maxWriteable; + } + } + if (amount) + { + //! @todo Limit max. amount of the overflowbuffer + m_overflowBuffer = (char*)realloc_simple(m_overflowBuffer, amount + m_overflowSize); + if(m_overflowBuffer == NULL) + { + CLog::Log(LOGWARNING, + "CCurlFile::CReadState::{} - ({}) Failed to grow overflow buffer from {} bytes to " + "{} bytes", + __FUNCTION__, fmt::ptr(this), m_overflowSize, amount + m_overflowSize); + return 0; + } + memcpy(m_overflowBuffer + m_overflowSize, buffer, amount); + m_overflowSize += amount; + } + return size * nitems; +} + +CCurlFile::CReadState::CReadState() +{ + m_easyHandle = NULL; + m_multiHandle = NULL; + m_overflowBuffer = NULL; + m_overflowSize = 0; + m_stillRunning = 0; + m_filePos = 0; + m_fileSize = 0; + m_bufferSize = 0; + m_cancelled = false; + m_bFirstLoop = true; + m_sendRange = true; + m_bLastError = false; + m_readBuffer = 0; + m_isPaused = false; + m_bRetry = true; + m_curlHeaderList = NULL; + m_curlAliasList = NULL; +} + +CCurlFile::CReadState::~CReadState() +{ + Disconnect(); + + if(m_easyHandle) + g_curlInterface.easy_release(&m_easyHandle, &m_multiHandle); +} + +bool CCurlFile::CReadState::Seek(int64_t pos) +{ + if(pos == m_filePos) + return true; + + if(FITS_INT(pos - m_filePos) && m_buffer.SkipBytes((int)(pos - m_filePos))) + { + m_filePos = pos; + return true; + } + + if(pos > m_filePos && pos < m_filePos + m_bufferSize) + { + int len = m_buffer.getMaxReadSize(); + m_filePos += len; + m_buffer.SkipBytes(len); + if (FillBuffer(m_bufferSize) != FILLBUFFER_OK) + { + if(!m_buffer.SkipBytes(-len)) + CLog::Log(LOGERROR, + "CCurlFile::CReadState::{} - ({}) Failed to restore position after failed fill", + __FUNCTION__, fmt::ptr(this)); + else + m_filePos -= len; + return false; + } + + if(!FITS_INT(pos - m_filePos) || !m_buffer.SkipBytes((int)(pos - m_filePos))) + { + CLog::Log( + LOGERROR, + "CCurlFile::CReadState::{} - ({}) Failed to skip to position after having filled buffer", + __FUNCTION__, fmt::ptr(this)); + if(!m_buffer.SkipBytes(-len)) + CLog::Log(LOGERROR, + "CCurlFile::CReadState::{} - ({}) Failed to restore position after failed seek", + __FUNCTION__, fmt::ptr(this)); + else + m_filePos -= len; + return false; + } + m_filePos = pos; + return true; + } + return false; +} + +void CCurlFile::CReadState::SetResume(void) +{ + /* + * Explicitly set RANGE header when filepos=0 as some http servers require us to always send the range + * request header. If we don't the server may provide different content causing seeking to fail. + * This only affects HTTP-like items, for FTP it's a null operation. + */ + if (m_sendRange && m_filePos == 0) + g_curlInterface.easy_setopt(m_easyHandle, CURLOPT_RANGE, "0-"); + else + { + g_curlInterface.easy_setopt(m_easyHandle, CURLOPT_RANGE, NULL); + m_sendRange = false; + } + + g_curlInterface.easy_setopt(m_easyHandle, CURLOPT_RESUME_FROM_LARGE, m_filePos); +} + +long CCurlFile::CReadState::Connect(unsigned int size) +{ + if (m_filePos != 0) + CLog::Log(LOGDEBUG, "CurlFile::CReadState::{} - ({}) Resume from position {}", __FUNCTION__, + fmt::ptr(this), m_filePos); + + SetResume(); + g_curlInterface.multi_add_handle(m_multiHandle, m_easyHandle); + + m_bufferSize = size; + m_buffer.Destroy(); + m_buffer.Create(size * 3); + m_httpheader.Clear(); + + // read some data in to try and obtain the length + // maybe there's a better way to get this info?? + m_stillRunning = 1; + + // (Try to) fill buffer + if (FillBuffer(1) != FILLBUFFER_OK) + { + // Check response code + long response; + if (CURLE_OK == g_curlInterface.easy_getinfo(m_easyHandle, CURLINFO_RESPONSE_CODE, &response)) + return response; + else + return -1; + } + + double length; + if (CURLE_OK == g_curlInterface.easy_getinfo(m_easyHandle, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &length)) + { + if (length < 0) + length = 0.0; + m_fileSize = m_filePos + (int64_t)length; + } + + long response; + if (CURLE_OK == g_curlInterface.easy_getinfo(m_easyHandle, CURLINFO_RESPONSE_CODE, &response)) + return response; + + return -1; +} + +void CCurlFile::CReadState::Disconnect() +{ + if(m_multiHandle && m_easyHandle) + g_curlInterface.multi_remove_handle(m_multiHandle, m_easyHandle); + + m_buffer.Clear(); + free(m_overflowBuffer); + m_overflowBuffer = NULL; + m_overflowSize = 0; + m_filePos = 0; + m_fileSize = 0; + m_bufferSize = 0; + m_readBuffer = 0; + + /* cleanup */ + if( m_curlHeaderList ) + g_curlInterface.slist_free_all(m_curlHeaderList); + m_curlHeaderList = NULL; + + if( m_curlAliasList ) + g_curlInterface.slist_free_all(m_curlAliasList); + m_curlAliasList = NULL; +} + + +CCurlFile::~CCurlFile() +{ + Close(); + delete m_state; + delete m_oldState; +} + +CCurlFile::CCurlFile() + : m_overflowBuffer(NULL) +{ + m_opened = false; + m_forWrite = false; + m_inError = false; + m_multisession = true; + m_seekable = true; + m_connecttimeout = 0; + m_redirectlimit = 5; + m_lowspeedtime = 0; + m_ftppasvip = false; + m_bufferSize = 32768; + m_postdataset = false; + m_state = new CReadState(); + m_oldState = NULL; + m_skipshout = false; + m_httpresponse = -1; + m_acceptCharset = "UTF-8,*;q=0.8"; /* prefer UTF-8 if available */ + m_allowRetry = true; + m_acceptencoding = "all"; /* Accept all supported encoding by default */ +} + +//Has to be called before Open() +void CCurlFile::SetBufferSize(unsigned int size) +{ + m_bufferSize = size; +} + +void CCurlFile::Close() +{ + if (m_opened && m_forWrite && !m_inError) + Write(NULL, 0); + + m_state->Disconnect(); + delete m_oldState; + m_oldState = NULL; + + m_url.clear(); + m_referer.clear(); + m_cookie.clear(); + + m_opened = false; + m_forWrite = false; + m_inError = false; + + if (m_dnsCacheList) + g_curlInterface.slist_free_all(m_dnsCacheList); + m_dnsCacheList = nullptr; +} + +void CCurlFile::SetCommonOptions(CReadState* state, bool failOnError /* = true */) +{ + CURL_HANDLE* h = state->m_easyHandle; + + g_curlInterface.easy_reset(h); + + g_curlInterface.easy_setopt(h, CURLOPT_DEBUGFUNCTION, debug_callback); + + if( CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevel >= LOG_LEVEL_DEBUG ) + g_curlInterface.easy_setopt(h, CURLOPT_VERBOSE, CURL_ON); + else + g_curlInterface.easy_setopt(h, CURLOPT_VERBOSE, CURL_OFF); + + g_curlInterface.easy_setopt(h, CURLOPT_WRITEDATA, state); + g_curlInterface.easy_setopt(h, CURLOPT_WRITEFUNCTION, write_callback); + + g_curlInterface.easy_setopt(h, CURLOPT_READDATA, state); + g_curlInterface.easy_setopt(h, CURLOPT_READFUNCTION, read_callback); + + // use DNS cache + g_curlInterface.easy_setopt(h, CURLOPT_RESOLVE, m_dnsCacheList); + + // make sure headers are separated from the data stream + g_curlInterface.easy_setopt(h, CURLOPT_WRITEHEADER, state); + g_curlInterface.easy_setopt(h, CURLOPT_HEADERFUNCTION, header_callback); + g_curlInterface.easy_setopt(h, CURLOPT_HEADER, CURL_OFF); + + g_curlInterface.easy_setopt(h, CURLOPT_FTP_USE_EPSV, 0); // turn off epsv + + // Allow us to follow redirects + g_curlInterface.easy_setopt(h, CURLOPT_FOLLOWLOCATION, m_redirectlimit != 0); + g_curlInterface.easy_setopt(h, CURLOPT_MAXREDIRS, m_redirectlimit); + + // Enable cookie engine for current handle + g_curlInterface.easy_setopt(h, CURLOPT_COOKIEFILE, ""); + + // Set custom cookie if requested + if (!m_cookie.empty()) + g_curlInterface.easy_setopt(h, CURLOPT_COOKIE, m_cookie.c_str()); + + g_curlInterface.easy_setopt(h, CURLOPT_COOKIELIST, "FLUSH"); + + // When using multiple threads you should set the CURLOPT_NOSIGNAL option to + // TRUE for all handles. Everything will work fine except that timeouts are not + // honored during the DNS lookup - which you can work around by building libcurl + // with c-ares support. c-ares is a library that provides asynchronous name + // resolves. Unfortunately, c-ares does not yet support IPv6. + g_curlInterface.easy_setopt(h, CURLOPT_NOSIGNAL, CURL_ON); + + if (failOnError) + { + // not interested in failed requests + g_curlInterface.easy_setopt(h, CURLOPT_FAILONERROR, 1); + } + + // enable support for icecast / shoutcast streams + if ( NULL == state->m_curlAliasList ) + // m_curlAliasList is used only by this one place, but SetCommonOptions can + // be called multiple times, only append to list if it's empty. + state->m_curlAliasList = g_curlInterface.slist_append(state->m_curlAliasList, "ICY 200 OK"); + g_curlInterface.easy_setopt(h, CURLOPT_HTTP200ALIASES, state->m_curlAliasList); + + if (!m_verifyPeer) + g_curlInterface.easy_setopt(h, CURLOPT_SSL_VERIFYPEER, 0); + + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_URL, m_url.c_str()); + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_TRANSFERTEXT, CURL_OFF); + + // setup POST data if it is set (and it may be empty) + if (m_postdataset) + { + g_curlInterface.easy_setopt(h, CURLOPT_POST, 1 ); + g_curlInterface.easy_setopt(h, CURLOPT_POSTFIELDSIZE, m_postdata.length()); + g_curlInterface.easy_setopt(h, CURLOPT_POSTFIELDS, m_postdata.c_str()); + } + + // setup Referer header if needed + if (!m_referer.empty()) + g_curlInterface.easy_setopt(h, CURLOPT_REFERER, m_referer.c_str()); + else + { + g_curlInterface.easy_setopt(h, CURLOPT_REFERER, NULL); + // Do not send referer header on redirects (same behaviour as ffmpeg and browsers) + g_curlInterface.easy_setopt(h, CURLOPT_AUTOREFERER, CURL_OFF); + } + + // setup any requested authentication + if( !m_ftpauth.empty() ) + { + g_curlInterface.easy_setopt(h, CURLOPT_FTP_SSL, CURLFTPSSL_TRY); + if( m_ftpauth == "any" ) + g_curlInterface.easy_setopt(h, CURLOPT_FTPSSLAUTH, CURLFTPAUTH_DEFAULT); + else if( m_ftpauth == "ssl" ) + g_curlInterface.easy_setopt(h, CURLOPT_FTPSSLAUTH, CURLFTPAUTH_SSL); + else if( m_ftpauth == "tls" ) + g_curlInterface.easy_setopt(h, CURLOPT_FTPSSLAUTH, CURLFTPAUTH_TLS); + } + + // setup requested http authentication method + bool bAuthSet = false; + if(!m_httpauth.empty()) + { + bAuthSet = true; + if( m_httpauth == "any" ) + g_curlInterface.easy_setopt(h, CURLOPT_HTTPAUTH, CURLAUTH_ANY); + else if( m_httpauth == "anysafe" ) + g_curlInterface.easy_setopt(h, CURLOPT_HTTPAUTH, CURLAUTH_ANYSAFE); + else if( m_httpauth == "digest" ) + g_curlInterface.easy_setopt(h, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); + else if( m_httpauth == "ntlm" ) + g_curlInterface.easy_setopt(h, CURLOPT_HTTPAUTH, CURLAUTH_NTLM); + else + bAuthSet = false; + } + + // set username and password for current handle + if (!m_username.empty()) + { + g_curlInterface.easy_setopt(h, CURLOPT_USERNAME, m_username.c_str()); + if (!m_password.empty()) + g_curlInterface.easy_setopt(h, CURLOPT_PASSWORD, m_password.c_str()); + + if (!bAuthSet) + g_curlInterface.easy_setopt(h, CURLOPT_HTTPAUTH, CURLAUTH_ANY); + } + + // allow passive mode for ftp + if( m_ftpport.length() > 0 ) + g_curlInterface.easy_setopt(h, CURLOPT_FTPPORT, m_ftpport.c_str()); + else + g_curlInterface.easy_setopt(h, CURLOPT_FTPPORT, NULL); + + // allow curl to not use the ip address in the returned pasv response + if( m_ftppasvip ) + g_curlInterface.easy_setopt(h, CURLOPT_FTP_SKIP_PASV_IP, 0); + else + g_curlInterface.easy_setopt(h, CURLOPT_FTP_SKIP_PASV_IP, 1); + + // setup Accept-Encoding if requested + if (m_acceptencoding.length() > 0) + g_curlInterface.easy_setopt(h, CURLOPT_ACCEPT_ENCODING, m_acceptencoding == "all" ? "" : m_acceptencoding.c_str()); + + if (!m_acceptCharset.empty()) + SetRequestHeader("Accept-Charset", m_acceptCharset); + + if (m_userAgent.length() > 0) + g_curlInterface.easy_setopt(h, CURLOPT_USERAGENT, m_userAgent.c_str()); + else /* set some default agent as shoutcast doesn't return proper stuff otherwise */ + g_curlInterface.easy_setopt(h, CURLOPT_USERAGENT, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_userAgent.c_str()); + + if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlDisableIPV6) + g_curlInterface.easy_setopt(h, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); + + if (!m_proxyhost.empty()) + { + g_curlInterface.easy_setopt(h, CURLOPT_PROXYTYPE, proxyType2CUrlProxyType[m_proxytype]); + + const std::string hostport = m_proxyhost + StringUtils::Format(":{}", m_proxyport); + g_curlInterface.easy_setopt(h, CURLOPT_PROXY, hostport.c_str()); + + std::string userpass; + + if (!m_proxyuser.empty() && !m_proxypassword.empty()) + userpass = CURL::Encode(m_proxyuser) + ":" + CURL::Encode(m_proxypassword); + + if (!userpass.empty()) + g_curlInterface.easy_setopt(h, CURLOPT_PROXYUSERPWD, userpass.c_str()); + } + if (m_customrequest.length() > 0) + g_curlInterface.easy_setopt(h, CURLOPT_CUSTOMREQUEST, m_customrequest.c_str()); + + if (m_connecttimeout == 0) + m_connecttimeout = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlconnecttimeout; + + // set our timeouts, we abort connection after m_timeout, and reads after no data for m_timeout seconds + g_curlInterface.easy_setopt(h, CURLOPT_CONNECTTIMEOUT, m_connecttimeout); + + // We abort in case we transfer less than 1byte/second + g_curlInterface.easy_setopt(h, CURLOPT_LOW_SPEED_LIMIT, 1); + + if (m_lowspeedtime == 0) + m_lowspeedtime = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curllowspeedtime; + + // Set the lowspeed time very low as it seems Curl takes much longer to detect a lowspeed condition + g_curlInterface.easy_setopt(h, CURLOPT_LOW_SPEED_TIME, m_lowspeedtime); + + // enable tcp keepalive + if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlKeepAliveInterval > 0) + { + g_curlInterface.easy_setopt(h, CURLOPT_TCP_KEEPALIVE, 1L); + g_curlInterface.easy_setopt( + h, CURLOPT_TCP_KEEPIDLE, + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlKeepAliveInterval / 2); + g_curlInterface.easy_setopt( + h, CURLOPT_TCP_KEEPINTVL, + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlKeepAliveInterval); + } + + // Setup allowed TLS/SSL ciphers. New versions of cURL may deprecate things that are still in use. + if (!m_cipherlist.empty()) + g_curlInterface.easy_setopt(h, CURLOPT_SSL_CIPHER_LIST, m_cipherlist.c_str()); + + if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlDisableHTTP2) + g_curlInterface.easy_setopt(h, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + else + // enable HTTP2 support. default: CURL_HTTP_VERSION_1_1. Curl >= 7.62.0 defaults to CURL_HTTP_VERSION_2TLS + g_curlInterface.easy_setopt(h, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS); + + // set CA bundle file + std::string caCert = CSpecialProtocol::TranslatePath( + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_caTrustFile); +#ifdef TARGET_WINDOWS_STORE + // UWP Curl - Setting CURLOPT_CAINFO with a valid cacert file path is required for UWP + g_curlInterface.easy_setopt(h, CURLOPT_CAINFO, "system\\certs\\cacert.pem"); +#endif + if (!caCert.empty() && XFILE::CFile::Exists(caCert)) + g_curlInterface.easy_setopt(h, CURLOPT_CAINFO, caCert.c_str()); +} + +void CCurlFile::SetRequestHeaders(CReadState* state) +{ + if(state->m_curlHeaderList) + { + g_curlInterface.slist_free_all(state->m_curlHeaderList); + state->m_curlHeaderList = NULL; + } + + for (const auto& it : m_requestheaders) + { + std::string buffer = it.first + ": " + it.second; + state->m_curlHeaderList = g_curlInterface.slist_append(state->m_curlHeaderList, buffer.c_str()); + } + + // add user defined headers + if (state->m_easyHandle) + g_curlInterface.easy_setopt(state->m_easyHandle, CURLOPT_HTTPHEADER, state->m_curlHeaderList); +} + +void CCurlFile::SetCorrectHeaders(CReadState* state) +{ + CHttpHeader& h = state->m_httpheader; + /* workaround for shoutcast server which doesn't set content type on standard mp3 */ + if( h.GetMimeType().empty() ) + { + if( !h.GetValue("icy-notice1").empty() + || !h.GetValue("icy-name").empty() + || !h.GetValue("icy-br").empty() ) + h.AddParam("Content-Type", "audio/mpeg"); + } + + /* hack for google video */ + if (StringUtils::EqualsNoCase(h.GetMimeType(),"text/html") + && !h.GetValue("Content-Disposition").empty() ) + { + std::string strValue = h.GetValue("Content-Disposition"); + if (strValue.find("filename=") != std::string::npos && + strValue.find(".flv") != std::string::npos) + h.AddParam("Content-Type", "video/flv"); + } +} + +void CCurlFile::ParseAndCorrectUrl(CURL &url2) +{ + std::string strProtocol = url2.GetTranslatedProtocol(); + url2.SetProtocol(strProtocol); + + // lookup host in DNS cache + std::string resolvedHost; + if (CDNSNameCache::GetCached(url2.GetHostName(), resolvedHost)) + { + struct curl_slist* tempCache; + int entryPort = url2.GetPort(); + + if (entryPort == 0) + { + if (strProtocol == "http") + entryPort = 80; + else if (strProtocol == "https") + entryPort = 443; + else if (strProtocol == "ftp") + entryPort = 21; + else if (strProtocol == "ftps") + entryPort = 990; + } + + std::string entryString = + url2.GetHostName() + ":" + std::to_string(entryPort) + ":" + resolvedHost; + tempCache = g_curlInterface.slist_append(m_dnsCacheList, entryString.c_str()); + + if (tempCache) + m_dnsCacheList = tempCache; + } + + if( url2.IsProtocol("ftp") + || url2.IsProtocol("ftps") ) + { + // we was using url options for urls, keep the old code work and warning + if (!url2.GetOptions().empty()) + { + CLog::Log(LOGWARNING, + "CCurlFile::{} - <{}> FTP url option is deprecated, please switch to use protocol " + "option (change " + "'?' to '|')", + __FUNCTION__, url2.GetRedacted()); + url2.SetProtocolOptions(url2.GetOptions().substr(1)); + /* ftp has no options */ + url2.SetOptions(""); + } + + /* this is ugly, depending on where we get */ + /* the link from, it may or may not be */ + /* url encoded. if handed from ftpdirectory */ + /* it won't be so let's handle that case */ + + std::string filename(url2.GetFileName()); + std::vector<std::string> array; + + // if server sent us the filename in non-utf8, we need send back with same encoding. + if (url2.GetProtocolOption("utf8") == "0") + g_charsetConverter.utf8ToStringCharset(filename); + + //! @todo create a tokenizer that doesn't skip empty's + StringUtils::Tokenize(filename, array, "/"); + filename.clear(); + for (std::vector<std::string>::iterator it = array.begin(); it != array.end(); ++it) + { + if(it != array.begin()) + filename += "/"; + + filename += CURL::Encode(*it); + } + + /* make sure we keep slashes */ + if(StringUtils::EndsWith(url2.GetFileName(), "/")) + filename += "/"; + + url2.SetFileName(filename); + + m_ftpauth.clear(); + if (url2.HasProtocolOption("auth")) + { + m_ftpauth = url2.GetProtocolOption("auth"); + StringUtils::ToLower(m_ftpauth); + if(m_ftpauth.empty()) + m_ftpauth = "any"; + } + m_ftpport = ""; + if (url2.HasProtocolOption("active")) + { + m_ftpport = url2.GetProtocolOption("active"); + if(m_ftpport.empty()) + m_ftpport = "-"; + } + if (url2.HasProtocolOption("verifypeer")) + { + if (url2.GetProtocolOption("verifypeer") == "false") + m_verifyPeer = false; + } + m_ftppasvip = url2.HasProtocolOption("pasvip") && url2.GetProtocolOption("pasvip") != "0"; + } + else if(url2.IsProtocol("http") || + url2.IsProtocol("https")) + { + std::shared_ptr<CSettings> s = CServiceBroker::GetSettingsComponent()->GetSettings(); + if (!s) + return; + + if (!url2.IsLocalHost() && + m_proxyhost.empty() && + s->GetBool(CSettings::SETTING_NETWORK_USEHTTPPROXY) && + !s->GetString(CSettings::SETTING_NETWORK_HTTPPROXYSERVER).empty() && + s->GetInt(CSettings::SETTING_NETWORK_HTTPPROXYPORT) > 0) + { + m_proxytype = (ProxyType)s->GetInt(CSettings::SETTING_NETWORK_HTTPPROXYTYPE); + m_proxyhost = s->GetString(CSettings::SETTING_NETWORK_HTTPPROXYSERVER); + m_proxyport = s->GetInt(CSettings::SETTING_NETWORK_HTTPPROXYPORT); + m_proxyuser = s->GetString(CSettings::SETTING_NETWORK_HTTPPROXYUSERNAME); + m_proxypassword = s->GetString(CSettings::SETTING_NETWORK_HTTPPROXYPASSWORD); + CLog::LogFC(LOGDEBUG, LOGCURL, "<{}> Using proxy {}, type {}", url2.GetRedacted(), + m_proxyhost, proxyType2CUrlProxyType[m_proxytype]); + } + + // get username and password + m_username = url2.GetUserName(); + m_password = url2.GetPassWord(); + + // handle any protocol options + std::map<std::string, std::string> options; + url2.GetProtocolOptions(options); + if (!options.empty()) + { + // set xbmc headers + for (const auto& it : options) + { + std::string name = it.first; + StringUtils::ToLower(name); + const std::string& value = it.second; + + if (name == "auth") + { + m_httpauth = value; + StringUtils::ToLower(m_httpauth); + if(m_httpauth.empty()) + m_httpauth = "any"; + } + else if (name == "referer") + SetReferer(value); + else if (name == "user-agent") + SetUserAgent(value); + else if (name == "cookie") + SetCookie(value); + else if (name == "acceptencoding" || name == "encoding") + SetAcceptEncoding(value); + else if (name == "noshout" && value == "true") + m_skipshout = true; + else if (name == "seekable" && value == "0") + m_seekable = false; + else if (name == "accept-charset") + SetAcceptCharset(value); + else if (name == "sslcipherlist") + m_cipherlist = value; + else if (name == "connection-timeout") + m_connecttimeout = strtol(value.c_str(), NULL, 10); + else if (name == "failonerror") + m_failOnError = value == "true"; + else if (name == "redirect-limit") + m_redirectlimit = strtol(value.c_str(), NULL, 10); + else if (name == "postdata") + { + m_postdata = Base64::Decode(value); + m_postdataset = true; + } + else if (name == "active-remote")// needed for DACP! + { + SetRequestHeader(it.first, value); + } + else if (name == "customrequest") + { + SetCustomRequest(value); + } + else if (name == "verifypeer") + { + if (value == "false") + m_verifyPeer = false; + } + else + { + if (name.length() > 0 && name[0] == '!') + { + SetRequestHeader(it.first.substr(1), value); + CLog::LogFC(LOGDEBUG, LOGCURL, "<{}> Adding custom header option '{}: ***********'", + url2.GetRedacted(), it.first.substr(1)); + } + else + { + SetRequestHeader(it.first, value); + if (name == "authorization") + CLog::LogFC(LOGDEBUG, LOGCURL, "<{}> Adding custom header option '{}: ***********'", + url2.GetRedacted(), it.first); + else + CLog::LogFC(LOGDEBUG, LOGCURL, "<{}> Adding custom header option '{}: {}'", + url2.GetRedacted(), it.first, value); + } + } + } + } + } + + // Unset the protocol options to have an url without protocol options + url2.SetProtocolOptions(""); + + if (m_username.length() > 0 && m_password.length() > 0) + m_url = url2.GetWithoutUserDetails(); + else + m_url = url2.Get(); +} + +bool CCurlFile::Post(const std::string& strURL, const std::string& strPostData, std::string& strHTML) +{ + m_postdata = strPostData; + m_postdataset = true; + return Service(strURL, strHTML); +} + +bool CCurlFile::Get(const std::string& strURL, std::string& strHTML) +{ + m_postdata = ""; + m_postdataset = false; + return Service(strURL, strHTML); +} + +bool CCurlFile::Service(const std::string& strURL, std::string& strHTML) +{ + const CURL pathToUrl(strURL); + if (Open(pathToUrl)) + { + if (ReadData(strHTML)) + { + Close(); + return true; + } + } + Close(); + return false; +} + +bool CCurlFile::ReadData(std::string& strHTML) +{ + int size_read = 0; + strHTML = ""; + char buffer[16384]; + while( (size_read = Read(buffer, sizeof(buffer)-1) ) > 0 ) + { + buffer[size_read] = 0; + strHTML.append(buffer, size_read); + } + if (m_state->m_cancelled) + return false; + return true; +} + +bool CCurlFile::Download(const std::string& strURL, const std::string& strFileName, unsigned int* pdwSize) +{ + CLog::Log(LOGINFO, "CCurlFile::{} - {}->{}", __FUNCTION__, CURL::GetRedacted(strURL), + strFileName); + + std::string strData; + if (!Get(strURL, strData)) + return false; + + XFILE::CFile file; + if (!file.OpenForWrite(strFileName, true)) + { + CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Unable to open file for write: {} ({})", __FUNCTION__, + CURL::GetRedacted(strURL), strFileName, GetLastError()); + return false; + } + ssize_t written = 0; + if (!strData.empty()) + written = file.Write(strData.c_str(), strData.size()); + + if (pdwSize != NULL) + *pdwSize = written > 0 ? written : 0; + + return written == static_cast<ssize_t>(strData.size()); +} + +// Detect whether we are "online" or not! Very simple and dirty! +bool CCurlFile::IsInternet() +{ + CURL url("http://www.msftconnecttest.com/connecttest.txt"); + bool found = Exists(url); + if (!found) + { + // fallback + Close(); + url.Parse("http://www.w3.org/"); + found = Exists(url); + } + Close(); + + return found; +} + +void CCurlFile::Cancel() +{ + m_state->m_cancelled = true; + while (m_opened) + KODI::TIME::Sleep(1ms); +} + +void CCurlFile::Reset() +{ + m_state->m_cancelled = false; +} + +void CCurlFile::SetProxy(const std::string &type, const std::string &host, + uint16_t port, const std::string &user, const std::string &password) +{ + m_proxytype = CCurlFile::PROXY_HTTP; + if (type == "http") + m_proxytype = CCurlFile::PROXY_HTTP; + else if (type == "https") + m_proxytype = CCurlFile::PROXY_HTTPS; + else if (type == "socks4") + m_proxytype = CCurlFile::PROXY_SOCKS4; + else if (type == "socks4a") + m_proxytype = CCurlFile::PROXY_SOCKS4A; + else if (type == "socks5") + m_proxytype = CCurlFile::PROXY_SOCKS5; + else if (type == "socks5-remote") + m_proxytype = CCurlFile::PROXY_SOCKS5_REMOTE; + else + CLog::Log(LOGERROR, "CCurFile::{} - <{}> Invalid proxy type \"{}\"", __FUNCTION__, + CURL::GetRedacted(m_url), type); + m_proxyhost = host; + m_proxyport = port; + m_proxyuser = user; + m_proxypassword = password; +} + +bool CCurlFile::Open(const CURL& url) +{ + m_opened = true; + m_seekable = true; + + CURL url2(url); + ParseAndCorrectUrl(url2); + + std::string redactPath = CURL::GetRedacted(m_url); + CLog::Log(LOGDEBUG, "CurlFile::{} - <{}>", __FUNCTION__, redactPath); + + assert(!(!m_state->m_easyHandle ^ !m_state->m_multiHandle)); + if( m_state->m_easyHandle == NULL ) + g_curlInterface.easy_acquire(url2.GetProtocol().c_str(), + url2.GetHostName().c_str(), + &m_state->m_easyHandle, + &m_state->m_multiHandle); + + // setup common curl options + SetCommonOptions(m_state, + m_failOnError && !CServiceBroker::GetLogging().CanLogComponent(LOGCURL)); + SetRequestHeaders(m_state); + m_state->m_sendRange = m_seekable; + m_state->m_bRetry = m_allowRetry; + + m_httpresponse = m_state->Connect(m_bufferSize); + + if (m_httpresponse <= 0 || (m_failOnError && m_httpresponse >= 400)) + { + std::string error; + if (m_httpresponse >= 400 && CServiceBroker::GetLogging().CanLogComponent(LOGCURL)) + { + error.resize(4096); + ReadString(&error[0], 4095); + } + + CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Failed with code {}:\n{}", __FUNCTION__, + redactPath, m_httpresponse, error); + + return false; + } + + SetCorrectHeaders(m_state); + + // since we can't know the stream size up front if we're gzipped/deflated + // flag the stream with an unknown file size rather than the compressed + // file size. + if (!m_state->m_httpheader.GetValue("Content-Encoding").empty() && !StringUtils::EqualsNoCase(m_state->m_httpheader.GetValue("Content-Encoding"), "identity")) + m_state->m_fileSize = 0; + + // check if this stream is a shoutcast stream. sometimes checking the protocol line is not enough so examine other headers as well. + // shoutcast streams should be handled by FileShoutcast. + if ((m_state->m_httpheader.GetProtoLine().substr(0, 3) == "ICY" || !m_state->m_httpheader.GetValue("icy-notice1").empty() + || !m_state->m_httpheader.GetValue("icy-name").empty() + || !m_state->m_httpheader.GetValue("icy-br").empty()) && !m_skipshout) + { + CLog::Log(LOGDEBUG, "CCurlFile::{} - <{}> File is a shoutcast stream. Re-opening", __FUNCTION__, + redactPath); + throw new CRedirectException(new CShoutcastFile); + } + + m_multisession = false; + if(url2.IsProtocol("http") || url2.IsProtocol("https")) + { + m_multisession = true; + if(m_state->m_httpheader.GetValue("Server").find("Portable SDK for UPnP devices") != std::string::npos) + { + CLog::Log(LOGWARNING, + "CCurlFile::{} - <{}> Disabling multi session due to broken libupnp server", + __FUNCTION__, redactPath); + m_multisession = false; + } + } + + if(StringUtils::EqualsNoCase(m_state->m_httpheader.GetValue("Transfer-Encoding"), "chunked")) + m_state->m_fileSize = 0; + + if(m_state->m_fileSize <= 0) + m_seekable = false; + if (m_seekable) + { + if(url2.IsProtocol("http") + || url2.IsProtocol("https")) + { + // if server says explicitly it can't seek, respect that + if(StringUtils::EqualsNoCase(m_state->m_httpheader.GetValue("Accept-Ranges"),"none")) + m_seekable = false; + } + } + + std::string efurl = GetInfoString(CURLINFO_EFFECTIVE_URL); + if (!efurl.empty()) + { + if (m_url != efurl) + { + std::string redactEfpath = CURL::GetRedacted(efurl); + CLog::Log(LOGDEBUG, "CCurlFile::{} - <{}> Effective URL is {}", __FUNCTION__, redactPath, + redactEfpath); + } + m_url = efurl; + } + + return true; +} + +bool CCurlFile::OpenForWrite(const CURL& url, bool bOverWrite) +{ + if(m_opened) + return false; + + if (Exists(url) && !bOverWrite) + return false; + + CURL url2(url); + ParseAndCorrectUrl(url2); + + CLog::Log(LOGDEBUG, "CCurlFile::{} - Opening {}", __FUNCTION__, CURL::GetRedacted(m_url)); + + assert(m_state->m_easyHandle == NULL); + g_curlInterface.easy_acquire(url2.GetProtocol().c_str(), + url2.GetHostName().c_str(), + &m_state->m_easyHandle, + &m_state->m_multiHandle); + + // setup common curl options + SetCommonOptions(m_state); + SetRequestHeaders(m_state); + + std::string efurl = GetInfoString(CURLINFO_EFFECTIVE_URL); + if (!efurl.empty()) + m_url = efurl; + + m_opened = true; + m_forWrite = true; + m_inError = false; + m_writeOffset = 0; + + assert(m_state->m_multiHandle); + + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_UPLOAD, 1); + + g_curlInterface.multi_add_handle(m_state->m_multiHandle, m_state->m_easyHandle); + + m_state->SetReadBuffer(NULL, 0); + + return true; +} + +ssize_t CCurlFile::Write(const void* lpBuf, size_t uiBufSize) +{ + if (!(m_opened && m_forWrite) || m_inError) + return -1; + + assert(m_state->m_multiHandle); + + m_state->SetReadBuffer(lpBuf, uiBufSize); + m_state->m_isPaused = false; + g_curlInterface.easy_pause(m_state->m_easyHandle, CURLPAUSE_CONT); + + CURLMcode result = CURLM_OK; + + m_stillRunning = 1; + while (m_stillRunning && !m_state->m_isPaused) + { + while ((result = g_curlInterface.multi_perform(m_state->m_multiHandle, &m_stillRunning)) == CURLM_CALL_MULTI_PERFORM); + + if (!m_stillRunning) + break; + + if (result != CURLM_OK) + { + long code; + if(g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_RESPONSE_CODE, &code) == CURLE_OK ) + CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Unable to write curl resource with code {}", + __FUNCTION__, CURL::GetRedacted(m_url), code); + m_inError = true; + return -1; + } + } + + m_writeOffset += m_state->m_filePos; + return m_state->m_filePos; +} + +bool CCurlFile::CReadState::ReadString(char *szLine, int iLineLength) +{ + unsigned int want = (unsigned int)iLineLength; + + if((m_fileSize == 0 || m_filePos < m_fileSize) && FillBuffer(want) != FILLBUFFER_OK) + return false; + + // ensure only available data is considered + want = std::min(m_buffer.getMaxReadSize(), want); + + /* check if we finished prematurely */ + if (!m_stillRunning && (m_fileSize == 0 || m_filePos != m_fileSize) && !want) + { + if (m_fileSize != 0) + CLog::Log( + LOGWARNING, + "CCurlFile::{} - ({}) Transfer ended before entire file was retrieved pos {}, size {}", + __FUNCTION__, fmt::ptr(this), m_filePos, m_fileSize); + + return false; + } + + char* pLine = szLine; + do + { + if (!m_buffer.ReadData(pLine, 1)) + break; + + pLine++; + } while (((pLine - 1)[0] != '\n') && ((unsigned int)(pLine - szLine) < want)); + pLine[0] = 0; + m_filePos += (pLine - szLine); + return (pLine - szLine) > 0; +} + +bool CCurlFile::ReOpen(const CURL& url) +{ + Close(); + return Open(url); +} + +bool CCurlFile::Exists(const CURL& url) +{ + // if file is already running, get info from it + if( m_opened ) + { + CLog::Log(LOGWARNING, "CCurlFile::{} - <{}> Exist called on open file", __FUNCTION__, + url.GetRedacted()); + return true; + } + + CURL url2(url); + ParseAndCorrectUrl(url2); + + assert(m_state->m_easyHandle == NULL); + g_curlInterface.easy_acquire(url2.GetProtocol().c_str(), + url2.GetHostName().c_str(), + &m_state->m_easyHandle, NULL); + + SetCommonOptions(m_state); + SetRequestHeaders(m_state); + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_TIMEOUT, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlconnecttimeout); + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_NOBODY, 1); + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_WRITEDATA, NULL); /* will cause write failure*/ + + if(url2.IsProtocol("ftp") || url2.IsProtocol("ftps")) + { + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_FILETIME, 1); + // nocwd is less standard, will return empty list for non-existed remote dir on some ftp server, avoid it. + if (StringUtils::EndsWith(url2.GetFileName(), "/")) + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_SINGLECWD); + else + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_NOCWD); + } + + CURLcode result = g_curlInterface.easy_perform(m_state->m_easyHandle); + + if (result == CURLE_WRITE_ERROR || result == CURLE_OK) + { + g_curlInterface.easy_release(&m_state->m_easyHandle, NULL); + return true; + } + + if (result == CURLE_HTTP_RETURNED_ERROR) + { + long code; + if(g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_RESPONSE_CODE, &code) == CURLE_OK && code != 404 ) + { + if (code == 405) + { + // If we get a Method Not Allowed response, retry with a GET Request + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_NOBODY, 0); + + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_FILETIME, 1); + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_XFERINFOFUNCTION, transfer_abort_callback); + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_NOPROGRESS, 0); + + curl_slist *list = NULL; + list = g_curlInterface.slist_append(list, "Range: bytes=0-1"); /* try to only request 1 byte */ + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_HTTPHEADER, list); + + CURLcode result = g_curlInterface.easy_perform(m_state->m_easyHandle); + g_curlInterface.slist_free_all(list); + + if (result == CURLE_WRITE_ERROR || result == CURLE_OK) + { + g_curlInterface.easy_release(&m_state->m_easyHandle, NULL); + return true; + } + + if (result == CURLE_HTTP_RETURNED_ERROR) + { + if (g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_RESPONSE_CODE, &code) == CURLE_OK && code != 404 ) + CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Failed: HTTP returned error {}", __FUNCTION__, + url.GetRedacted(), code); + } + } + else + CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Failed: HTTP returned error {}", __FUNCTION__, + url.GetRedacted(), code); + } + } + else if (result != CURLE_REMOTE_FILE_NOT_FOUND && result != CURLE_FTP_COULDNT_RETR_FILE) + { + CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Failed: {}({})", __FUNCTION__, url.GetRedacted(), + g_curlInterface.easy_strerror(result), result); + } + + errno = ENOENT; + g_curlInterface.easy_release(&m_state->m_easyHandle, NULL); + return false; +} + +int64_t CCurlFile::Seek(int64_t iFilePosition, int iWhence) +{ + int64_t nextPos = m_state->m_filePos; + + if(!m_seekable) + return -1; + + switch(iWhence) + { + case SEEK_SET: + nextPos = iFilePosition; + break; + case SEEK_CUR: + nextPos += iFilePosition; + break; + case SEEK_END: + if (m_state->m_fileSize) + nextPos = m_state->m_fileSize + iFilePosition; + else + return -1; + break; + default: + return -1; + } + + // We can't seek beyond EOF + if (m_state->m_fileSize && nextPos > m_state->m_fileSize) return -1; + + if(m_state->Seek(nextPos)) + return nextPos; + + if (m_multisession) + { + if (!m_oldState) + { + CURL url(m_url); + m_oldState = m_state; + m_state = new CReadState(); + m_state->m_fileSize = m_oldState->m_fileSize; + g_curlInterface.easy_acquire(url.GetProtocol().c_str(), + url.GetHostName().c_str(), + &m_state->m_easyHandle, + &m_state->m_multiHandle ); + } + else + { + CReadState *tmp; + tmp = m_state; + m_state = m_oldState; + m_oldState = tmp; + + if (m_state->Seek(nextPos)) + return nextPos; + + m_state->Disconnect(); + } + } + else + m_state->Disconnect(); + + // re-setup common curl options + SetCommonOptions(m_state); + + /* caller might have changed some headers (needed for daap)*/ + //! @todo daap is gone. is this needed for something else? + SetRequestHeaders(m_state); + + m_state->m_filePos = nextPos; + m_state->m_sendRange = true; + m_state->m_bRetry = m_allowRetry; + + long response = m_state->Connect(m_bufferSize); + if(response < 0 && (m_state->m_fileSize == 0 || m_state->m_fileSize != m_state->m_filePos)) + { + if(m_multisession) + { + if (m_oldState) + { + delete m_state; + m_state = m_oldState; + m_oldState = NULL; + } + // Retry without multisession + m_multisession = false; + return Seek(iFilePosition, iWhence); + } + else + { + m_seekable = false; + return -1; + } + } + + SetCorrectHeaders(m_state); + + return m_state->m_filePos; +} + +int64_t CCurlFile::GetLength() +{ + if (!m_opened) return 0; + return m_state->m_fileSize; +} + +int64_t CCurlFile::GetPosition() +{ + if (!m_opened) return 0; + return m_state->m_filePos; +} + +int CCurlFile::Stat(const CURL& url, struct __stat64* buffer) +{ + // if file is already running, get info from it + if( m_opened ) + { + CLog::Log(LOGWARNING, "CCurlFile::{} - <{}> Stat called on open file", __FUNCTION__, + url.GetRedacted()); + if (buffer) + { + *buffer = {}; + buffer->st_size = GetLength(); + buffer->st_mode = _S_IFREG; + } + return 0; + } + + CURL url2(url); + ParseAndCorrectUrl(url2); + + assert(m_state->m_easyHandle == NULL); + g_curlInterface.easy_acquire(url2.GetProtocol().c_str(), + url2.GetHostName().c_str(), + &m_state->m_easyHandle, NULL); + + SetCommonOptions(m_state); + SetRequestHeaders(m_state); + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_TIMEOUT, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlconnecttimeout); + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_NOBODY, 1); + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_FILETIME , 1); + + if(url2.IsProtocol("ftp")) + { + // nocwd is less standard, will return empty list for non-existed remote dir on some ftp server, avoid it. + if (StringUtils::EndsWith(url2.GetFileName(), "/")) + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_SINGLECWD); + else + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_NOCWD); + } + + CURLcode result = g_curlInterface.easy_perform(m_state->m_easyHandle); + + if(result == CURLE_HTTP_RETURNED_ERROR) + { + long code; + if(g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_RESPONSE_CODE, &code) == CURLE_OK && code == 404 ) + { + g_curlInterface.easy_release(&m_state->m_easyHandle, NULL); + errno = ENOENT; + return -1; + } + } + + if(result == CURLE_GOT_NOTHING + || result == CURLE_HTTP_RETURNED_ERROR + || result == CURLE_RECV_ERROR /* some silly shoutcast servers */ ) + { + /* some http servers and shoutcast servers don't give us any data on a head request */ + /* request normal and just bail out via progress meter callback after we received data */ + /* somehow curl doesn't reset CURLOPT_NOBODY properly so reset everything */ + SetCommonOptions(m_state); + SetRequestHeaders(m_state); + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_TIMEOUT, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlconnecttimeout); + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_FILETIME, 1); +#if LIBCURL_VERSION_NUM >= 0x072000 // 0.7.32 + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_XFERINFOFUNCTION, transfer_abort_callback); +#else + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_PROGRESSFUNCTION, transfer_abort_callback); +#endif + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_NOPROGRESS, 0); + + result = g_curlInterface.easy_perform(m_state->m_easyHandle); + + } + + if( result != CURLE_ABORTED_BY_CALLBACK && result != CURLE_OK ) + { + g_curlInterface.easy_release(&m_state->m_easyHandle, NULL); + errno = ENOENT; + CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Failed: {}({})", __FUNCTION__, url.GetRedacted(), + g_curlInterface.easy_strerror(result), result); + return -1; + } + + double length; + result = g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &length); + if (result != CURLE_OK || length < 0.0) + { + if (url.IsProtocol("ftp")) + { + g_curlInterface.easy_release(&m_state->m_easyHandle, NULL); + CLog::Log(LOGINFO, "CCurlFile::{} - <{}> Content length failed: {}({})", __FUNCTION__, + url.GetRedacted(), g_curlInterface.easy_strerror(result), result); + errno = ENOENT; + return -1; + } + else + length = 0.0; + } + + SetCorrectHeaders(m_state); + + if (buffer) + { + *buffer = {}; + buffer->st_size = static_cast<int64_t>(length); + + // Note: CURLINFO_CONTENT_TYPE returns the last received content-type response header value. + // In case there is authentication required there might be multiple requests involved and if + // the last request which actually returns the data does not return a content-type header, but + // one of the preceding requests, CURLINFO_CONTENT_TYPE returns not the content type of the + // actual resource requested! m_state contains only the values of the last request, which is + // what we want here. + const std::string mimeType = m_state->m_httpheader.GetMimeType(); + if (mimeType.find("text/html") != std::string::npos) // consider html files directories + buffer->st_mode = _S_IFDIR; + else + buffer->st_mode = _S_IFREG; + + long filetime; + result = g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_FILETIME, &filetime); + if (result != CURLE_OK) + { + CLog::Log(LOGINFO, "CCurlFile::{} - <{}> Filetime failed: {}({})", __FUNCTION__, + url.GetRedacted(), g_curlInterface.easy_strerror(result), result); + } + else + { + if (filetime != -1) + buffer->st_mtime = filetime; + } + } + g_curlInterface.easy_release(&m_state->m_easyHandle, NULL); + return 0; +} + +ssize_t CCurlFile::CReadState::Read(void* lpBuf, size_t uiBufSize) +{ + /* only request 1 byte, for truncated reads (only if not eof) */ + if (m_fileSize == 0 || m_filePos < m_fileSize) + { + int8_t result = FillBuffer(1); + if (result == FILLBUFFER_FAIL) + return -1; // Fatal error + + if (result == FILLBUFFER_NO_DATA) + return 0; + } + + /* ensure only available data is considered */ + unsigned int want = std::min<unsigned int>(m_buffer.getMaxReadSize(), uiBufSize); + + /* xfer data to caller */ + if (m_buffer.ReadData((char *)lpBuf, want)) + { + m_filePos += want; + return want; + } + + /* check if we finished prematurely */ + if (!m_stillRunning && (m_fileSize == 0 || m_filePos != m_fileSize)) + { + CLog::Log(LOGWARNING, + "CCurlFile::CReadState::{} - ({}) Transfer ended before entire file was retrieved " + "pos {}, size {}", + __FUNCTION__, fmt::ptr(this), m_filePos, m_fileSize); + return -1; + } + + return 0; +} + +/* use to attempt to fill the read buffer up to requested number of bytes */ +int8_t CCurlFile::CReadState::FillBuffer(unsigned int want) +{ + int retry = 0; + fd_set fdread; + fd_set fdwrite; + fd_set fdexcep; + + // only attempt to fill buffer if transactions still running and buffer + // doesn't exceed required size already + while (m_buffer.getMaxReadSize() < want && m_buffer.getMaxWriteSize() > 0 ) + { + if (m_cancelled) + return FILLBUFFER_NO_DATA; + + /* if there is data in overflow buffer, try to use that first */ + if (m_overflowSize) + { + unsigned amount = std::min(m_buffer.getMaxWriteSize(), m_overflowSize); + m_buffer.WriteData(m_overflowBuffer, amount); + + if (amount < m_overflowSize) + memmove(m_overflowBuffer, m_overflowBuffer + amount, m_overflowSize - amount); + + m_overflowSize -= amount; + // Shrink memory: + m_overflowBuffer = (char*)realloc_simple(m_overflowBuffer, m_overflowSize); + continue; + } + + CURLMcode result = g_curlInterface.multi_perform(m_multiHandle, &m_stillRunning); + if (!m_stillRunning) + { + if (result == CURLM_OK) + { + /* if we still have stuff in buffer, we are fine */ + if (m_buffer.getMaxReadSize()) + return FILLBUFFER_OK; + + // check for errors + int msgs; + CURLMsg* msg; + bool bRetryNow = true; + bool bError = false; + while ((msg = g_curlInterface.multi_info_read(m_multiHandle, &msgs))) + { + if (msg->msg == CURLMSG_DONE) + { + if (msg->data.result == CURLE_OK) + return FILLBUFFER_OK; + + long httpCode = 0; + if (msg->data.result == CURLE_HTTP_RETURNED_ERROR) + { + g_curlInterface.easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &httpCode); + + // Don't log 404 not-found errors to prevent log-spam + if (httpCode != 404) + CLog::Log(LOGERROR, + "CCurlFile::CReadState::{} - ({}) Failed: HTTP returned code {}", + __FUNCTION__, fmt::ptr(this), httpCode); + } + else + { + CLog::Log(LOGERROR, "CCurlFile::CReadState::{} - ({}) Failed: {}({})", __FUNCTION__, + fmt::ptr(this), g_curlInterface.easy_strerror(msg->data.result), + msg->data.result); + } + + if ( (msg->data.result == CURLE_OPERATION_TIMEDOUT || + msg->data.result == CURLE_PARTIAL_FILE || + msg->data.result == CURLE_COULDNT_CONNECT || + msg->data.result == CURLE_RECV_ERROR) && + !m_bFirstLoop) + { + bRetryNow = false; // Leave it to caller whether the operation is retried + bError = true; + } + else if ( (msg->data.result == CURLE_HTTP_RANGE_ERROR || + httpCode == 416 /* = Requested Range Not Satisfiable */ || + httpCode == 406 /* = Not Acceptable (fixes issues with non compliant HDHomerun servers */) && + m_bFirstLoop && + m_filePos == 0 && + m_sendRange) + { + // If server returns a (possible) range error, disable range and retry (handled below) + bRetryNow = true; + bError = true; + m_sendRange = false; + } + else + { + // For all other errors, abort the operation + return FILLBUFFER_FAIL; + } + } + } + + // Check for an actual error, if not, just return no-data + if (!bError && !m_bLastError) + return FILLBUFFER_NO_DATA; + + // Close handle + if (m_multiHandle && m_easyHandle) + g_curlInterface.multi_remove_handle(m_multiHandle, m_easyHandle); + + // Reset all the stuff like we would in Disconnect() + m_buffer.Clear(); + free(m_overflowBuffer); + m_overflowBuffer = NULL; + m_overflowSize = 0; + m_bLastError = true; // Flag error for the next run + + // Retry immediately or leave it up to the caller? + if ((m_bRetry && retry < CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlretries) || (bRetryNow && retry == 0)) + { + retry++; + + // Connect + seek to current position (again) + SetResume(); + g_curlInterface.multi_add_handle(m_multiHandle, m_easyHandle); + + CLog::Log(LOGWARNING, "CCurlFile::CReadState::{} - ({}) Reconnect, (re)try {}", + __FUNCTION__, fmt::ptr(this), retry); + + // Return to the beginning of the loop: + continue; + } + + return FILLBUFFER_NO_DATA; // We failed but flag no data to caller, so it can retry the operation + } + return FILLBUFFER_FAIL; + } + + // We've finished out first loop + if(m_bFirstLoop && m_buffer.getMaxReadSize() > 0) + m_bFirstLoop = false; + + // No error this run + m_bLastError = false; + + switch (result) + { + case CURLM_OK: + { + int maxfd = -1; + FD_ZERO(&fdread); + FD_ZERO(&fdwrite); + FD_ZERO(&fdexcep); + + // get file descriptors from the transfers + g_curlInterface.multi_fdset(m_multiHandle, &fdread, &fdwrite, &fdexcep, &maxfd); + + long timeout = 0; + if (CURLM_OK != g_curlInterface.multi_timeout(m_multiHandle, &timeout) || timeout == -1 || timeout < 200) + timeout = 200; + + XbmcThreads::EndTime<> endTime{std::chrono::milliseconds(timeout)}; + int rc; + + do + { + /* On success the value of maxfd is guaranteed to be >= -1. We call + * select(maxfd + 1, ...); specially in case of (maxfd == -1) there are + * no fds ready yet so we call select(0, ...) --or Sleep() on Windows-- + * to sleep 100ms, which is the minimum suggested value in the + * curl_multi_fdset() doc. + */ + if (maxfd == -1) + { +#ifdef TARGET_WINDOWS + /* Windows does not support using select() for sleeping without a dummy + * socket. Instead use Windows' Sleep() and sleep for 100ms which is the + * minimum suggested value in the curl_multi_fdset() doc. + */ + KODI::TIME::Sleep(100ms); + rc = 0; +#else + /* Portable sleep for platforms other than Windows. */ + struct timeval wait = { 0, 100 * 1000 }; /* 100ms */ + rc = select(0, NULL, NULL, NULL, &wait); +#endif + } + else + { + unsigned int time_left = endTime.GetTimeLeft().count(); + struct timeval wait = { (int)time_left / 1000, ((int)time_left % 1000) * 1000 }; + rc = select(maxfd + 1, &fdread, &fdwrite, &fdexcep, &wait); + } +#ifdef TARGET_WINDOWS + } while(rc == SOCKET_ERROR && WSAGetLastError() == WSAEINTR); +#else + } while(rc == SOCKET_ERROR && errno == EINTR); +#endif + + if(rc == SOCKET_ERROR) + { +#ifdef TARGET_WINDOWS + char buf[256]; + strerror_s(buf, 256, WSAGetLastError()); + CLog::Log(LOGERROR, "CCurlFile::CReadState::{} - ({}) Failed with socket error:{}", + __FUNCTION__, fmt::ptr(this), buf); +#else + char const * str = strerror(errno); + CLog::Log(LOGERROR, "CCurlFile::CReadState::{} - ({}) Failed with socket error:{}", + __FUNCTION__, fmt::ptr(this), str); +#endif + + return FILLBUFFER_FAIL; + } + } + break; + case CURLM_CALL_MULTI_PERFORM: + { + // we don't keep calling here as that can easily overwrite our buffer which we want to avoid + // docs says we should call it soon after, but as long as we are reading data somewhere + // this aught to be soon enough. should stay in socket otherwise + continue; + } + break; + default: + { + CLog::Log(LOGERROR, + "CCurlFile::CReadState::{} - ({}) Multi perform failed with code {}, aborting", + __FUNCTION__, fmt::ptr(this), result); + return FILLBUFFER_FAIL; + } + break; + } + } + return FILLBUFFER_OK; +} + +void CCurlFile::CReadState::SetReadBuffer(const void* lpBuf, int64_t uiBufSize) +{ + m_readBuffer = const_cast<char*>((const char*)lpBuf); + m_fileSize = uiBufSize; + m_filePos = 0; +} + +void CCurlFile::ClearRequestHeaders() +{ + m_requestheaders.clear(); +} + +void CCurlFile::SetRequestHeader(const std::string& header, const std::string& value) +{ + m_requestheaders[header] = value; +} + +void CCurlFile::SetRequestHeader(const std::string& header, long value) +{ + m_requestheaders[header] = std::to_string(value); +} + +std::string CCurlFile::GetURL(void) +{ + return m_url; +} + +std::string CCurlFile::GetRedirectURL() +{ + return GetInfoString(CURLINFO_REDIRECT_URL); +} + +std::string CCurlFile::GetInfoString(int infoType) +{ + char* info{}; + CURLcode result = g_curlInterface.easy_getinfo(m_state->m_easyHandle, static_cast<CURLINFO> (infoType), &info); + if (result != CURLE_OK) + { + CLog::Log(LOGERROR, + "CCurlFile::{} - <{}> Info string request for type {} failed with result code {}", + __FUNCTION__, CURL::GetRedacted(m_url), infoType, result); + return ""; + } + return (info ? info : ""); +} + +/* STATIC FUNCTIONS */ +bool CCurlFile::GetHttpHeader(const CURL &url, CHttpHeader &headers) +{ + try + { + CCurlFile file; + if(file.Stat(url, NULL) == 0) + { + headers = file.GetHttpHeader(); + return true; + } + return false; + } + catch(...) + { + CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Exception thrown while trying to retrieve header", + __FUNCTION__, url.GetRedacted()); + return false; + } +} + +bool CCurlFile::GetMimeType(const CURL &url, std::string &content, const std::string &useragent) +{ + CCurlFile file; + if (!useragent.empty()) + file.SetUserAgent(useragent); + + struct __stat64 buffer; + std::string redactUrl = url.GetRedacted(); + if( file.Stat(url, &buffer) == 0 ) + { + if (buffer.st_mode == _S_IFDIR) + content = "x-directory/normal"; + else + content = file.GetProperty(XFILE::FILE_PROPERTY_MIME_TYPE); + CLog::Log(LOGDEBUG, "CCurlFile::{} - <{}> -> {}", __FUNCTION__, redactUrl, content); + return true; + } + CLog::Log(LOGDEBUG, "CCurlFile::{} - <{}> -> failed", __FUNCTION__, redactUrl); + content.clear(); + return false; +} + +bool CCurlFile::GetContentType(const CURL &url, std::string &content, const std::string &useragent) +{ + CCurlFile file; + if (!useragent.empty()) + file.SetUserAgent(useragent); + + struct __stat64 buffer; + std::string redactUrl = url.GetRedacted(); + if (file.Stat(url, &buffer) == 0) + { + if (buffer.st_mode == _S_IFDIR) + content = "x-directory/normal"; + else + content = file.GetProperty(XFILE::FILE_PROPERTY_CONTENT_TYPE, ""); + CLog::Log(LOGDEBUG, "CCurlFile::{} - <{}> -> {}", __FUNCTION__, redactUrl, content); + return true; + } + CLog::Log(LOGDEBUG, "CCurlFile::{} - <{}> -> failed", __FUNCTION__, redactUrl); + content.clear(); + return false; +} + +bool CCurlFile::GetCookies(const CURL &url, std::string &cookies) +{ + std::string cookiesStr; + curl_slist* curlCookies; + CURL_HANDLE* easyHandle; + CURLM* multiHandle; + + // get the cookies list + g_curlInterface.easy_acquire(url.GetProtocol().c_str(), + url.GetHostName().c_str(), + &easyHandle, &multiHandle); + if (CURLE_OK == g_curlInterface.easy_getinfo(easyHandle, CURLINFO_COOKIELIST, &curlCookies)) + { + // iterate over each cookie and format it into an RFC 2109 formatted Set-Cookie string + curl_slist* curlCookieIter = curlCookies; + while(curlCookieIter) + { + // tokenize the CURL cookie string + std::vector<std::string> valuesVec; + StringUtils::Tokenize(curlCookieIter->data, valuesVec, "\t"); + + // ensure the length is valid + if (valuesVec.size() < 7) + { + CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Invalid cookie: '{}'", __FUNCTION__, + url.GetRedacted(), curlCookieIter->data); + curlCookieIter = curlCookieIter->next; + continue; + } + + // create a http-header formatted cookie string + std::string cookieStr = valuesVec[5] + "=" + valuesVec[6] + + "; path=" + valuesVec[2] + + "; domain=" + valuesVec[0]; + + // append this cookie to the string containing all cookies + if (!cookiesStr.empty()) + cookiesStr += "\n"; + cookiesStr += cookieStr; + + // move on to the next cookie + curlCookieIter = curlCookieIter->next; + } + + // free the curl cookies + g_curlInterface.slist_free_all(curlCookies); + + // release our handles + g_curlInterface.easy_release(&easyHandle, &multiHandle); + + // if we have a non-empty cookie string, return it + if (!cookiesStr.empty()) + { + cookies = cookiesStr; + return true; + } + } + + // no cookies to return + return false; +} + +int CCurlFile::IoControl(EIoControl request, void* param) +{ + if (request == IOCTRL_SEEK_POSSIBLE) + return m_seekable ? 1 : 0; + + if (request == IOCTRL_SET_RETRY) + { + m_allowRetry = *(bool*) param; + return 0; + } + + return -1; +} + +const std::string CCurlFile::GetProperty(XFILE::FileProperty type, const std::string &name) const +{ + switch (type) + { + case FILE_PROPERTY_RESPONSE_PROTOCOL: + return m_state->m_httpheader.GetProtoLine(); + case FILE_PROPERTY_RESPONSE_HEADER: + return m_state->m_httpheader.GetValue(name); + case FILE_PROPERTY_CONTENT_TYPE: + return m_state->m_httpheader.GetValue("content-type"); + case FILE_PROPERTY_CONTENT_CHARSET: + return m_state->m_httpheader.GetCharset(); + case FILE_PROPERTY_MIME_TYPE: + return m_state->m_httpheader.GetMimeType(); + case FILE_PROPERTY_EFFECTIVE_URL: + { + char *url = nullptr; + g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_EFFECTIVE_URL, &url); + return url ? url : ""; + } + default: + return ""; + } +} + +const std::vector<std::string> CCurlFile::GetPropertyValues(XFILE::FileProperty type, const std::string &name) const +{ + if (type == FILE_PROPERTY_RESPONSE_HEADER) + { + return m_state->m_httpheader.GetValues(name); + } + std::vector<std::string> values; + std::string value = GetProperty(type, name); + if (!value.empty()) + { + values.emplace_back(value); + } + return values; +} + +double CCurlFile::GetDownloadSpeed() +{ +#if LIBCURL_VERSION_NUM >= 0x073a00 // 0.7.58.0 + double speed = 0.0; + if (g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_SPEED_DOWNLOAD, &speed) == CURLE_OK) + return speed; +#else + double time = 0.0, size = 0.0; + if (g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_TOTAL_TIME, &time) == CURLE_OK + && g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_SIZE_DOWNLOAD, &size) == CURLE_OK + && time > 0.0) + { + return size / time; + } +#endif + return 0.0; +} |