From 4d444fea011987a8b795edeba0ef5743cb6d7d46 Mon Sep 17 00:00:00 2001 From: Amine Mzoughi Date: Sun, 25 Nov 2018 18:18:11 +0100 Subject: [PATCH 01/14] Changed CMakeLists.txt to be able to build library and test binary withe a single make command in a custom build directory (classic way). --- .gitignore | 1 + CMakeLists.txt | 26 +++++++++++++++++--------- HTTP/CMakeLists.txt | 14 ++++++++++++++ TestHTTP/CMakeLists.txt | 19 ++++++++----------- 4 files changed, 40 insertions(+), 20 deletions(-) create mode 100644 HTTP/CMakeLists.txt 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..8377599 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,17 +1,25 @@ cmake_minimum_required(VERSION 2.6) -# 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}) +add_definitions(-DLINUX) include_directories(HTTP) -file(GLOB_RECURSE source_files HTTP/*) -add_library(httpclient STATIC ${source_files}) +add_subdirectory(HTTP) +add_subdirectory(TestHTTP) + +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}) diff --git a/HTTP/CMakeLists.txt b/HTTP/CMakeLists.txt new file mode 100644 index 0000000..bf511a0 --- /dev/null +++ b/HTTP/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 2.6) + +project(HTTPClient) + +IF(NOT CMAKE_BUILD_TYPE MATCHES Coverage) + +# Locate libcURL +find_package(CURL REQUIRED) +include_directories(${CURL_INCLUDE_DIRS}) + +file(GLOB_RECURSE source_files ./*) +add_library(httpclient STATIC ${source_files}) + +ENDIF() diff --git a/TestHTTP/CMakeLists.txt b/TestHTTP/CMakeLists.txt index 7304194..96f421f 100644 --- a/TestHTTP/CMakeLists.txt +++ b/TestHTTP/CMakeLists.txt @@ -1,9 +1,6 @@ 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) @@ -12,20 +9,19 @@ 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}) + include_directories(../HTTP) include_directories(./simpleini) include_directories(./rapidjson) include_directories(./) - IF(CMAKE_BUILD_TYPE MATCHES Coverage) file(GLOB_RECURSE http_source_files ../HTTP/*) @@ -42,12 +38,13 @@ 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}) +#link_directories(${CMAKE_BINARY_DIR}/lib) #Output Setup add_executable(test_httpclient main.cpp test_utils.cpp) From 473660ed547a397b064cda1e3da300a82557bf12 Mon Sep 17 00:00:00 2001 From: Amine Mzoughi Date: Sun, 25 Nov 2018 18:50:41 +0100 Subject: [PATCH 02/14] TestUploadForm will send dummy file to the toilet kv6od-1543167696 (visit: ptvs2.com to check if the file has been uploaded) --- TestHTTP/main.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/TestHTTP/main.cpp b/TestHTTP/main.cpp index ba7ba24..d258255 100644 --- a/TestHTTP/main.cpp +++ b/TestHTTP/main.cpp @@ -275,10 +275,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); From 6e10b6cddc80641c04269921642bf18f765ecb5b Mon Sep 17 00:00:00 2001 From: Amine Mzoughi Date: Sun, 25 Nov 2018 19:37:17 +0100 Subject: [PATCH 03/14] Updated build commands. --- README.md | 61 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 49bd889..0d1641b 100644 --- a/README.md +++ b/README.md @@ -202,8 +202,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 +210,24 @@ 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" -For the unit tests program, first build the static library and use the same build type when -building it : - -```Shell -cd TestHTTP/ -cmake . -DCMAKE_BUILD_TYPE=Debug # or Release -make -``` - -To run it, you must indicate the path of the INI conf file (see the section below) +To directly run the unit test binary, you must indicate the path of the INI conf file (see the section below) ```Shell -./bin/[BUILD_TYPE]/test_httpclient /path_to_your_ini_file/conf.ini +./[Debug|Release]/bin/test_httpclient /path_to_your_ini_file/conf.ini ``` ## Run Unit Tests @@ -258,12 +249,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 +275,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 From 1b16977f15eecaaf92089bf18d10a93b9d156f6f Mon Sep 17 00:00:00 2001 From: Amine Mzoughi Date: Sat, 26 Jan 2019 20:44:03 +0100 Subject: [PATCH 04/14] Allow the project to be generated with CMake into a Visual Studio solution. --- CMakeLists.txt | 7 +++++- HTTP/CMakeLists.txt | 2 +- TestHTTP/CMakeLists.txt | 50 +++++++++++++++++++++++++++++++++----- TestHTTP/CMakeLists.txt.in | 15 ++++++++++++ 4 files changed, 66 insertions(+), 8 deletions(-) create mode 100644 TestHTTP/CMakeLists.txt.in diff --git a/CMakeLists.txt b/CMakeLists.txt index 8377599..7037f6d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,13 +6,18 @@ 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) -add_definitions(-DLINUX) +if(NOT MSVC) + add_definitions(-DLINUX) +else() + add_definitions(-DWINDOWS) +endif() include_directories(HTTP) add_subdirectory(HTTP) add_subdirectory(TestHTTP) +include(CTest) enable_testing () # test if the test INI file exist, otherwise default it to the one in TestHTTP folder diff --git a/HTTP/CMakeLists.txt b/HTTP/CMakeLists.txt index bf511a0..985dbbe 100644 --- a/HTTP/CMakeLists.txt +++ b/HTTP/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 2.6) project(HTTPClient) -IF(NOT CMAKE_BUILD_TYPE MATCHES Coverage) +IF(MSVC OR NOT CMAKE_BUILD_TYPE MATCHES Coverage) # Locate libcURL find_package(CURL REQUIRED) diff --git a/TestHTTP/CMakeLists.txt b/TestHTTP/CMakeLists.txt index 96f421f..0eadc39 100644 --- a/TestHTTP/CMakeLists.txt +++ b/TestHTTP/CMakeLists.txt @@ -13,16 +13,50 @@ ENDIF(CMAKE_BUILD_TYPE MATCHES Coverage) find_package(CURL REQUIRED) include_directories(${CURL_INCLUDE_DIRS}) +# For Windows +# https://crascit.com/2015/07/25/cmake-gtest/ +if (MSVC) + # Download and unpack googletest at configure time + configure_file(CMakeLists.txt.in "${CMAKE_BINARY_DIR}/googletest-download/CMakeLists.txt") + execute_process(COMMAND "${CMAKE_COMMAND}" -G "${CMAKE_GENERATOR}" . + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/googletest-download" ) + execute_process(COMMAND "${CMAKE_COMMAND}" --build . + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/googletest-download" ) + + # Prevent GoogleTest from overriding our compiler/linker options + # when building with Visual Studio + set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) + + # Add googletest directly to our build. This adds + # the following targets: gtest, gtest_main, gmock + # and gmock_main + add_subdirectory("${CMAKE_BINARY_DIR}/googletest-src" + "${CMAKE_BINARY_DIR}/googletest-build") + + # The gtest/gmock targets carry header search path + # dependencies automatically when using CMake 2.8.11 or + # later. Otherwise we have to add them here ourselves. + if(CMAKE_VERSION VERSION_LESS 2.8.11) + include_directories("${gtest_SOURCE_DIR}/include" + "${gmock_SOURCE_DIR}/include") + endif() + + # Now simply link your own targets against gtest, gmock, + # etc. as appropriate +endif() + # Locate GTest -find_package(GTest REQUIRED) -include_directories(${GTEST_INCLUDE_DIRS}) +if(NOT MSVC) + find_package(GTest REQUIRED) + include_directories(${GTEST_INCLUDE_DIRS}) +endif() 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,7 +76,7 @@ SETUP_TARGET_FOR_COVERAGE( # Pass them in list form, e.g.: "-j;2" for -j 2 ) -ELSE(CMAKE_BUILD_TYPE MATCHES Coverage) +ELSE() #link_directories(${CMAKE_BINARY_DIR}/lib) @@ -50,6 +84,10 @@ ELSE(CMAKE_BUILD_TYPE MATCHES Coverage) 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 ${CURL_LIBRARIES}) +endif() -ENDIF(CMAKE_BUILD_TYPE MATCHES Coverage) +ENDIF() diff --git a/TestHTTP/CMakeLists.txt.in b/TestHTTP/CMakeLists.txt.in new file mode 100644 index 0000000..e700161 --- /dev/null +++ b/TestHTTP/CMakeLists.txt.in @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 2.8.2) + +project(googletest-download-for-windows NONE) + +include(ExternalProject) +ExternalProject_Add(googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG master + SOURCE_DIR "${CMAKE_BINARY_DIR}/googletest-src" + BINARY_DIR "${CMAKE_BINARY_DIR}/googletest-build" + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + TEST_COMMAND "" +) From 6dc2e3c3e19fe56dd1d454febb48415007fe0525 Mon Sep 17 00:00:00 2001 From: Amine Mzoughi Date: Sat, 26 Jan 2019 20:44:45 +0100 Subject: [PATCH 05/14] Added Windows build instructions. --- README.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/README.md b/README.md index 0d1641b..ec07ae4 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,67 @@ To directly run the unit test binary, you must indicate the path of the INI conf ./[Debug|Release]/bin/test_httpclient /path_to_your_ini_file/conf.ini ``` +### Building under Windows via Visual Studio + +This can be enhanced in future... + +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 +``` + +Then just build libcurl using : + +```Shell +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 [simpleini](https://github.com/brofield/simpleini) is used to gather unit tests parameters from From fdb84d0dce1936c23fda64984c050030a40bc673 Mon Sep 17 00:00:00 2001 From: Amine Mzoughi Date: Fri, 9 Aug 2019 11:41:39 +0200 Subject: [PATCH 06/14] Changed httpbin.org protocol scheme to make some unit tests succeed. --- TestHTTP/main.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/TestHTTP/main.cpp b/TestHTTP/main.cpp index d258255..f07f4c5 100644 --- a/TestHTTP/main.cpp +++ b/TestHTTP/main.cpp @@ -353,7 +353,7 @@ TEST_F(RestClientTest, TestRestClientGETBodyCode) rapidjson::Value::MemberIterator itTokenUrl = document.FindMember("url"); ASSERT_TRUE(itTokenUrl != document.MemberEnd()); ASSERT_TRUE(itTokenUrl->value.IsString()); - EXPECT_STREQ("http://httpbin.org/get", itTokenUrl->value.GetString()); + EXPECT_STREQ("https://httpbin.org/get", itTokenUrl->value.GetString()); rapidjson::Value::MemberIterator itTokenHeaders = document.FindMember("headers"); ASSERT_TRUE(itTokenHeaders != document.MemberEnd()); @@ -428,7 +428,7 @@ TEST_F(RestClientTest, TestRestClientPOSTBody) rapidjson::Value::MemberIterator itTokenUrl = document.FindMember("url"); ASSERT_TRUE(itTokenUrl != document.MemberEnd()); ASSERT_TRUE(itTokenUrl->value.IsString()); - EXPECT_STREQ("http://httpbin.org/post", itTokenUrl->value.GetString()); + EXPECT_STREQ("https://httpbin.org/post", itTokenUrl->value.GetString()); rapidjson::Value::MemberIterator itTokenHeaders = document.FindMember("headers"); ASSERT_TRUE(itTokenHeaders != document.MemberEnd()); @@ -473,7 +473,7 @@ TEST_F(RestClientTest, TestRestClientPUTString) rapidjson::Value::MemberIterator itTokenUrl = document.FindMember("url"); ASSERT_TRUE(itTokenUrl != document.MemberEnd()); ASSERT_TRUE(itTokenUrl->value.IsString()); - EXPECT_STREQ("http://httpbin.org/put", itTokenUrl->value.GetString()); + EXPECT_STREQ("https://httpbin.org/put", itTokenUrl->value.GetString()); rapidjson::Value::MemberIterator itTokenHeaders = document.FindMember("headers"); ASSERT_TRUE(itTokenHeaders != document.MemberEnd()); @@ -505,7 +505,7 @@ TEST_F(RestClientTest, TestRestClientPUTBuffer) rapidjson::Value::MemberIterator itTokenUrl = document.FindMember("url"); ASSERT_TRUE(itTokenUrl != document.MemberEnd()); ASSERT_TRUE(itTokenUrl->value.IsString()); - EXPECT_STREQ("http://httpbin.org/put", itTokenUrl->value.GetString()); + EXPECT_STREQ("https://httpbin.org/put", itTokenUrl->value.GetString()); rapidjson::Value::MemberIterator itTokenHeaders = document.FindMember("headers"); ASSERT_TRUE(itTokenHeaders != document.MemberEnd()); @@ -552,7 +552,7 @@ TEST_F(RestClientTest, TestRestClientDeleteBody) rapidjson::Value::MemberIterator itTokenUrl = document.FindMember("url"); ASSERT_TRUE(itTokenUrl != document.MemberEnd()); ASSERT_TRUE(itTokenUrl->value.IsString()); - EXPECT_STREQ("http://httpbin.org/delete", itTokenUrl->value.GetString()); + EXPECT_STREQ("https://httpbin.org/delete", itTokenUrl->value.GetString()); rapidjson::Value::MemberIterator itTokenHeaders = document.FindMember("headers"); ASSERT_TRUE(itTokenHeaders != document.MemberEnd()); From e4383abe591f015e1467a3ada80ba6f891f2ce51 Mon Sep 17 00:00:00 2001 From: Mohamed Amine Mzoughi Date: Mon, 26 Apr 2021 09:56:37 +0200 Subject: [PATCH 07/14] Fixing issue #3 and updating unit tests (httpbin.org incorrect protocol scheme). --- HTTP/CurlHandle.cpp | 18 ++++++++++++++++++ HTTP/CurlHandle.h | 20 ++++++++++++++++++++ HTTP/HTTPClient.cpp | 20 +++----------------- HTTP/HTTPClient.h | 8 ++++---- TestHTTP/main.cpp | 13 +++++-------- 5 files changed, 50 insertions(+), 29 deletions(-) create mode 100644 HTTP/CurlHandle.cpp create mode 100644 HTTP/CurlHandle.h 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..96a7b36 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(); } /** diff --git a/HTTP/HTTPClient.h b/HTTP/HTTPClient.h index d7b87b7..e7bc4ee 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 @@ -219,9 +220,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,6 +237,8 @@ class CHTTPClient static std::string s_strCurlTraceLogDirectory; mutable std::ofstream m_ofFileCurlTrace; #endif + + CurlHandle& m_curlHandle; }; // Logs messages diff --git a/TestHTTP/main.cpp b/TestHTTP/main.cpp index f07f4c5..4bfb2a9 100644 --- a/TestHTTP/main.cpp +++ b/TestHTTP/main.cpp @@ -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 @@ -353,7 +350,7 @@ TEST_F(RestClientTest, TestRestClientGETBodyCode) rapidjson::Value::MemberIterator itTokenUrl = document.FindMember("url"); ASSERT_TRUE(itTokenUrl != document.MemberEnd()); ASSERT_TRUE(itTokenUrl->value.IsString()); - EXPECT_STREQ("https://httpbin.org/get", itTokenUrl->value.GetString()); + EXPECT_STREQ("http://httpbin.org/get", itTokenUrl->value.GetString()); rapidjson::Value::MemberIterator itTokenHeaders = document.FindMember("headers"); ASSERT_TRUE(itTokenHeaders != document.MemberEnd()); @@ -428,7 +425,7 @@ TEST_F(RestClientTest, TestRestClientPOSTBody) rapidjson::Value::MemberIterator itTokenUrl = document.FindMember("url"); ASSERT_TRUE(itTokenUrl != document.MemberEnd()); ASSERT_TRUE(itTokenUrl->value.IsString()); - EXPECT_STREQ("https://httpbin.org/post", itTokenUrl->value.GetString()); + EXPECT_STREQ("http://httpbin.org/post", itTokenUrl->value.GetString()); rapidjson::Value::MemberIterator itTokenHeaders = document.FindMember("headers"); ASSERT_TRUE(itTokenHeaders != document.MemberEnd()); @@ -473,7 +470,7 @@ TEST_F(RestClientTest, TestRestClientPUTString) rapidjson::Value::MemberIterator itTokenUrl = document.FindMember("url"); ASSERT_TRUE(itTokenUrl != document.MemberEnd()); ASSERT_TRUE(itTokenUrl->value.IsString()); - EXPECT_STREQ("https://httpbin.org/put", itTokenUrl->value.GetString()); + EXPECT_STREQ("http://httpbin.org/put", itTokenUrl->value.GetString()); rapidjson::Value::MemberIterator itTokenHeaders = document.FindMember("headers"); ASSERT_TRUE(itTokenHeaders != document.MemberEnd()); @@ -505,7 +502,7 @@ TEST_F(RestClientTest, TestRestClientPUTBuffer) rapidjson::Value::MemberIterator itTokenUrl = document.FindMember("url"); ASSERT_TRUE(itTokenUrl != document.MemberEnd()); ASSERT_TRUE(itTokenUrl->value.IsString()); - EXPECT_STREQ("https://httpbin.org/put", itTokenUrl->value.GetString()); + EXPECT_STREQ("http://httpbin.org/put", itTokenUrl->value.GetString()); rapidjson::Value::MemberIterator itTokenHeaders = document.FindMember("headers"); ASSERT_TRUE(itTokenHeaders != document.MemberEnd()); @@ -552,7 +549,7 @@ TEST_F(RestClientTest, TestRestClientDeleteBody) rapidjson::Value::MemberIterator itTokenUrl = document.FindMember("url"); ASSERT_TRUE(itTokenUrl != document.MemberEnd()); ASSERT_TRUE(itTokenUrl->value.IsString()); - EXPECT_STREQ("https://httpbin.org/delete", itTokenUrl->value.GetString()); + EXPECT_STREQ("http://httpbin.org/delete", itTokenUrl->value.GetString()); rapidjson::Value::MemberIterator itTokenHeaders = document.FindMember("headers"); ASSERT_TRUE(itTokenHeaders != document.MemberEnd()); From e88fff0fd36c4fed6e18830375eb42285069ac4c Mon Sep 17 00:00:00 2001 From: Mohamed Amine Mzoughi Date: Mon, 26 Apr 2021 10:12:46 +0200 Subject: [PATCH 08/14] Fixed Issue #2 --- HTTP/HTTPClient.cpp | 12 ++++-------- HTTP/HTTPClient.h | 4 ++++ TestHTTP/main.cpp | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/HTTP/HTTPClient.cpp b/HTTP/HTTPClient.cpp index 96a7b36..51a4dcf 100644 --- a/HTTP/HTTPClient.cpp +++ b/HTTP/HTTPClient.cpp @@ -242,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()); diff --git a/HTTP/HTTPClient.h b/HTTP/HTTPClient.h index e7bc4ee..73c2c1a 100644 --- a/HTTP/HTTPClient.h +++ b/HTTP/HTTPClient.h @@ -241,6 +241,10 @@ class CHTTPClient 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/TestHTTP/main.cpp b/TestHTTP/main.cpp index 4bfb2a9..7fa7afd 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); From e9d54e029190e588251666bdb2ac6263968268a5 Mon Sep 17 00:00:00 2001 From: Mohamed Amine Mzoughi Date: Wed, 28 Apr 2021 10:26:55 +0200 Subject: [PATCH 09/14] Fixed Issue #6 and added a download file to memory methode. --- HTTP/HTTPClient.cpp | 204 ++++++++++++++++++++++++++++++-------------- HTTP/HTTPClient.h | 10 ++- TestHTTP/main.cpp | 31 +++++++ 3 files changed, 180 insertions(+), 65 deletions(-) diff --git a/HTTP/HTTPClient.cpp b/HTTP/HTTPClient.cpp index 51a4dcf..65bdc1b 100644 --- a/HTTP/HTTPClient.cpp +++ b/HTTP/HTTPClient.cpp @@ -170,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; @@ -285,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. * @@ -320,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); @@ -346,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. @@ -370,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) { @@ -413,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. * @@ -445,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); @@ -503,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) @@ -519,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) @@ -538,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 */ @@ -562,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); @@ -619,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 * @@ -647,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 * @@ -674,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 * @@ -722,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 * @@ -762,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 * @@ -802,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 @@ -925,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() @@ -1148,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 73c2c1a..b3f9015 100644 --- a/HTTP/HTTPClient.h +++ b/HTTP/HTTPClient.h @@ -133,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); @@ -170,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 @@ -181,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); @@ -189,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); diff --git a/TestHTTP/main.cpp b/TestHTTP/main.cpp index 7fa7afd..ca8d9ed 100644 --- a/TestHTTP/main.cpp +++ b/TestHTTP/main.cpp @@ -223,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; @@ -233,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 From 62b8b34fa149499b55085327c13f7b16401a2262 Mon Sep 17 00:00:00 2001 From: Amine Mzoughi Date: Sun, 29 May 2022 16:08:30 +0200 Subject: [PATCH 10/14] Fixed some issues with CMake scripts --- CMakeLists.txt | 16 +++++++++++++++- HTTP/CMakeLists.txt | 7 ++++--- TestHTTP/CMakeLists.txt | 4 ---- TestHTTP/CMakeLists.txt.in | 2 +- VERSION | 1 + 5 files changed, 21 insertions(+), 9 deletions(-) create mode 100644 VERSION diff --git a/CMakeLists.txt b/CMakeLists.txt index 7037f6d..5e623c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 2.6) +cmake_minimum_required (VERSION 3.10.0) set(CMAKE_CXX_STANDARD 14) @@ -6,15 +6,28 @@ 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) +# 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(FTPClient 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) add_subdirectory(HTTP) + +if(NOT SKIP_TESTS_BUILD) add_subdirectory(TestHTTP) include(CTest) @@ -28,3 +41,4 @@ IF (NOT TEST_INI_FILE) 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 index 985dbbe..2a03e89 100644 --- a/HTTP/CMakeLists.txt +++ b/HTTP/CMakeLists.txt @@ -1,9 +1,8 @@ -cmake_minimum_required(VERSION 2.6) - -project(HTTPClient) 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}) @@ -11,4 +10,6 @@ include_directories(${CURL_INCLUDE_DIRS}) file(GLOB_RECURSE source_files ./*) add_library(httpclient STATIC ${source_files}) +install(TARGETS httpclient) + ENDIF() diff --git a/TestHTTP/CMakeLists.txt b/TestHTTP/CMakeLists.txt index 0eadc39..e845143 100644 --- a/TestHTTP/CMakeLists.txt +++ b/TestHTTP/CMakeLists.txt @@ -1,7 +1,3 @@ -cmake_minimum_required(VERSION 2.6) - -project(TestHTTPClient) - # Code coverage setup IF(CMAKE_BUILD_TYPE MATCHES Coverage) INCLUDE(CodeCoverage.cmake) diff --git a/TestHTTP/CMakeLists.txt.in b/TestHTTP/CMakeLists.txt.in index e700161..911dca3 100644 --- a/TestHTTP/CMakeLists.txt.in +++ b/TestHTTP/CMakeLists.txt.in @@ -5,7 +5,7 @@ project(googletest-download-for-windows NONE) include(ExternalProject) ExternalProject_Add(googletest GIT_REPOSITORY https://github.com/google/googletest.git - GIT_TAG master + GIT_TAG main SOURCE_DIR "${CMAKE_BINARY_DIR}/googletest-src" BINARY_DIR "${CMAKE_BINARY_DIR}/googletest-build" CONFIGURE_COMMAND "" diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0 From 36fd2c71841a84828cf59a75f81d8ff8be440b89 Mon Sep 17 00:00:00 2001 From: Amine Mzoughi Date: Sun, 29 May 2022 16:47:49 +0200 Subject: [PATCH 11/14] Fixed project name. --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e623c3..c9d03e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ STRING(STRIP "${project_VERSION}" project_VERSION) SET(LICENSE "MIT") cmake_policy(SET CMP0022 NEW) -PROJECT(FTPClient VERSION ${project_VERSION} LANGUAGES CXX) +PROJECT(HTTPClient VERSION ${project_VERSION} LANGUAGES CXX) if(NOT MSVC) add_definitions(-DLINUX) From e677aa9710cdfbb9e4b0663a3251fb5b6dd01183 Mon Sep 17 00:00:00 2001 From: Amine Mzoughi Date: Sun, 29 May 2022 16:48:11 +0200 Subject: [PATCH 12/14] Added instructions related to vcpkg. --- README.md | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ec07ae4..45d6899 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 : @@ -232,7 +234,35 @@ To directly run the unit test binary, you must indicate the path of the INI conf ### Building under Windows via Visual Studio -This can be enhanced in future... +1. New Procedure (with vcpkg) : + +Install [vcpkg](https://github.com/microsoft/vcpkg) then install libcurl (use 'x86-windows' for the 32-bit version) : +```Shell +.\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 From 0b0fe766afc664364e13e052d268f30928d88a53 Mon Sep 17 00:00:00 2001 From: Mohamed Amine Mzoughi Date: Thu, 2 Jun 2022 09:00:04 +0200 Subject: [PATCH 13/14] Added infos related to file names on Windows. --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 45d6899..c4b60f0 100644 --- a/README.md +++ b/README.md @@ -172,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. @@ -406,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. From 4a39326bf47503802e7c2c930e9634dff7075e23 Mon Sep 17 00:00:00 2001 From: Amine Mzoughi Date: Thu, 2 Jun 2022 22:55:13 +0200 Subject: [PATCH 14/14] Use vcpkg on Windows to install Google Test (GTest). --- TestHTTP/CMakeLists.txt | 40 +++----------------------------------- TestHTTP/CMakeLists.txt.in | 15 -------------- 2 files changed, 3 insertions(+), 52 deletions(-) delete mode 100644 TestHTTP/CMakeLists.txt.in diff --git a/TestHTTP/CMakeLists.txt b/TestHTTP/CMakeLists.txt index e845143..7e18db8 100644 --- a/TestHTTP/CMakeLists.txt +++ b/TestHTTP/CMakeLists.txt @@ -9,43 +9,9 @@ ENDIF(CMAKE_BUILD_TYPE MATCHES Coverage) find_package(CURL REQUIRED) include_directories(${CURL_INCLUDE_DIRS}) -# For Windows -# https://crascit.com/2015/07/25/cmake-gtest/ -if (MSVC) - # Download and unpack googletest at configure time - configure_file(CMakeLists.txt.in "${CMAKE_BINARY_DIR}/googletest-download/CMakeLists.txt") - execute_process(COMMAND "${CMAKE_COMMAND}" -G "${CMAKE_GENERATOR}" . - WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/googletest-download" ) - execute_process(COMMAND "${CMAKE_COMMAND}" --build . - WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/googletest-download" ) - - # Prevent GoogleTest from overriding our compiler/linker options - # when building with Visual Studio - set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) - - # Add googletest directly to our build. This adds - # the following targets: gtest, gtest_main, gmock - # and gmock_main - add_subdirectory("${CMAKE_BINARY_DIR}/googletest-src" - "${CMAKE_BINARY_DIR}/googletest-build") - - # The gtest/gmock targets carry header search path - # dependencies automatically when using CMake 2.8.11 or - # later. Otherwise we have to add them here ourselves. - if(CMAKE_VERSION VERSION_LESS 2.8.11) - include_directories("${gtest_SOURCE_DIR}/include" - "${gmock_SOURCE_DIR}/include") - endif() - - # Now simply link your own targets against gtest, gmock, - # etc. as appropriate -endif() - # Locate GTest -if(NOT MSVC) - find_package(GTest REQUIRED) - include_directories(${GTEST_INCLUDE_DIRS}) -endif() +find_package(GTest REQUIRED) +include_directories(${GTEST_INCLUDE_DIRS}) # useless but test before removing it include_directories(../HTTP) include_directories(./simpleini) @@ -83,7 +49,7 @@ add_executable(test_httpclient main.cpp test_utils.cpp) if(NOT MSVC) target_link_libraries(test_httpclient httpclient ${GTEST_LIBRARIES} pthread curl) else() - target_link_libraries(test_httpclient httpclient gtest ${CURL_LIBRARIES}) + target_link_libraries(test_httpclient httpclient ${GTEST_LIBRARIES} ${CURL_LIBRARIES}) endif() ENDIF() diff --git a/TestHTTP/CMakeLists.txt.in b/TestHTTP/CMakeLists.txt.in deleted file mode 100644 index 911dca3..0000000 --- a/TestHTTP/CMakeLists.txt.in +++ /dev/null @@ -1,15 +0,0 @@ -cmake_minimum_required(VERSION 2.8.2) - -project(googletest-download-for-windows NONE) - -include(ExternalProject) -ExternalProject_Add(googletest - GIT_REPOSITORY https://github.com/google/googletest.git - GIT_TAG main - SOURCE_DIR "${CMAKE_BINARY_DIR}/googletest-src" - BINARY_DIR "${CMAKE_BINARY_DIR}/googletest-build" - CONFIGURE_COMMAND "" - BUILD_COMMAND "" - INSTALL_COMMAND "" - TEST_COMMAND "" -)