diff --git a/.gitignore b/.gitignore index b746f21..43d4a27 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ HTTP_VS14.VC.db TestHTTP/LastCoverageResults.log TestHTTP/TestHTTP.vcxproj.user TestHTTP/TestHTTP_VS14.vcxproj.user +*build* diff --git a/CMakeLists.txt b/CMakeLists.txt index 2160d78..c9d03e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,17 +1,44 @@ -cmake_minimum_required(VERSION 2.6) +cmake_minimum_required (VERSION 3.10.0) -# Project configuration -project(HTTPClient) -set(LIBRARY_OUTPUT_PATH lib/${CMAKE_BUILD_TYPE}) set(CMAKE_CXX_STANDARD 14) -add_definitions(-DLINUX) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_BUILD_TYPE}/bin) +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_BUILD_TYPE}/lib) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_BUILD_TYPE}/lib) -# Locate libcURL -find_package(CURL REQUIRED) -include_directories(${CURL_INCLUDE_DIRS}) +# Read version from file VERSION +FILE(READ "VERSION" project_VERSION) +STRING(STRIP "${project_VERSION}" project_VERSION) + +SET(LICENSE "MIT") +cmake_policy(SET CMP0022 NEW) + +PROJECT(HTTPClient VERSION ${project_VERSION} LANGUAGES CXX) + +if(NOT MSVC) + add_definitions(-DLINUX) +else() + add_definitions(-DWINDOWS) +endif() + +option(SKIP_TESTS_BUILD "Skip tests build" ON) include_directories(HTTP) -file(GLOB_RECURSE source_files HTTP/*) -add_library(httpclient STATIC ${source_files}) +add_subdirectory(HTTP) + +if(NOT SKIP_TESTS_BUILD) +add_subdirectory(TestHTTP) + +include(CTest) +enable_testing () + +# test if the test INI file exist, otherwise default it to the one in TestHTTP folder +IF (NOT TEST_INI_FILE) + SET(TEST_INI_FILE "./TestHTTP/template_test_conf.ini") + MESSAGE(WARNING "You didn't provide an INI test configuration file.\ + Defaulting TEST_INI_FILE to ./TestHTTP/template_test_conf.ini") +ENDIF() + +add_test (NAME HttpClientTest COMMAND test_httpclient ${TEST_INI_FILE}) +endif(NOT SKIP_TESTS_BUILD) diff --git a/HTTP/CMakeLists.txt b/HTTP/CMakeLists.txt new file mode 100644 index 0000000..2a03e89 --- /dev/null +++ b/HTTP/CMakeLists.txt @@ -0,0 +1,15 @@ + +IF(MSVC OR NOT CMAKE_BUILD_TYPE MATCHES Coverage) + +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +# Locate libcURL +find_package(CURL REQUIRED) +include_directories(${CURL_INCLUDE_DIRS}) + +file(GLOB_RECURSE source_files ./*) +add_library(httpclient STATIC ${source_files}) + +install(TARGETS httpclient) + +ENDIF() diff --git a/HTTP/CurlHandle.cpp b/HTTP/CurlHandle.cpp new file mode 100644 index 0000000..c6af406 --- /dev/null +++ b/HTTP/CurlHandle.cpp @@ -0,0 +1,18 @@ +#include "CurlHandle.h" + +#include +#include + +CurlHandle::CurlHandle() { + const auto eCode = curl_global_init(CURL_GLOBAL_ALL); + if (eCode != CURLE_OK) { + throw std::runtime_error{"Error initializing libCURL"}; + } +} + +CurlHandle::~CurlHandle() { curl_global_cleanup(); } + +CurlHandle &CurlHandle::instance() { + static CurlHandle inst{}; + return inst; +} diff --git a/HTTP/CurlHandle.h b/HTTP/CurlHandle.h new file mode 100644 index 0000000..fbe8caf --- /dev/null +++ b/HTTP/CurlHandle.h @@ -0,0 +1,20 @@ +#ifndef INCLUDE_CURLHANDLE_H_ +#define INCLUDE_CURLHANDLE_H_ + +class CurlHandle { + public: + static CurlHandle &instance(); + + CurlHandle(CurlHandle const &) = delete; + CurlHandle(CurlHandle &&) = delete; + + CurlHandle &operator=(CurlHandle const &) = delete; + CurlHandle &operator=(CurlHandle &&) = delete; + + ~CurlHandle(); + + private: + CurlHandle(); +}; + +#endif diff --git a/HTTP/HTTPClient.cpp b/HTTP/HTTPClient.cpp index 462170c..65bdc1b 100644 --- a/HTTP/HTTPClient.cpp +++ b/HTTP/HTTPClient.cpp @@ -7,9 +7,7 @@ #include "HTTPClient.h" // Static members initialization -volatile int CHTTPClient::s_iCurlSession = 0; std::string CHTTPClient::s_strCertificationAuthorityFile; -std::mutex CHTTPClient::s_mtxCurlSession; #ifdef DEBUG_CURL std::string CHTTPClient::s_strCurlTraceLogDirectory; @@ -29,15 +27,10 @@ CHTTPClient::CHTTPClient(LogFnCallback Logger) : m_bProgressCallbackSet(false), m_eSettingsFlags(ALL_FLAGS), m_pCurlSession(nullptr), - m_pHeaderlist(nullptr) + m_pHeaderlist(nullptr), + m_curlHandle(CurlHandle::instance()) { - s_mtxCurlSession.lock(); - if (s_iCurlSession++ == 0) - { - /* In windows, this will init the winsock stuff */ - curl_global_init(CURL_GLOBAL_ALL); - } - s_mtxCurlSession.unlock(); + } /** @@ -53,13 +46,6 @@ CHTTPClient::~CHTTPClient() CleanupSession(); } - - s_mtxCurlSession.lock(); - if (--s_iCurlSession <= 0) - { - curl_global_cleanup(); - } - s_mtxCurlSession.unlock(); } /** @@ -184,7 +170,7 @@ const bool CHTTPClient::CleanupSession() * * @param [in] strURL user URI */ -inline void CHTTPClient::CheckURL(const std::string& strURL) +inline void CHTTPClient::UpdateURL(const std::string& strURL) { std::string strTmp = strURL; @@ -256,18 +242,14 @@ const CURLcode CHTTPClient::Perform() curl_easy_setopt(m_pCurlSession, CURLOPT_NOPROGRESS, 0L); } - // SSL if (m_bHTTPS) { - curl_easy_setopt(m_pCurlSession, CURLOPT_USE_SSL, CURLUSESSL_ALL); + // SSL (TLS) + curl_easy_setopt(m_pCurlSession, CURLOPT_USE_SSL, CURLUSESSL_ALL); + curl_easy_setopt(m_pCurlSession, CURLOPT_SSL_VERIFYPEER, (m_eSettingsFlags & VERIFY_PEER) ? 1L : 0L); + curl_easy_setopt(m_pCurlSession, CURLOPT_SSL_VERIFYPEER, (m_eSettingsFlags & CURLOPT_SSL_VERIFYHOST) ? 2L : 0L); } - if (m_bHTTPS && !(m_eSettingsFlags & VERIFY_PEER)) - curl_easy_setopt(m_pCurlSession, CURLOPT_SSL_VERIFYPEER, 0L); - - if (m_bHTTPS && !(m_eSettingsFlags & VERIFY_HOST)) - curl_easy_setopt(m_pCurlSession, CURLOPT_SSL_VERIFYHOST, 0L); // use 2L for strict name check - if (m_bHTTPS && !s_strCertificationAuthorityFile.empty()) curl_easy_setopt(m_pCurlSession, CURLOPT_CAINFO, s_strCertificationAuthorityFile.c_str()); @@ -303,7 +285,7 @@ const CURLcode CHTTPClient::Perform() /** * @brief requests the content of a URI * - * @param [in] strURL URI of the remote location (with the file name). + * @param [in] strURL URI of the remote location (with the file name) encoded in UTF-8 format. * @param [out] strOutput reference to an output string. * @param [out] lHTTPStatusCode HTTP Status code of the response. * @@ -338,7 +320,7 @@ const bool CHTTPClient::GetText(const std::string& strURL, // Reset is mandatory to avoid bad surprises curl_easy_reset(m_pCurlSession); - CheckURL(strURL); + UpdateURL(strURL); curl_easy_setopt(m_pCurlSession, CURLOPT_HTTPGET, 1L); curl_easy_setopt(m_pCurlSession, CURLOPT_WRITEFUNCTION, WriteInStringCallback); @@ -364,8 +346,8 @@ const bool CHTTPClient::GetText(const std::string& strURL, /** * @brief Downloads a remote file to a local file. * - * @param [in] strLocalFile Complete path of the local file to download. - * @param [in] strURL URI of the remote location (with the file name). + * @param [in] strLocalFile Complete path of the local file to download in UTF-8 format. + * @param [in] strURL URI of the remote location (with the file name) encoded in UTF-8 format. * @param [out] lHTTPStatusCode HTTP Status code of the response. * * @retval true Successfully downloaded the file. @@ -388,10 +370,16 @@ const bool CHTTPClient::DownloadFile(const std::string& strLocalFile, // Reset is mandatory to avoid bad surprises curl_easy_reset(m_pCurlSession); - CheckURL(strURL); + UpdateURL(strURL); std::ofstream ofsOutput; - ofsOutput.open(strLocalFile, std::ofstream::out | std::ofstream::binary | std::ofstream::trunc); + ofsOutput.open( +#ifdef LINUX + strLocalFile, // UTF-8 +#else + Utf8ToUtf16(strLocalFile), +#endif + std::ofstream::out | std::ofstream::binary | std::ofstream::trunc); if (ofsOutput) { @@ -431,11 +419,61 @@ const bool CHTTPClient::DownloadFile(const std::string& strLocalFile, return true; } +/** + * @brief downloads a remote file to memory + * + * @param [out] data vector of bytes + * @param [in] strURL URI of the remote location (with the file name) encoded in UTF-8 format. + * @param [out] lHTTPStatusCode HTTP Status code of the response. + * + * @retval true Successfully downloaded the file. + * @retval false The content couldn't be downloaded. Check the log messages for + * more information. + */ +const bool CHTTPClient::DownloadFile(std::vector& data, const std::string& strURL, long& lHTTPStatusCode) { + if (strURL.empty()) + return false; + + if (!m_pCurlSession) + { + if (m_eSettingsFlags & ENABLE_LOG) + m_oLog(LOG_ERROR_CURL_NOT_INIT_MSG); + + return false; + } + + data.clear(); + + // Reset is mandatory to avoid bad surprises + curl_easy_reset(m_pCurlSession); + + UpdateURL(strURL); + + curl_easy_setopt(m_pCurlSession, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(m_pCurlSession, CURLOPT_WRITEFUNCTION, WriteToMemoryCallback); + curl_easy_setopt(m_pCurlSession, CURLOPT_WRITEDATA, &data); + + CURLcode res = Perform(); + + curl_easy_getinfo(m_pCurlSession, CURLINFO_RESPONSE_CODE, &lHTTPStatusCode); + + if (res != CURLE_OK) + { + if (m_eSettingsFlags & ENABLE_LOG) + m_oLog(StringFormat(LOG_ERROR_CURL_DOWNLOAD_FAILURE_FORMAT, "Download to a byte buffer", + strURL.c_str(), res, curl_easy_strerror(res), lHTTPStatusCode)); + + return false; + } + + return true; +} + /** * @brief uploads a POST form * * - * @param [in] strURL URL to which the form will be posted. + * @param [in] strURL URL to which the form will be posted encoded in UTF-8 format. * @param [in] data post form information * @param [out] lHTTPStatusCode HTTP Status code of the response. * @@ -463,7 +501,7 @@ const bool CHTTPClient::UploadForm(const std::string& strURL, // Reset is mandatory to avoid bad surprises curl_easy_reset(m_pCurlSession); - CheckURL(strURL); + UpdateURL(strURL); /** Now specify we want to POST data */ curl_easy_setopt(m_pCurlSession, CURLOPT_POST, 1L); @@ -521,8 +559,8 @@ CHTTPClient::PostFormInfo::~PostFormInfo() /** * @brief set the name and the value of the HTML "file" form's input * - * @param fieldName name of the "file" input - * @param fieldValue path to the file to upload + * @param fieldName name of the "file" input encoded in UTF8. + * @param fieldValue path to the file to upload encoded in UTF8. */ void CHTTPClient::PostFormInfo::AddFormFile(const std::string& strFieldName, const std::string& strFieldValue) @@ -537,8 +575,8 @@ void CHTTPClient::PostFormInfo::AddFormFile(const std::string& strFieldName, * @brief set the name and the value of an HTML form's input * (other than "file" like "text", "hidden" or "submit") * - * @param fieldName name of the input element - * @param fieldValue value to be assigned to the input element + * @param fieldName name of the input element encoded in UTF8 for Linux and in ANSI for Windows (so the file gets located and uploaded). + * @param fieldValue value to be assigned to the input element encoded in UTF8 for Linux and in ANSI for Windows. */ void CHTTPClient::PostFormInfo::AddFormContent(const std::string& strFieldName, const std::string& strFieldValue) @@ -556,6 +594,7 @@ void CHTTPClient::PostFormInfo::AddFormContent(const std::string& strFieldName, * some common operations to REST requests are performed here, * the others are performed in Perform method * +* @param [in] strUrl URI encoded in UTF-8 format. * @param [in] Headers headers to send * @param [out] Response response data */ @@ -580,7 +619,7 @@ inline const bool CHTTPClient::InitRestRequest(const std::string& strUrl, // Reset is mandatory to avoid bad surprises curl_easy_reset(m_pCurlSession); - CheckURL(strUrl); + UpdateURL(strUrl); // set the received body's callback function curl_easy_setopt(m_pCurlSession, CURLOPT_WRITEFUNCTION, &CHTTPClient::RestWriteCallback); @@ -637,7 +676,7 @@ inline const bool CHTTPClient::PostRestRequest(const CURLcode ePerformCode, /** * @brief performs a HEAD request * -* @param [in] strUrl url to request +* @param [in] strUrl url to request encoded in UTF-8 format. * @param [in] Headers headers to send * @param [out] Response response data * @@ -665,7 +704,7 @@ const bool CHTTPClient::Head(const std::string& strUrl, /** * @brief performs a GET request * -* @param [in] strUrl url to request +* @param [in] strUrl url to request encoded in UTF-8 format. * @param [in] Headers headers to send * @param [out] Response response data * @@ -692,7 +731,7 @@ const bool CHTTPClient::Get(const std::string& strUrl, /** * @brief performs a DELETE request * -* @param [in] strUrl url to request +* @param [in] strUrl url to request encoded in UTF-8 format. * @param [in] Headers headers to send * @param [out] Response response data * @@ -740,7 +779,7 @@ const bool CHTTPClient::Post(const std::string& strUrl, /** * @brief performs a PUT request with a string * -* @param [in] strUrl url to request +* @param [in] strUrl url to request encoded in UTF-8 format. * @param [in] Headers headers to send * @param [out] Response response data * @@ -780,7 +819,7 @@ const bool CHTTPClient::Put(const std::string& strUrl, const CHTTPClient::Header /** * @brief performs a PUT request with a byte buffer (vector of char) * -* @param [in] strUrl url to request +* @param [in] strUrl url to request encoded in UTF-8 format. * @param [in] Headers headers to send * @param [out] Response response data * @@ -820,52 +859,24 @@ const bool CHTTPClient::Put(const std::string& strUrl, const CHTTPClient::Header // STRING HELPERS /** -* @brief returns a formatted string -* -* @param [in] strFormat string with one or many format specifiers -* @param [in] parameters to be placed in the format specifiers of strFormat -* -* @retval string formatted string -*/ -std::string CHTTPClient::StringFormat(const std::string strFormat, ...) -{ - int n = (static_cast(strFormat.size())) * 2; // Reserve two times as much as the length of the strFormat - - std::unique_ptr pFormatted; - - va_list ap; - - while(true) - { - pFormatted.reset(new char[n]); // Wrap the plain char array into the unique_ptr - strcpy(&pFormatted[0], strFormat.c_str()); - - va_start(ap, strFormat); - int iFinaln = vsnprintf(&pFormatted[0], n, strFormat.c_str(), ap); - va_end(ap); - - if (iFinaln < 0 || iFinaln >= n) - { - n += abs(iFinaln - n + 1); - } - else - { - break; - } - } - - return std::string(pFormatted.get()); -} -/*std::string CHTTPClient::StringFormat(const std::string strFormat, ...) -{ - char buffer[1024]; - va_list args; - va_start(args, strFormat); - vsnprintf(buffer, 1024, strFormat.c_str(), args); - va_end (args); - return std::string(buffer); + * @brief returns a formatted string + * + * @param [in] strFormat string with one or many format specifiers + * @param [in] parameters to be placed in the format specifiers of strFormat + * + * @retval string formatted string + */ +std::string CHTTPClient::StringFormat(std::string strFormat, ...) { + va_list args; + va_start(args, strFormat); + size_t len = std::vsnprintf(NULL, 0, strFormat.c_str(), args); + va_end(args); + std::vector vec(len + 1); + va_start(args, strFormat); + std::vsnprintf(&vec[0], len + 1, strFormat.c_str(), args); + va_end(args); + return &vec[0]; } -*/ /** * @brief removes leading and trailing whitespace from a string @@ -943,6 +954,27 @@ size_t CHTTPClient::WriteToFileCallback(void* buff, size_t size, size_t nmemb, v return size * nmemb; } +/** + * @brief stores the server response in std::vector + * + * @param buff pointer of max size (size*nmemb) to read data from it + * @param size size parameter + * @param nmemb memblock parameter + * @param userdata pointer to user data (file stream) + * + * @return (size * nmemb) + */ +size_t CHTTPClient::WriteToMemoryCallback(void* buff, size_t size, size_t nmemb, void* data) { + if ((size == 0) || (nmemb == 0) || (data == nullptr)) return 0; + + auto* vec = reinterpret_cast *>(data); + size_t ssize = size * nmemb; + std::copy(reinterpret_cast(buff), reinterpret_cast(buff) + ssize, + std::back_inserter(*vec)); + + return ssize; +} + /** * @brief reads the content of an already opened file stream * used by UploadFile() @@ -1166,3 +1198,29 @@ void CHTTPClient::EndCurlDebug() const } } #endif + +#ifdef WINDOWS +std::string CHTTPClient::AnsiToUtf8(const std::string& codepage_str) { + // Transcode Windows ANSI to UTF-16 + int size = MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, codepage_str.c_str(), codepage_str.length(), nullptr, 0); + std::wstring utf16_str(size, '\0'); + MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, codepage_str.c_str(), codepage_str.length(), &utf16_str[0], size); + + // Transcode UTF-16 to UTF-8 + int utf8_size = WideCharToMultiByte(CP_UTF8, 0, utf16_str.c_str(), utf16_str.length(), nullptr, 0, nullptr, nullptr); + std::string utf8_str(utf8_size, '\0'); + WideCharToMultiByte(CP_UTF8, 0, utf16_str.c_str(), utf16_str.length(), &utf8_str[0], utf8_size, nullptr, nullptr); + + return utf8_str; +} + +std::wstring CHTTPClient::Utf8ToUtf16(const std::string& str) { + std::wstring ret; + int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), str.length(), NULL, 0); + if (len > 0) { + ret.resize(len); + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), str.length(), &ret[0], len); + } + return ret; +} +#endif diff --git a/HTTP/HTTPClient.h b/HTTP/HTTPClient.h index d7b87b7..b3f9015 100644 --- a/HTTP/HTTPClient.h +++ b/HTTP/HTTPClient.h @@ -34,6 +34,8 @@ #include #include +#include "CurlHandle.h" + class CHTTPClient { public: @@ -120,7 +122,6 @@ class CHTTPClient const bool InitSession(const bool& bHTTPS = false, const SettingsFlag& SettingsFlags = ALL_FLAGS); virtual const bool CleanupSession(); - static int GetCurlSessionCount() { return s_iCurlSession; } const CURL* GetCurlPointer() const { return m_pCurlSession; } // HTTP requests @@ -132,6 +133,8 @@ class CHTTPClient const std::string& strURL, long& lHTTPStatusCode); + const bool DownloadFile(std::vector& data, const std::string& strURL, long& lHTTPStatusCode); + const bool UploadForm(const std::string& strURL, const PostFormInfo& data, long& lHTTPStatusCode); @@ -169,6 +172,11 @@ class CHTTPClient static void SetCurlTraceLogDirectory(const std::string& strPath); #endif +#ifdef WINDOWS + static std::string AnsiToUtf8(const std::string& ansiStr); + static std::wstring Utf8ToUtf16(const std::string& str); +#endif + protected: // payload to upload on POST requests. struct UploadObject @@ -180,7 +188,7 @@ class CHTTPClient /* common operations are performed here */ inline const CURLcode Perform(); - inline void CheckURL(const std::string& strURL); + inline void UpdateURL(const std::string& strURL); inline const bool InitRestRequest(const std::string& strUrl, const HeadersMap& Headers, HttpResponse& Response); inline const bool PostRestRequest(const CURLcode ePerformCode, HttpResponse& Response); @@ -188,6 +196,7 @@ class CHTTPClient // Curl callbacks static size_t WriteInStringCallback(void* ptr, size_t size, size_t nmemb, void* data); static size_t WriteToFileCallback(void* ptr, size_t size, size_t nmemb, void* data); + static size_t WriteToMemoryCallback(void* ptr, size_t size, size_t nmemb, void* data); static size_t ReadFromFileCallback(void* ptr, size_t size, size_t nmemb, void* stream); static size_t ThrowAwayCallback(void* ptr, size_t size, size_t nmemb, void* data); static size_t RestWriteCallback(void* ptr, size_t size, size_t nmemb, void* userdata); @@ -219,9 +228,6 @@ class CHTTPClient std::string m_strSSLCertFile; std::string m_strSSLKeyFile; std::string m_strSSLKeyPwd; - - static std::mutex s_mtxCurlSession; // mutex used to manage API global operations - volatile static int s_iCurlSession; // Count of the actual sessions CURL* m_pCurlSession; int m_iCurlTimeout; @@ -239,8 +245,14 @@ class CHTTPClient static std::string s_strCurlTraceLogDirectory; mutable std::ofstream m_ofFileCurlTrace; #endif + + CurlHandle& m_curlHandle; }; +inline CHTTPClient::SettingsFlag operator|(CHTTPClient::SettingsFlag a, CHTTPClient::SettingsFlag b) { + return static_cast(static_cast(a) | static_cast(b)); +} + // Logs messages #define LOG_ERROR_EMPTY_HOST_MSG "[HTTPClient][Error] Empty hostname." #define LOG_WARNING_OBJECT_NOT_CLEANED "[HTTPClient][Warning] Object was freed before calling " \ diff --git a/README.md b/README.md index 49bd889..c4b60f0 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ Compilation has been tested with: Underlying libraries: - [libcurl](http://curl.haxx.se/libcurl/) +Windows Users : vcpkg (Microsoft C++ Library Manager) can be used to easily install libcurl and generate the Visual Studio solution with CMake. With vcpkg, no need to manually copy the DLL in the output directory, vcpkg handles all that ! Look at "Building under Windows via Visual Studio" section, for instructions. + ## Usage Create an object and provide to its constructor a callable object (for log printing) having this signature : @@ -170,11 +172,6 @@ The unit test "TestDownloadFile" demonstrates how to use a progress function to ## Thread Safety -A mutex is used to increment/decrement atomically the count of CHTTPClient objects. - -`curl_global_init` is called when the count of CHTTPClient objects equals to zero (when instanciating the first object). -`curl_global_cleanup` is called when the count of CHTTPClient objects become zero (when all CHTTPClient objects are destroyed). - Do not share CHTTPClient objects across threads as this would mean accessing libcurl handles from multiple threads at the same time which is not allowed. @@ -202,8 +199,7 @@ HTTPClient.GetText("https://www.google.com", strWebPage, lWebPageCode); ``` ## Installation -You will need CMake to generate a makefile for the static library or to build the tests/code coverage -program. +You will need CMake to generate a makefile for the static library or to build the tests/code coverage program. Also make sure you have libcurl and Google Test installed. @@ -211,32 +207,113 @@ You can follow this script https://gist.github.com/fideloper/f72997d2e2c9fbe6645 This tutorial will help you installing properly Google Test on Ubuntu: https://www.eriksmistad.no/getting-started-with-google-test-on-ubuntu/ -The CMake script located in the tree will produce a makefile for the creation of a static library, -whereas the one under TestHTTP will produce the unit tests program. +The CMake script located in the tree will produce Makefiles for the creation of the static library and for the unit tests program. -To create a debug static library, change directory to the one containing the first CMakeLists.txt +To create a debug static library and a test binary, change directory to the one containing the first CMakeLists.txt and : ```Shell -cmake . -DCMAKE_BUILD_TYPE:STRING=Debug +mkdir build +cd build +cmake .. -DCMAKE_BUILD_TYPE:STRING=Debug make ``` To create a release static library, just change "Debug" by "Release". -The library will be found under lib/[BUILD_TYPE]/libHTTPClient.a +The library will be found under "build/[Debug|Release]/lib/libhttpclient.a" whereas the test program will be located in "build/[Debug|Release]/bin/test_httpclient" + +To directly run the unit test binary, you must indicate the path of the INI conf file (see the section below) +```Shell +./[Debug|Release]/bin/test_httpclient /path_to_your_ini_file/conf.ini +``` -For the unit tests program, first build the static library and use the same build type when -building it : +### Building under Windows via Visual Studio +1. New Procedure (with vcpkg) : + +Install [vcpkg](https://github.com/microsoft/vcpkg) then install libcurl (use 'x86-windows' for the 32-bit version) : ```Shell -cd TestHTTP/ -cmake . -DCMAKE_BUILD_TYPE=Debug # or Release -make +.\vcpkg install curl curl[openssl] --triplet=x64-windows +``` + +If you have a french Visual Studio version, don't forget to install the english language pack (vcpkg will tell you this anyway). + +Download and install the latest version of CMake : https://cmake.org/download/ (e.g. Windows win64-x64 Installer) + +Open CMake (cmake-gui) + +In "Where is the source code", put the httpclient-cpp path (e.g. C:/Users/Amine/Documents/Work/PROJECTS/GitHub/httpclient-cpp), where the main CMakeLists.txt file exist. + +In "Where to build binaries", paste the directory where you want to build the project (e.g. C:/Users/Amine/Documents/Work/PROJECTS/GitHub/httpclient_build) + +Click on "Configure". + +Select your Visual Studio version (if it isn't already set). +In "Optional platform for generator", you can leave it empty (x64 by default) or choose another value. + +Click on the radio button "Specify toolchain file for cross-compiling, then hit the "Next" button. + +In "Specify the toolchain file", browse to vcpkg toolchain file (vcpkg/scripts/buildsystems/vcpkg.cmake) and select it. + +Press "Finish", wait until CMake configures the project then hit "Generate" to create the Visual Studio solution (library and unit test binary). + +2. Old Procedure (without vcpkg) : + +First of all, build libcurl using this fork of build-libcurl-windows : https://github.com/ribtoks/build-libcurl-windows + +In fact, this fork will allow you to build libcurl under Visual Studio 2017. + +```Shell +git clone https://github.com/ribtoks/build-libcurl-windows.git ``` -To run it, you must indicate the path of the INI conf file (see the section below) +Then just build libcurl using : + ```Shell -./bin/[BUILD_TYPE]/test_httpclient /path_to_your_ini_file/conf.ini +build.bat +``` + +This batch script will automatically download the latest libcurl source code and build it using the most recent Visual Studio compiler +that it will find on your computer. For a particular version, you can modify the batch script... + +Under YOUR_DIRECTORY\build-libcurl-windows\third-party\libcurl, you will find the curl include directory and a lib directory containing different type of libraries : dynamic and static x86 and x64 libraries compiled in Debug and Release mode (8 libraries). Later, we will be using the libraries located in lib\dll-debug-x64 and lib\dll-release-x64 as an example. + +Concerning Google Test, the library will be downloaded and built automatically from its github repository. Someone with enough free time can do the same for libcurl and submit a pull request... + +Download and install the latest version of CMake : https://cmake.org/download/ (e.g. Windows win64-x64 Installer) + +Open CMake (cmake-gui) + +In "Where is the source code", put the httpclient-cpp path (e.g. C:/Users/Amine/Documents/Work/PROJECTS/GitHub/httpclient-cpp), where the main CMakeLists.txt file exist. + +In "Where to build binaries", paste the directory where you want to build the project (e.g. C:/Users/Amine/Documents/Work/PROJECTS/GitHub/httpclient_build) + +Click on "Configure". After some time, an error message will be shown, that's normal as CMake is unable to find libcurl. + +Make sure "Advanced" checkbox is checked, in the list, locate the variables prefixed with "CURL_" and update them with the path of libcurl include directory and libraries, for example : + +CURL_INCLUDE_DIR C:\LIBS\build-libcurl-windows\third-party\libcurl\include +CURL_LIBRARY_DEBUG C:\LIBS\build-libcurl-windows\third-party\libcurl\lib\dll-debug-x64\libcurl_debug.lib +CURL_LIBRARY_RELEASE C:\LIBS\build-libcurl-windows\third-party\libcurl\lib\dll-release-x64\libcurl.lib + +Click on "Configure" again ! You should not have errors this time. You can ignore the warning related to the test configuration file. + +Then click on "Generate", you can choose a Visual Studio version if it is not done before (e.g. Visual Studio 15 2017 Win64, I think it should be the same version used to build libcurl...) + +Finally, click on "Open Project" to open the solution in Visual Studio. + +In Visual Studio, you can change the build type (Debug -> Release). Build the solution (press F7). It must succeed without any errors. You can close Visual Studio. + +The library will be found under C:\Users\Amine\Documents\Work\PROJECTS\GitHub\httpclient_build\lib\Release\httpclient.lib + +After building a program using "hhtpclient.lib", do not forget to copy libcurl DLL in the directory where the program binary is located. + +For example, in the build directory (e.g. C:\Users\Amine\Documents\Work\PROJECTS\GitHub\httpclient_build), under "bin", directory, you may find "Debug", "Release" or both according to the build type used during the build in Visual Studio, and in it, the test program "test_httpclient.exe". Before executing it, make sure to copy the libcurl DLL in the same directory (e.g. copy C:\LIBS\build-libcurl-windows\third-party\libcurl\lib\dll-release-x64\libcurl.dll and the PDB file too if you want, do not change the name of the DLL !) The type of the library MUST correspond to the type of the .lib file fed to CMake-gui ! + +If you want to run the test program, in the command line, launch http_httpclient.exe with the path of you test configuration file (INI) : + +```Shell +C:\Users\Amine\Documents\Work\PROJECTS\GitHub\httpclient_build\bin\[Debug | Release]\httpclient.exe PATH_TO_YOUR_TEST_CONF_FILE\conf.ini ``` ## Run Unit Tests @@ -258,12 +335,24 @@ host=127.0.0.1:3128 host_invalid=127.0.0.1:6666 ``` -You can also generate an XML file of test results by adding this argument when calling the test program +You can also generate an XML file of test results by adding --getst_output argument when calling the test program ```Shell -./bin/[BUILD_TYPE]/test_httpclient /path_to_your_ini_file/conf.ini --gtest_output="xml:./TestHTTP.xml" +./[Debug|Release]/bin/test_httpclient /path_to_your_ini_file/conf.ini --gtest_output="xml:./TestHTTP.xml" ``` +An alternative way to compile and run unit tests : + +```Shell +mkdir build +cd build +cmake .. -DCMAKE_BUILD_TYPE=Debug -DTEST_INI_FILE="full_or_relative_path_to_your_test_conf.ini" +make +make test +``` + +You may use a tool like https://github.com/adarmalik/gtest2html to convert your XML test result in an HTML file. + ## Memory Leak Check Visual Leak Detector has been used to check memory leaks with the Windows build (Visual Sutdio 2015) @@ -272,29 +361,27 @@ You can download it here: https://vld.codeplex.com/ To perform a leak check with the Linux build, you can do so : ```Shell -valgrind --leak-check=full ./bin/Debug/test_httpclient /path_to_ini_file/conf.ini +valgrind --leak-check=full ./Debug/bin/test_httpclient /path_to_ini_file/conf.ini ``` ## Code Coverage The code coverage build doesn't use the static library but compiles and uses directly the -HTTPClient-C++ API in the test program. - -First of all, in TestHTTP/CMakeLists.txt, find and repalce : -``` -"/home/amzoughi/Test/http_github.ini" -``` -by the location of your ini file and launch the code coverage : +HTTP Client API in the test program. ```Shell -cd TestHTTP/ -cmake . -DCMAKE_BUILD_TYPE=Coverage +mkdir build +cd build +cmake .. -DCMAKE_BUILD_TYPE=Coverage -DCOVERAGE_INI_FILE:STRING="full_path_to_your_test_conf.ini" make make coverage_httpclient ``` If everything is OK, the results will be found under ./TestHTTP/coverage/index.html +Make sure you feed CMake with a full path to your test conf INI file, otherwise, the coverage test +will be useless. + Under Visual Studio, you can simply use OpenCppCoverage (https://opencppcoverage.codeplex.com/) ## CppCheck Compliancy @@ -314,3 +401,9 @@ Try to preserve the existing coding style (Hungarian notation, indentation etc.. If you compile the test program with the preprocessor macro DEBUG_CURL, to enable curl debug informations, the static library used must also be compiled with that macro. Don't forget to mention a path where to store log files in the INI file if you want to use that feature in the unit test program (curl_logs_folder under [local]) + +### File names format when compiling with Visual Studio (Windows users) + +It is assumed that the FTP server is supporting UTF-8. You must feed the FTP client API with paths/file names encoded in UTF-8 and NOT in ANSI (Windows-1252 on Western/U.S. systems but iy can represent certain other Windows code pages on other systems, ANSI is just an extension for ASCII). Look at the unit tests for examples (look for the preprocessor macro WINDOWS to find them quickly). + +If you limit to ASCII characters, you don't need to convert your ANSI strings to UTF-8. diff --git a/TestHTTP/CMakeLists.txt b/TestHTTP/CMakeLists.txt index 7304194..7e18db8 100644 --- a/TestHTTP/CMakeLists.txt +++ b/TestHTTP/CMakeLists.txt @@ -1,10 +1,3 @@ -cmake_minimum_required(VERSION 2.6) - -project(TestHTTPClient) -set(EXECUTABLE_OUTPUT_PATH bin/${CMAKE_BUILD_TYPE}) -set(CMAKE_CXX_STANDARD 14) # c++14 -add_definitions(-DLINUX) - # Code coverage setup IF(CMAKE_BUILD_TYPE MATCHES Coverage) INCLUDE(CodeCoverage.cmake) @@ -12,21 +5,20 @@ IF(CMAKE_BUILD_TYPE MATCHES Coverage) SET(CMAKE_C_FLAGS "-g -O0 -fprofile-arcs -ftest-coverage") ENDIF(CMAKE_BUILD_TYPE MATCHES Coverage) -# Locate GTest -find_package(GTest REQUIRED) -include_directories(${GTEST_INCLUDE_DIRS}) - # Locate libcURL find_package(CURL REQUIRED) include_directories(${CURL_INCLUDE_DIRS}) +# Locate GTest +find_package(GTest REQUIRED) +include_directories(${GTEST_INCLUDE_DIRS}) # useless but test before removing it + include_directories(../HTTP) include_directories(./simpleini) include_directories(./rapidjson) include_directories(./) - -IF(CMAKE_BUILD_TYPE MATCHES Coverage) +IF(NOT MSVC AND CMAKE_BUILD_TYPE MATCHES Coverage) file(GLOB_RECURSE http_source_files ../HTTP/*) @@ -42,17 +34,22 @@ SETUP_TARGET_FOR_COVERAGE( # NOTE! This should always have a ZERO as exit code # otherwise the coverage generation will not complete. coverage # Name of output directory. - "/home/amzoughi/Test/http_github.ini" # Optional fourth parameter is passed as arguments to _testrunner - # Pass them in list form, e.g.: "-j;2" for -j 2 + ${COVERAGE_INI_FILE} # Optional fourth parameter is passed as arguments to _testrunner + # Pass them in list form, e.g.: "-j;2" for -j 2 ) -ELSE(CMAKE_BUILD_TYPE MATCHES Coverage) -link_directories(../lib/${CMAKE_BUILD_TYPE}) +ELSE() + +#link_directories(${CMAKE_BINARY_DIR}/lib) #Output Setup add_executable(test_httpclient main.cpp test_utils.cpp) #Link setup -target_link_libraries(test_httpclient httpclient ${GTEST_LIBRARIES} pthread curl) +if(NOT MSVC) + target_link_libraries(test_httpclient httpclient ${GTEST_LIBRARIES} pthread curl) +else() + target_link_libraries(test_httpclient httpclient ${GTEST_LIBRARIES} ${CURL_LIBRARIES}) +endif() -ENDIF(CMAKE_BUILD_TYPE MATCHES Coverage) +ENDIF() diff --git a/TestHTTP/main.cpp b/TestHTTP/main.cpp index ba7ba24..ca8d9ed 100644 --- a/TestHTTP/main.cpp +++ b/TestHTTP/main.cpp @@ -121,10 +121,10 @@ TEST(HTTPClient, TestSession) EXPECT_EQ(CHTTPClient::SettingsFlag::ALL_FLAGS, HTTPClient.GetSettingsFlags()); /* after init. a session */ - ASSERT_TRUE(HTTPClient.InitSession(true, CHTTPClient::ENABLE_LOG)); + ASSERT_TRUE(HTTPClient.InitSession(true, CHTTPClient::ENABLE_LOG | CHTTPClient::VERIFY_PEER)); // check that the parameters provided to InitSession - EXPECT_EQ(CHTTPClient::ENABLE_LOG, HTTPClient.GetSettingsFlags()); + EXPECT_EQ(CHTTPClient::ENABLE_LOG | CHTTPClient::VERIFY_PEER, HTTPClient.GetSettingsFlags()); EXPECT_TRUE(HTTPClient.GetHTTPS()); EXPECT_TRUE(HTTPClient.GetCurlPointer() != nullptr); @@ -180,7 +180,6 @@ TEST(HTTPClient, TestCleanUpWithoutInit) TEST(HTTPClient, TestMultithreading) { const char* arrDataArray[3] = { "Thread 1", "Thread 2", "Thread 3" }; - unsigned uInitialCount = CHTTPClient::GetCurlSessionCount(); auto ThreadFunction = [](const char* pszThreadName) { @@ -201,8 +200,6 @@ TEST(HTTPClient, TestMultithreading) FirstThread.join(); // pauses until first finishes SecondThread.join(); // pauses until second finishes ThirdThread.join(); // pauses until third finishes - - ASSERT_EQ(uInitialCount, CHTTPClient::GetCurlSessionCount()); } // HTTP tests using a fixture @@ -226,6 +223,7 @@ TEST_F(HTTPTest, TestDownloadFile) // to display a beautiful progress bar on console m_pHTTPClient->SetProgressFnCallback(nullptr, &TestProgressCallback); +#ifdef LINUX ASSERT_TRUE(m_pHTTPClient->DownloadFile("test.pem", "https://curl.haxx.se/ca/cacert.pem", lHTTPSCode)); /* to properly show the progress bar */ std::cout << std::endl; @@ -236,6 +234,36 @@ TEST_F(HTTPTest, TestDownloadFile) /* delete test file */ EXPECT_TRUE(remove("test.pem") == 0); +#else + // Convert file name from ANSI to UTF8 + std::string localFile = CHTTPClient::AnsiToUtf8("test_nom_accentué.pem"); + + ASSERT_TRUE(m_pHTTPClient->DownloadFile(localFile, "https://curl.haxx.se/ca/cacert.pem", lHTTPSCode)); + /* to properly show the progress bar */ + std::cout << std::endl; + + /* TODO : we can check the SHA1 of the downloaded file with a value provided in the INI file */ + + EXPECT_EQ(200, lHTTPSCode); + + /* delete test file */ + EXPECT_TRUE(remove("test_nom_accentué.pem") == 0); +#endif +} + +TEST_F(HTTPTest, TestDownloadFileToMemory) +{ + std::vector output; + long lHTTPSCode = 0; + + // to display a beautiful progress bar on console + m_pHTTPClient->SetProgressFnCallback(nullptr, &TestProgressCallback); + + ASSERT_TRUE(m_pHTTPClient->DownloadFile(output, "https://curl.haxx.se/ca/cacert.pem", lHTTPSCode)); + /* to properly show the progress bar */ + std::cout << std::endl; + + EXPECT_EQ(200, lHTTPSCode); } // check for failure: inexistant file @@ -275,10 +303,8 @@ TEST_F(HTTPTest, TestUploadForm) UploadInfo.AddFormFile("submitted", fileName.str()); UploadInfo.AddFormContent("filename", fileName.str()); - /* The details of the upload of the dummy test file can be found under - http://posttestserver.com/data/[year]/[month]/[day]/restclientcpptests/ */ - m_pHTTPClient->UploadForm("http://posttestserver.com/post.php?dir=restclientcpptests", - UploadInfo, lResultHTTPCode); + /* Toilet : kv6od-1543167696 */ + m_pHTTPClient->UploadForm("http://ptsv2.com/t/kv6od-1543167696/post", UploadInfo, lResultHTTPCode); EXPECT_EQ(200, lResultHTTPCode); diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0