From 528b3b235e9bf4c7e1b66e1b8d3a4451ba37d5e0 Mon Sep 17 00:00:00 2001 From: hashirama Date: Sat, 13 Apr 2024 21:01:22 -0400 Subject: [PATCH] initial commit --- CMakeLists.txt | 373 ++++ CODE_OF_CONDUCT.md | 128 ++ CONTRIBUTING.md | 27 + CppCheckSuppressions.txt | 3 + LICENSE | 25 + README.md | 175 ++ README.md~ | 173 ++ cmake/FindMbedTLS.cmake | 14 + cmake/clang-tidy.cmake | 13 + cmake/clear_variable.cmake | 11 + cmake/code_coverage.cmake | 29 + cmake/cppcheck.cmake | 10 + cmake/cprConfig.cmake.in | 8 + cmake/cprver.h.in | 30 + cmake/mongoose.CMakeLists.txt | 12 + cmake/sanitizer.cmake | 69 + cmake/zlib_external.cmake | 18 + cpr-config.cmake | 26 + cpr/CMakeLists.txt | 85 + cpr/accept_encoding.cpp | 37 + cpr/async.cpp | 8 + cpr/auth.cpp | 16 + cpr/bearer.cpp | 16 + cpr/callback.cpp | 14 + cpr/cert_info.cpp | 43 + cpr/cookies.cpp | 106 ++ cpr/cprtypes.cpp | 10 + cpr/curl_container.cpp | 58 + cpr/curlholder.cpp | 49 + cpr/curlmultiholder.cpp | 15 + cpr/error.cpp | 68 + cpr/file.cpp | 60 + cpr/interceptor.cpp | 53 + cpr/multipart.cpp | 5 + cpr/multiperform.cpp | 331 ++++ cpr/parameters.cpp | 10 + cpr/payload.cpp | 10 + cpr/proxies.cpp | 21 + cpr/proxyauth.cpp | 21 + cpr/redirect.cpp | 40 + cpr/response.cpp | 44 + cpr/session.cpp | 975 +++++++++++ cpr/ssl_ctx.cpp | 70 + cpr/threadpool.cpp | 148 ++ cpr/timeout.cpp | 31 + cpr/unix_socket.cpp | 8 + cpr/util.cpp | 228 +++ include/CMakeLists.txt | 68 + include/cpr/accept_encoding.h | 41 + include/cpr/api.h | 392 +++++ include/cpr/async.h | 50 + include/cpr/async_wrapper.h | 140 ++ include/cpr/auth.h | 32 + include/cpr/bearer.h | 34 + include/cpr/body.h | 54 + include/cpr/buffer.h | 33 + include/cpr/callback.h | 111 ++ include/cpr/cert_info.h | 35 + include/cpr/connect_timeout.h | 18 + include/cpr/cookies.h | 92 + include/cpr/cpr.h | 46 + include/cpr/cprtypes.h | 144 ++ include/cpr/curl_container.h | 51 + include/cpr/curlholder.h | 54 + include/cpr/curlmultiholder.h | 18 + include/cpr/error.h | 53 + include/cpr/file.h | 59 + include/cpr/filesystem.h | 26 + include/cpr/http_version.h | 67 + include/cpr/interceptor.h | 74 + include/cpr/interface.h | 32 + include/cpr/limit_rate.h | 18 + include/cpr/local_port.h | 23 + include/cpr/local_port_range.h | 23 + include/cpr/low_speed.h | 18 + include/cpr/multipart.h | 43 + include/cpr/multiperform.h | 137 ++ include/cpr/parameters.h | 18 + include/cpr/payload.h | 23 + include/cpr/proxies.h | 23 + include/cpr/proxyauth.h | 43 + include/cpr/range.h | 44 + include/cpr/redirect.h | 84 + include/cpr/reserve_size.h | 17 + include/cpr/resolve.h | 23 + include/cpr/response.h | 58 + include/cpr/session.h | 308 ++++ include/cpr/singleton.h | 47 + include/cpr/ssl_ctx.h | 26 + include/cpr/ssl_options.h | 622 +++++++ include/cpr/status_codes.h | 100 ++ include/cpr/threadpool.h | 122 ++ include/cpr/timeout.h | 27 + include/cpr/unix_socket.h | 21 + include/cpr/user_agent.h | 31 + include/cpr/util.h | 45 + include/cpr/verbose.h | 18 + nuget/build/native/libcpr.props | 26 + nuget/build/native/libcpr.targets | 11 + nuget/libcpr.nuspec | 19 + nuget/resources/cpr.png | Bin 0 -> 6047 bytes package-build/build-package.sh | 27 + package-build/debian-libcpr/README.Debian | 5 + package-build/debian-libcpr/changelog | 6 + package-build/debian-libcpr/control | 39 + package-build/debian-libcpr/copyright | 52 + .../debian-libcpr/libcpr-dev.install | 3 + package-build/debian-libcpr/libcpr1.install | 1 + package-build/debian-libcpr/rules | 14 + package-build/debian-libcpr/source/format | 1 + test/CMakeLists.txt | 82 + test/LICENSE | 677 ++++++++ test/abstractServer.cpp | 143 ++ test/abstractServer.hpp | 70 + test/alternating_tests.cpp | 163 ++ test/async_tests.cpp | 81 + test/callback_tests.cpp | 931 ++++++++++ test/data/certificates/client.crt | 10 + test/data/certificates/root-ca.crt | 12 + test/data/certificates/server.crt | 10 + test/data/client.cnf | 8 + test/data/generate-certificates.sh | 76 + test/data/keys/client.key | 3 + test/data/keys/root-ca.key | 3 + test/data/keys/server.key | 3 + test/data/keys/server.pub | 3 + test/data/root-ca.cnf | 69 + test/data/server.cnf | 12 + test/delete_tests.cpp | 259 +++ test/download_tests.cpp | 140 ++ test/encoded_auth_tests.cpp | 20 + test/error_tests.cpp | 97 ++ test/get_tests.cpp | 1305 ++++++++++++++ test/head_tests.cpp | 226 +++ test/httpServer.cpp | 927 ++++++++++ test/httpServer.hpp | 66 + test/httpsServer.cpp | 65 + test/httpsServer.hpp | 42 + test/interceptor_multi_tests.cpp | 470 +++++ test/interceptor_tests.cpp | 369 ++++ test/multiasync_tests.cpp | 437 +++++ test/multiasync_tests.hpp | 18 + test/multiperform_tests.cpp | 673 ++++++++ test/options_tests.cpp | 73 + test/patch_tests.cpp | 276 +++ test/post_tests.cpp | 756 +++++++++ test/prepare_tests.cpp | 138 ++ test/proxy_auth_tests.cpp | 94 + test/proxy_tests.cpp | 92 + test/put_tests.cpp | 276 +++ test/raw_body_tests.cpp | 134 ++ test/resolve_tests.cpp | 44 + test/session_tests.cpp | 1510 +++++++++++++++++ test/ssl_tests.cpp | 170 ++ test/structures_tests.cpp | 62 + test/util_tests.cpp | 237 +++ test/version_tests.cpp | 65 + 157 files changed, 19011 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 CppCheckSuppressions.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 README.md~ create mode 100644 cmake/FindMbedTLS.cmake create mode 100644 cmake/clang-tidy.cmake create mode 100644 cmake/clear_variable.cmake create mode 100644 cmake/code_coverage.cmake create mode 100644 cmake/cppcheck.cmake create mode 100644 cmake/cprConfig.cmake.in create mode 100644 cmake/cprver.h.in create mode 100644 cmake/mongoose.CMakeLists.txt create mode 100644 cmake/sanitizer.cmake create mode 100644 cmake/zlib_external.cmake create mode 100644 cpr-config.cmake create mode 100644 cpr/CMakeLists.txt create mode 100644 cpr/accept_encoding.cpp create mode 100644 cpr/async.cpp create mode 100644 cpr/auth.cpp create mode 100644 cpr/bearer.cpp create mode 100644 cpr/callback.cpp create mode 100644 cpr/cert_info.cpp create mode 100644 cpr/cookies.cpp create mode 100644 cpr/cprtypes.cpp create mode 100644 cpr/curl_container.cpp create mode 100644 cpr/curlholder.cpp create mode 100644 cpr/curlmultiholder.cpp create mode 100644 cpr/error.cpp create mode 100644 cpr/file.cpp create mode 100644 cpr/interceptor.cpp create mode 100644 cpr/multipart.cpp create mode 100644 cpr/multiperform.cpp create mode 100644 cpr/parameters.cpp create mode 100644 cpr/payload.cpp create mode 100644 cpr/proxies.cpp create mode 100644 cpr/proxyauth.cpp create mode 100644 cpr/redirect.cpp create mode 100644 cpr/response.cpp create mode 100644 cpr/session.cpp create mode 100644 cpr/ssl_ctx.cpp create mode 100644 cpr/threadpool.cpp create mode 100644 cpr/timeout.cpp create mode 100644 cpr/unix_socket.cpp create mode 100644 cpr/util.cpp create mode 100644 include/CMakeLists.txt create mode 100644 include/cpr/accept_encoding.h create mode 100644 include/cpr/api.h create mode 100644 include/cpr/async.h create mode 100644 include/cpr/async_wrapper.h create mode 100644 include/cpr/auth.h create mode 100644 include/cpr/bearer.h create mode 100644 include/cpr/body.h create mode 100644 include/cpr/buffer.h create mode 100644 include/cpr/callback.h create mode 100644 include/cpr/cert_info.h create mode 100644 include/cpr/connect_timeout.h create mode 100644 include/cpr/cookies.h create mode 100644 include/cpr/cpr.h create mode 100644 include/cpr/cprtypes.h create mode 100644 include/cpr/curl_container.h create mode 100644 include/cpr/curlholder.h create mode 100644 include/cpr/curlmultiholder.h create mode 100644 include/cpr/error.h create mode 100644 include/cpr/file.h create mode 100644 include/cpr/filesystem.h create mode 100644 include/cpr/http_version.h create mode 100644 include/cpr/interceptor.h create mode 100644 include/cpr/interface.h create mode 100644 include/cpr/limit_rate.h create mode 100644 include/cpr/local_port.h create mode 100644 include/cpr/local_port_range.h create mode 100644 include/cpr/low_speed.h create mode 100644 include/cpr/multipart.h create mode 100644 include/cpr/multiperform.h create mode 100644 include/cpr/parameters.h create mode 100644 include/cpr/payload.h create mode 100644 include/cpr/proxies.h create mode 100644 include/cpr/proxyauth.h create mode 100644 include/cpr/range.h create mode 100644 include/cpr/redirect.h create mode 100644 include/cpr/reserve_size.h create mode 100644 include/cpr/resolve.h create mode 100644 include/cpr/response.h create mode 100644 include/cpr/session.h create mode 100644 include/cpr/singleton.h create mode 100644 include/cpr/ssl_ctx.h create mode 100644 include/cpr/ssl_options.h create mode 100644 include/cpr/status_codes.h create mode 100644 include/cpr/threadpool.h create mode 100644 include/cpr/timeout.h create mode 100644 include/cpr/unix_socket.h create mode 100644 include/cpr/user_agent.h create mode 100644 include/cpr/util.h create mode 100644 include/cpr/verbose.h create mode 100644 nuget/build/native/libcpr.props create mode 100644 nuget/build/native/libcpr.targets create mode 100644 nuget/libcpr.nuspec create mode 100644 nuget/resources/cpr.png create mode 100755 package-build/build-package.sh create mode 100644 package-build/debian-libcpr/README.Debian create mode 100644 package-build/debian-libcpr/changelog create mode 100644 package-build/debian-libcpr/control create mode 100644 package-build/debian-libcpr/copyright create mode 100644 package-build/debian-libcpr/libcpr-dev.install create mode 100644 package-build/debian-libcpr/libcpr1.install create mode 100755 package-build/debian-libcpr/rules create mode 100644 package-build/debian-libcpr/source/format create mode 100644 test/CMakeLists.txt create mode 100644 test/LICENSE create mode 100644 test/abstractServer.cpp create mode 100644 test/abstractServer.hpp create mode 100644 test/alternating_tests.cpp create mode 100644 test/async_tests.cpp create mode 100644 test/callback_tests.cpp create mode 100644 test/data/certificates/client.crt create mode 100644 test/data/certificates/root-ca.crt create mode 100644 test/data/certificates/server.crt create mode 100644 test/data/client.cnf create mode 100755 test/data/generate-certificates.sh create mode 100644 test/data/keys/client.key create mode 100644 test/data/keys/root-ca.key create mode 100644 test/data/keys/server.key create mode 100644 test/data/keys/server.pub create mode 100644 test/data/root-ca.cnf create mode 100644 test/data/server.cnf create mode 100644 test/delete_tests.cpp create mode 100644 test/download_tests.cpp create mode 100644 test/encoded_auth_tests.cpp create mode 100644 test/error_tests.cpp create mode 100644 test/get_tests.cpp create mode 100644 test/head_tests.cpp create mode 100644 test/httpServer.cpp create mode 100644 test/httpServer.hpp create mode 100644 test/httpsServer.cpp create mode 100644 test/httpsServer.hpp create mode 100644 test/interceptor_multi_tests.cpp create mode 100644 test/interceptor_tests.cpp create mode 100644 test/multiasync_tests.cpp create mode 100644 test/multiasync_tests.hpp create mode 100644 test/multiperform_tests.cpp create mode 100644 test/options_tests.cpp create mode 100644 test/patch_tests.cpp create mode 100644 test/post_tests.cpp create mode 100644 test/prepare_tests.cpp create mode 100644 test/proxy_auth_tests.cpp create mode 100644 test/proxy_tests.cpp create mode 100644 test/put_tests.cpp create mode 100644 test/raw_body_tests.cpp create mode 100644 test/resolve_tests.cpp create mode 100644 test/session_tests.cpp create mode 100644 test/ssl_tests.cpp create mode 100644 test/structures_tests.cpp create mode 100644 test/util_tests.cpp create mode 100644 test/version_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..5b3fd49 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,373 @@ +cmake_minimum_required(VERSION 3.15) +project(cpr VERSION 1.10.5 LANGUAGES CXX) + +math(EXPR cpr_VERSION_NUM "${cpr_VERSION_MAJOR} * 0x10000 + ${cpr_VERSION_MINOR} * 0x100 + ${cpr_VERSION_PATCH}" OUTPUT_FORMAT HEXADECIMAL) +configure_file("${cpr_SOURCE_DIR}/cmake/cprver.h.in" "${cpr_BINARY_DIR}/cpr_generated_includes/cpr/cprver.h") + +# Only change the folder behaviour if cpr is not a subproject +if(${CMAKE_PROJECT_NAME} STREQUAL ${PROJECT_NAME}) + set_property(GLOBAL PROPERTY USE_FOLDERS ON) + set_property(GLOBAL PROPERTY PREDEFINED_TARGETS_FOLDER "CMake") + set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin) + set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR}/lib) +else() + # Check required c++ standard of parent project + if(CMAKE_CXX_STANDARD) + set(PARENT_CXX_STANDARD ${CMAKE_CXX_STANDARD}) + message(STATUS "CXX standard of parent project: ${PARENT_CXX_STANDARD}") + endif() +endif() + +# Avoid the dll boilerplate code for windows +set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) + +if (PARENT_CXX_STANDARD) + # Don't set CMAKE_CXX_STANDARD if it is already set by parent project + if (PARENT_CXX_STANDARD LESS 17) + message(FATAL_ERROR "cpr ${cpr_VERSION} does not support ${PARENT_CXX_STANDARD}. Please use cpr <= 1.9.x") + endif() +else() + # Set standard version if not already set by potential parent project + set(CMAKE_CXX_STANDARD 17) +endif() + +message(STATUS "CXX standard: ${CMAKE_CXX_STANDARD}") +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(CPR_LIBRARIES cpr CACHE INTERNAL "") + +macro(cpr_option OPTION_NAME OPTION_TEXT OPTION_DEFAULT) + option(${OPTION_NAME} ${OPTION_TEXT} ${OPTION_DEFAULT}) + if(DEFINED ENV{${OPTION_NAME}}) + # Allow overriding the option through an environment variable + set(${OPTION_NAME} $ENV{${OPTION_NAME}}) + endif() + if(${OPTION_NAME}) + add_definitions(-D${OPTION_NAME}) + endif() + message(STATUS " ${OPTION_NAME}: ${${OPTION_NAME}}") +endmacro() + +option(BUILD_SHARED_LIBS "Build libraries as shared libraries" ON) +message(STATUS "C++ Requests CMake Options") +message(STATUS "=======================================================") +cpr_option(CPR_GENERATE_COVERAGE "Set to ON to generate coverage reports." OFF) +cpr_option(CPR_CURL_NOSIGNAL "Set to ON to disable use of signals in libcurl." OFF) +cpr_option(CURL_VERBOSE_LOGGING "Curl verbose logging during building curl" OFF) +cpr_option(CPR_USE_SYSTEM_GTEST "If ON, this project will look in the system paths for an installed gtest library. If none is found it will use the built-in one." OFF) +cpr_option(CPR_USE_SYSTEM_CURL "If enabled we will use the curl lib already installed on this system." OFF) +cpr_option(CPR_ENABLE_CURL_HTTP_ONLY "If enabled we will only use the HTTP/HTTPS protocols from CURL. If disabled, all the CURL protocols are enabled. This is useful if your project uses libcurl and you need support for other CURL features e.g. sending emails." ON) +cpr_option(CPR_ENABLE_SSL "Enables or disables the SSL backend. Required to perform HTTPS requests." ON) +cpr_option(CPR_FORCE_OPENSSL_BACKEND "Force to use the OpenSSL backend. If CPR_FORCE_OPENSSL_BACKEND, CPR_FORCE_DARWINSSL_BACKEND, CPR_FORCE_MBEDTLS_BACKEND, and CPR_FORCE_WINSSL_BACKEND are set to to OFF, cpr will try to automatically detect the best available SSL backend (WinSSL - Windows, OpenSSL - Linux, DarwinSSL - Mac ...)." OFF) +cpr_option(CPR_FORCE_WINSSL_BACKEND "Force to use the WinSSL backend. If CPR_FORCE_OPENSSL_BACKEND, CPR_FORCE_DARWINSSL_BACKEND, CPR_FORCE_MBEDTLS_BACKEND, and CPR_FORCE_WINSSL_BACKEND are set to to OFF, cpr will try to automatically detect the best available SSL backend (WinSSL - Windows, OpenSSL - Linux, DarwinSSL - Mac ...)." OFF) +cpr_option(CPR_FORCE_DARWINSSL_BACKEND "Force to use the DarwinSSL backend. If CPR_FORCE_OPENSSL_BACKEND, CPR_FORCE_DARWINSSL_BACKEND, CPR_FORCE_MBEDTLS_BACKEND, and CPR_FORCE_WINSSL_BACKEND are set to to OFF, cpr will try to automatically detect the best available SSL backend (WinSSL - Windows, OpenSSL - Linux, DarwinSSL - Mac ...)." OFF) +cpr_option(CPR_FORCE_MBEDTLS_BACKEND "Force to use the Mbed TLS backend. If CPR_FORCE_OPENSSL_BACKEND, CPR_FORCE_DARWINSSL_BACKEND, CPR_FORCE_MBEDTLS_BACKEND, and CPR_FORCE_WINSSL_BACKEND are set to to OFF, cpr will try to automatically detect the best available SSL backend (WinSSL - Windows, OpenSSL - Linux, DarwinSSL - Mac ...)." OFF) +cpr_option(CPR_ENABLE_LINTING "Set to ON to enable clang linting." OFF) +cpr_option(CPR_ENABLE_CPPCHECK "Set to ON to enable Cppcheck static analysis. Requires CPR_BUILD_TESTS and CPR_BUILD_TESTS_SSL to be OFF to prevent checking google tests source code." OFF) +cpr_option(CPR_BUILD_TESTS "Set to ON to build cpr tests." OFF) +cpr_option(CPR_BUILD_TESTS_SSL "Set to ON to build cpr ssl tests" ${CPR_BUILD_TESTS}) +cpr_option(CPR_BUILD_TESTS_PROXY "Set to ON to build proxy tests. They fail in case there is no valid proxy server available in proxy_tests.cpp" OFF) +cpr_option(CPR_SKIP_CA_BUNDLE_SEARCH "Skip searching for Certificate Authority certs. Turn ON for systems like iOS where file access is restricted and prevents https from working." OFF) +cpr_option(CPR_USE_BOOST_FILESYSTEM "Set to ON to use the Boost.Filesystem library. This is useful, on, e.g., Apple platforms, where std::filesystem may not always be available when targeting older OS versions." OFF) +cpr_option(CPR_DEBUG_SANITIZER_FLAG_THREAD "Enables the ThreadSanitizer for debug builds." OFF) +cpr_option(CPR_DEBUG_SANITIZER_FLAG_ADDR "Enables the AddressSanitizer for debug builds." OFF) +cpr_option(CPR_DEBUG_SANITIZER_FLAG_LEAK "Enables the LeakSanitizer for debug builds." OFF) +cpr_option(CPR_DEBUG_SANITIZER_FLAG_UB "Enables the UndefinedBehaviorSanitizer for debug builds." OFF) +cpr_option(CPR_DEBUG_SANITIZER_FLAG_ALL "Enables all sanitizers for debug builds except the ThreadSanitizer since it is incompatible with the other sanitizers." OFF) +message(STATUS "=======================================================") + +if (CPR_FORCE_USE_SYSTEM_CURL) + message(WARNING "The variable CPR_FORCE_USE_SYSTEM_CURL is deprecated, please use CPR_USE_SYSTEM_CURL instead") + set(CPR_USE_SYSTEM_CURL ${CPR_FORCE_USE_SYSTEM_CURL}) +endif() + +include(GNUInstallDirs) +include(FetchContent) +include(cmake/code_coverage.cmake) +include(cmake/sanitizer.cmake) +include(cmake/clear_variable.cmake) + +# So CMake can find FindMbedTLS.cmake +set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake;${CMAKE_MODULE_PATH}") + +# Linting +if(CPR_ENABLE_LINTING) + include(cmake/clang-tidy.cmake) +endif() + +# Cppcheck +if(CPR_ENABLE_CPPCHECK) + if(CPR_BUILD_TESTS OR CPR_BUILD_TESTS_SSL) + message(FATAL_ERROR "Cppcheck is incompatible with building tests. Make sure to disable CPR_ENABLE_CPPCHECK or disable tests by setting CPR_BUILD_TESTS and CPR_BUILD_TESTS_SSL to OFF. This is because Cppcheck would try to check the google tests source code and then fail. ") + endif() + include(cmake/cppcheck.cmake) +endif() + +if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") +else() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic -Werror") +endif() + +# SSL +if(CPR_ENABLE_SSL) + if(CPR_FORCE_OPENSSL_BACKEND OR CPR_FORCE_WINSSL_BACKEND OR CPR_FORCE_DARWINSSL_BACKEND OR CPR_FORCE_MBEDTLS_BACKEND) + message(STATUS "Disabled SSL backend auto detect since either CPR_FORCE_OPENSSL_BACKEND, CPR_FORCE_DARWINSSL_BACKEND, CPR_FORCE_MBEDTLS_BACKEND, or CPR_FORCE_WINSSL_BACKEND is enabled.") + set(DETECT_SSL_BACKEND OFF CACHE INTERNAL "" FORCE) + else() + message(STATUS "Automatically detecting SSL backend.") + set(DETECT_SSL_BACKEND ON CACHE INTERNAL "" FORCE) + endif() + + if(CPR_FORCE_WINSSL_BACKEND AND (NOT WIN32)) + message(FATAL_ERROR "WinSSL is only available on Windows! Use either OpenSSL (CPR_FORCE_OPENSSL_BACKEND) or DarwinSSL (CPR_FORCE_DARWINSSL_BACKEND) instead.") + endif() + + if(DETECT_SSL_BACKEND) + message(STATUS "Detecting SSL backend...") + if(WIN32) + message(STATUS "SSL auto detect: Using WinSSL.") + set(SSL_BACKEND_USED "WinSSL") + elseif(APPLE) + message(STATUS "SSL auto detect: Using DarwinSSL.") + set(CPR_BUILD_TESTS_SSL OFF) + set(SSL_BACKEND_USED "DarwinSSL") + else() + find_package(OpenSSL) + if(OPENSSL_FOUND) + message(STATUS "SSL auto detect: Using OpenSSL.") + set(SSL_BACKEND_USED "OpenSSL") + else() + find_package(MbedTLS) + if(MBEDTLS_FOUND) + set(SSL_BACKEND_USED "MbedTLS") + else() + message(FATAL_ERROR "No valid SSL backend found! Please install OpenSSL, Mbed TLS or disable SSL by setting CPR_ENABLE_SSL to OFF.") + endif() + endif() + endif() + else() + if(CPR_FORCE_OPENSSL_BACKEND) + find_package(OpenSSL) + if(OPENSSL_FOUND) + message(STATUS "Using OpenSSL.") + set(SSL_BACKEND_USED "OpenSSL") + else() + message(FATAL_ERROR "CPR_FORCE_OPENSSL_BACKEND enabled but we were not able to find OpenSSL!") + endif() + elseif(CPR_FORCE_WINSSL_BACKEND) + message(STATUS "Using WinSSL.") + set(SSL_BACKEND_USED "WinSSL") + elseif(CPR_FORCE_DARWINSSL_BACKEND) + message(STATUS "Using DarwinSSL.") + set(CPR_BUILD_TESTS_SSL OFF) + set(SSL_BACKEND_USED "DarwinSSL") + elseif(CPR_FORCE_MBEDTLS_BACKEND) + message(STATUS "Using Mbed TLS.") + set(CPR_BUILD_TESTS_SSL OFF) + set(SSL_BACKEND_USED "MbedTLS") + endif() + endif() +endif() + +if(SSL_BACKEND_USED STREQUAL "OpenSSL") +# Fix missing OpenSSL includes for Windows since in 'ssl_ctx.cpp' we include OpenSSL directly +find_package(OpenSSL REQUIRED) + add_compile_definitions(OPENSSL_BACKEND_USED) +endif() + +# Curl configuration +if(CPR_USE_SYSTEM_CURL) + if(CPR_ENABLE_SSL) + find_package(CURL COMPONENTS HTTP HTTPS) + if(CURL_FOUND) + message(STATUS "Curl ${CURL_VERSION_STRING} found on this system.") + # To be able to load certificates under Windows when using OpenSSL: + if(CMAKE_USE_OPENSSL AND WIN32 AND (NOT (CURL_VERSION_STRING VERSION_GREATER_EQUAL "7.71.0"))) + message(FATAL_ERROR "Your system curl version (${CURL_VERSION_STRING}) is too old to support OpenSSL on Windows which requires curl >= 7.71.0. Update your curl version, use WinSSL, disable SSL or use the built-in version of curl.") + endif() + else() + find_package(CURL COMPONENTS HTTP) + if(CURL_FOUND) + message(FATAL_ERROR "Curl found on this system but WITHOUT HTTPS/SSL support. Either disable SSL by setting CPR_ENABLE_SSL to OFF or use the built-in version of curl by setting CPR_USE_SYSTEM_CURL to OFF.") + else() + message(FATAL_ERROR "Curl not found on this system. To use the built-in version set CPR_USE_SYSTEM_CURL to OFF.") + endif() + endif() + else() + find_package(CURL COMPONENTS HTTP) + if(CURL_FOUND) + message(STATUS "Curl found on this system.") + else() + message(FATAL_ERROR "Curl not found on this system. To use the built-in version set CPR_USE_SYSTEM_CURL to OFF.") + endif() + endif() +else() + message(STATUS "Configuring built-in curl...") + + # ZLIB is optional for curl + # to disable it: + # * from command line: + # -DCURL_ZLIB=OFF + # * from CMake script: + # SET(CURL_ZLIB OFF CACHE STRING "" FORCE) + if (CURL_ZLIB OR CURL_ZLIB STREQUAL AUTO OR NOT DEFINED CACHE{CURL_ZLIB}) + include(cmake/zlib_external.cmake) + endif() + + if (CPR_ENABLE_CURL_HTTP_ONLY) + # We only need HTTP (and HTTPS) support: + set(HTTP_ONLY ON CACHE INTERNAL "" FORCE) + endif() + set(BUILD_CURL_EXE OFF CACHE INTERNAL "" FORCE) + set(BUILD_TESTING OFF) + + if (CURL_VERBOSE_LOGGING) + message(STATUS "Enabled curl debug features") + set(ENABLE_DEBUG ON CACHE INTERNAL "" FORCE) + endif() + + if (CPR_ENABLE_SSL) + set(CURL_ENABLE_SSL ON CACHE INTERNAL "" FORCE) + if(ANDROID) + set(CURL_CA_PATH "/system/etc/security/cacerts" CACHE INTERNAL "") + elseif(CPR_SKIP_CA_BUNDLE_SEARCH) + set(CURL_CA_PATH "none" CACHE INTERNAL "") + else() + set(CURL_CA_PATH "auto" CACHE INTERNAL "") + endif() + + if(CPR_SKIP_CA_BUNDLE_SEARCH) + set(CURL_CA_BUNDLE "none" CACHE INTERNAL "") + elseif(NOT DEFINED CURL_CA_BUNDLE) + set(CURL_CA_BUNDLE "auto" CACHE INTERNAL "") + endif() + + if(SSL_BACKEND_USED STREQUAL "WinSSL") + set(CURL_USE_SCHANNEL ON CACHE INTERNAL "" FORCE) + set(CURL_WINDOWS_SSPI ON CACHE INTERNAL "" FORCE) + endif() + + if(SSL_BACKEND_USED STREQUAL "OpenSSL") + set(CURL_USE_OPENSSL ON CACHE INTERNAL "" FORCE) + endif() + + if(SSL_BACKEND_USED STREQUAL "DarwinSSL") + set(CURL_USE_SECTRANSP ON CACHE INTERNAL "" FORCE) + endif() + + if(SSL_BACKEND_USED STREQUAL "MbedTLS") + set(CURL_USE_MBEDTLS ON CACHE INTERNAL "" FORCE) + endif() + + message(STATUS "Enabled curl SSL") + else() + set(CURL_ENABLE_SSL OFF CACHE INTERNAL "" FORCE) + + set(CURL_CA_PATH "none" CACHE INTERNAL "" FORCE) + set(CURL_USE_SCHANNEL OFF CACHE INTERNAL "" FORCE) + set(CURL_WINDOWS_SSPI OFF CACHE INTERNAL "" FORCE) + set(CURL_USE_OPENSSL OFF CACHE INTERNAL "" FORCE) + set(CURL_USE_SECTRANSP OFF CACHE INTERNAL "" FORCE) + set(CURL_USE_MBEDTLS OFF CACHE INTERNAL "" FORCE) + message(STATUS "Disabled curl SSL") + endif() + # Disable linting for curl + clear_variable(DESTINATION CMAKE_CXX_CLANG_TIDY BACKUP CMAKE_CXX_CLANG_TIDY_BKP) + + if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") + cmake_policy(SET CMP0135 NEW) + endif() + + #FetchContent + + restore_variable(DESTINATION CMAKE_CXX_CLANG_TIDY BACKUP CMAKE_CXX_CLANG_TIDY_BKP) +endif() + +# GTest configuration +if(CPR_BUILD_TESTS) + if(CPR_USE_SYSTEM_GTEST) + find_package(GTest) + endif() + if(NOT CPR_USE_SYSTEM_GTEST OR NOT GTEST_FOUND) + message(STATUS "Not using system gtest, using built-in googletest project instead.") + if(MSVC) + # By default, GTest compiles on Windows in CRT static linkage mode. We use this + # variable to force it into using the CRT in dynamic linkage (DLL), just as CPR + # does. + set(gtest_force_shared_crt ON CACHE BOOL "Force gtest to use the shared c runtime") + endif() + + # Disable linting for google test + clear_variable(DESTINATION CMAKE_CXX_CLANG_TIDY BACKUP CMAKE_CXX_CLANG_TIDY_BKP) + + FetchContent_Declare(googletest + URL https://github.com/google/googletest/archive/release-1.11.0.tar.gz + URL_HASH SHA256=b4870bf121ff7795ba20d20bcdd8627b8e088f2d1dab299a031c1034eddc93d5 # the file hash for release-1.11.0.tar.gz + USES_TERMINAL_DOWNLOAD TRUE) # <---- This is needed only for Ninja to show download progress + FetchContent_MakeAvailable(googletest) + + restore_variable(DESTINATION CMAKE_CXX_CLANG_TIDY BACKUP CMAKE_CXX_CLANG_TIDY_BKP) + + add_library(gtest_int INTERFACE) + target_link_libraries(gtest_int INTERFACE gtest) + target_include_directories(gtest_int INTERFACE ${googletest_SOURCE_DIR}/include) + + add_library(GTest::GTest ALIAS gtest_int) + + # Group under the "tests/gtest" project folder in IDEs such as Visual Studio. + set_property(TARGET gtest PROPERTY FOLDER "tests/gtest") + set_property(TARGET gtest_main PROPERTY FOLDER "tests/gtest") + endif() +endif() + + +# Mongoose configuration +if(CPR_BUILD_TESTS) + message(STATUS "Building mongoose project for test support.") + + if(CPR_BUILD_TESTS_SSL) + if(NOT CPR_ENABLE_SSL) + message(FATAL_ERROR "OpenSSL is required to build SSL test but CPR_ENABLE_SSL is disabled. Either set CPR_ENABLE_SSL to ON or disable CPR_BUILD_TESTS_SSL.") + endif() + + if(NOT(SSL_BACKEND_USED STREQUAL "OpenSSL")) + message(FATAL_ERROR "OpenSSL is required for SSL test, but it seams like OpenSSL is not being used as SSL backend. Either set CPR_BUILD_TESTS_SSL to OFF or set CPR_FORCE_OPENSSL_BACKEND to ON and try again.") + endif() + + set(ENABLE_SSL_TESTS ON CACHE INTERNAL "") + else() + set(ENABLE_SSL_TESTS OFF CACHE INTERNAL "") + endif() + + # Disable linting for mongoose + clear_variable(DESTINATION CMAKE_CXX_CLANG_TIDY BACKUP CMAKE_CXX_CLANG_TIDY_BKP) + + FetchContent_Declare(mongoose + URL https://github.com/cesanta/mongoose/archive/7.7.tar.gz + URL_HASH SHA256=4e5733dae31c3a81156af63ca9aa3a6b9b736547f21f23c3ab2f8e3f1ecc16c0 # the hash for 7.7.tar.gz + USES_TERMINAL_DOWNLOAD TRUE) # <---- This is needed only for Ninja to show download progress + # We can not use FetchContent_MakeAvailable, since we need to patch mongoose to use CMake + if (NOT mongoose_POPULATED) + FetchContent_POPULATE(mongoose) + + file(INSTALL cmake/mongoose.CMakeLists.txt DESTINATION ${mongoose_SOURCE_DIR}) + file(RENAME ${mongoose_SOURCE_DIR}/mongoose.CMakeLists.txt ${mongoose_SOURCE_DIR}/CMakeLists.txt) + add_subdirectory(${mongoose_SOURCE_DIR} ${mongoose_BINARY_DIR}) + + endif() + # Group under the "external" project folder in IDEs such as Visual Studio. + set_property(TARGET mongoose PROPERTY FOLDER "external") + restore_variable(DESTINATION CMAKE_CXX_CLANG_TIDY BACKUP CMAKE_CXX_CLANG_TIDY_BKP) +endif() + + +add_subdirectory(cpr) +add_subdirectory(include) + +if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME AND CPR_BUILD_TESTS) + # Disable linting for tests since they are currently not up to the standard + clear_variable(DESTINATION CMAKE_CXX_CLANG_TIDY BACKUP CMAKE_CXX_CLANG_TIDY_BKP) + enable_testing() + add_subdirectory(test) + restore_variable(DESTINATION CMAKE_CXX_CLANG_TIDY BACKUP CMAKE_CXX_CLANG_TIDY_BKP) +endif() diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..1c9ee05 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +cc@libcpr.org. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..abfb97c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,27 @@ +# Contributing to C++ Requests + +Please fork this repository and contribute back using [pull requests](https://github.com/whoshuu/cpr/pulls). Features can be requested using [issues](https://github.com/whoshuu/cpr/issues). All code, comments, and critiques are greatly appreciated. + +## Formatting + +To avoid unproductive debates on formatting, this project uses `clang-format` to ensure a consistent style across all source files. Currently, `clang-format` 3.8 is the version of `clang-format` we use. The format file can be found [here](https://github.com/whoshuu/cpr/blob/master/.clang-format). To install `clang-format` on Ubuntu, run this: + +``` +apt-get install clang-format-3.8 +``` + +To install `clang-format` on OS X, run this: + +``` +brew install clang-format +``` + +Note that `brew` might install a later version of `clang-format`, but it should be mostly compatible with what's run on the Travis servers. + +To run `clang-format` on every source file, run this in the root directory: + +``` +./format-check.sh +``` + +This should indicate which files need formatting and also show a diff of the requested changes. More specific usage instructions can be found on the official [LLVM website](http://releases.llvm.org/3.8.0/tools/clang/docs/ClangFormat.html). diff --git a/CppCheckSuppressions.txt b/CppCheckSuppressions.txt new file mode 100644 index 0000000..fec131b --- /dev/null +++ b/CppCheckSuppressions.txt @@ -0,0 +1,3 @@ +noExplicitConstructor +ConfigurationNotChecked +passedByValue diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c63edeb --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +This license applies to everything except the contents of the "test" +directory and its subdirectories. + +MIT License + +Copyright (c) 2017-2021 Huu Nguyen +Copyright (c) 2022 libcpr and many other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..08eb363 --- /dev/null +++ b/README.md @@ -0,0 +1,175 @@ +My fork of libcpr that works on guix. + +# C++ Requests: Curl for People + +[![Documentation](https://img.shields.io/badge/docs-online-informational?style=flat&link=https://docs.libcpr.org/)](https://docs.libcpr.org/) +![CI](https://github.com/libcpr/cpr/workflows/CI/badge.svg) +[![Gitter](https://badges.gitter.im/libcpr/community.svg)](https://gitter.im/libcpr/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) + +## Announcements + +* This project is being maintained by [Fabian Sauter](https://github.com/com8) and [Kilian Traub](https://github.com/KingKili). +* For quick help, and discussion libcpr also offer a [gitter](https://gitter.im/libcpr/community?utm_source=share-link&utm_medium=link&utm_campaign=share-link) chat. + +## Supported Releases +| Release | Min. C++ Standard | Status | Notes | +|----------|-------------------|--------|-------| +| master | `cpp17` | ![alt text][preview] | | +| 1.10.x | `cpp17` | ![alt text][supported] | | +| 1.9.x | `cpp11` | ![alt text][supported] | Supported until 01.01.2025 | +| <= 1.8.x | `cpp11` | ![alt text][unsupported] | | + +[unsupported]: https://img.shields.io/badge/-unsupported-red "unsupported" +[supported]: https://img.shields.io/badge/-supported-green "supported" +[preview]: https://img.shields.io/badge/-preview-orange "preview" + +## TLDR + +C++ Requests is a simple wrapper around [libcurl](http://curl.haxx.se/libcurl) inspired by the excellent [Python Requests](https://github.com/kennethreitz/requests) project. + +Despite its name, libcurl's easy interface is anything but, and making mistakes, misusing it is a common source of error and frustration. Using the more expressive language facilities of `C++17` (or `C++11` in case you use cpr < 1.10.0), this library captures the essence of making network calls into a few concise idioms. + +Here's a quick GET request: + +```c++ +#include + +int main(int argc, char** argv) { + cpr::Response r = cpr::Get(cpr::Url{"https://api.github.com/repos/whoshuu/cpr/contributors"}, + cpr::Authentication{"user", "pass", cpr::AuthMode::BASIC}, + cpr::Parameters{{"anon", "true"}, {"key", "value"}}); + r.status_code; // 200 + r.header["content-type"]; // application/json; charset=utf-8 + r.text; // JSON text string + return 0; +} +``` + +And here's [less functional, more complicated code, without cpr](https://gist.github.com/whoshuu/2dc858b8730079602044). + +## Documentation + +[![Documentation](https://img.shields.io/badge/docs-online-informational?style=for-the-badge&link=https://docs.libcpr.org/)](https://docs.libcpr.org/) +You can find the latest documentation [here](https://docs.libcpr.org/). It's a work in progress, but it should give you a better idea of how to use the library than the [tests](https://github.com/libcpr/cpr/tree/master/test) currently do. + +## Features + +C++ Requests currently supports: + +* Custom headers +* Url encoded parameters +* Url encoded POST values +* Multipart form POST upload +* File POST upload +* Basic authentication +* Bearer authentication +* Digest authentication +* NTLM authentication +* Connection and request timeout specification +* Timeout for low speed connection +* Asynchronous requests +* :cookie: support! +* Proxy support +* Callback interfaces +* PUT methods +* DELETE methods +* HEAD methods +* OPTIONS methods +* PATCH methods +* Thread Safe access to [libCurl](https://curl.haxx.se/libcurl/c/threadsafe.html) +* OpenSSL and WinSSL support for HTTPS requests + +## Planned + +For a quick overview about the planed features, have a look at the next [Milestones](https://github.com/libcpr/cpr/milestones). + +## Usage + +### CMake + +#### fetch_content: +If you already have a CMake project you need to integrate C++ Requests with, the primary way is to use `fetch_content`. +Add the following to your `CMakeLists.txt`. + + +```cmake +include(FetchContent) +FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git + GIT_TAG 0817715923c9705e68994eb52ef9df3f6845beba) # The commit hash for 1.10.x. Replace with the latest from: https://github.com/libcpr/cpr/releases +FetchContent_MakeAvailable(cpr) +``` + +This will produce the target `cpr::cpr` which you can link against the typical way: + +```cmake +target_link_libraries(your_target_name PRIVATE cpr::cpr) +``` + +That should do it! +There's no need to handle `libcurl` yourself. All dependencies are taken care of for you. +All of this can be found in an example [**here**](https://github.com/libcpr/example-cmake-fetch-content). + +#### find_package(): +If you prefer not to use `fetch_content`, you can download, build, and install the library and then use CMake `find_package()` function to integrate it into a project. + +**Note:** this feature is feasible only if CPR_USE_SYSTEM_CURL is set. (see [#645](https://github.com/libcpr/cpr/pull/645)) +```Bash +$ git clone https://github.com/libcpr/cpr.git +$ cd cpr && mkdir build && cd build +$ cmake .. -DCPR_USE_SYSTEM_CURL=ON +$ cmake --build . +$ sudo cmake --install . +``` +In your `CMakeLists.txt`: +```cmake +find_package(cpr REQUIRED) +add_executable(your_target_name your_target_name.cpp) +target_link_libraries(your_target_name PRIVATE cpr::cpr) +``` + +### Bazel + +Please refer to [hedronvision/bazel-make-cc-https-easy](https://github.com/hedronvision/bazel-make-cc-https-easy). + +### Packages for Linux Distributions + +Alternatively, you may install a package specific to your Linux distribution. Since so few distributions currently have a package for cpr, most users will not be able to run your program with this approach. + +Currently, we are aware of packages for the following distributions: + +* [Arch Linux (AUR)](https://aur.archlinux.org/packages/cpr) + +If there's no package for your distribution, try making one! If you do, and it is added to your distribution's repositories, please submit a pull request to add it to the list above. However, please only do this if you plan to actively maintain the package. + +### NuGet Package + +For Windows, there is also a libcpr NuGet package available. Currently, x86 and x64 builds are supported with release and debug configuration. + +The package can be found here: [NuGet.org](https://www.nuget.org/packages/libcpr/) + +## Requirements + +The only explicit requirements are: + +* a `C++17` compatible compiler such as Clang or GCC. The minimum required version of GCC is unknown, so if anyone has trouble building this library with a specific version of GCC, do let us know +* in case you only have a `C++11` compatible compiler available, all versions below cpr 1.9.x are for you. The 1.10.0 release of cpr switches to `C++17` as a requirement. +* If you would like to perform https requests `OpenSSL` and its development libraries are required. + +## Building cpr - Using vcpkg + +You can download and install cpr using the [vcpkg](https://github.com/Microsoft/vcpkg) dependency manager: +```Bash +git clone https://github.com/Microsoft/vcpkg.git +cd vcpkg +./bootstrap-vcpkg.sh +./vcpkg integrate install +./vcpkg install cpr +``` +The `cpr` port in vcpkg is kept up to date by Microsoft team members and community contributors. If the version is out of date, please [create an issue or pull request](https://github.com/Microsoft/vcpkg) on the vcpkg repository. + +## Building cpr - Using Conan + +You can download and install `cpr` using the [Conan](https://conan.io/) package manager. Setup your CMakeLists.txt (see [Conan documentation](https://docs.conan.io/en/latest/integrations/build_system.html) on how to use MSBuild, Meson and others). +An example can be found [**here**](https://github.com/libcpr/example-cmake-conan). + +The `cpr` package in Conan is kept up to date by Conan contributors. If the version is out of date, please [create an issue or pull request](https://github.com/conan-io/conan-center-index) on the `conan-center-index` repository. diff --git a/README.md~ b/README.md~ new file mode 100644 index 0000000..f6accae --- /dev/null +++ b/README.md~ @@ -0,0 +1,173 @@ +# C++ Requests: Curl for People + +[![Documentation](https://img.shields.io/badge/docs-online-informational?style=flat&link=https://docs.libcpr.org/)](https://docs.libcpr.org/) +![CI](https://github.com/libcpr/cpr/workflows/CI/badge.svg) +[![Gitter](https://badges.gitter.im/libcpr/community.svg)](https://gitter.im/libcpr/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) + +## Announcements + +* This project is being maintained by [Fabian Sauter](https://github.com/com8) and [Kilian Traub](https://github.com/KingKili). +* For quick help, and discussion libcpr also offer a [gitter](https://gitter.im/libcpr/community?utm_source=share-link&utm_medium=link&utm_campaign=share-link) chat. + +## Supported Releases +| Release | Min. C++ Standard | Status | Notes | +|----------|-------------------|--------|-------| +| master | `cpp17` | ![alt text][preview] | | +| 1.10.x | `cpp17` | ![alt text][supported] | | +| 1.9.x | `cpp11` | ![alt text][supported] | Supported until 01.01.2025 | +| <= 1.8.x | `cpp11` | ![alt text][unsupported] | | + +[unsupported]: https://img.shields.io/badge/-unsupported-red "unsupported" +[supported]: https://img.shields.io/badge/-supported-green "supported" +[preview]: https://img.shields.io/badge/-preview-orange "preview" + +## TLDR + +C++ Requests is a simple wrapper around [libcurl](http://curl.haxx.se/libcurl) inspired by the excellent [Python Requests](https://github.com/kennethreitz/requests) project. + +Despite its name, libcurl's easy interface is anything but, and making mistakes, misusing it is a common source of error and frustration. Using the more expressive language facilities of `C++17` (or `C++11` in case you use cpr < 1.10.0), this library captures the essence of making network calls into a few concise idioms. + +Here's a quick GET request: + +```c++ +#include + +int main(int argc, char** argv) { + cpr::Response r = cpr::Get(cpr::Url{"https://api.github.com/repos/whoshuu/cpr/contributors"}, + cpr::Authentication{"user", "pass", cpr::AuthMode::BASIC}, + cpr::Parameters{{"anon", "true"}, {"key", "value"}}); + r.status_code; // 200 + r.header["content-type"]; // application/json; charset=utf-8 + r.text; // JSON text string + return 0; +} +``` + +And here's [less functional, more complicated code, without cpr](https://gist.github.com/whoshuu/2dc858b8730079602044). + +## Documentation + +[![Documentation](https://img.shields.io/badge/docs-online-informational?style=for-the-badge&link=https://docs.libcpr.org/)](https://docs.libcpr.org/) +You can find the latest documentation [here](https://docs.libcpr.org/). It's a work in progress, but it should give you a better idea of how to use the library than the [tests](https://github.com/libcpr/cpr/tree/master/test) currently do. + +## Features + +C++ Requests currently supports: + +* Custom headers +* Url encoded parameters +* Url encoded POST values +* Multipart form POST upload +* File POST upload +* Basic authentication +* Bearer authentication +* Digest authentication +* NTLM authentication +* Connection and request timeout specification +* Timeout for low speed connection +* Asynchronous requests +* :cookie: support! +* Proxy support +* Callback interfaces +* PUT methods +* DELETE methods +* HEAD methods +* OPTIONS methods +* PATCH methods +* Thread Safe access to [libCurl](https://curl.haxx.se/libcurl/c/threadsafe.html) +* OpenSSL and WinSSL support for HTTPS requests + +## Planned + +For a quick overview about the planed features, have a look at the next [Milestones](https://github.com/libcpr/cpr/milestones). + +## Usage + +### CMake + +#### fetch_content: +If you already have a CMake project you need to integrate C++ Requests with, the primary way is to use `fetch_content`. +Add the following to your `CMakeLists.txt`. + + +```cmake +include(FetchContent) +FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git + GIT_TAG 0817715923c9705e68994eb52ef9df3f6845beba) # The commit hash for 1.10.x. Replace with the latest from: https://github.com/libcpr/cpr/releases +FetchContent_MakeAvailable(cpr) +``` + +This will produce the target `cpr::cpr` which you can link against the typical way: + +```cmake +target_link_libraries(your_target_name PRIVATE cpr::cpr) +``` + +That should do it! +There's no need to handle `libcurl` yourself. All dependencies are taken care of for you. +All of this can be found in an example [**here**](https://github.com/libcpr/example-cmake-fetch-content). + +#### find_package(): +If you prefer not to use `fetch_content`, you can download, build, and install the library and then use CMake `find_package()` function to integrate it into a project. + +**Note:** this feature is feasible only if CPR_USE_SYSTEM_CURL is set. (see [#645](https://github.com/libcpr/cpr/pull/645)) +```Bash +$ git clone https://github.com/libcpr/cpr.git +$ cd cpr && mkdir build && cd build +$ cmake .. -DCPR_USE_SYSTEM_CURL=ON +$ cmake --build . +$ sudo cmake --install . +``` +In your `CMakeLists.txt`: +```cmake +find_package(cpr REQUIRED) +add_executable(your_target_name your_target_name.cpp) +target_link_libraries(your_target_name PRIVATE cpr::cpr) +``` + +### Bazel + +Please refer to [hedronvision/bazel-make-cc-https-easy](https://github.com/hedronvision/bazel-make-cc-https-easy). + +### Packages for Linux Distributions + +Alternatively, you may install a package specific to your Linux distribution. Since so few distributions currently have a package for cpr, most users will not be able to run your program with this approach. + +Currently, we are aware of packages for the following distributions: + +* [Arch Linux (AUR)](https://aur.archlinux.org/packages/cpr) + +If there's no package for your distribution, try making one! If you do, and it is added to your distribution's repositories, please submit a pull request to add it to the list above. However, please only do this if you plan to actively maintain the package. + +### NuGet Package + +For Windows, there is also a libcpr NuGet package available. Currently, x86 and x64 builds are supported with release and debug configuration. + +The package can be found here: [NuGet.org](https://www.nuget.org/packages/libcpr/) + +## Requirements + +The only explicit requirements are: + +* a `C++17` compatible compiler such as Clang or GCC. The minimum required version of GCC is unknown, so if anyone has trouble building this library with a specific version of GCC, do let us know +* in case you only have a `C++11` compatible compiler available, all versions below cpr 1.9.x are for you. The 1.10.0 release of cpr switches to `C++17` as a requirement. +* If you would like to perform https requests `OpenSSL` and its development libraries are required. + +## Building cpr - Using vcpkg + +You can download and install cpr using the [vcpkg](https://github.com/Microsoft/vcpkg) dependency manager: +```Bash +git clone https://github.com/Microsoft/vcpkg.git +cd vcpkg +./bootstrap-vcpkg.sh +./vcpkg integrate install +./vcpkg install cpr +``` +The `cpr` port in vcpkg is kept up to date by Microsoft team members and community contributors. If the version is out of date, please [create an issue or pull request](https://github.com/Microsoft/vcpkg) on the vcpkg repository. + +## Building cpr - Using Conan + +You can download and install `cpr` using the [Conan](https://conan.io/) package manager. Setup your CMakeLists.txt (see [Conan documentation](https://docs.conan.io/en/latest/integrations/build_system.html) on how to use MSBuild, Meson and others). +An example can be found [**here**](https://github.com/libcpr/example-cmake-conan). + +The `cpr` package in Conan is kept up to date by Conan contributors. If the version is out of date, please [create an issue or pull request](https://github.com/conan-io/conan-center-index) on the `conan-center-index` repository. diff --git a/cmake/FindMbedTLS.cmake b/cmake/FindMbedTLS.cmake new file mode 100644 index 0000000..61ec464 --- /dev/null +++ b/cmake/FindMbedTLS.cmake @@ -0,0 +1,14 @@ +# Source: https://github.com/curl/curl/blob/curl-7_82_0/CMake/FindMbedTLS.cmake +find_path(MBEDTLS_INCLUDE_DIRS mbedtls/ssl.h) + +find_library(MBEDTLS_LIBRARY mbedtls) +find_library(MBEDX509_LIBRARY mbedx509) +find_library(MBEDCRYPTO_LIBRARY mbedcrypto) + +set(MBEDTLS_LIBRARIES "${MBEDTLS_LIBRARY}" "${MBEDX509_LIBRARY}" "${MBEDCRYPTO_LIBRARY}") + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(MbedTLS DEFAULT_MSG + MBEDTLS_INCLUDE_DIRS MBEDTLS_LIBRARY MBEDX509_LIBRARY MBEDCRYPTO_LIBRARY) + +mark_as_advanced(MBEDTLS_INCLUDE_DIRS MBEDTLS_LIBRARY MBEDX509_LIBRARY MBEDCRYPTO_LIBRARY) diff --git a/cmake/clang-tidy.cmake b/cmake/clang-tidy.cmake new file mode 100644 index 0000000..26defad --- /dev/null +++ b/cmake/clang-tidy.cmake @@ -0,0 +1,13 @@ +if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + find_program(CLANG_TIDY_EXECUTABLE NAMES clang-tidy) + mark_as_advanced(CLANG_TIDY_EXECUTABLE) + + if (${CLANG_TIDY_EXECUTABLE}) + message(FATAL_ERROR "Clang-tidy not found") + else() + message(STATUS "Enabling clang-tidy") + set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_EXECUTABLE};-warnings-as-errors=*") + endif() +else() + message(FATAL_ERROR "Clang-tidy is not supported when building for windows") +endif() diff --git a/cmake/clear_variable.cmake b/cmake/clear_variable.cmake new file mode 100644 index 0000000..469ac82 --- /dev/null +++ b/cmake/clear_variable.cmake @@ -0,0 +1,11 @@ +macro(clear_variable) + cmake_parse_arguments(CLEAR_VAR "" "DESTINATION;BACKUP;REPLACE" "" ${ARGN}) + set(${CLEAR_VAR_BACKUP} ${${CLEAR_VAR_DESTINATION}}) + set(${CLEAR_VAR_DESTINATION} ${CLEAR_VAR_REPLACE}) +endmacro() + +macro(restore_variable) + cmake_parse_arguments(CLEAR_VAR "" "DESTINATION;BACKUP" "" ${ARGN}) + set(${CLEAR_VAR_DESTINATION} ${${CLEAR_VAR_BACKUP}}) + unset(${CLEAR_VAR_BACKUP}) +endmacro() diff --git a/cmake/code_coverage.cmake b/cmake/code_coverage.cmake new file mode 100644 index 0000000..eefc4fb --- /dev/null +++ b/cmake/code_coverage.cmake @@ -0,0 +1,29 @@ +# Code coverage +if(CPR_BUILD_TESTS AND CPR_GENERATE_COVERAGE) + set(CMAKE_BUILD_TYPE COVERAGE CACHE INTERNAL "Coverage enabled build") + message(STATUS "Enabling gcov support") + if(NOT "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + set(COVERAGE_FLAG "--coverage") + endif() + set(CMAKE_CXX_FLAGS_COVERAGE + "-g -O0 ${COVERAGE_FLAG} -fprofile-arcs -ftest-coverage" + CACHE STRING "Flags used by the C++ compiler during coverage builds." + FORCE) + set(CMAKE_C_FLAGS_COVERAGE + "-g -O0 ${COVERAGE_FLAG} -fprofile-arcs -ftest-coverage" + CACHE STRING "Flags used by the C compiler during coverage builds." + FORCE) + set(CMAKE_EXE_LINKER_FLAGS_COVERAGE + "" + CACHE STRING "Flags used for linking binaries during coverage builds." + FORCE) + set(CMAKE_SHARED_LINKER_FLAGS_COVERAGE + "" + CACHE STRING "Flags used by the shared libraries linker during coverage builds." + FORCE) + mark_as_advanced( + CMAKE_CXX_FLAGS_COVERAGE + CMAKE_C_FLAGS_COVERAGE + CMAKE_EXE_LINKER_FLAGS_COVERAGE + CMAKE_SHARED_LINKER_FLAGS_COVERAGE) +endif() diff --git a/cmake/cppcheck.cmake b/cmake/cppcheck.cmake new file mode 100644 index 0000000..8ef4688 --- /dev/null +++ b/cmake/cppcheck.cmake @@ -0,0 +1,10 @@ +find_program(CMAKE_CXX_CPPCHECK NAMES cppcheck) +if(CMAKE_CXX_CPPCHECK) + list(APPEND CMAKE_CXX_CPPCHECK + "--error-exitcode=1" + "--enable=warning,style" + "--force" + "--inline-suppr" + "--std=c++${CMAKE_CXX_STANDARD}" + "--suppressions-list=${CMAKE_SOURCE_DIR}/CppCheckSuppressions.txt") +endif() diff --git a/cmake/cprConfig.cmake.in b/cmake/cprConfig.cmake.in new file mode 100644 index 0000000..9c0bda5 --- /dev/null +++ b/cmake/cprConfig.cmake.in @@ -0,0 +1,8 @@ +include(CMakeFindDependencyMacro) +@PACKAGE_INIT@ + +find_dependency(CURL REQUIRED) + +include(${CMAKE_CURRENT_LIST_DIR}/cprTargets.cmake) + +check_required_components(cpr) \ No newline at end of file diff --git a/cmake/cprver.h.in b/cmake/cprver.h.in new file mode 100644 index 0000000..e353324 --- /dev/null +++ b/cmake/cprver.h.in @@ -0,0 +1,30 @@ +#ifndef CPR_CPRVER_H +#define CPR_CPRVER_H + +/** + * CPR version as a string. + **/ +#define CPR_VERSION "${cpr_VERSION}" + +/** + * CPR version split up into parts. + **/ +#define CPR_VERSION_MAJOR ${cpr_VERSION_MAJOR} +#define CPR_VERSION_MINOR ${cpr_VERSION_MINOR} +#define CPR_VERSION_PATCH ${cpr_VERSION_PATCH} + +/** + * CPR version as a single hex digit. + * it can be split up into three parts: + * 0xAABBCC + * AA: The current CPR major version number in a hex format. + * BB: The current CPR minor version number in a hex format. + * CC: The current CPR patch version number in a hex format. + * + * Examples: + * '0x010702' -> 01.07.02 -> CPR_VERSION: 1.7.2 + * '0xA13722' -> A1.37.22 -> CPR_VERSION: 161.55.34 + **/ +#define CPR_VERSION_NUM ${cpr_VERSION_NUM} + +#endif diff --git a/cmake/mongoose.CMakeLists.txt b/cmake/mongoose.CMakeLists.txt new file mode 100644 index 0000000..8059997 --- /dev/null +++ b/cmake/mongoose.CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.15) +project(mongoose C) + + +add_library(mongoose STATIC mongoose.c) +target_include_directories(mongoose PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +if(ENABLE_SSL_TESTS) + # Enable mongoose SSL + target_compile_definitions(mongoose PUBLIC MG_ENABLE_OPENSSL) + target_link_libraries(mongoose PRIVATE OpenSSL::SSL) +endif() diff --git a/cmake/sanitizer.cmake b/cmake/sanitizer.cmake new file mode 100644 index 0000000..10cdc46 --- /dev/null +++ b/cmake/sanitizer.cmake @@ -0,0 +1,69 @@ +include(CheckCXXCompilerFlag) +include(CheckCXXSourceRuns) + +set(ALL_SAN_FLAGS "") + + # No sanitizers when cross compiling to prevent stuff like this: https://github.com/whoshuu/cpr/issues/582 +if(NOT CMAKE_CROSSCOMPILING) + # Thread sanitizer + set(THREAD_SAN_FLAGS "-fsanitize=thread") + set(PREV_FLAG ${CMAKE_REQUIRED_FLAGS}) + set(CMAKE_REQUIRED_FLAGS "${THREAD_SAN_FLAGS}") + check_cxx_source_runs("int main() { return 0; }" THREAD_SANITIZER_AVAILABLE) + set(CMAKE_REQUIRED_FLAGS ${PREV_FLAG}) + # Do not add the ThreadSanitizer for builds with all sanitizers enabled because it is incompatible with other sanitizers. + + # Address sanitizer + set(ADDR_SAN_FLAGS "-fsanitize=address") + set(PREV_FLAG ${CMAKE_REQUIRED_FLAGS}) + set(CMAKE_REQUIRED_FLAGS "${ADDR_SAN_FLAGS}") + check_cxx_source_runs("int main() { return 0; }" ADDRESS_SANITIZER_AVAILABLE) + set(CMAKE_REQUIRED_FLAGS ${PREV_FLAG}) + if(ADDRESS_SANITIZER_AVAILABLE) + set(ALL_SAN_FLAGS "${ALL_SAN_FLAGS} ${ADDR_SAN_FLAGS}") + endif() + + # Leak sanitizer + set(LEAK_SAN_FLAGS "-fsanitize=leak") + check_cxx_compiler_flag(${LEAK_SAN_FLAGS} LEAK_SANITIZER_AVAILABLE) + if(LEAK_SANITIZER_AVAILABLE) + set(ALL_SAN_FLAGS "${ALL_SAN_FLAGS} ${LEAK_SAN_FLAGS}") + endif() + + # Undefined behavior sanitizer + set(UDEF_SAN_FLAGS "-fsanitize=undefined") + check_cxx_compiler_flag(${UDEF_SAN_FLAGS} UNDEFINED_BEHAVIOUR_SANITIZER_AVAILABLE) + if(UNDEFINED_BEHAVIOUR_SANITIZER_AVAILABLE) + set(ALL_SAN_FLAGS "${ALL_SAN_FLAGS} ${UDEF_SAN_FLAGS}") + endif() + + # All sanitizer (without thread sanitizer) + if(NOT ALL_SAN_FLAGS STREQUAL "") + set(PREV_FLAG ${CMAKE_REQUIRED_FLAGS}) + set(CMAKE_REQUIRED_FLAGS "${ALL_SAN_FLAGS}") + check_cxx_source_runs("int main() { return 0; }" ALL_SANITIZERS_AVAILABLE) + set(CMAKE_REQUIRED_FLAGS ${PREV_FLAG}) + endif() + + if(CPR_DEBUG_SANITIZER_FLAG_THREAD AND THREAD_SANITIZER_AVAILABLE) + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} ${THREAD_SAN_FLAGS}" CACHE INTERNAL "Flags used by the C compiler during thread sanitizer builds." FORCE) + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${THREAD_SAN_FLAGS}" CACHE INTERNAL "Flags used by the C++ compiler during thread sanitizer builds." FORCE) + set(CMAKE_SHARED_LINKER_FLAGS_DEBUG "${CMAKE_SHARED_LINKER_FLAGS_DEBUG}" CACHE INTERNAL "Flags used for the linker during thread sanitizer builds" FORCE) + elseif(CPR_DEBUG_SANITIZER_FLAG_ADDR AND ADDRESS_SANITIZER_AVAILABLE) + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} ${ADDR_SAN_FLAGS} -fno-omit-frame-pointer -fno-optimize-sibling-calls" CACHE INTERNAL "Flags used by the C compiler during address sanitizer builds." FORCE) + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${ADDR_SAN_FLAGS} -fno-omit-frame-pointer -fno-optimize-sibling-calls" CACHE INTERNAL "Flags used by the C++ compiler during address sanitizer builds." FORCE) + set(CMAKE_SHARED_LINKER_FLAGS_DEBUG "${CMAKE_SHARED_LINKER_FLAGS_DEBUG}" CACHE INTERNAL "Flags used for the linker during address sanitizer builds" FORCE) + elseif(CPR_DEBUG_SANITIZER_FLAG_LEAK AND LEAK_SANITIZER_AVAILABLE) + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} ${LEAK_SAN_FLAGS} -fno-omit-frame-pointer" CACHE INTERNAL "Flags used by the C compiler during leak sanitizer builds." FORCE) + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${LEAK_SAN_FLAGS} -fno-omit-frame-pointer" CACHE INTERNAL "Flags used by the C++ compiler during leak sanitizer builds." FORCE) + set(CMAKE_SHARED_LINKER_FLAGS_DEBUG "${CMAKE_SHARED_LINKER_FLAGS_DEBUG}" CACHE INTERNAL "Flags used for the linker during leak sanitizer builds" FORCE) + elseif(CPR_DEBUG_SANITIZER_FLAG_UB AND UNDEFINED_BEHAVIOUR_SANITIZER_AVAILABLE) + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} ${UDEF_SAN_FLAGS}" CACHE INTERNAL "Flags used by the C compiler during undefined behaviour sanitizer builds." FORCE) + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${UDEF_SAN_FLAGS}" CACHE INTERNAL "Flags used by the C++ compiler during undefined behaviour sanitizer builds." FORCE) + set(CMAKE_SHARED_LINKER_FLAGS_DEBUG "${CMAKE_SHARED_LINKER_FLAGS_DEBUG}" CACHE INTERNAL "Flags used for the linker during undefined behaviour sanitizer builds" FORCE) + elseif(CPR_DEBUG_SANITIZER_FLAG_ALL AND ALL_SANITIZERS_AVAILABLE) + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} ${ALL_SAN_FLAGS} -fno-omit-frame-pointer -fno-optimize-sibling-calls" CACHE INTERNAL "Flags used by the C compiler during most possible sanitizer builds." FORCE) + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${ALL_SAN_FLAGS} -fno-omit-frame-pointer -fno-optimize-sibling-calls" CACHE INTERNAL "Flags used by the C++ compiler during most possible sanitizer builds." FORCE) + set(CMAKE_SHARED_LINKER_FLAGS_DEBUG "${CMAKE_SHARED_LINKER_FLAGS_DEBUG}" CACHE INTERNAL "Flags used for the linker during most possible sanitizer builds" FORCE) + endif() +endif() diff --git a/cmake/zlib_external.cmake b/cmake/zlib_external.cmake new file mode 100644 index 0000000..76c18c4 --- /dev/null +++ b/cmake/zlib_external.cmake @@ -0,0 +1,18 @@ +# ZLIB + +# Fix Windows missing "zlib.dll": +if(WIN32 AND (${CMAKE_PROJECT_NAME} STREQUAL ${PROJECT_NAME})) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${EXECUTABLE_OUTPUT_PATH}/$ CACHE INTERNAL "" FORCE) +endif() + +set(ZLIB_COMPAT ON CACHE INTERNAL "" FORCE) +set(ZLIB_ENABLE_TESTS OFF CACHE INTERNAL "" FORCE) + +#FetchContent Zlib + +# Fix Windows zlib dll names from "zlibd1.dll" to "zlib.dll": +if(WIN32) + set_target_properties(zlib PROPERTIES OUTPUT_NAME "zlib") + set_target_properties(zlib PROPERTIES DEBUG_POSTFIX "") + set_target_properties(zlib PROPERTIES SUFFIX ".dll") +endif() diff --git a/cpr-config.cmake b/cpr-config.cmake new file mode 100644 index 0000000..58ab483 --- /dev/null +++ b/cpr-config.cmake @@ -0,0 +1,26 @@ +# - C++ Requests, Curl for People +# This module is a libcurl wrapper written in modern C++. +# It provides an easy, intuitive, and efficient interface to +# a host of networking methods. +# +# Finding this module will define the following variables: +# CPR_FOUND - True if the core library has been found +# CPR_LIBRARIES - Path to the core library archive +# CPR_INCLUDE_DIRS - Path to the include directories. Gives access +# to cpr.h, which must be included in every +# file that uses this interface + +find_path(CPR_INCLUDE_DIR + NAMES cpr.h) + +find_library(CPR_LIBRARY + NAMES cpr + HINTS ${CPR_LIBRARY_ROOT}) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(CPR REQUIRED_VARS CPR_LIBRARY CPR_INCLUDE_DIR) + +if(CPR_FOUND) + set(CPR_LIBRARIES ${CPR_LIBRARY}) + set(CPR_INCLUDE_DIRS ${CPR_INCLUDE_DIR}) +endif() diff --git a/cpr/CMakeLists.txt b/cpr/CMakeLists.txt new file mode 100644 index 0000000..5a2ef82 --- /dev/null +++ b/cpr/CMakeLists.txt @@ -0,0 +1,85 @@ +cmake_minimum_required(VERSION 3.15) + +add_library(cpr + accept_encoding.cpp + async.cpp + auth.cpp + bearer.cpp + callback.cpp + cert_info.cpp + cookies.cpp + cprtypes.cpp + curl_container.cpp + curlholder.cpp + error.cpp + file.cpp + multipart.cpp + parameters.cpp + payload.cpp + proxies.cpp + proxyauth.cpp + session.cpp + threadpool.cpp + timeout.cpp + unix_socket.cpp + util.cpp + response.cpp + redirect.cpp + interceptor.cpp + ssl_ctx.cpp + curlmultiholder.cpp + multiperform.cpp) + +add_library(cpr::cpr ALIAS cpr) + +target_link_libraries(cpr PUBLIC -lcurl) # todo should be private, but first dependencies in ssl_options need to be removed + +# Fix missing OpenSSL includes for Windows since in 'ssl_ctx.cpp' we include OpenSSL directly +if(SSL_BACKEND_USED STREQUAL "OpenSSL") + target_link_libraries(cpr PRIVATE OpenSSL::SSL) + target_include_directories(cpr PRIVATE ${OPENSSL_INCLUDE_DIR}) +endif() + +# Set version for shared libraries. +set_target_properties(cpr + PROPERTIES + VERSION ${${PROJECT_NAME}_VERSION} + SOVERSION ${${PROJECT_NAME}_VERSION_MAJOR}) + +# Import GNU common install directory variables +include(GNUInstallDirs) + +if(CPR_USE_SYSTEM_CURL) + install(TARGETS cpr + EXPORT cprTargets + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) + + # Include CMake helpers for package config files + # Follow this installation guideline: https://cmake.org/cmake/help/latest/manual/cmake-packages.7.html + include(CMakePackageConfigHelpers) + + write_basic_package_version_file( + "${PROJECT_BINARY_DIR}/cpr/cprConfigVersion.cmake" + VERSION ${${PROJECT_NAME}_VERSION} + COMPATIBILITY ExactVersion) + + configure_package_config_file(${PROJECT_SOURCE_DIR}/cmake/cprConfig.cmake.in + "${PROJECT_BINARY_DIR}/cpr/cprConfig.cmake" + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/cpr) + + install(EXPORT cprTargets + FILE cprTargets.cmake + NAMESPACE cpr:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/cpr) + + install(FILES ${PROJECT_BINARY_DIR}/cpr/cprConfig.cmake + ${PROJECT_BINARY_DIR}/cpr/cprConfigVersion.cmake DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/cpr) + +else() + install(TARGETS cpr + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) +endif() diff --git a/cpr/accept_encoding.cpp b/cpr/accept_encoding.cpp new file mode 100644 index 0000000..b757668 --- /dev/null +++ b/cpr/accept_encoding.cpp @@ -0,0 +1,37 @@ +#include "cpr/accept_encoding.h" + +#include +#include +#include +#include +#include +#include + +namespace cpr { + +AcceptEncoding::AcceptEncoding(const std::initializer_list& methods) { + methods_.clear(); + std::transform(methods.begin(), methods.end(), std::inserter(methods_, methods_.begin()), [&](cpr::AcceptEncodingMethods method) { return cpr::AcceptEncodingMethodsStringMap.at(method); }); +} + +AcceptEncoding::AcceptEncoding(const std::initializer_list& string_methods) : methods_{string_methods} {} + +bool AcceptEncoding::empty() const noexcept { + return methods_.empty(); +} + +const std::string AcceptEncoding::getString() const { + return std::accumulate(std::next(methods_.begin()), methods_.end(), *methods_.begin(), [](std::string a, std::string b) { return std::move(a) + ", " + std::move(b); }); +} + +[[nodiscard]] bool AcceptEncoding::disabled() const { + if (methods_.find(cpr::AcceptEncodingMethodsStringMap.at(AcceptEncodingMethods::disabled)) != methods_.end()) { + if (methods_.size() != 1) { + throw std::invalid_argument("AcceptEncoding does not accept any other values if 'disabled' is present. You set the following encodings: " + getString()); + } + return true; + } + return false; +} + +} // namespace cpr diff --git a/cpr/async.cpp b/cpr/async.cpp new file mode 100644 index 0000000..e10d09e --- /dev/null +++ b/cpr/async.cpp @@ -0,0 +1,8 @@ +#include "cpr/async.h" + +namespace cpr { + +// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables) +CPR_SINGLETON_IMPL(GlobalThreadPool) + +} // namespace cpr diff --git a/cpr/auth.cpp b/cpr/auth.cpp new file mode 100644 index 0000000..b3576f5 --- /dev/null +++ b/cpr/auth.cpp @@ -0,0 +1,16 @@ +#include "cpr/auth.h" +#include "cpr/util.h" + +namespace cpr { +Authentication::~Authentication() noexcept { + util::secureStringClear(auth_string_); +} + +const char* Authentication::GetAuthString() const noexcept { + return auth_string_.c_str(); +} + +AuthMode Authentication::GetAuthMode() const noexcept { + return auth_mode_; +} +} // namespace cpr diff --git a/cpr/bearer.cpp b/cpr/bearer.cpp new file mode 100644 index 0000000..02bd728 --- /dev/null +++ b/cpr/bearer.cpp @@ -0,0 +1,16 @@ +#include "cpr/bearer.h" +#include "cpr/util.h" + +namespace cpr { +// Only supported with libcurl >= 7.61.0. +// As an alternative use SetHeader and add the token manually. +#if LIBCURL_VERSION_NUM >= 0x073D00 +Bearer::~Bearer() noexcept { + util::secureStringClear(token_string_); +} + +const char* Bearer::GetToken() const noexcept { + return token_string_.c_str(); +} +#endif +} // namespace cpr diff --git a/cpr/callback.cpp b/cpr/callback.cpp new file mode 100644 index 0000000..3af230b --- /dev/null +++ b/cpr/callback.cpp @@ -0,0 +1,14 @@ +#include +#include +#include + +namespace cpr { + +void CancellationCallback::SetProgressCallback(ProgressCallback& u_cb) { + user_cb.emplace(std::reference_wrapper{u_cb}); +} +bool CancellationCallback::operator()(cpr_pf_arg_t dltotal, cpr_pf_arg_t dlnow, cpr_pf_arg_t ultotal, cpr_pf_arg_t ulnow) const { + const bool cont_operation{!cancellation_state->load()}; + return user_cb ? (cont_operation && (*user_cb)(dltotal, dlnow, ultotal, ulnow)) : cont_operation; +} +} // namespace cpr diff --git a/cpr/cert_info.cpp b/cpr/cert_info.cpp new file mode 100644 index 0000000..a77a027 --- /dev/null +++ b/cpr/cert_info.cpp @@ -0,0 +1,43 @@ +#include "cpr/cert_info.h" + +namespace cpr { + +std::string& CertInfo::operator[](const size_t& pos) { + return cert_info_[pos]; +} + +CertInfo::iterator CertInfo::begin() { + return cert_info_.begin(); +} +CertInfo::iterator CertInfo::end() { + return cert_info_.end(); +} + +CertInfo::const_iterator CertInfo::begin() const { + return cert_info_.begin(); +} + +CertInfo::const_iterator CertInfo::end() const { + return cert_info_.end(); +} + +CertInfo::const_iterator CertInfo::cbegin() const { + return cert_info_.cbegin(); +} + +CertInfo::const_iterator CertInfo::cend() const { + return cert_info_.cend(); +} + +void CertInfo::emplace_back(const std::string& str) { + cert_info_.emplace_back(str); +} + +void CertInfo::push_back(const std::string& str) { + cert_info_.push_back(str); +} + +void CertInfo::pop_back() { + cert_info_.pop_back(); +} +} // namespace cpr diff --git a/cpr/cookies.cpp b/cpr/cookies.cpp new file mode 100644 index 0000000..41e1246 --- /dev/null +++ b/cpr/cookies.cpp @@ -0,0 +1,106 @@ +#include "cpr/cookies.h" +#include +#include + +namespace cpr { +const std::string Cookie::GetDomain() const { + return domain_; +} + +bool Cookie::IsIncludingSubdomains() const { + return includeSubdomains_; +} + +const std::string Cookie::GetPath() const { + return path_; +} + +bool Cookie::IsHttpsOnly() const { + return httpsOnly_; +} + +const std::chrono::system_clock::time_point Cookie::GetExpires() const { + return expires_; +} + +const std::string Cookie::GetExpiresString() const { + std::stringstream ss; + std::tm tm{}; + const std::time_t tt = std::chrono::system_clock::to_time_t(expires_); +#ifdef _WIN32 + gmtime_s(&tm, &tt); +#else + gmtime_r(&tt, &tm); +#endif + ss << std::put_time(&tm, "%a, %d %b %Y %H:%M:%S GMT"); + return ss.str(); +} + +const std::string Cookie::GetName() const { + return name_; +} + +const std::string Cookie::GetValue() const { + return value_; +} + +const std::string Cookies::GetEncoded(const CurlHolder& holder) const { + std::stringstream stream; + for (const cpr::Cookie& item : cookies_) { + // Depending on if encoding is set to "true", we will URL-encode cookies + stream << (encode ? holder.urlEncode(item.GetName()) : item.GetName()) << "="; + + // special case version 1 cookies, which can be distinguished by + // beginning and trailing quotes + if (!item.GetValue().empty() && item.GetValue().front() == '"' && item.GetValue().back() == '"') { + stream << item.GetValue(); + } else { + // Depending on if encoding is set to "true", we will URL-encode cookies + stream << (encode ? holder.urlEncode(item.GetValue()) : item.GetValue()); + } + stream << "; "; + } + return stream.str(); +} + +cpr::Cookie& Cookies::operator[](size_t pos) { + return cookies_[pos]; +} + +Cookies::iterator Cookies::begin() { + return cookies_.begin(); +} + +Cookies::iterator Cookies::end() { + return cookies_.end(); +} + +Cookies::const_iterator Cookies::begin() const { + return cookies_.begin(); +} + +Cookies::const_iterator Cookies::end() const { + return cookies_.end(); +} + +Cookies::const_iterator Cookies::cbegin() const { + return cookies_.cbegin(); +} + +Cookies::const_iterator Cookies::cend() const { + return cookies_.cend(); +} + +void Cookies::emplace_back(const Cookie& str) { + cookies_.emplace_back(str); +} + +void Cookies::push_back(const Cookie& str) { + cookies_.push_back(str); +} + +void Cookies::pop_back() { + cookies_.pop_back(); +} + +} // namespace cpr diff --git a/cpr/cprtypes.cpp b/cpr/cprtypes.cpp new file mode 100644 index 0000000..7927b03 --- /dev/null +++ b/cpr/cprtypes.cpp @@ -0,0 +1,10 @@ +#include "cpr/cprtypes.h" + +#include +#include + +namespace cpr { +bool CaseInsensitiveCompare::operator()(const std::string& a, const std::string& b) const noexcept { + return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end(), [](unsigned char ac, unsigned char bc) { return std::tolower(ac) < std::tolower(bc); }); +} +} // namespace cpr diff --git a/cpr/curl_container.cpp b/cpr/curl_container.cpp new file mode 100644 index 0000000..ea141f2 --- /dev/null +++ b/cpr/curl_container.cpp @@ -0,0 +1,58 @@ +#include "cpr/curl_container.h" +#include +#include + + +namespace cpr { +template +CurlContainer::CurlContainer(const std::initializer_list& containerList) : containerList_(containerList) {} + +template +void CurlContainer::Add(const std::initializer_list& containerList) { + std::transform(containerList.begin(), containerList.end(), std::back_inserter(containerList_), [](const T& elem) { return std::move(elem); }); +} + +template +void CurlContainer::Add(const T& element) { + containerList_.push_back(std::move(element)); +} + +template <> +const std::string CurlContainer::GetContent(const CurlHolder& holder) const { + std::string content{}; + for (const Parameter& parameter : containerList_) { + if (!content.empty()) { + content += "&"; + } + + const std::string escapedKey = encode ? holder.urlEncode(parameter.key) : parameter.key; + if (parameter.value.empty()) { + content += escapedKey; + } else { + const std::string escapedValue = encode ? holder.urlEncode(parameter.value) : parameter.value; + content += escapedKey + "="; + content += escapedValue; + } + }; + + return content; +} + +template <> +const std::string CurlContainer::GetContent(const CurlHolder& holder) const { + std::string content{}; + for (const cpr::Pair& element : containerList_) { + if (!content.empty()) { + content += "&"; + } + const std::string escaped = encode ? holder.urlEncode(element.value) : element.value; + content += element.key + "=" + escaped; + } + + return content; +} + +template class CurlContainer; +template class CurlContainer; + +} // namespace cpr diff --git a/cpr/curlholder.cpp b/cpr/curlholder.cpp new file mode 100644 index 0000000..0dcd6d7 --- /dev/null +++ b/cpr/curlholder.cpp @@ -0,0 +1,49 @@ +#include "cpr/curlholder.h" +#include + +namespace cpr { +CurlHolder::CurlHolder() { + /** + * Allow multithreaded access to CPR by locking curl_easy_init(). + * curl_easy_init() is not thread safe. + * References: + * https://curl.haxx.se/libcurl/c/curl_easy_init.html + * https://curl.haxx.se/libcurl/c/threadsafe.html + **/ + curl_easy_init_mutex_().lock(); + // NOLINTNEXTLINE (cppcoreguidelines-prefer-member-initializer) since we need it to happen inside the lock + handle = curl_easy_init(); + curl_easy_init_mutex_().unlock(); + + assert(handle); +} // namespace cpr + +CurlHolder::~CurlHolder() { + curl_slist_free_all(chunk); + curl_slist_free_all(resolveCurlList); + curl_mime_free(multipart); + curl_easy_cleanup(handle); +} + +std::string CurlHolder::urlEncode(const std::string& s) const { + assert(handle); + char* output = curl_easy_escape(handle, s.c_str(), static_cast(s.length())); + if (output) { + std::string result = output; + curl_free(output); + return result; + } + return ""; +} + +std::string CurlHolder::urlDecode(const std::string& s) const { + assert(handle); + char* output = curl_easy_unescape(handle, s.c_str(), static_cast(s.length()), nullptr); + if (output) { + std::string result = output; + curl_free(output); + return result; + } + return ""; +} +} // namespace cpr diff --git a/cpr/curlmultiholder.cpp b/cpr/curlmultiholder.cpp new file mode 100644 index 0000000..7607937 --- /dev/null +++ b/cpr/curlmultiholder.cpp @@ -0,0 +1,15 @@ +#include "cpr/curlmultiholder.h" + +#include + +namespace cpr { + +CurlMultiHolder::CurlMultiHolder() : handle{curl_multi_init()} { + assert(handle); +} + +CurlMultiHolder::~CurlMultiHolder() { + curl_multi_cleanup(handle); +} + +} // namespace cpr \ No newline at end of file diff --git a/cpr/error.cpp b/cpr/error.cpp new file mode 100644 index 0000000..f085051 --- /dev/null +++ b/cpr/error.cpp @@ -0,0 +1,68 @@ +#include "cpr/error.h" + +#include + +namespace cpr { +ErrorCode Error::getErrorCodeForCurlError(std::int32_t curl_code) { + switch (curl_code) { + case CURLE_OK: + return ErrorCode::OK; + case CURLE_UNSUPPORTED_PROTOCOL: + return ErrorCode::UNSUPPORTED_PROTOCOL; + case CURLE_URL_MALFORMAT: + return ErrorCode::INVALID_URL_FORMAT; + case CURLE_COULDNT_RESOLVE_PROXY: + return ErrorCode::PROXY_RESOLUTION_FAILURE; + case CURLE_COULDNT_RESOLVE_HOST: + return ErrorCode::HOST_RESOLUTION_FAILURE; + case CURLE_COULDNT_CONNECT: + return ErrorCode::CONNECTION_FAILURE; + case CURLE_OPERATION_TIMEDOUT: + return ErrorCode::OPERATION_TIMEDOUT; + case CURLE_SSL_CONNECT_ERROR: + return ErrorCode::SSL_CONNECT_ERROR; +#if LIBCURL_VERSION_NUM < 0x073e00 + case CURLE_PEER_FAILED_VERIFICATION: + return ErrorCode::SSL_REMOTE_CERTIFICATE_ERROR; +#endif + case CURLE_ABORTED_BY_CALLBACK: + case CURLE_WRITE_ERROR: + return ErrorCode::REQUEST_CANCELLED; + case CURLE_GOT_NOTHING: + return ErrorCode::EMPTY_RESPONSE; + case CURLE_SSL_ENGINE_NOTFOUND: + case CURLE_SSL_ENGINE_SETFAILED: + return ErrorCode::GENERIC_SSL_ERROR; + case CURLE_SEND_ERROR: + return ErrorCode::NETWORK_SEND_FAILURE; + case CURLE_RECV_ERROR: + return ErrorCode::NETWORK_RECEIVE_ERROR; + case CURLE_SSL_CERTPROBLEM: + return ErrorCode::SSL_LOCAL_CERTIFICATE_ERROR; + case CURLE_SSL_CIPHER: + return ErrorCode::GENERIC_SSL_ERROR; +#if LIBCURL_VERSION_NUM >= 0x073e00 + case CURLE_PEER_FAILED_VERIFICATION: + return ErrorCode::SSL_REMOTE_CERTIFICATE_ERROR; +#else + case CURLE_SSL_CACERT: + return ErrorCode::SSL_CACERT_ERROR; +#endif + case CURLE_USE_SSL_FAILED: + case CURLE_SSL_ENGINE_INITFAILED: + return ErrorCode::GENERIC_SSL_ERROR; + case CURLE_SSL_CACERT_BADFILE: + return ErrorCode::SSL_CACERT_ERROR; + case CURLE_SSL_SHUTDOWN_FAILED: + return ErrorCode::GENERIC_SSL_ERROR; + case CURLE_SSL_CRL_BADFILE: + case CURLE_SSL_ISSUER_ERROR: + return ErrorCode::SSL_CACERT_ERROR; + case CURLE_TOO_MANY_REDIRECTS: + return ErrorCode::TOO_MANY_REDIRECTS; + default: + return ErrorCode::INTERNAL_ERROR; + } +} + +} // namespace cpr diff --git a/cpr/file.cpp b/cpr/file.cpp new file mode 100644 index 0000000..eadb377 --- /dev/null +++ b/cpr/file.cpp @@ -0,0 +1,60 @@ +#include "cpr/file.h" + +namespace cpr { + +Files::Files(const std::initializer_list& p_filepaths) { + for (const std::string& filepath : p_filepaths) { + files.emplace_back(filepath); + } +} + +Files::iterator Files::begin() { + return files.begin(); +} + +Files::iterator Files::end() { + return files.end(); +} + +Files::const_iterator Files::begin() const { + return files.begin(); +} + +Files::const_iterator Files::end() const { + return files.end(); +} + +Files::const_iterator Files::cbegin() const { + return files.cbegin(); +} + +Files::const_iterator Files::cend() const { + return files.cend(); +} + +void Files::emplace_back(const File& file) { + files.emplace_back(file); +} + +void Files::push_back(const File& file) { + files.push_back(file); +} + +void Files::pop_back() { + files.pop_back(); +} + +Files& Files::operator=(const Files& other) { + if (&other != this) { + files = other.files; + } + return *this; +} + +Files& Files::operator=(Files&& old) noexcept { + if (&old != this) { + files = std::move(old.files); + } + return *this; +} +} // namespace cpr diff --git a/cpr/interceptor.cpp b/cpr/interceptor.cpp new file mode 100644 index 0000000..72d1cab --- /dev/null +++ b/cpr/interceptor.cpp @@ -0,0 +1,53 @@ +#include "cpr/interceptor.h" + +#include + +namespace cpr { + +Response Interceptor::proceed(Session& session) { + return session.proceed(); +} + +Response Interceptor::proceed(Session& session, ProceedHttpMethod httpMethod) { + switch (httpMethod) { + case ProceedHttpMethod::DELETE_REQUEST: + return session.Delete(); + case ProceedHttpMethod::GET_REQUEST: + return session.Get(); + case ProceedHttpMethod::HEAD_REQUEST: + return session.Head(); + case ProceedHttpMethod::OPTIONS_REQUEST: + return session.Options(); + case ProceedHttpMethod::PATCH_REQUEST: + return session.Patch(); + case ProceedHttpMethod::POST_REQUEST: + return session.Post(); + case ProceedHttpMethod::PUT_REQUEST: + return session.Put(); + default: + throw std::invalid_argument{"Can't proceed the session with the provided http method!"}; + } +} + +Response Interceptor::proceed(Session& session, ProceedHttpMethod httpMethod, std::ofstream& file) { + if (httpMethod == ProceedHttpMethod::DOWNLOAD_FILE_REQUEST) { + return session.Download(file); + } + throw std::invalid_argument{"std::ofstream argument is only valid for ProceedHttpMethod::DOWNLOAD_FILE!"}; +} + +Response Interceptor::proceed(Session& session, ProceedHttpMethod httpMethod, const WriteCallback& write) { + if (httpMethod == ProceedHttpMethod::DOWNLOAD_CALLBACK_REQUEST) { + return session.Download(write); + } + throw std::invalid_argument{"WriteCallback argument is only valid for ProceedHttpMethod::DOWNLOAD_CALLBACK!"}; +} + +std::vector InterceptorMulti::proceed(MultiPerform& multi) { + return multi.proceed(); +} + +void InterceptorMulti::PrepareDownloadSession(MultiPerform& multi, size_t sessions_index, const WriteCallback& write) { + multi.PrepareDownloadSessions(sessions_index, write); +} +} // namespace cpr \ No newline at end of file diff --git a/cpr/multipart.cpp b/cpr/multipart.cpp new file mode 100644 index 0000000..d82d9a4 --- /dev/null +++ b/cpr/multipart.cpp @@ -0,0 +1,5 @@ +#include "cpr/multipart.h" + +namespace cpr { +Multipart::Multipart(const std::initializer_list& p_parts) : parts{p_parts} {} +} // namespace cpr diff --git a/cpr/multiperform.cpp b/cpr/multiperform.cpp new file mode 100644 index 0000000..e9e3f00 --- /dev/null +++ b/cpr/multiperform.cpp @@ -0,0 +1,331 @@ +#include "cpr/multiperform.h" + +#include "cpr/interceptor.h" +#include "cpr/multipart.h" +#include "cpr/response.h" +#include "cpr/session.h" +#include +#include +#include +#include + +namespace cpr { + +MultiPerform::MultiPerform() : multicurl_(new CurlMultiHolder()) {} + +MultiPerform::~MultiPerform() { + // Unlock all sessions + for (const std::pair, HttpMethod>& pair : sessions_) { + pair.first->isUsedInMultiPerform = false; + + // Remove easy handle from multi handle + const CURLMcode error_code = curl_multi_remove_handle(multicurl_->handle, pair.first->curl_->handle); + if (error_code) { + std::cerr << "curl_multi_remove_handle() failed, code " << static_cast(error_code) << std::endl; + return; + } + } +} + +void MultiPerform::AddSession(std::shared_ptr& session, HttpMethod method) { + // Check if this multiperform is download only + if (((method != HttpMethod::DOWNLOAD_REQUEST && is_download_multi_perform) && method != HttpMethod::UNDEFINED) || (method == HttpMethod::DOWNLOAD_REQUEST && !is_download_multi_perform && !sessions_.empty())) { + // Currently it is not possible to mix download and non-download methods, as download needs additional parameters + throw std::invalid_argument("Failed to add session: Cannot mix download and non-download methods!"); + } + + // Set download only if neccessary + if (method == HttpMethod::DOWNLOAD_REQUEST) { + is_download_multi_perform = true; + } + + // Add easy handle to multi handle + const CURLMcode error_code = curl_multi_add_handle(multicurl_->handle, session->curl_->handle); + if (error_code) { + std::cerr << "curl_multi_add_handle() failed, code " << static_cast(error_code) << std::endl; + return; + } + + // Lock session to the multihandle + session->isUsedInMultiPerform = true; + + // Add session to sessions_ + sessions_.emplace_back(session, method); +} + +void MultiPerform::RemoveSession(const std::shared_ptr& session) { + // Remove easy handle from multihandle + const CURLMcode error_code = curl_multi_remove_handle(multicurl_->handle, session->curl_->handle); + if (error_code) { + std::cerr << "curl_multi_remove_handle() failed, code " << static_cast(error_code) << std::endl; + return; + } + + // Unock session + session->isUsedInMultiPerform = false; + + // Remove session from sessions_ + auto it = std::find_if(sessions_.begin(), sessions_.end(), [&session](const std::pair, HttpMethod>& pair) { return session->curl_->handle == pair.first->curl_->handle; }); + if (it == sessions_.end()) { + throw std::invalid_argument("Failed to find session!"); + } + sessions_.erase(it); + + // Reset download only if empty + if (sessions_.empty()) { + is_download_multi_perform = false; + } +} + +std::vector, MultiPerform::HttpMethod>>& MultiPerform::GetSessions() { + return sessions_; +} + +const std::vector, MultiPerform::HttpMethod>>& MultiPerform::GetSessions() const { + return sessions_; +} + +void MultiPerform::DoMultiPerform() { + // Do multi perform until every handle has finished + int still_running{0}; + do { + CURLMcode error_code = curl_multi_perform(multicurl_->handle, &still_running); + if (error_code) { + std::cerr << "curl_multi_perform() failed, code " << static_cast(error_code) << std::endl; + break; + } + + if (still_running) { + const int timeout_ms{250}; + error_code = curl_multi_poll(multicurl_->handle, nullptr, 0, timeout_ms, nullptr); + if (error_code) { + std::cerr << "curl_multi_poll() failed, code " << static_cast(error_code) << std::endl; + break; + } + } + } while (still_running); +} + +std::vector MultiPerform::ReadMultiInfo(std::function&& complete_function) { + // Get infos and create Response objects + std::vector responses; + struct CURLMsg* info{nullptr}; + do { + int msgq = 0; + + // Read info from multihandle + info = curl_multi_info_read(multicurl_->handle, &msgq); + + if (info) { + // Find current session + auto it = std::find_if(sessions_.begin(), sessions_.end(), [&info](const std::pair, HttpMethod>& pair) { return pair.first->curl_->handle == info->easy_handle; }); + if (it == sessions_.end()) { + std::cerr << "Failed to find current session!" << std::endl; + break; + } + const std::shared_ptr current_session = (*it).first; + + // Add response object + // NOLINTNEXTLINE (cppcoreguidelines-pro-type-union-access) + responses.push_back(complete_function(*current_session, info->data.result)); + } + } while (info); + + // Sort response objects to match order of added sessions + std::vector sorted_responses; + for (const std::pair, HttpMethod>& pair : sessions_) { + Session& current_session = *(pair.first); + auto it = std::find_if(responses.begin(), responses.end(), [¤t_session](const Response& response) { return current_session.curl_->handle == response.curl_->handle; }); + const Response current_response = *it; + // Erase response from original vector to increase future search speed + responses.erase(it); + sorted_responses.push_back(current_response); + } + + return sorted_responses; +} + +std::vector MultiPerform::MakeRequest() { + if (!interceptors_.empty()) { + return intercept(); + } + + DoMultiPerform(); + return ReadMultiInfo([](Session& session, CURLcode curl_error) -> Response { return session.Complete(curl_error); }); +} + +std::vector MultiPerform::MakeDownloadRequest() { + if (!interceptors_.empty()) { + return intercept(); + } + + DoMultiPerform(); + return ReadMultiInfo([](Session& session, CURLcode curl_error) -> Response { return session.CompleteDownload(curl_error); }); +} + +void MultiPerform::PrepareSessions() { + for (const std::pair, HttpMethod>& pair : sessions_) { + switch (pair.second) { + case HttpMethod::GET_REQUEST: + pair.first->PrepareGet(); + break; + case HttpMethod::POST_REQUEST: + pair.first->PreparePost(); + break; + case HttpMethod::PUT_REQUEST: + pair.first->PreparePut(); + break; + case HttpMethod::DELETE_REQUEST: + pair.first->PrepareDelete(); + break; + case HttpMethod::PATCH_REQUEST: + pair.first->PreparePatch(); + break; + case HttpMethod::HEAD_REQUEST: + pair.first->PrepareHead(); + break; + case HttpMethod::OPTIONS_REQUEST: + pair.first->PrepareOptions(); + break; + default: + std::cerr << "PrepareSessions failed: Undefined HttpMethod or download without arguments!" << std::endl; + return; + } + } +} + +void MultiPerform::PrepareDownloadSession(size_t sessions_index, const WriteCallback& write) { + const std::pair, HttpMethod>& pair = sessions_[sessions_index]; + switch (pair.second) { + case HttpMethod::DOWNLOAD_REQUEST: + pair.first->PrepareDownload(write); + break; + default: + std::cerr << "PrepareSessions failed: Undefined HttpMethod or non download method with arguments!" << std::endl; + return; + } +} + +void MultiPerform::PrepareDownloadSession(size_t sessions_index, std::ofstream& file) { + const std::pair, HttpMethod>& pair = sessions_[sessions_index]; + switch (pair.second) { + case HttpMethod::DOWNLOAD_REQUEST: + pair.first->PrepareDownload(file); + break; + default: + std::cerr << "PrepareSessions failed: Undefined HttpMethod or non download method with arguments!" << std::endl; + return; + } +} + +void MultiPerform::SetHttpMethod(HttpMethod method) { + for (std::pair, HttpMethod>& pair : sessions_) { + pair.second = method; + } +} + +void MultiPerform::PrepareGet() { + SetHttpMethod(HttpMethod::GET_REQUEST); + PrepareSessions(); +} + +void MultiPerform::PrepareDelete() { + SetHttpMethod(HttpMethod::DELETE_REQUEST); + PrepareSessions(); +} + +void MultiPerform::PreparePut() { + SetHttpMethod(HttpMethod::PUT_REQUEST); + PrepareSessions(); +} + +void MultiPerform::PreparePatch() { + SetHttpMethod(HttpMethod::PATCH_REQUEST); + PrepareSessions(); +} + +void MultiPerform::PrepareHead() { + SetHttpMethod(HttpMethod::HEAD_REQUEST); + PrepareSessions(); +} + +void MultiPerform::PrepareOptions() { + SetHttpMethod(HttpMethod::OPTIONS_REQUEST); + PrepareSessions(); +} + +void MultiPerform::PreparePost() { + SetHttpMethod(HttpMethod::POST_REQUEST); + PrepareSessions(); +} + +std::vector MultiPerform::Get() { + PrepareGet(); + return MakeRequest(); +} + +std::vector MultiPerform::Delete() { + PrepareDelete(); + return MakeRequest(); +} + +std::vector MultiPerform::Put() { + PreparePut(); + return MakeRequest(); +} + +std::vector MultiPerform::Head() { + PrepareHead(); + return MakeRequest(); +} + +std::vector MultiPerform::Options() { + PrepareOptions(); + return MakeRequest(); +} + +std::vector MultiPerform::Patch() { + PreparePatch(); + return MakeRequest(); +} + +std::vector MultiPerform::Post() { + PreparePost(); + return MakeRequest(); +} + +std::vector MultiPerform::Perform() { + PrepareSessions(); + return MakeRequest(); +} + +std::vector MultiPerform::proceed() { + // Check if this multiperform mixes download and non download requests + if (!sessions_.empty()) { + const bool new_is_download_multi_perform = sessions_.front().second == HttpMethod::DOWNLOAD_REQUEST; + + for (const std::pair, HttpMethod>& s : sessions_) { + const HttpMethod method = s.second; + if ((new_is_download_multi_perform && method != HttpMethod::DOWNLOAD_REQUEST) || (!new_is_download_multi_perform && method == HttpMethod::DOWNLOAD_REQUEST)) { + throw std::invalid_argument("Failed to proceed with session: Cannot mix download and non-download methods!"); + } + } + is_download_multi_perform = new_is_download_multi_perform; + } + + PrepareSessions(); + return MakeRequest(); +} + +std::vector MultiPerform::intercept() { + // At least one interceptor exists -> Execute its intercept function + const std::shared_ptr interceptor = interceptors_.front(); + interceptors_.pop(); + return interceptor->intercept(*this); +} + +void MultiPerform::AddInterceptor(const std::shared_ptr& pinterceptor) { + interceptors_.push(pinterceptor); +} + +} // namespace cpr \ No newline at end of file diff --git a/cpr/parameters.cpp b/cpr/parameters.cpp new file mode 100644 index 0000000..a24c393 --- /dev/null +++ b/cpr/parameters.cpp @@ -0,0 +1,10 @@ +#include "cpr/parameters.h" + +#include +#include + +#include "cpr/util.h" + +namespace cpr { +Parameters::Parameters(const std::initializer_list& parameters) : CurlContainer(parameters) {} +} // namespace cpr diff --git a/cpr/payload.cpp b/cpr/payload.cpp new file mode 100644 index 0000000..78373fa --- /dev/null +++ b/cpr/payload.cpp @@ -0,0 +1,10 @@ +#include "cpr/payload.h" + +#include +#include + +#include "cpr/util.h" + +namespace cpr { +Payload::Payload(const std::initializer_list& pairs) : CurlContainer(pairs) {} +} // namespace cpr diff --git a/cpr/proxies.cpp b/cpr/proxies.cpp new file mode 100644 index 0000000..0d3fe98 --- /dev/null +++ b/cpr/proxies.cpp @@ -0,0 +1,21 @@ +#include "cpr/proxies.h" + +#include +#include +#include +#include + +namespace cpr { + +Proxies::Proxies(const std::initializer_list>& hosts) : hosts_{hosts} {} +Proxies::Proxies(const std::map& hosts) : hosts_{hosts} {} + +bool Proxies::has(const std::string& protocol) const { + return hosts_.count(protocol) > 0; +} + +const std::string& Proxies::operator[](const std::string& protocol) { + return hosts_[protocol]; +} + +} // namespace cpr diff --git a/cpr/proxyauth.cpp b/cpr/proxyauth.cpp new file mode 100644 index 0000000..01e6303 --- /dev/null +++ b/cpr/proxyauth.cpp @@ -0,0 +1,21 @@ +#include "cpr/proxyauth.h" +#include "cpr/util.h" + +namespace cpr { +EncodedAuthentication::~EncodedAuthentication() noexcept { + util::secureStringClear(auth_string_); +} + +const char* EncodedAuthentication::GetAuthString() const noexcept { + return auth_string_.c_str(); +} + +bool ProxyAuthentication::has(const std::string& protocol) const { + return proxyAuth_.count(protocol) > 0; +} + +const char* ProxyAuthentication::operator[](const std::string& protocol) { + return proxyAuth_[protocol].GetAuthString(); +} + +} // namespace cpr diff --git a/cpr/redirect.cpp b/cpr/redirect.cpp new file mode 100644 index 0000000..f95dc75 --- /dev/null +++ b/cpr/redirect.cpp @@ -0,0 +1,40 @@ +#include "cpr/redirect.h" + +namespace cpr { +PostRedirectFlags operator|(PostRedirectFlags lhs, PostRedirectFlags rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} + +PostRedirectFlags operator&(PostRedirectFlags lhs, PostRedirectFlags rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} + +PostRedirectFlags operator^(PostRedirectFlags lhs, PostRedirectFlags rhs) { + return static_cast(static_cast(lhs) ^ static_cast(rhs)); +} + +PostRedirectFlags operator~(PostRedirectFlags flag) { + return static_cast(~static_cast(flag)); +} + +PostRedirectFlags& operator|=(PostRedirectFlags& lhs, PostRedirectFlags rhs) { + lhs = static_cast(static_cast(lhs) | static_cast(rhs)); + const uint8_t tmp = static_cast(lhs); + lhs = static_cast(tmp); + return lhs; +} + +PostRedirectFlags& operator&=(PostRedirectFlags& lhs, PostRedirectFlags rhs) { + lhs = static_cast(static_cast(lhs) & static_cast(rhs)); + return lhs; +} + +PostRedirectFlags& operator^=(PostRedirectFlags& lhs, PostRedirectFlags rhs) { + lhs = static_cast(static_cast(lhs) ^ static_cast(rhs)); + return lhs; +} + +bool any(PostRedirectFlags flag) { + return flag != PostRedirectFlags::NONE; +} +} // namespace cpr diff --git a/cpr/response.cpp b/cpr/response.cpp new file mode 100644 index 0000000..effe23d --- /dev/null +++ b/cpr/response.cpp @@ -0,0 +1,44 @@ +#include "cpr/response.h" + +namespace cpr { + +Response::Response(std::shared_ptr curl, std::string&& p_text, std::string&& p_header_string, Cookies&& p_cookies = Cookies{}, Error&& p_error = Error{}) : curl_(std::move(curl)), text(std::move(p_text)), cookies(std::move(p_cookies)), error(std::move(p_error)), raw_header(std::move(p_header_string)) { + header = cpr::util::parseHeader(raw_header, &status_line, &reason); + assert(curl_); + assert(curl_->handle); + curl_easy_getinfo(curl_->handle, CURLINFO_RESPONSE_CODE, &status_code); + curl_easy_getinfo(curl_->handle, CURLINFO_TOTAL_TIME, &elapsed); + char* url_string{nullptr}; + curl_easy_getinfo(curl_->handle, CURLINFO_EFFECTIVE_URL, &url_string); + url = Url(url_string); +#if LIBCURL_VERSION_NUM >= 0x073700 + curl_easy_getinfo(curl_->handle, CURLINFO_SIZE_DOWNLOAD_T, &downloaded_bytes); + curl_easy_getinfo(curl_->handle, CURLINFO_SIZE_UPLOAD_T, &uploaded_bytes); +#else + double downloaded_bytes_double, uploaded_bytes_double; + curl_easy_getinfo(curl_->handle, CURLINFO_SIZE_DOWNLOAD, &downloaded_bytes_double); + curl_easy_getinfo(curl_->handle, CURLINFO_SIZE_UPLOAD, &uploaded_bytes_double); + downloaded_bytes = downloaded_bytes_double; + uploaded_bytes = uploaded_bytes_double; +#endif + curl_easy_getinfo(curl_->handle, CURLINFO_REDIRECT_COUNT, &redirect_count); +} + +std::vector Response::GetCertInfos() { + assert(curl_); + assert(curl_->handle); + curl_certinfo* ci{nullptr}; + curl_easy_getinfo(curl_->handle, CURLINFO_CERTINFO, &ci); + + std::vector cert_infos; + for (int i = 0; i < ci->num_of_certs; i++) { + CertInfo cert_info; + // NOLINTNEXTLINE (cppcoreguidelines-pro-bounds-pointer-arithmetic) + for (curl_slist* slist = ci->certinfo[i]; slist; slist = slist->next) { + cert_info.emplace_back(std::string{slist->data}); + } + cert_infos.emplace_back(cert_info); + } + return cert_infos; +} +} // namespace cpr diff --git a/cpr/session.cpp b/cpr/session.cpp new file mode 100644 index 0000000..f4944b6 --- /dev/null +++ b/cpr/session.cpp @@ -0,0 +1,975 @@ +#include "cpr/session.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "cpr/async.h" +#include "cpr/cprtypes.h" +#include "cpr/interceptor.h" +#include "cpr/util.h" + +#if SUPPORT_CURLOPT_SSL_CTX_FUNCTION +#include "cpr/ssl_ctx.h" +#endif + + +namespace cpr { +// Ignored here since libcurl reqires a long: +// NOLINTNEXTLINE(google-runtime-int) +constexpr long ON = 1L; +// Ignored here since libcurl reqires a long: +// NOLINTNEXTLINE(google-runtime-int) +constexpr long OFF = 0L; + +CURLcode Session::DoEasyPerform() { + if (isUsedInMultiPerform) { + std::cerr << "curl_easy_perform cannot be executed if the CURL handle is used in a MultiPerform." << std::endl; + return CURLcode::CURLE_FAILED_INIT; + } + return curl_easy_perform(curl_->handle); +} + +void Session::SetHeaderInternal() { + curl_slist* chunk = nullptr; + for (const std::pair& item : header_) { + std::string header_string = item.first; + if (item.second.empty()) { + header_string += ";"; + } else { + header_string += ": " + item.second; + } + + curl_slist* temp = curl_slist_append(chunk, header_string.c_str()); + if (temp) { + chunk = temp; + } + } + + // Set the chunked transfer encoding in case it does not already exist: + if (chunkedTransferEncoding_ && header_.find("Transfer-Encoding") == header_.end()) { + curl_slist* temp = curl_slist_append(chunk, "Transfer-Encoding:chunked"); + if (temp) { + chunk = temp; + } + } + + // libcurl would prepare the header "Expect: 100-continue" by default when uploading files larger than 1 MB. + // Here we would like to disable this feature: + curl_slist* temp = curl_slist_append(chunk, "Expect:"); + if (temp) { + chunk = temp; + } + + curl_easy_setopt(curl_->handle, CURLOPT_HTTPHEADER, chunk); + + curl_slist_free_all(curl_->chunk); + curl_->chunk = chunk; +} + +// Only supported with libcurl >= 7.61.0. +// As an alternative use SetHeader and add the token manually. +#if LIBCURL_VERSION_NUM >= 0x073D00 +void Session::SetBearer(const Bearer& token) { + // Ignore here since this has been defined by libcurl. + curl_easy_setopt(curl_->handle, CURLOPT_HTTPAUTH, CURLAUTH_BEARER); + curl_easy_setopt(curl_->handle, CURLOPT_XOAUTH2_BEARER, token.GetToken()); +} +#endif + +Session::Session() : curl_(new CurlHolder()) { + // Set up some sensible defaults + curl_version_info_data* version_info = curl_version_info(CURLVERSION_NOW); + const std::string version = "curl/" + std::string{version_info->version}; + curl_easy_setopt(curl_->handle, CURLOPT_USERAGENT, version.c_str()); + SetRedirect(Redirect()); + curl_easy_setopt(curl_->handle, CURLOPT_NOPROGRESS, 1L); + curl_easy_setopt(curl_->handle, CURLOPT_ERRORBUFFER, curl_->error.data()); + curl_easy_setopt(curl_->handle, CURLOPT_COOKIEFILE, ""); +#ifdef CPR_CURL_NOSIGNAL + curl_easy_setopt(curl_->handle, CURLOPT_NOSIGNAL, 1L); +#endif + +#if LIBCURL_VERSION_NUM >= 0x071900 + curl_easy_setopt(curl_->handle, CURLOPT_TCP_KEEPALIVE, 1L); +#endif +} + +Response Session::makeDownloadRequest() { + if (!interceptors_.empty()) { + return intercept(); + } + + const CURLcode curl_error = DoEasyPerform(); + + return CompleteDownload(curl_error); +} + +void Session::prepareCommon() { + assert(curl_->handle); + + // Set Header: + SetHeaderInternal(); + + const std::string parametersContent = parameters_.GetContent(*curl_); + if (!parametersContent.empty()) { + const Url new_url{url_ + "?" + parametersContent}; + curl_easy_setopt(curl_->handle, CURLOPT_URL, new_url.c_str()); + } else { + curl_easy_setopt(curl_->handle, CURLOPT_URL, url_.c_str()); + } + + // Proxy: + const std::string protocol = url_.str().substr(0, url_.str().find(':')); + if (proxies_.has(protocol)) { + curl_easy_setopt(curl_->handle, CURLOPT_PROXY, proxies_[protocol].c_str()); + if (proxyAuth_.has(protocol)) { + curl_easy_setopt(curl_->handle, CURLOPT_PROXYAUTH, CURLAUTH_ANY); + curl_easy_setopt(curl_->handle, CURLOPT_PROXYUSERPWD, proxyAuth_[protocol]); + } + } + +#if LIBCURL_VERSION_NUM >= 0x072100 + if (acceptEncoding_.empty()) { + // Enable all supported built-in compressions + curl_easy_setopt(curl_->handle, CURLOPT_ACCEPT_ENCODING, ""); + } else if (acceptEncoding_.disabled()) { + // Disable curl adding the 'Accept-Encoding' header + curl_easy_setopt(curl_->handle, CURLOPT_ACCEPT_ENCODING, nullptr); + } else { + curl_easy_setopt(curl_->handle, CURLOPT_ACCEPT_ENCODING, acceptEncoding_.getString().c_str()); + } +#endif + +#if LIBCURL_VERSION_NUM >= 0x077100 +#if SUPPORT_SSL_NO_REVOKE + // NOLINTNEXTLINE (google-runtime-int) + long bitmask{0}; + curl_easy_setopt(curl_->handle, CURLOPT_SSL_OPTIONS, &bitmask); + const bool noRevoke = bitmask & CURLSSLOPT_NO_REVOKE; +#endif + + // Fix loading certs from Windows cert store when using OpenSSL: + curl_easy_setopt(curl_->handle, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); + +// Ensure SSL no revoke is still set +#if SUPPORT_SSL_NO_REVOKE + if (noRevoke) { + curl_easy_setopt(curl_->handle, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NO_REVOKE); + } +#endif +#endif + + curl_->error[0] = '\0'; + + response_string_.clear(); + if (response_string_reserve_size_ > 0) { + response_string_.reserve(response_string_reserve_size_); + } + header_string_.clear(); + if (!this->writecb_.callback) { + curl_easy_setopt(curl_->handle, CURLOPT_WRITEFUNCTION, cpr::util::writeFunction); + curl_easy_setopt(curl_->handle, CURLOPT_WRITEDATA, &response_string_); + } + if (!this->headercb_.callback) { + curl_easy_setopt(curl_->handle, CURLOPT_HEADERFUNCTION, cpr::util::writeFunction); + curl_easy_setopt(curl_->handle, CURLOPT_HEADERDATA, &header_string_); + } + + // Enable so we are able to retrive certificate information: + curl_easy_setopt(curl_->handle, CURLOPT_CERTINFO, 1L); +} + +void Session::prepareCommonDownload() { + assert(curl_->handle); + + // Set Header: + SetHeaderInternal(); + + const std::string parametersContent = parameters_.GetContent(*curl_); + if (!parametersContent.empty()) { + const Url new_url{url_ + "?" + parametersContent}; + curl_easy_setopt(curl_->handle, CURLOPT_URL, new_url.c_str()); + } else { + curl_easy_setopt(curl_->handle, CURLOPT_URL, url_.c_str()); + } + + const std::string protocol = url_.str().substr(0, url_.str().find(':')); + if (proxies_.has(protocol)) { + curl_easy_setopt(curl_->handle, CURLOPT_PROXY, proxies_[protocol].c_str()); + if (proxyAuth_.has(protocol)) { + curl_easy_setopt(curl_->handle, CURLOPT_PROXYAUTH, CURLAUTH_ANY); + curl_easy_setopt(curl_->handle, CURLOPT_PROXYUSERPWD, proxyAuth_[protocol]); + } + } + + curl_->error[0] = '\0'; + + header_string_.clear(); + if (headercb_.callback) { + curl_easy_setopt(curl_->handle, CURLOPT_HEADERFUNCTION, cpr::util::headerUserFunction); + curl_easy_setopt(curl_->handle, CURLOPT_HEADERDATA, &headercb_); + } else { + curl_easy_setopt(curl_->handle, CURLOPT_HEADERFUNCTION, cpr::util::writeFunction); + curl_easy_setopt(curl_->handle, CURLOPT_HEADERDATA, &header_string_); + } +} + +Response Session::makeRequest() { + if (!interceptors_.empty()) { + return intercept(); + } + + const CURLcode curl_error = DoEasyPerform(); + return Complete(curl_error); +} + +void Session::SetLimitRate(const LimitRate& limit_rate) { + curl_easy_setopt(curl_->handle, CURLOPT_MAX_RECV_SPEED_LARGE, limit_rate.downrate); + curl_easy_setopt(curl_->handle, CURLOPT_MAX_SEND_SPEED_LARGE, limit_rate.uprate); +} + +void Session::SetReadCallback(const ReadCallback& read) { + readcb_ = read; + curl_easy_setopt(curl_->handle, CURLOPT_INFILESIZE_LARGE, read.size); + curl_easy_setopt(curl_->handle, CURLOPT_POSTFIELDSIZE_LARGE, read.size); + curl_easy_setopt(curl_->handle, CURLOPT_READFUNCTION, cpr::util::readUserFunction); + curl_easy_setopt(curl_->handle, CURLOPT_READDATA, &readcb_); + chunkedTransferEncoding_ = read.size == -1; +} + +void Session::SetHeaderCallback(const HeaderCallback& header) { + curl_easy_setopt(curl_->handle, CURLOPT_HEADERFUNCTION, cpr::util::headerUserFunction); + headercb_ = header; + curl_easy_setopt(curl_->handle, CURLOPT_HEADERDATA, &headercb_); +} + +void Session::SetWriteCallback(const WriteCallback& write) { + curl_easy_setopt(curl_->handle, CURLOPT_WRITEFUNCTION, cpr::util::writeUserFunction); + writecb_ = write; + curl_easy_setopt(curl_->handle, CURLOPT_WRITEDATA, &writecb_); +} + +void Session::SetProgressCallback(const ProgressCallback& progress) { + progresscb_ = progress; + if (isCancellable) { + cancellationcb_.SetProgressCallback(progresscb_); + return; + } +#if LIBCURL_VERSION_NUM < 0x072000 + curl_easy_setopt(curl_->handle, CURLOPT_PROGRESSFUNCTION, cpr::util::progressUserFunction); + curl_easy_setopt(curl_->handle, CURLOPT_PROGRESSDATA, &progresscb_); +#else + curl_easy_setopt(curl_->handle, CURLOPT_XFERINFOFUNCTION, cpr::util::progressUserFunction); + curl_easy_setopt(curl_->handle, CURLOPT_XFERINFODATA, &progresscb_); +#endif + curl_easy_setopt(curl_->handle, CURLOPT_NOPROGRESS, 0L); +} + +void Session::SetDebugCallback(const DebugCallback& debug) { + curl_easy_setopt(curl_->handle, CURLOPT_DEBUGFUNCTION, cpr::util::debugUserFunction); + debugcb_ = debug; + curl_easy_setopt(curl_->handle, CURLOPT_DEBUGDATA, &debugcb_); + curl_easy_setopt(curl_->handle, CURLOPT_VERBOSE, 1L); +} + +void Session::SetUrl(const Url& url) { + url_ = url; +} + +void Session::SetResolve(const Resolve& resolve) { + SetResolves({resolve}); +} + +void Session::SetResolves(const std::vector& resolves) { + curl_slist_free_all(curl_->resolveCurlList); + curl_->resolveCurlList = nullptr; + for (const Resolve& resolve : resolves) { + for (const uint16_t port : resolve.ports) { + curl_->resolveCurlList = curl_slist_append(curl_->resolveCurlList, (resolve.host + ":" + std::to_string(port) + ":" + resolve.addr).c_str()); + } + } + curl_easy_setopt(curl_->handle, CURLOPT_RESOLVE, curl_->resolveCurlList); +} + +void Session::SetParameters(const Parameters& parameters) { + parameters_ = parameters; +} + +void Session::SetParameters(Parameters&& parameters) { + parameters_ = std::move(parameters); +} + +void Session::SetHeader(const Header& header) { + header_ = header; +} + +void Session::UpdateHeader(const Header& header) { + for (const std::pair& item : header) { + header_[item.first] = item.second; + } +} + +void Session::SetTimeout(const Timeout& timeout) { + curl_easy_setopt(curl_->handle, CURLOPT_TIMEOUT_MS, timeout.Milliseconds()); +} + +void Session::SetConnectTimeout(const ConnectTimeout& timeout) { + curl_easy_setopt(curl_->handle, CURLOPT_CONNECTTIMEOUT_MS, timeout.Milliseconds()); +} + +void Session::SetAuth(const Authentication& auth) { + // Ignore here since this has been defined by libcurl. + switch (auth.GetAuthMode()) { + case AuthMode::BASIC: + curl_easy_setopt(curl_->handle, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + curl_easy_setopt(curl_->handle, CURLOPT_USERPWD, auth.GetAuthString()); + break; + case AuthMode::DIGEST: + curl_easy_setopt(curl_->handle, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); + curl_easy_setopt(curl_->handle, CURLOPT_USERPWD, auth.GetAuthString()); + break; + case AuthMode::NTLM: + curl_easy_setopt(curl_->handle, CURLOPT_HTTPAUTH, CURLAUTH_NTLM); + curl_easy_setopt(curl_->handle, CURLOPT_USERPWD, auth.GetAuthString()); + break; + } +} + +void Session::SetUserAgent(const UserAgent& ua) { + curl_easy_setopt(curl_->handle, CURLOPT_USERAGENT, ua.c_str()); +} + +void Session::SetPayload(const Payload& payload) { + hasBodyOrPayload_ = true; + const std::string content = payload.GetContent(*curl_); + curl_easy_setopt(curl_->handle, CURLOPT_POSTFIELDSIZE_LARGE, static_cast(content.length())); + curl_easy_setopt(curl_->handle, CURLOPT_COPYPOSTFIELDS, content.c_str()); +} + +void Session::SetPayload(Payload&& payload) { + hasBodyOrPayload_ = true; + const std::string content = payload.GetContent(*curl_); + curl_easy_setopt(curl_->handle, CURLOPT_POSTFIELDSIZE_LARGE, static_cast(content.length())); + curl_easy_setopt(curl_->handle, CURLOPT_COPYPOSTFIELDS, content.c_str()); +} + +void Session::SetProxies(const Proxies& proxies) { + proxies_ = proxies; +} + +void Session::SetProxies(Proxies&& proxies) { + proxies_ = std::move(proxies); +} + +void Session::SetProxyAuth(ProxyAuthentication&& proxy_auth) { + proxyAuth_ = std::move(proxy_auth); +} + +void Session::SetProxyAuth(const ProxyAuthentication& proxy_auth) { + proxyAuth_ = proxy_auth; +} + +void Session::SetMultipart(const Multipart& multipart) { + // Make sure, we have a empty multipart to start with: + if (curl_->multipart) { + curl_mime_free(curl_->multipart); + } + curl_->multipart = curl_mime_init(curl_->handle); + + // Add all multipart pieces: + for (const Part& part : multipart.parts) { + if (part.is_file) { + for (const File& file : part.files) { + curl_mimepart* mimePart = curl_mime_addpart(curl_->multipart); + if (!part.content_type.empty()) { + curl_mime_type(mimePart, part.content_type.c_str()); + } + + curl_mime_filedata(mimePart, file.filepath.c_str()); + curl_mime_name(mimePart, part.name.c_str()); + + if (file.hasOverridenFilename()) { + curl_mime_filename(mimePart, file.overriden_filename.c_str()); + } + } + } else { + curl_mimepart* mimePart = curl_mime_addpart(curl_->multipart); + if (!part.content_type.empty()) { + curl_mime_type(mimePart, part.content_type.c_str()); + } + if (part.is_buffer) { + // Do not use formdata, to prevent having to use reinterpreter_cast: + curl_mime_name(mimePart, part.name.c_str()); + curl_mime_data(mimePart, part.data, part.datalen); + curl_mime_filename(mimePart, part.value.c_str()); + } else { + curl_mime_name(mimePart, part.name.c_str()); + curl_mime_data(mimePart, part.value.c_str(), CURL_ZERO_TERMINATED); + } + } + } + + curl_easy_setopt(curl_->handle, CURLOPT_MIMEPOST, curl_->multipart); + hasBodyOrPayload_ = true; +} + +void Session::SetMultipart(Multipart&& multipart) { + SetMultipart(multipart); +} + +void Session::SetRedirect(const Redirect& redirect) { + curl_easy_setopt(curl_->handle, CURLOPT_FOLLOWLOCATION, redirect.follow ? 1L : 0L); + curl_easy_setopt(curl_->handle, CURLOPT_MAXREDIRS, redirect.maximum); + curl_easy_setopt(curl_->handle, CURLOPT_UNRESTRICTED_AUTH, redirect.cont_send_cred ? 1L : 0L); + + // NOLINTNEXTLINE (google-runtime-int) + long mask = 0; + if (any(redirect.post_flags & PostRedirectFlags::POST_301)) { + mask |= CURL_REDIR_POST_301; + } + if (any(redirect.post_flags & PostRedirectFlags::POST_302)) { + mask |= CURL_REDIR_POST_302; + } + if (any(redirect.post_flags & PostRedirectFlags::POST_303)) { + mask |= CURL_REDIR_POST_303; + } + curl_easy_setopt(curl_->handle, CURLOPT_POSTREDIR, mask); +} + +void Session::SetCookies(const Cookies& cookies) { + curl_easy_setopt(curl_->handle, CURLOPT_COOKIELIST, "ALL"); + curl_easy_setopt(curl_->handle, CURLOPT_COOKIE, cookies.GetEncoded(*curl_).c_str()); +} + +void Session::SetBody(const Body& body) { + hasBodyOrPayload_ = true; + curl_easy_setopt(curl_->handle, CURLOPT_POSTFIELDSIZE_LARGE, static_cast(body.str().length())); + curl_easy_setopt(curl_->handle, CURLOPT_POSTFIELDS, body.c_str()); +} + +void Session::SetBody(Body&& body) { + hasBodyOrPayload_ = true; + curl_easy_setopt(curl_->handle, CURLOPT_POSTFIELDSIZE_LARGE, static_cast(body.str().length())); + curl_easy_setopt(curl_->handle, CURLOPT_COPYPOSTFIELDS, body.c_str()); +} + +void Session::SetLowSpeed(const LowSpeed& low_speed) { + curl_easy_setopt(curl_->handle, CURLOPT_LOW_SPEED_LIMIT, low_speed.limit); + curl_easy_setopt(curl_->handle, CURLOPT_LOW_SPEED_TIME, low_speed.time); +} + +void Session::SetVerifySsl(const VerifySsl& verify) { + curl_easy_setopt(curl_->handle, CURLOPT_SSL_VERIFYPEER, verify ? ON : OFF); + curl_easy_setopt(curl_->handle, CURLOPT_SSL_VERIFYHOST, verify ? 2L : 0L); +} + +void Session::SetUnixSocket(const UnixSocket& unix_socket) { + curl_easy_setopt(curl_->handle, CURLOPT_UNIX_SOCKET_PATH, unix_socket.GetUnixSocketString()); +} + +void Session::SetSslOptions(const SslOptions& options) { + if (!options.cert_file.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_SSLCERT, options.cert_file.c_str()); + if (!options.cert_type.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_SSLCERTTYPE, options.cert_type.c_str()); + } + } + if (!options.key_file.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_SSLKEY, options.key_file.c_str()); + if (!options.key_type.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_SSLKEYTYPE, options.key_type.c_str()); + } + if (!options.key_pass.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_KEYPASSWD, options.key_pass.c_str()); + } +#if SUPPORT_CURLOPT_SSLKEY_BLOB + } else if (!options.key_blob.empty()) { + std::string key_blob(options.key_blob); + curl_blob blob{}; + // NOLINTNEXTLINE (readability-container-data-pointer) + blob.data = &key_blob[0]; + blob.len = key_blob.length(); + curl_easy_setopt(curl_->handle, CURLOPT_SSLKEY_BLOB, &blob); + if (!options.key_type.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_SSLKEYTYPE, options.key_type.c_str()); + } + if (!options.key_pass.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_KEYPASSWD, options.key_pass.c_str()); + } +#endif + } + if (!options.pinned_public_key.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_PINNEDPUBLICKEY, options.pinned_public_key.c_str()); + } +#if SUPPORT_ALPN + curl_easy_setopt(curl_->handle, CURLOPT_SSL_ENABLE_ALPN, options.enable_alpn ? ON : OFF); +#endif +#if SUPPORT_NPN + curl_easy_setopt(curl_->handle, CURLOPT_SSL_ENABLE_NPN, options.enable_npn ? ON : OFF); +#endif + curl_easy_setopt(curl_->handle, CURLOPT_SSL_VERIFYPEER, options.verify_peer ? ON : OFF); + curl_easy_setopt(curl_->handle, CURLOPT_SSL_VERIFYHOST, options.verify_host ? 2L : 0L); +#if LIBCURL_VERSION_NUM >= 0x072900 + curl_easy_setopt(curl_->handle, CURLOPT_SSL_VERIFYSTATUS, options.verify_status ? ON : OFF); +#endif + + int maxTlsVersion = options.ssl_version; +#if SUPPORT_MAX_TLS_VERSION + maxTlsVersion |= options.max_version; +#endif + + curl_easy_setopt(curl_->handle, CURLOPT_SSLVERSION, + // Ignore here since this has been defined by libcurl. + maxTlsVersion); +#if SUPPORT_SSL_NO_REVOKE + if (options.ssl_no_revoke) { + curl_easy_setopt(curl_->handle, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NO_REVOKE); + } +#endif + if (!options.ca_info.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_CAINFO, options.ca_info.c_str()); + } + if (!options.ca_path.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_CAPATH, options.ca_path.c_str()); + } +#if SUPPORT_CURLOPT_SSL_CTX_FUNCTION +#ifdef OPENSSL_BACKEND_USED + if (!options.ca_buffer.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_SSL_CTX_FUNCTION, sslctx_function_load_ca_cert_from_buffer); + curl_easy_setopt(curl_->handle, CURLOPT_SSL_CTX_DATA, options.ca_buffer.c_str()); + } +#endif +#endif + if (!options.crl_file.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_CRLFILE, options.crl_file.c_str()); + } + if (!options.ciphers.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_SSL_CIPHER_LIST, options.ciphers.c_str()); + } +#if SUPPORT_TLSv13_CIPHERS + if (!options.tls13_ciphers.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_TLS13_CIPHERS, options.ciphers.c_str()); + } +#endif +#if SUPPORT_SESSIONID_CACHE + curl_easy_setopt(curl_->handle, CURLOPT_SSL_SESSIONID_CACHE, options.session_id_cache ? ON : OFF); +#endif +} + +void Session::SetVerbose(const Verbose& verbose) { + curl_easy_setopt(curl_->handle, CURLOPT_VERBOSE, verbose.verbose ? ON : OFF); +} + +void Session::SetInterface(const Interface& iface) { + if (iface.str().empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_INTERFACE, nullptr); + } else { + curl_easy_setopt(curl_->handle, CURLOPT_INTERFACE, iface.c_str()); + } +} + +void Session::SetLocalPort(const LocalPort& local_port) { + curl_easy_setopt(curl_->handle, CURLOPT_LOCALPORT, local_port); +} + +void Session::SetLocalPortRange(const LocalPortRange& local_port_range) { + curl_easy_setopt(curl_->handle, CURLOPT_LOCALPORTRANGE, local_port_range); +} + +void Session::SetHttpVersion(const HttpVersion& version) { + switch (version.code) { + case HttpVersionCode::VERSION_NONE: + curl_easy_setopt(curl_->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_NONE); + break; + + case HttpVersionCode::VERSION_1_0: + curl_easy_setopt(curl_->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); + break; + + case HttpVersionCode::VERSION_1_1: + curl_easy_setopt(curl_->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + break; + +#if LIBCURL_VERSION_NUM >= 0x072100 // 7.33.0 + case HttpVersionCode::VERSION_2_0: + curl_easy_setopt(curl_->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0); + break; +#endif + +#if LIBCURL_VERSION_NUM >= 0x072F00 // 7.47.0 + case HttpVersionCode::VERSION_2_0_TLS: + curl_easy_setopt(curl_->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS); + break; +#endif + +#if LIBCURL_VERSION_NUM >= 0x073100 // 7.49.0 + case HttpVersionCode::VERSION_2_0_PRIOR_KNOWLEDGE: + curl_easy_setopt(curl_->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE); + break; +#endif + +#if LIBCURL_VERSION_NUM >= 0x074200 // 7.66.0 + case HttpVersionCode::VERSION_3_0: + curl_easy_setopt(curl_->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_3); + break; +#endif + + default: // Should not happen + throw std::invalid_argument("Invalid/Unknown HTTP version type."); + break; + } +} + +void Session::SetRange(const Range& range) { + const std::string range_str = range.str(); + curl_easy_setopt(curl_->handle, CURLOPT_RANGE, range_str.c_str()); +} + +void Session::SetMultiRange(const MultiRange& multi_range) { + const std::string multi_range_str = multi_range.str(); + curl_easy_setopt(curl_->handle, CURLOPT_RANGE, multi_range_str.c_str()); +} + +void Session::SetReserveSize(const ReserveSize& reserve_size) { + ResponseStringReserve(reserve_size.size); +} + +void Session::SetAcceptEncoding(const AcceptEncoding& accept_encoding) { + acceptEncoding_ = accept_encoding; +} + +void Session::SetAcceptEncoding(AcceptEncoding&& accept_encoding) { + acceptEncoding_ = std::move(accept_encoding); +} + +cpr_off_t Session::GetDownloadFileLength() { + cpr_off_t downloadFileLenth = -1; + curl_easy_setopt(curl_->handle, CURLOPT_URL, url_.c_str()); + + const std::string protocol = url_.str().substr(0, url_.str().find(':')); + if (proxies_.has(protocol)) { + curl_easy_setopt(curl_->handle, CURLOPT_PROXY, proxies_[protocol].c_str()); + if (proxyAuth_.has(protocol)) { + curl_easy_setopt(curl_->handle, CURLOPT_PROXYAUTH, CURLAUTH_ANY); + curl_easy_setopt(curl_->handle, CURLOPT_PROXYUSERPWD, proxyAuth_[protocol]); + } + } + + curl_easy_setopt(curl_->handle, CURLOPT_HTTPGET, 1); + curl_easy_setopt(curl_->handle, CURLOPT_NOBODY, 1); + if (DoEasyPerform() == CURLE_OK) { + // NOLINTNEXTLINE (google-runtime-int) + long status_code{}; + curl_easy_getinfo(curl_->handle, CURLINFO_RESPONSE_CODE, &status_code); + if (200 == status_code) { + curl_easy_getinfo(curl_->handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &downloadFileLenth); + } + } + return downloadFileLenth; +} + +void Session::ResponseStringReserve(size_t size) { + response_string_reserve_size_ = size; +} + +Response Session::Delete() { + PrepareDelete(); + return makeRequest(); +} + +Response Session::Download(const WriteCallback& write) { + PrepareDownload(write); + return makeDownloadRequest(); +} + +Response Session::Download(std::ofstream& file) { + PrepareDownload(file); + return makeDownloadRequest(); +} + +Response Session::Get() { + PrepareGet(); + return makeRequest(); +} + +Response Session::Head() { + PrepareHead(); + return makeRequest(); +} + +Response Session::Options() { + PrepareOptions(); + return makeRequest(); +} + +Response Session::Patch() { + PreparePatch(); + return makeRequest(); +} + +Response Session::Post() { + PreparePost(); + return makeRequest(); +} + +Response Session::Put() { + PreparePut(); + return makeRequest(); +} + +std::shared_ptr Session::GetSharedPtrFromThis() { + try { + return shared_from_this(); + } catch (std::bad_weak_ptr&) { + throw std::runtime_error("Failed to get a shared pointer from this. The reason is probably that the session object is not managed by a shared pointer, which is required to use this functionality."); + } +} + +AsyncResponse Session::GetAsync() { + auto shared_this = shared_from_this(); + return async([shared_this]() { return shared_this->Get(); }); +} + +AsyncResponse Session::DeleteAsync() { + return async([shared_this = GetSharedPtrFromThis()]() { return shared_this->Delete(); }); +} + +AsyncResponse Session::DownloadAsync(const WriteCallback& write) { + return async([shared_this = GetSharedPtrFromThis(), write]() { return shared_this->Download(write); }); +} + +AsyncResponse Session::DownloadAsync(std::ofstream& file) { + return async([shared_this = GetSharedPtrFromThis(), &file]() { return shared_this->Download(file); }); +} + +AsyncResponse Session::HeadAsync() { + return async([shared_this = GetSharedPtrFromThis()]() { return shared_this->Head(); }); +} + +AsyncResponse Session::OptionsAsync() { + return async([shared_this = GetSharedPtrFromThis()]() { return shared_this->Options(); }); +} + +AsyncResponse Session::PatchAsync() { + return async([shared_this = GetSharedPtrFromThis()]() { return shared_this->Patch(); }); +} + +AsyncResponse Session::PostAsync() { + return async([shared_this = GetSharedPtrFromThis()]() { return shared_this->Post(); }); +} + +AsyncResponse Session::PutAsync() { + return async([shared_this = GetSharedPtrFromThis()]() { return shared_this->Put(); }); +} + +std::shared_ptr Session::GetCurlHolder() { + return curl_; +} + +std::string Session::GetFullRequestUrl() { + const std::string parametersContent = parameters_.GetContent(*curl_); + return url_.str() + (parametersContent.empty() ? "" : "?") + parametersContent; +} + +void Session::PrepareDelete() { + curl_easy_setopt(curl_->handle, CURLOPT_HTTPGET, 0L); + curl_easy_setopt(curl_->handle, CURLOPT_NOBODY, 0L); + curl_easy_setopt(curl_->handle, CURLOPT_CUSTOMREQUEST, "DELETE"); + prepareCommon(); +} + +void Session::PrepareGet() { + // In case there is a body or payload for this request, we create a custom GET-Request since a + // GET-Request with body is based on the HTTP RFC **not** a leagal request. + if (hasBodyOrPayload_) { + curl_easy_setopt(curl_->handle, CURLOPT_NOBODY, 0L); + curl_easy_setopt(curl_->handle, CURLOPT_CUSTOMREQUEST, "GET"); + } else { + curl_easy_setopt(curl_->handle, CURLOPT_NOBODY, 0L); + curl_easy_setopt(curl_->handle, CURLOPT_CUSTOMREQUEST, nullptr); + curl_easy_setopt(curl_->handle, CURLOPT_HTTPGET, 1L); + } + prepareCommon(); +} + +void Session::PrepareHead() { + curl_easy_setopt(curl_->handle, CURLOPT_NOBODY, 1L); + curl_easy_setopt(curl_->handle, CURLOPT_CUSTOMREQUEST, nullptr); + prepareCommon(); +} + +void Session::PrepareOptions() { + curl_easy_setopt(curl_->handle, CURLOPT_NOBODY, 0L); + curl_easy_setopt(curl_->handle, CURLOPT_CUSTOMREQUEST, "OPTIONS"); + prepareCommon(); +} + +void Session::PreparePatch() { + curl_easy_setopt(curl_->handle, CURLOPT_NOBODY, 0L); + curl_easy_setopt(curl_->handle, CURLOPT_CUSTOMREQUEST, "PATCH"); + prepareCommon(); +} + +void Session::PreparePost() { + curl_easy_setopt(curl_->handle, CURLOPT_NOBODY, 0L); + + // In case there is no body or payload set it to an empty post: + if (hasBodyOrPayload_) { + curl_easy_setopt(curl_->handle, CURLOPT_CUSTOMREQUEST, nullptr); + } else { + curl_easy_setopt(curl_->handle, CURLOPT_POSTFIELDS, readcb_.callback ? nullptr : ""); + curl_easy_setopt(curl_->handle, CURLOPT_CUSTOMREQUEST, "POST"); + } + prepareCommon(); +} + +void Session::PreparePut() { + curl_easy_setopt(curl_->handle, CURLOPT_NOBODY, 0L); + if (!hasBodyOrPayload_ && readcb_.callback) { + /** + * Yes, this one has to be CURLOPT_POSTFIELDS even if we are performing a PUT request. + * In case we don't set this one, performing a POST-request with PUT won't work. + * It in theory this only enforces the usage of the readcallback for POST requests, but works here as well. + **/ + curl_easy_setopt(curl_->handle, CURLOPT_POSTFIELDS, nullptr); + } + curl_easy_setopt(curl_->handle, CURLOPT_CUSTOMREQUEST, "PUT"); + curl_easy_setopt(curl_->handle, CURLOPT_RANGE, nullptr); + prepareCommon(); +} + +void Session::PrepareDownload(std::ofstream& file) { + curl_easy_setopt(curl_->handle, CURLOPT_NOBODY, 0L); + curl_easy_setopt(curl_->handle, CURLOPT_HTTPGET, 1); + curl_easy_setopt(curl_->handle, CURLOPT_WRITEFUNCTION, cpr::util::writeFileFunction); + curl_easy_setopt(curl_->handle, CURLOPT_WRITEDATA, &file); + curl_easy_setopt(curl_->handle, CURLOPT_CUSTOMREQUEST, nullptr); + + prepareCommonDownload(); +} + +void Session::PrepareDownload(const WriteCallback& write) { + curl_easy_setopt(curl_->handle, CURLOPT_NOBODY, 0L); + curl_easy_setopt(curl_->handle, CURLOPT_HTTPGET, 1); + curl_easy_setopt(curl_->handle, CURLOPT_CUSTOMREQUEST, nullptr); + + SetWriteCallback(write); + + prepareCommonDownload(); +} + +Response Session::Complete(CURLcode curl_error) { + curl_slist* raw_cookies{nullptr}; + curl_easy_getinfo(curl_->handle, CURLINFO_COOKIELIST, &raw_cookies); + Cookies cookies = util::parseCookies(raw_cookies); + curl_slist_free_all(raw_cookies); + + // Reset the has no body property: + hasBodyOrPayload_ = false; + + std::string errorMsg = curl_->error.data(); + return Response(curl_, std::move(response_string_), std::move(header_string_), std::move(cookies), Error(curl_error, std::move(errorMsg))); +} + +Response Session::CompleteDownload(CURLcode curl_error) { + if (!headercb_.callback) { + curl_easy_setopt(curl_->handle, CURLOPT_HEADERFUNCTION, nullptr); + curl_easy_setopt(curl_->handle, CURLOPT_HEADERDATA, 0); + } + + curl_slist* raw_cookies{nullptr}; + curl_easy_getinfo(curl_->handle, CURLINFO_COOKIELIST, &raw_cookies); + Cookies cookies = util::parseCookies(raw_cookies); + curl_slist_free_all(raw_cookies); + std::string errorMsg = curl_->error.data(); + + return Response(curl_, "", std::move(header_string_), std::move(cookies), Error(curl_error, std::move(errorMsg))); +} + +void Session::AddInterceptor(const std::shared_ptr& pinterceptor) { + interceptors_.push(pinterceptor); +} + +Response Session::proceed() { + prepareCommon(); + return makeRequest(); +} + +Response Session::intercept() { + // At least one interceptor exists -> Execute its intercept function + const std::shared_ptr interceptor = interceptors_.front(); + interceptors_.pop(); + return interceptor->intercept(*this); +} + +// clang-format off +void Session::SetOption(const Resolve& resolve) { SetResolve(resolve); } +void Session::SetOption(const std::vector& resolves) { SetResolves(resolves); } +void Session::SetOption(const ReadCallback& read) { SetReadCallback(read); } +void Session::SetOption(const HeaderCallback& header) { SetHeaderCallback(header); } +void Session::SetOption(const WriteCallback& write) { SetWriteCallback(write); } +void Session::SetOption(const ProgressCallback& progress) { SetProgressCallback(progress); } +void Session::SetOption(const DebugCallback& debug) { SetDebugCallback(debug); } +void Session::SetOption(const Url& url) { SetUrl(url); } +void Session::SetOption(const Parameters& parameters) { SetParameters(parameters); } +void Session::SetOption(Parameters&& parameters) { SetParameters(std::move(parameters)); } +void Session::SetOption(const Header& header) { SetHeader(header); } +void Session::SetOption(const Timeout& timeout) { SetTimeout(timeout); } +void Session::SetOption(const ConnectTimeout& timeout) { SetConnectTimeout(timeout); } +void Session::SetOption(const Authentication& auth) { SetAuth(auth); } +void Session::SetOption(const LimitRate& limit_rate) { SetLimitRate(limit_rate); } +// Only supported with libcurl >= 7.61.0. +// As an alternative use SetHeader and add the token manually. +#if LIBCURL_VERSION_NUM >= 0x073D00 +void Session::SetOption(const Bearer& auth) { SetBearer(auth); } +#endif +void Session::SetOption(const UserAgent& ua) { SetUserAgent(ua); } +void Session::SetOption(const Payload& payload) { SetPayload(payload); } +void Session::SetOption(Payload&& payload) { SetPayload(std::move(payload)); } +void Session::SetOption(const Proxies& proxies) { SetProxies(proxies); } +void Session::SetOption(Proxies&& proxies) { SetProxies(std::move(proxies)); } +void Session::SetOption(ProxyAuthentication&& proxy_auth) { SetProxyAuth(std::move(proxy_auth)); } +void Session::SetOption(const ProxyAuthentication& proxy_auth) { SetProxyAuth(proxy_auth); } +void Session::SetOption(const Multipart& multipart) { SetMultipart(multipart); } +void Session::SetOption(Multipart&& multipart) { SetMultipart(std::move(multipart)); } +void Session::SetOption(const Redirect& redirect) { SetRedirect(redirect); } +void Session::SetOption(const Cookies& cookies) { SetCookies(cookies); } +void Session::SetOption(const Body& body) { SetBody(body); } +void Session::SetOption(Body&& body) { SetBody(std::move(body)); } +void Session::SetOption(const LowSpeed& low_speed) { SetLowSpeed(low_speed); } +void Session::SetOption(const VerifySsl& verify) { SetVerifySsl(verify); } +void Session::SetOption(const Verbose& verbose) { SetVerbose(verbose); } +void Session::SetOption(const UnixSocket& unix_socket) { SetUnixSocket(unix_socket); } +void Session::SetOption(const SslOptions& options) { SetSslOptions(options); } +void Session::SetOption(const Interface& iface) { SetInterface(iface); } +void Session::SetOption(const LocalPort& local_port) { SetLocalPort(local_port); } +void Session::SetOption(const LocalPortRange& local_port_range) { SetLocalPortRange(local_port_range); } +void Session::SetOption(const HttpVersion& version) { SetHttpVersion(version); } +void Session::SetOption(const Range& range) { SetRange(range); } +void Session::SetOption(const MultiRange& multi_range) { SetMultiRange(multi_range); } +void Session::SetOption(const ReserveSize& reserve_size) { SetReserveSize(reserve_size.size); } +void Session::SetOption(const AcceptEncoding& accept_encoding) { SetAcceptEncoding(accept_encoding); } +void Session::SetOption(AcceptEncoding&& accept_encoding) { SetAcceptEncoding(accept_encoding); } +// clang-format on + +void Session::SetCancellationParam(std::shared_ptr param) { + cancellationcb_ = CancellationCallback{std::move(param)}; + isCancellable = true; +#if LIBCURL_VERSION_NUM < 0x072000 + curl_easy_setopt(curl_->handle, CURLOPT_PROGRESSFUNCTION, cpr::util::progressUserFunction); + curl_easy_setopt(curl_->handle, CURLOPT_PROGRESSDATA, &cancellationcb_); +#else + curl_easy_setopt(curl_->handle, CURLOPT_XFERINFOFUNCTION, cpr::util::progressUserFunction); + curl_easy_setopt(curl_->handle, CURLOPT_XFERINFODATA, &cancellationcb_); +#endif + curl_easy_setopt(curl_->handle, CURLOPT_NOPROGRESS, 0L); +} +} // namespace cpr diff --git a/cpr/ssl_ctx.cpp b/cpr/ssl_ctx.cpp new file mode 100644 index 0000000..c0753d3 --- /dev/null +++ b/cpr/ssl_ctx.cpp @@ -0,0 +1,70 @@ + +#include "cpr/ssl_ctx.h" + +#if SUPPORT_CURLOPT_SSL_CTX_FUNCTION + +#ifdef OPENSSL_BACKEND_USED + +#include +#include +#include + +namespace cpr { + +/** + * The ssl_ctx parameter is actually a pointer to the SSL library's SSL_CTX for OpenSSL. + * If an error is returned from the callback no attempt to establish a connection is made and + * the perform operation will return the callback's error code. + * + * Sources: https://curl.se/libcurl/c/CURLOPT_SSL_CTX_FUNCTION.html + * https://curl.se/libcurl/c/CURLOPT_SSL_CTX_DATA.html + */ +CURLcode sslctx_function_load_ca_cert_from_buffer(CURL* /*curl*/, void* sslctx, void* raw_cert_buf) { + // Check arguments + if (raw_cert_buf == nullptr || sslctx == nullptr) { + printf("Invalid callback arguments\n"); + return CURLE_ABORTED_BY_CALLBACK; + } + + // Setup pointer + X509_STORE* store = nullptr; + X509* cert = nullptr; + BIO* bio = nullptr; + char* cert_buf = static_cast(raw_cert_buf); + + // Create a memory BIO using the data of cert_buf. + // Note: It is assumed, that cert_buf is nul terminated and its length is determined by strlen. + bio = BIO_new_mem_buf(cert_buf, -1); + + // Load the PEM formatted certicifate into an X509 structure which OpenSSL can use. + PEM_read_bio_X509(bio, &cert, nullptr, nullptr); + if (cert == nullptr) { + printf("PEM_read_bio_X509 failed\n"); + return CURLE_ABORTED_BY_CALLBACK; + } + + // Get a pointer to the current certificate verification storage + store = SSL_CTX_get_cert_store(static_cast(sslctx)); + + // Add the loaded certificate to the verification storage + const int status = X509_STORE_add_cert(store, cert); + if (status == 0) { + printf("Error adding certificate\n"); + return CURLE_ABORTED_BY_CALLBACK; + } + + // Decrement the reference count of the X509 structure cert and frees it up + X509_free(cert); + + // Free the entire bio chain + BIO_free(bio); + + // The CA certificate was loaded successfully into the verification storage + return CURLE_OK; +} + +} // namespace cpr + +#endif // OPENSSL_BACKEND_USED + +#endif // SUPPORT_CURLOPT_SSL_CTX_FUNCTION \ No newline at end of file diff --git a/cpr/threadpool.cpp b/cpr/threadpool.cpp new file mode 100644 index 0000000..8ab1ee8 --- /dev/null +++ b/cpr/threadpool.cpp @@ -0,0 +1,148 @@ +#include "cpr/threadpool.h" + +namespace cpr { + +ThreadPool::ThreadPool(size_t min_threads, size_t max_threads, std::chrono::milliseconds max_idle_ms) : min_thread_num(min_threads), max_thread_num(max_threads), max_idle_time(max_idle_ms), status(STOP), cur_thread_num(0), idle_thread_num(0) {} + +ThreadPool::~ThreadPool() { + Stop(); +} + +int ThreadPool::Start(size_t start_threads) { + if (status != STOP) { + return -1; + } + status = RUNNING; + if (start_threads < min_thread_num) { + start_threads = min_thread_num; + } + if (start_threads > max_thread_num) { + start_threads = max_thread_num; + } + for (size_t i = 0; i < start_threads; ++i) { + CreateThread(); + } + return 0; +} + +int ThreadPool::Stop() { + if (status == STOP) { + return -1; + } + status = STOP; + task_cond.notify_all(); + for (auto& i : threads) { + if (i.thread->joinable()) { + i.thread->join(); + } + } + threads.clear(); + cur_thread_num = 0; + idle_thread_num = 0; + return 0; +} + +int ThreadPool::Pause() { + if (status == RUNNING) { + status = PAUSE; + } + return 0; +} + +int ThreadPool::Resume() { + if (status == PAUSE) { + status = RUNNING; + } + return 0; +} + +int ThreadPool::Wait() { + while (true) { + if (status == STOP || (tasks.empty() && idle_thread_num == cur_thread_num)) { + break; + } + std::this_thread::yield(); + } + return 0; +} + +bool ThreadPool::CreateThread() { + if (cur_thread_num >= max_thread_num) { + return false; + } + std::thread* thread = new std::thread([this] { + bool initialRun = true; + while (status != STOP) { + while (status == PAUSE) { + std::this_thread::yield(); + } + + Task task; + { + std::unique_lock locker(task_mutex); + task_cond.wait_for(locker, std::chrono::milliseconds(max_idle_time), [this]() { return status == STOP || !tasks.empty(); }); + if (status == STOP) { + return; + } + if (tasks.empty()) { + if (cur_thread_num > min_thread_num) { + DelThread(std::this_thread::get_id()); + return; + } + continue; + } + if (!initialRun) { + --idle_thread_num; + } + task = std::move(tasks.front()); + tasks.pop(); + } + if (task) { + task(); + ++idle_thread_num; + if (initialRun) { + initialRun = false; + } + } + } + }); + AddThread(thread); + return true; +} + +void ThreadPool::AddThread(std::thread* thread) { + thread_mutex.lock(); + ++cur_thread_num; + ThreadData data; + data.thread = std::shared_ptr(thread); + data.id = thread->get_id(); + data.status = RUNNING; + data.start_time = time(nullptr); + data.stop_time = 0; + threads.emplace_back(data); + thread_mutex.unlock(); +} + +void ThreadPool::DelThread(std::thread::id id) { + const time_t now = time(nullptr); + thread_mutex.lock(); + --cur_thread_num; + --idle_thread_num; + auto iter = threads.begin(); + while (iter != threads.end()) { + if (iter->status == STOP && now > iter->stop_time) { + if (iter->thread->joinable()) { + iter->thread->join(); + iter = threads.erase(iter); + continue; + } + } else if (iter->id == id) { + iter->status = STOP; + iter->stop_time = time(nullptr); + } + ++iter; + } + thread_mutex.unlock(); +} + +} // namespace cpr diff --git a/cpr/timeout.cpp b/cpr/timeout.cpp new file mode 100644 index 0000000..5bcd73b --- /dev/null +++ b/cpr/timeout.cpp @@ -0,0 +1,31 @@ +#include "cpr/timeout.h" + +#include +#include +#include +#include + +namespace cpr { + +// No way around since curl uses a long here. +// NOLINTNEXTLINE(google-runtime-int) +long Timeout::Milliseconds() const { + static_assert(std::is_same::value, "Following casting expects milliseconds."); + + // No way around since curl uses a long here. + // NOLINTNEXTLINE(google-runtime-int) + if (ms.count() > static_cast(std::numeric_limits::max())) { + throw std::overflow_error("cpr::Timeout: timeout value overflow: " + std::to_string(ms.count()) + " ms."); + } + // No way around since curl uses a long here. + // NOLINTNEXTLINE(google-runtime-int) + if (ms.count() < static_cast(std::numeric_limits::min())) { + throw std::underflow_error("cpr::Timeout: timeout value underflow: " + std::to_string(ms.count()) + " ms."); + } + + // No way around since curl uses a long here. + // NOLINTNEXTLINE(google-runtime-int) + return static_cast(ms.count()); +} + +} // namespace cpr diff --git a/cpr/unix_socket.cpp b/cpr/unix_socket.cpp new file mode 100644 index 0000000..3171dbc --- /dev/null +++ b/cpr/unix_socket.cpp @@ -0,0 +1,8 @@ + +#include "cpr/unix_socket.h" + +namespace cpr { +const char* UnixSocket::GetUnixSocketString() const noexcept { + return unix_socket_.data(); +} +} // namespace cpr diff --git a/cpr/util.cpp b/cpr/util.cpp new file mode 100644 index 0000000..951ef02 --- /dev/null +++ b/cpr/util.cpp @@ -0,0 +1,228 @@ +#include "cpr/util.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_Win32) +#include +#else +// https://en.cppreference.com/w/c/string/byte/memset +// NOLINTNEXTLINE(bugprone-reserved-identifier, cert-dcl37-c, cert-dcl51-cpp, cppcoreguidelines-macro-usage) +#define __STDC_WANT_LIB_EXT1__ 1 +#include +#endif + +namespace cpr::util { + +enum class CurlHTTPCookieField : size_t { + Domain = 0, + IncludeSubdomains, + Path, + HttpsOnly, + Expires, + Name, + Value, +}; + +Cookies parseCookies(curl_slist* raw_cookies) { + const int CURL_HTTP_COOKIE_SIZE = static_cast(CurlHTTPCookieField::Value) + 1; + Cookies cookies; + for (curl_slist* nc = raw_cookies; nc; nc = nc->next) { + std::vector tokens = cpr::util::split(nc->data, '\t'); + while (tokens.size() < CURL_HTTP_COOKIE_SIZE) { + tokens.emplace_back(""); + } + const std::time_t expires = static_cast(std::stoul(tokens.at(static_cast(CurlHTTPCookieField::Expires)))); + cookies.emplace_back(Cookie{ + tokens.at(static_cast(CurlHTTPCookieField::Name)), + tokens.at(static_cast(CurlHTTPCookieField::Value)), + tokens.at(static_cast(CurlHTTPCookieField::Domain)), + isTrue(tokens.at(static_cast(CurlHTTPCookieField::IncludeSubdomains))), + tokens.at(static_cast(CurlHTTPCookieField::Path)), + isTrue(tokens.at(static_cast(CurlHTTPCookieField::HttpsOnly))), + std::chrono::system_clock::from_time_t(expires), + }); + } + return cookies; +} + +Header parseHeader(const std::string& headers, std::string* status_line, std::string* reason) { + Header header; + std::vector lines; + std::istringstream stream(headers); + { + std::string line; + while (std::getline(stream, line, '\n')) { + lines.push_back(line); + } + } + + for (std::string& line : lines) { + if (line.substr(0, 5) == "HTTP/") { + // set the status_line if it was given + if ((status_line != nullptr) || (reason != nullptr)) { + line.resize(std::min(line.size(), line.find_last_not_of("\t\n\r ") + 1)); + if (status_line != nullptr) { + *status_line = line; + } + + // set the reason if it was given + if (reason != nullptr) { + const size_t pos1 = line.find_first_of("\t "); + size_t pos2 = std::string::npos; + if (pos1 != std::string::npos) { + pos2 = line.find_first_of("\t ", pos1 + 1); + } + if (pos2 != std::string::npos) { + line.erase(0, pos2 + 1); + *reason = line; + } + } + } + header.clear(); + } + + if (line.length() > 0) { + const size_t found = line.find(':'); + if (found != std::string::npos) { + std::string value = line.substr(found + 1); + value.erase(0, value.find_first_not_of("\t ")); + value.resize(std::min(value.size(), value.find_last_not_of("\t\n\r ") + 1)); + header[line.substr(0, found)] = value; + } + } + } + + return header; +} + +std::vector split(const std::string& to_split, char delimiter) { + std::vector tokens; + + std::stringstream stream(to_split); + std::string item; + while (std::getline(stream, item, delimiter)) { + tokens.push_back(item); + } + + return tokens; +} + +size_t readUserFunction(char* ptr, size_t size, size_t nitems, const ReadCallback* read) { + size *= nitems; + return (*read)(ptr, size) ? size : CURL_READFUNC_ABORT; +} + +size_t headerUserFunction(char* ptr, size_t size, size_t nmemb, const HeaderCallback* header) { + size *= nmemb; + return (*header)({ptr, size}) ? size : 0; +} + +size_t writeFunction(char* ptr, size_t size, size_t nmemb, std::string* data) { + size *= nmemb; + data->append(ptr, size); + return size; +} + +size_t writeFileFunction(char* ptr, size_t size, size_t nmemb, std::ofstream* file) { + size *= nmemb; + file->write(ptr, static_cast(size)); + return size; +} + +size_t writeUserFunction(char* ptr, size_t size, size_t nmemb, const WriteCallback* write) { + size *= nmemb; + return (*write)({ptr, size}) ? size : 0; +} + +int debugUserFunction(CURL* /*handle*/, curl_infotype type, char* data, size_t size, const DebugCallback* debug) { + (*debug)(static_cast(type), std::string(data, size)); + return 0; +} + +/** + * Creates a temporary CurlHolder object and uses it to escape the given string. + * If you plan to use this methode on a regular basis think about creating a CurlHolder + * object and calling urlEncode(std::string) on it. + * + * Example: + * CurlHolder holder; + * std::string input = "Hello World!"; + * std::string result = holder.urlEncode(input); + **/ +std::string urlEncode(const std::string& s) { + const CurlHolder holder; // Create a temporary new holder for URL encoding + return holder.urlEncode(s); +} + +/** + * Creates a temporary CurlHolder object and uses it to unescape the given string. + * If you plan to use this methode on a regular basis think about creating a CurlHolder + * object and calling urlDecode(std::string) on it. + * + * Example: + * CurlHolder holder; + * std::string input = "Hello%20World%21"; + * std::string result = holder.urlDecode(input); + **/ +std::string urlDecode(const std::string& s) { + const CurlHolder holder; // Create a temporary new holder for URL decoding + return holder.urlDecode(s); +} + +#if defined(__STDC_LIB_EXT1__) +void secureStringClear(std::string& s) { + if (s.empty()) { + return; + } + memset_s(&s.front(), s.length(), 0, s.length()); + s.clear(); +} +#elif defined(_WIN32) +void secureStringClear(std::string& s) { + if (s.empty()) { + return; + } + SecureZeroMemory(&s.front(), s.length()); + s.clear(); +} +#else +#if defined(__clang__) +#pragma clang optimize off // clang +#elif defined(__GNUC__) || defined(__MINGW32__) || defined(__MINGW32__) || defined(__MINGW64__) +#pragma GCC push_options // g++ +#pragma GCC optimize("O0") // g++ +#endif +void secureStringClear(std::string& s) { + if (s.empty()) { + return; + } + // NOLINTNEXTLINE (readability-container-data-pointer) + char* ptr = &(s[0]); + memset(ptr, '\0', s.length()); + s.clear(); +} + +#if defined(__clang__) +#pragma clang optimize on // clang +#elif defined(__GNUC__) || defined(__MINGW32__) || defined(__MINGW32__) || defined(__MINGW64__) +#pragma GCC pop_options // g++ +#endif +#endif + +bool isTrue(const std::string& s) { + std::string temp_string{s}; + std::transform(temp_string.begin(), temp_string.end(), temp_string.begin(), [](unsigned char c) { return std::tolower(c); }); + return temp_string == "true"; +} + +} // namespace cpr::util diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt new file mode 100644 index 0000000..3f14410 --- /dev/null +++ b/include/CMakeLists.txt @@ -0,0 +1,68 @@ +cmake_minimum_required(VERSION 3.15) + +target_include_directories(cpr PUBLIC + $ + $ + $) + +target_sources(cpr PRIVATE + # Header files (useful in IDEs) + cpr/accept_encoding.h + cpr/api.h + cpr/async.h + cpr/async_wrapper.h + cpr/auth.h + cpr/bearer.h + cpr/body.h + cpr/buffer.h + cpr/cert_info.h + cpr/cookies.h + cpr/cpr.h + cpr/cprtypes.h + cpr/curlholder.h + cpr/curlholder.h + cpr/error.h + cpr/file.h + cpr/limit_rate.h + cpr/local_port.h + cpr/local_port_range.h + cpr/multipart.h + cpr/parameters.h + cpr/payload.h + cpr/proxies.h + cpr/proxyauth.h + cpr/response.h + cpr/session.h + cpr/singleton.h + cpr/ssl_ctx.h + cpr/ssl_options.h + cpr/threadpool.h + cpr/timeout.h + cpr/unix_socket.h + cpr/util.h + cpr/verbose.h + cpr/interface.h + cpr/redirect.h + cpr/http_version.h + cpr/interceptor.h + cpr/filesystem.h + cpr/curlmultiholder.h + cpr/multiperform.h + cpr/resolve.h + ${PROJECT_BINARY_DIR}/cpr_generated_includes/cpr/cprver.h +) + +# Filesystem +if(CPR_USE_BOOST_FILESYSTEM) + find_package(Boost 1.44 REQUIRED COMPONENTS filesystem) + if(Boost_FOUND) + target_link_libraries(cpr PUBLIC Boost::filesystem) + endif() +endif() + +if (((CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9.1) OR (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND NOT WIN32)) AND NOT CPR_USE_BOOST_FILESYSTEM) + target_link_libraries(cpr PUBLIC stdc++fs) +endif() + +install(DIRECTORY cpr DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +install(DIRECTORY ${PROJECT_BINARY_DIR}/cpr_generated_includes/cpr DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) diff --git a/include/cpr/accept_encoding.h b/include/cpr/accept_encoding.h new file mode 100644 index 0000000..167d7c2 --- /dev/null +++ b/include/cpr/accept_encoding.h @@ -0,0 +1,41 @@ +#ifndef CPR_ACCEPT_ENCODING_H +#define CPR_ACCEPT_ENCODING_H + +#include +#include +#include +#include +#include + +namespace cpr { + +enum class AcceptEncodingMethods { + identity, + deflate, + zlib, + gzip, + disabled, +}; + +// NOLINTNEXTLINE(cert-err58-cpp) +static const std::map AcceptEncodingMethodsStringMap{{AcceptEncodingMethods::identity, "identity"}, {AcceptEncodingMethods::deflate, "deflate"}, {AcceptEncodingMethods::zlib, "zlib"}, {AcceptEncodingMethods::gzip, "gzip"}, {AcceptEncodingMethods::disabled, "disabled"}}; + +class AcceptEncoding { + public: + AcceptEncoding() = default; + // NOLINTNEXTLINE(google-explicit-constructor) + AcceptEncoding(const std::initializer_list& methods); + // NOLINTNEXTLINE(google-explicit-constructor) + AcceptEncoding(const std::initializer_list& methods); + + [[nodiscard]] bool empty() const noexcept; + [[nodiscard]] const std::string getString() const; + [[nodiscard]] bool disabled() const; + + private: + std::unordered_set methods_; +}; + +} // namespace cpr + +#endif diff --git a/include/cpr/api.h b/include/cpr/api.h new file mode 100644 index 0000000..ba9c64e --- /dev/null +++ b/include/cpr/api.h @@ -0,0 +1,392 @@ +#ifndef CPR_API_H +#define CPR_API_H + +#include +#include +#include +#include +#include + +#include "cpr/async.h" +#include "cpr/async_wrapper.h" +#include "cpr/auth.h" +#include "cpr/bearer.h" +#include "cpr/cprtypes.h" +#include "cpr/multipart.h" +#include "cpr/multiperform.h" +#include "cpr/payload.h" +#include "cpr/response.h" +#include "cpr/session.h" +#include + +namespace cpr { + +using AsyncResponse = AsyncWrapper; + +namespace priv { + +template +void set_option_internal(Session& session, CurrentType&& current_option) { + session.SetOption(std::forward(current_option)); +} + +template <> +inline void set_option_internal(Session& session, Header&& current_option) { + // Header option was already provided -> Update previous header + session.UpdateHeader(std::forward
(current_option)); +} + +template +void set_option_internal(Session& session, CurrentType&& current_option, Ts&&... ts) { + set_option_internal(session, std::forward(current_option)); + + if (std::is_same::value) { + set_option_internal(session, std::forward(ts)...); + } else { + set_option_internal(session, std::forward(ts)...); + } +} + +template +void set_option(Session& session, Ts&&... ts) { + set_option_internal(session, std::forward(ts)...); +} + +// Idea: https://stackoverflow.com/a/19060157 +template +void apply_set_option_internal(Session& session, Tuple&& t, std::index_sequence) { + set_option(session, std::get(std::forward(t))...); +} + +// Idea: https://stackoverflow.com/a/19060157 +template +void apply_set_option(Session& session, Tuple&& t) { + using Indices = std::make_index_sequence>::value>; + apply_set_option_internal(session, std::forward(t), Indices()); +} + +template +void setup_multiperform_internal(MultiPerform& multiperform, T&& t) { + std::shared_ptr session = std::make_shared(); + apply_set_option(*session, t); + multiperform.AddSession(session); +} + +template +void setup_multiperform_internal(MultiPerform& multiperform, T&& t, Ts&&... ts) { + std::shared_ptr session = std::make_shared(); + apply_set_option(*session, t); + multiperform.AddSession(session); + setup_multiperform_internal(multiperform, std::forward(ts)...); +} + +template +void setup_multiperform(MultiPerform& multiperform, Ts&&... ts) { + setup_multiperform_internal(multiperform, std::forward(ts)...); +} + +using session_action_t = cpr::Response (cpr::Session::*)(); + +template +void setup_multiasync(std::vector>& responses, T&& parameters) { + std::shared_ptr cancellation_state = std::make_shared(false); + + std::function execFn{[cancellation_state](T params) { + if (cancellation_state->load()) { + return Response{}; + } + cpr::Session s{}; + s.SetCancellationParam(cancellation_state); + apply_set_option(s, std::forward(params)); + return std::invoke(SessionAction, s); + }}; + responses.emplace_back(GlobalThreadPool::GetInstance()->Submit(std::move(execFn), std::forward(parameters)), std::move(cancellation_state)); +} + +template +void setup_multiasync(std::vector>& responses, T&& head, Ts&&... tail) { + setup_multiasync(responses, std::forward(head)); + if constexpr (sizeof...(Ts) > 0) { + setup_multiasync(responses, std::forward(tail)...); + } +} + +} // namespace priv + +// Get methods +template +Response Get(Ts&&... ts) { + Session session; + priv::set_option(session, std::forward(ts)...); + return session.Get(); +} + +// Get async methods +template +AsyncResponse GetAsync(Ts... ts) { + return cpr::async([](Ts... ts_inner) { return Get(std::move(ts_inner)...); }, std::move(ts)...); +} + +// Get callback methods +template +// NOLINTNEXTLINE(fuchsia-trailing-return) +auto GetCallback(Then then, Ts... ts) { + return cpr::async([](Then then_inner, Ts... ts_inner) { return then_inner(Get(std::move(ts_inner)...)); }, std::move(then), std::move(ts)...); +} + +// Post methods +template +Response Post(Ts&&... ts) { + Session session; + priv::set_option(session, std::forward(ts)...); + return session.Post(); +} + +// Post async methods +template +AsyncResponse PostAsync(Ts... ts) { + return cpr::async([](Ts... ts_inner) { return Post(std::move(ts_inner)...); }, std::move(ts)...); +} + +// Post callback methods +template +// NOLINTNEXTLINE(fuchsia-trailing-return) +auto PostCallback(Then then, Ts... ts) { + return cpr::async([](Then then_inner, Ts... ts_inner) { return then_inner(Post(std::move(ts_inner)...)); }, std::move(then), std::move(ts)...); +} + +// Put methods +template +Response Put(Ts&&... ts) { + Session session; + priv::set_option(session, std::forward(ts)...); + return session.Put(); +} + +// Put async methods +template +AsyncResponse PutAsync(Ts... ts) { + return cpr::async([](Ts... ts_inner) { return Put(std::move(ts_inner)...); }, std::move(ts)...); +} + +// Put callback methods +template +// NOLINTNEXTLINE(fuchsia-trailing-return) +auto PutCallback(Then then, Ts... ts) { + return cpr::async([](Then then_inner, Ts... ts_inner) { return then_inner(Put(std::move(ts_inner)...)); }, std::move(then), std::move(ts)...); +} + +// Head methods +template +Response Head(Ts&&... ts) { + Session session; + priv::set_option(session, std::forward(ts)...); + return session.Head(); +} + +// Head async methods +template +AsyncResponse HeadAsync(Ts... ts) { + return cpr::async([](Ts... ts_inner) { return Head(std::move(ts_inner)...); }, std::move(ts)...); +} + +// Head callback methods +template +// NOLINTNEXTLINE(fuchsia-trailing-return) +auto HeadCallback(Then then, Ts... ts) { + return cpr::async([](Then then_inner, Ts... ts_inner) { return then_inner(Head(std::move(ts_inner)...)); }, std::move(then), std::move(ts)...); +} + +// Delete methods +template +Response Delete(Ts&&... ts) { + Session session; + priv::set_option(session, std::forward(ts)...); + return session.Delete(); +} + +// Delete async methods +template +AsyncResponse DeleteAsync(Ts... ts) { + return cpr::async([](Ts... ts_inner) { return Delete(std::move(ts_inner)...); }, std::move(ts)...); +} + +// Delete callback methods +template +// NOLINTNEXTLINE(fuchsia-trailing-return) +auto DeleteCallback(Then then, Ts... ts) { + return cpr::async([](Then then_inner, Ts... ts_inner) { return then_inner(Delete(std::move(ts_inner)...)); }, std::move(then), std::move(ts)...); +} + +// Options methods +template +Response Options(Ts&&... ts) { + Session session; + priv::set_option(session, std::forward(ts)...); + return session.Options(); +} + +// Options async methods +template +AsyncResponse OptionsAsync(Ts... ts) { + return cpr::async([](Ts... ts_inner) { return Options(std::move(ts_inner)...); }, std::move(ts)...); +} + +// Options callback methods +template +// NOLINTNEXTLINE(fuchsia-trailing-return) +auto OptionsCallback(Then then, Ts... ts) { + return cpr::async([](Then then_inner, Ts... ts_inner) { return then_inner(Options(std::move(ts_inner)...)); }, std::move(then), std::move(ts)...); +} + +// Patch methods +template +Response Patch(Ts&&... ts) { + Session session; + priv::set_option(session, std::forward(ts)...); + return session.Patch(); +} + +// Patch async methods +template +AsyncResponse PatchAsync(Ts... ts) { + return cpr::async([](Ts... ts_inner) { return Patch(std::move(ts_inner)...); }, std::move(ts)...); +} + +// Patch callback methods +template +// NOLINTNEXTLINE(fuchsia-trailing-return) +auto PatchCallback(Then then, Ts... ts) { + return cpr::async([](Then then_inner, Ts... ts_inner) { return then_inner(Patch(std::move(ts_inner)...)); }, std::move(then), std::move(ts)...); +} + +// Download methods +template +Response Download(std::ofstream& file, Ts&&... ts) { + Session session; + priv::set_option(session, std::forward(ts)...); + return session.Download(file); +} + +// Download async method +template +AsyncResponse DownloadAsync(fs::path local_path, Ts... ts) { + return AsyncWrapper{std::async( + std::launch::async, + [](fs::path local_path_, Ts... ts_) { + std::ofstream f(local_path_.c_str()); + return Download(f, std::move(ts_)...); + }, + std::move(local_path), std::move(ts)...)}; +} + +// Download with user callback +template +Response Download(const WriteCallback& write, Ts&&... ts) { + Session session; + priv::set_option(session, std::forward(ts)...); + return session.Download(write); +} + +// Multi requests +template +std::vector MultiGet(Ts&&... ts) { + MultiPerform multiperform; + priv::setup_multiperform(multiperform, std::forward(ts)...); + return multiperform.Get(); +} + +template +std::vector MultiDelete(Ts&&... ts) { + MultiPerform multiperform; + priv::setup_multiperform(multiperform, std::forward(ts)...); + return multiperform.Delete(); +} + +template +std::vector MultiPut(Ts&&... ts) { + MultiPerform multiperform; + priv::setup_multiperform(multiperform, std::forward(ts)...); + return multiperform.Put(); +} + +template +std::vector MultiHead(Ts&&... ts) { + MultiPerform multiperform; + priv::setup_multiperform(multiperform, std::forward(ts)...); + return multiperform.Head(); +} + +template +std::vector MultiOptions(Ts&&... ts) { + MultiPerform multiperform; + priv::setup_multiperform(multiperform, std::forward(ts)...); + return multiperform.Options(); +} + +template +std::vector MultiPatch(Ts&&... ts) { + MultiPerform multiperform; + priv::setup_multiperform(multiperform, std::forward(ts)...); + return multiperform.Patch(); +} + +template +std::vector MultiPost(Ts&&... ts) { + MultiPerform multiperform; + priv::setup_multiperform(multiperform, std::forward(ts)...); + return multiperform.Post(); +} + +template +std::vector> MultiGetAsync(Ts&&... ts) { + std::vector> ret{}; + priv::setup_multiasync<&cpr::Session::Get>(ret, std::forward(ts)...); + return ret; +} + +template +std::vector> MultiDeleteAsync(Ts&&... ts) { + std::vector> ret{}; + priv::setup_multiasync<&cpr::Session::Delete>(ret, std::forward(ts)...); + return ret; +} + +template +std::vector> MultiHeadAsync(Ts&&... ts) { + std::vector> ret{}; + priv::setup_multiasync<&cpr::Session::Head>(ret, std::forward(ts)...); + return ret; +} +template +std::vector> MultiOptionsAsync(Ts&&... ts) { + std::vector> ret{}; + priv::setup_multiasync<&cpr::Session::Options>(ret, std::forward(ts)...); + return ret; +} + +template +std::vector> MultiPatchAsync(Ts&&... ts) { + std::vector> ret{}; + priv::setup_multiasync<&cpr::Session::Patch>(ret, std::forward(ts)...); + return ret; +} + +template +std::vector> MultiPostAsync(Ts&&... ts) { + std::vector> ret{}; + priv::setup_multiasync<&cpr::Session::Post>(ret, std::forward(ts)...); + return ret; +} + +template +std::vector> MultiPutAsync(Ts&&... ts) { + std::vector> ret{}; + priv::setup_multiasync<&cpr::Session::Put>(ret, std::forward(ts)...); + return ret; +} + + +} // namespace cpr + +#endif diff --git a/include/cpr/async.h b/include/cpr/async.h new file mode 100644 index 0000000..1305834 --- /dev/null +++ b/include/cpr/async.h @@ -0,0 +1,50 @@ +#ifndef CPR_ASYNC_H +#define CPR_ASYNC_H + +#include "async_wrapper.h" +#include "singleton.h" +#include "threadpool.h" + +namespace cpr { + +class GlobalThreadPool : public ThreadPool { + CPR_SINGLETON_DECL(GlobalThreadPool) + protected: + GlobalThreadPool() = default; + + public: + ~GlobalThreadPool() override = default; +}; + +/** + * Return a wrapper for a future, calling future.get() will wait until the task is done and return RetType. + * async(fn, args...) + * async(std::bind(&Class::mem_fn, &obj)) + * async(std::mem_fn(&Class::mem_fn, &obj)) + **/ +template +auto async(Fn&& fn, Args&&... args) { + return AsyncWrapper{GlobalThreadPool::GetInstance()->Submit(std::forward(fn), std::forward(args)...)}; +} + +class async { + public: + static void startup(size_t min_threads = CPR_DEFAULT_THREAD_POOL_MIN_THREAD_NUM, size_t max_threads = CPR_DEFAULT_THREAD_POOL_MAX_THREAD_NUM, std::chrono::milliseconds max_idle_ms = CPR_DEFAULT_THREAD_POOL_MAX_IDLE_TIME) { + GlobalThreadPool* gtp = GlobalThreadPool::GetInstance(); + if (gtp->IsStarted()) { + return; + } + gtp->SetMinThreadNum(min_threads); + gtp->SetMaxThreadNum(max_threads); + gtp->SetMaxIdleTime(max_idle_ms); + gtp->Start(); + } + + static void cleanup() { + GlobalThreadPool::ExitInstance(); + } +}; + +} // namespace cpr + +#endif diff --git a/include/cpr/async_wrapper.h b/include/cpr/async_wrapper.h new file mode 100644 index 0000000..bb46bf9 --- /dev/null +++ b/include/cpr/async_wrapper.h @@ -0,0 +1,140 @@ +#ifndef CPR_ASYNC_WRAPPER_H +#define CPR_ASYNC_WRAPPER_H + +#include +#include +#include + +#include "cpr/response.h" + +namespace cpr { +enum class [[nodiscard]] CancellationResult { failure, success, invalid_operation }; + +/** + * A class template intended to wrap results of async operations (instances of std::future) + * and also provide extended capablilities relaed to these requests, for example cancellation. + * + * The RAII semantics are the same as std::future - moveable, not copyable. + */ +template +class AsyncWrapper { + private: + std::future future; + std::shared_ptr is_cancelled; + + public: + // Constructors + explicit AsyncWrapper(std::future&& f) : future{std::move(f)} {} + AsyncWrapper(std::future&& f, std::shared_ptr&& cancelledState) : future{std::move(f)}, is_cancelled{std::move(cancelledState)} {} + + // Copy Semantics + AsyncWrapper(const AsyncWrapper&) = delete; + AsyncWrapper& operator=(const AsyncWrapper&) = delete; + + // Move Semantics + AsyncWrapper(AsyncWrapper&&) noexcept = default; + AsyncWrapper& operator=(AsyncWrapper&&) noexcept = default; + + // Destructor + ~AsyncWrapper() { + if constexpr (isCancellable) { + if(is_cancelled) { + is_cancelled->store(true); + } + } + } + // These methods replicate the behaviour of std::future + [[nodiscard]] T get() { + if constexpr (isCancellable) { + if (IsCancelled()) { + throw std::logic_error{"Calling AsyncWrapper::get on a cancelled request!"}; + } + } + if (!future.valid()) { + throw std::logic_error{"Calling AsyncWrapper::get when the associated future instance is invalid!"}; + } + return future.get(); + } + + [[nodiscard]] bool valid() const noexcept { + if constexpr (isCancellable) { + return !is_cancelled->load() && future.valid(); + } else { + return future.valid(); + } + } + + void wait() const { + if constexpr (isCancellable) { + if (is_cancelled->load()) { + throw std::logic_error{"Calling AsyncWrapper::wait when the associated future is invalid or cancelled!"}; + } + } + if (!future.valid()) { + throw std::logic_error{"Calling AsyncWrapper::wait_until when the associated future is invalid!"}; + } + future.wait(); + } + + template + std::future_status wait_for(const std::chrono::duration& timeout_duration) const { + if constexpr (isCancellable) { + if (IsCancelled()) { + throw std::logic_error{"Calling AsyncWrapper::wait_for when the associated future is cancelled!"}; + } + } + if (!future.valid()) { + throw std::logic_error{"Calling AsyncWrapper::wait_until when the associated future is invalid!"}; + } + return future.wait_for(timeout_duration); + } + + template + std::future_status wait_until(const std::chrono::time_point& timeout_time) const { + if constexpr (isCancellable) { + if (IsCancelled()) { + throw std::logic_error{"Calling AsyncWrapper::wait_until when the associated future is cancelled!"}; + } + } + if (!future.valid()) { + throw std::logic_error{"Calling AsyncWrapper::wait_until when the associated future is invalid!"}; + } + return future.wait_until(timeout_time); + } + + std::shared_future share() noexcept { + return future.share(); + } + + // Cancellation-related methods + CancellationResult Cancel() { + if constexpr (!isCancellable) { + return CancellationResult::invalid_operation; + } + if (!future.valid() || is_cancelled->load()) { + return CancellationResult::invalid_operation; + } + is_cancelled->store(true); + return CancellationResult::success; + } + + [[nodiscard]] bool IsCancelled() const { + if constexpr (isCancellable) { + return is_cancelled->load(); + } else { + return false; + } + } +}; + +// Deduction guides +template +AsyncWrapper(std::future&&) -> AsyncWrapper; + +template +AsyncWrapper(std::future&&, std::shared_ptr&&) -> AsyncWrapper; + +} // namespace cpr + + +#endif diff --git a/include/cpr/auth.h b/include/cpr/auth.h new file mode 100644 index 0000000..e783969 --- /dev/null +++ b/include/cpr/auth.h @@ -0,0 +1,32 @@ +#ifndef CPR_AUTH_H +#define CPR_AUTH_H + +#include + +#include + +namespace cpr { + +enum class AuthMode { BASIC, DIGEST, NTLM }; + +class Authentication { + public: + Authentication(std::string username, std::string password, AuthMode auth_mode) : auth_string_{std::move(username) + ":" + std::move(password)}, auth_mode_{std::move(auth_mode)} {} + Authentication(const Authentication& other) = default; + Authentication(Authentication&& old) noexcept = default; + ~Authentication() noexcept; + + Authentication& operator=(Authentication&& old) noexcept = default; + Authentication& operator=(const Authentication& other) = default; + + const char* GetAuthString() const noexcept; + AuthMode GetAuthMode() const noexcept; + + private: + std::string auth_string_; + AuthMode auth_mode_; +}; + +} // namespace cpr + +#endif diff --git a/include/cpr/bearer.h b/include/cpr/bearer.h new file mode 100644 index 0000000..5e58a7d --- /dev/null +++ b/include/cpr/bearer.h @@ -0,0 +1,34 @@ +#ifndef CPR_BEARER_H +#define CPR_BEARER_H + +#include +#include + +#include + +namespace cpr { + +// Only supported with libcurl >= 7.61.0. +// As an alternative use SetHeader and add the token manually. +#if LIBCURL_VERSION_NUM >= 0x073D00 +class Bearer { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Bearer(std::string token) : token_string_{std::move(token)} {} + Bearer(const Bearer& other) = default; + Bearer(Bearer&& old) noexcept = default; + virtual ~Bearer() noexcept; + + Bearer& operator=(Bearer&& old) noexcept = default; + Bearer& operator=(const Bearer& other) = default; + + virtual const char* GetToken() const noexcept; + + protected: + std::string token_string_; +}; +#endif + +} // namespace cpr + +#endif diff --git a/include/cpr/body.h b/include/cpr/body.h new file mode 100644 index 0000000..f691d9c --- /dev/null +++ b/include/cpr/body.h @@ -0,0 +1,54 @@ +#ifndef CPR_BODY_H +#define CPR_BODY_H + +#include +#include +#include +#include +#include + +#include "cpr/buffer.h" +#include "cpr/cprtypes.h" +#include "cpr/file.h" + +namespace cpr { + +class Body : public StringHolder { + public: + Body() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Body(std::string body) : StringHolder(std::move(body)) {} + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Body(std::string_view body) : StringHolder(body) {} + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Body(const char* body) : StringHolder(body) {} + Body(const char* str, size_t len) : StringHolder(str, len) {} + Body(const std::initializer_list args) : StringHolder(args) {} + // NOLINTNEXTLINE(google-explicit-constructor, cppcoreguidelines-pro-type-reinterpret-cast) + Body(const Buffer& buffer) : StringHolder(reinterpret_cast(buffer.data), static_cast(buffer.datalen)) {} + // NOLINTNEXTLINE(google-explicit-constructor) + Body(const File& file) { + std::ifstream is(file.filepath, std::ifstream::binary); + if (!is) { + throw std::invalid_argument("Can't open the file for HTTP request body!"); + } + + is.seekg(0, std::ios::end); + const std::streampos length = is.tellg(); + is.seekg(0, std::ios::beg); + std::string buffer; + buffer.resize(static_cast(length)); + is.read(buffer.data(), length); + str_ = std::move(buffer); + } + Body(const Body& other) = default; + Body(Body&& old) noexcept = default; + ~Body() override = default; + + Body& operator=(Body&& old) noexcept = default; + Body& operator=(const Body& other) = default; +}; + +} // namespace cpr + +#endif diff --git a/include/cpr/buffer.h b/include/cpr/buffer.h new file mode 100644 index 0000000..5665faa --- /dev/null +++ b/include/cpr/buffer.h @@ -0,0 +1,33 @@ +#ifndef CPR_BUFFER_H +#define CPR_BUFFER_H + +#include + +#include + +namespace cpr { + +struct Buffer { + using data_t = const char*; + + template + Buffer(Iterator begin, Iterator end, fs::path&& p_filename) + // Ignored here since libcurl reqires a long. + // There is also no way around the reinterpret_cast. + // NOLINTNEXTLINE(google-runtime-int, cppcoreguidelines-pro-type-reinterpret-cast) + : data{reinterpret_cast(&(*begin))}, datalen{static_cast(std::distance(begin, end))}, filename(std::move(p_filename)) { + is_random_access_iterator(begin, end); + static_assert(sizeof(*begin) == 1, "Only byte buffers can be used"); + } + + template + typename std::enable_if::iterator_category, std::random_access_iterator_tag>::value>::type is_random_access_iterator(Iterator /* begin */, Iterator /* end */) {} + + data_t data; + size_t datalen; + const fs::path filename; +}; + +} // namespace cpr + +#endif diff --git a/include/cpr/callback.h b/include/cpr/callback.h new file mode 100644 index 0000000..dc1c6ee --- /dev/null +++ b/include/cpr/callback.h @@ -0,0 +1,111 @@ +#ifndef CPR_CALLBACK_H +#define CPR_CALLBACK_H + +#include "cprtypes.h" + +#include +#include +#include +#include + +namespace cpr { + +class ReadCallback { + public: + ReadCallback() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + ReadCallback(std::function p_callback, intptr_t p_userdata = 0) : userdata(p_userdata), size{-1}, callback{std::move(p_callback)} {} + ReadCallback(cpr_off_t p_size, std::function p_callback, intptr_t p_userdata = 0) : userdata(p_userdata), size{p_size}, callback{std::move(p_callback)} {} + bool operator()(char* buffer, size_t& buffer_size) const { + return callback(buffer, buffer_size, userdata); + } + + intptr_t userdata{}; + cpr_off_t size{}; + std::function callback; +}; + +class HeaderCallback { + public: + HeaderCallback() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + HeaderCallback(std::function p_callback, intptr_t p_userdata = 0) : userdata(p_userdata), callback(std::move(p_callback)) {} + bool operator()(std::string header) const { + return callback(std::move(header), userdata); + } + + intptr_t userdata{}; + std::function callback; +}; + +class WriteCallback { + public: + WriteCallback() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + WriteCallback(std::function p_callback, intptr_t p_userdata = 0) : userdata(p_userdata), callback(std::move(p_callback)) {} + bool operator()(std::string data) const { + return callback(std::move(data), userdata); + } + + intptr_t userdata{}; + std::function callback; +}; + +class ProgressCallback { + public: + ProgressCallback() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + ProgressCallback(std::function p_callback, intptr_t p_userdata = 0) : userdata(p_userdata), callback(std::move(p_callback)) {} + bool operator()(cpr_pf_arg_t downloadTotal, cpr_pf_arg_t downloadNow, cpr_pf_arg_t uploadTotal, cpr_pf_arg_t uploadNow) const { + return callback(downloadTotal, downloadNow, uploadTotal, uploadNow, userdata); + } + + intptr_t userdata{}; + std::function callback; +}; + +class DebugCallback { + public: + enum class InfoType { + TEXT = 0, + HEADER_IN = 1, + HEADER_OUT = 2, + DATA_IN = 3, + DATA_OUT = 4, + SSL_DATA_IN = 5, + SSL_DATA_OUT = 6, + }; + DebugCallback() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + DebugCallback(std::function p_callback, intptr_t p_userdata = 0) : userdata(p_userdata), callback(std::move(p_callback)) {} + void operator()(InfoType type, std::string data) const { + callback(type, std::move(data), userdata); + } + + intptr_t userdata{}; + std::function callback; +}; + +/** + * Functor class for progress functions that will be used in cancellable requests. + */ +class CancellationCallback { + public: + CancellationCallback() = default; + explicit CancellationCallback(std::shared_ptr&& cs) : cancellation_state{std::move(cs)} {} + + CancellationCallback(std::shared_ptr&& cs, ProgressCallback& u_cb) : cancellation_state{std::move(cs)}, user_cb{std::reference_wrapper{u_cb}} {} + + bool operator()(cpr_pf_arg_t dltotal, cpr_pf_arg_t dlnow, cpr_pf_arg_t ultotal, cpr_pf_arg_t ulnow) const; + + void SetProgressCallback(ProgressCallback& u_cb); + + private: + std::shared_ptr cancellation_state; + std::optional> user_cb; +}; + + +} // namespace cpr + +#endif diff --git a/include/cpr/cert_info.h b/include/cpr/cert_info.h new file mode 100644 index 0000000..6c328ee --- /dev/null +++ b/include/cpr/cert_info.h @@ -0,0 +1,35 @@ +#ifndef CPR_CERT_INFO_H +#define CPR_CERT_INFO_H + +#include +#include +#include + +namespace cpr { + +class CertInfo { + private: + std::vector cert_info_; + + public: + CertInfo() = default; + CertInfo(const std::initializer_list& entry) : cert_info_{entry} {}; + ~CertInfo() noexcept = default; + + using iterator = std::vector::iterator; + using const_iterator = std::vector::const_iterator; + + std::string& operator[](const size_t& pos); + iterator begin(); + iterator end(); + const_iterator begin() const; + const_iterator end() const; + const_iterator cbegin() const; + const_iterator cend() const; + void emplace_back(const std::string& str); + void push_back(const std::string& str); + void pop_back(); +}; +} // namespace cpr + +#endif diff --git a/include/cpr/connect_timeout.h b/include/cpr/connect_timeout.h new file mode 100644 index 0000000..546e8a5 --- /dev/null +++ b/include/cpr/connect_timeout.h @@ -0,0 +1,18 @@ +#ifndef CPR_CONNECT_TIMEOUT_H +#define CPR_CONNECT_TIMEOUT_H + +#include "cpr/timeout.h" + +namespace cpr { + +class ConnectTimeout : public Timeout { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + ConnectTimeout(const std::chrono::milliseconds& duration) : Timeout{duration} {} + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + ConnectTimeout(const std::int32_t& milliseconds) : Timeout{milliseconds} {} +}; + +} // namespace cpr + +#endif diff --git a/include/cpr/cookies.h b/include/cpr/cookies.h new file mode 100644 index 0000000..c018ea4 --- /dev/null +++ b/include/cpr/cookies.h @@ -0,0 +1,92 @@ +#ifndef CPR_COOKIES_H +#define CPR_COOKIES_H + +#include "cpr/curlholder.h" +#include +#include +#include +#include +#include + +namespace cpr { +/** + * EXPIRES_STRING_SIZE is an explicitly static and const variable that could be only accessed within the same namespace and is immutable. + * To be used for "std::array", the expression must have a constant value, so EXPIRES_STRING_SIZE must be a const value. + **/ +static const std::size_t EXPIRES_STRING_SIZE = 100; + +class Cookie { + public: + Cookie() = default; + /** + * Some notes for the default value used by expires: + * std::chrono::system_clock::time_point::min() won't work on Windows due to the min, max clash there. + * So we fall back to std::chrono::system_clock::from_time_t(0) for the minimum value here. + **/ + Cookie(const std::string& name, const std::string& value, const std::string& domain = "", bool p_isIncludingSubdomains = false, const std::string& path = "/", bool p_isHttpsOnly = false, std::chrono::system_clock::time_point expires = std::chrono::system_clock::from_time_t(0)) : name_{name}, value_{value}, domain_{domain}, includeSubdomains_{p_isIncludingSubdomains}, path_{path}, httpsOnly_{p_isHttpsOnly}, expires_{expires} {}; + const std::string GetDomain() const; + bool IsIncludingSubdomains() const; + const std::string GetPath() const; + bool IsHttpsOnly() const; + const std::chrono::system_clock::time_point GetExpires() const; + const std::string GetExpiresString() const; + const std::string GetName() const; + const std::string GetValue() const; + + private: + std::string name_; + std::string value_; + std::string domain_; + bool includeSubdomains_{}; + std::string path_; + bool httpsOnly_{}; + /** + * TODO: Update the implementation using `std::chrono::utc_clock` of C++20 + **/ + std::chrono::system_clock::time_point expires_{}; +}; + +class Cookies { + public: + /** + * Should we URL-encode cookies when making a request. + * Based on RFC6265, it is recommended but not mandatory to encode cookies. + * + * ------- + * To maximize compatibility with user agents, servers that wish to + * store arbitrary data in a cookie-value SHOULD encode that data, for + * example, using Base64 [RFC4648]. + * ------- + * Source: RFC6265 (https://www.ietf.org/rfc/rfc6265.txt) + **/ + bool encode{true}; + + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Cookies(bool p_encode = true) : encode{p_encode} {}; + Cookies(const std::initializer_list& cookies, bool p_encode = true) : encode{p_encode}, cookies_{cookies} {}; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Cookies(const cpr::Cookie& cookie, bool p_encode = true) : encode{p_encode}, cookies_{cookie} {}; + + cpr::Cookie& operator[](size_t pos); + const std::string GetEncoded(const CurlHolder& holder) const; + + using iterator = std::vector::iterator; + using const_iterator = std::vector::const_iterator; + + iterator begin(); + iterator end(); + const_iterator begin() const; + const_iterator end() const; + const_iterator cbegin() const; + const_iterator cend() const; + void emplace_back(const Cookie& str); + void push_back(const Cookie& str); + void pop_back(); + + private: + std::vector cookies_; +}; + +} // namespace cpr + +#endif diff --git a/include/cpr/cpr.h b/include/cpr/cpr.h new file mode 100644 index 0000000..fbad172 --- /dev/null +++ b/include/cpr/cpr.h @@ -0,0 +1,46 @@ +#ifndef CPR_CPR_H +#define CPR_CPR_H + +#include "cpr/api.h" +#include "cpr/auth.h" +#include "cpr/bearer.h" +#include "cpr/callback.h" +#include "cpr/cert_info.h" +#include "cpr/connect_timeout.h" +#include "cpr/cookies.h" +#include "cpr/cprtypes.h" +#include "cpr/cprver.h" +#include "cpr/curl_container.h" +#include "cpr/curlholder.h" +#include "cpr/error.h" +#include "cpr/http_version.h" +#include "cpr/interceptor.h" +#include "cpr/interface.h" +#include "cpr/limit_rate.h" +#include "cpr/local_port.h" +#include "cpr/local_port_range.h" +#include "cpr/low_speed.h" +#include "cpr/multipart.h" +#include "cpr/multiperform.h" +#include "cpr/parameters.h" +#include "cpr/payload.h" +#include "cpr/proxies.h" +#include "cpr/proxyauth.h" +#include "cpr/range.h" +#include "cpr/redirect.h" +#include "cpr/reserve_size.h" +#include "cpr/resolve.h" +#include "cpr/response.h" +#include "cpr/session.h" +#include "cpr/ssl_ctx.h" +#include "cpr/ssl_options.h" +#include "cpr/status_codes.h" +#include "cpr/timeout.h" +#include "cpr/unix_socket.h" +#include "cpr/user_agent.h" +#include "cpr/util.h" +#include "cpr/verbose.h" + +#define CPR_LIBCURL_VERSION_NUM LIBCURL_VERSION_NUM + +#endif diff --git a/include/cpr/cprtypes.h b/include/cpr/cprtypes.h new file mode 100644 index 0000000..65da738 --- /dev/null +++ b/include/cpr/cprtypes.h @@ -0,0 +1,144 @@ +#ifndef CPR_CPR_TYPES_H +#define CPR_CPR_TYPES_H + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cpr { + +/** + * Wrapper around "curl_off_t" to prevent applications from having to link against libcurl. + **/ +using cpr_off_t = curl_off_t; + +/** + * The argument type for progress functions, dependent on libcurl version + **/ +#if LIBCURL_VERSION_NUM < 0x072000 +using cpr_pf_arg_t = double; +#else +using cpr_pf_arg_t = cpr_off_t; +#endif + +template +class StringHolder { + public: + StringHolder() = default; + explicit StringHolder(std::string str) : str_(std::move(str)) {} + explicit StringHolder(std::string_view str) : str_(str) {} + explicit StringHolder(const char* str) : str_(str) {} + StringHolder(const char* str, size_t len) : str_(str, len) {} + StringHolder(const std::initializer_list args) { + str_ = std::accumulate(args.begin(), args.end(), str_); + } + StringHolder(const StringHolder& other) = default; + StringHolder(StringHolder&& old) noexcept = default; + virtual ~StringHolder() = default; + + StringHolder& operator=(StringHolder&& old) noexcept = default; + + StringHolder& operator=(const StringHolder& other) = default; + + explicit operator std::string() const { + return str_; + } + + T operator+(const char* rhs) const { + return T(str_ + rhs); + } + + T operator+(const std::string& rhs) const { + return T(str_ + rhs); + } + + T operator+(const StringHolder& rhs) const { + return T(str_ + rhs.str_); + } + + void operator+=(const char* rhs) { + str_ += rhs; + } + void operator+=(const std::string& rhs) { + str_ += rhs; + } + void operator+=(const StringHolder& rhs) { + str_ += rhs; + } + + bool operator==(const char* rhs) const { + return str_ == rhs; + } + bool operator==(const std::string& rhs) const { + return str_ == rhs; + } + bool operator==(const StringHolder& rhs) const { + return str_ == rhs.str_; + } + + bool operator!=(const char* rhs) const { + return str_.c_str() != rhs; + } + bool operator!=(const std::string& rhs) const { + return str_ != rhs; + } + bool operator!=(const StringHolder& rhs) const { + return str_ != rhs.str_; + } + + const std::string& str() { + return str_; + } + const std::string& str() const { + return str_; + } + const char* c_str() const { + return str_.c_str(); + } + const char* data() const { + return str_.data(); + } + + protected: + std::string str_{}; +}; + +template +std::ostream& operator<<(std::ostream& os, const StringHolder& s) { + os << s.str(); + return os; +} + +class Url : public StringHolder { + public: + Url() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Url(std::string url) : StringHolder(std::move(url)) {} + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Url(std::string_view url) : StringHolder(url) {} + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Url(const char* url) : StringHolder(url) {} + Url(const char* str, size_t len) : StringHolder(std::string(str, len)) {} + Url(const std::initializer_list args) : StringHolder(args) {} + Url(const Url& other) = default; + Url(Url&& old) noexcept = default; + ~Url() override = default; + + Url& operator=(Url&& old) noexcept = default; + Url& operator=(const Url& other) = default; +}; + +struct CaseInsensitiveCompare { + bool operator()(const std::string& a, const std::string& b) const noexcept; +}; + +using Header = std::map; + +} // namespace cpr + +#endif diff --git a/include/cpr/curl_container.h b/include/cpr/curl_container.h new file mode 100644 index 0000000..c2409b2 --- /dev/null +++ b/include/cpr/curl_container.h @@ -0,0 +1,51 @@ +#ifndef CURL_CONTAINER_H +#define CURL_CONTAINER_H + +#include +#include +#include +#include + +#include "cpr/curlholder.h" + + +namespace cpr { + +struct Parameter { + Parameter(std::string p_key, std::string p_value) : key{std::move(p_key)}, value{std::move(p_value)} {} + + std::string key; + std::string value; +}; + +struct Pair { + Pair(std::string p_key, std::string p_value) : key(std::move(p_key)), value(std::move(p_value)) {} + + std::string key; + std::string value; +}; + + +template +class CurlContainer { + public: + /** + * Enables or disables URL encoding for keys and values when calling GetContent(...). + **/ + bool encode = true; + + CurlContainer() = default; + CurlContainer(const std::initializer_list&); + + void Add(const std::initializer_list&); + void Add(const T&); + + const std::string GetContent(const CurlHolder&) const; + + protected: + std::vector containerList_; +}; + +} // namespace cpr + +#endif // diff --git a/include/cpr/curlholder.h b/include/cpr/curlholder.h new file mode 100644 index 0000000..a9e1dc8 --- /dev/null +++ b/include/cpr/curlholder.h @@ -0,0 +1,54 @@ +#ifndef CPR_CURL_HOLDER_H +#define CPR_CURL_HOLDER_H + +#include +#include +#include + +#include + +namespace cpr { +struct CurlHolder { + private: + /** + * Mutex for curl_easy_init(). + * curl_easy_init() is not thread save. + * References: + * https://curl.haxx.se/libcurl/c/curl_easy_init.html + * https://curl.haxx.se/libcurl/c/threadsafe.html + **/ + + // Avoids initalization order problems in a static build + static std::mutex& curl_easy_init_mutex_() { + static std::mutex curl_easy_init_mutex_; + return curl_easy_init_mutex_; + } + + public: + CURL* handle{nullptr}; + struct curl_slist* chunk{nullptr}; + struct curl_slist* resolveCurlList{nullptr}; + curl_mime* multipart{nullptr}; + std::array error{}; + + CurlHolder(); + CurlHolder(const CurlHolder& other) = default; + CurlHolder(CurlHolder&& old) noexcept = default; + ~CurlHolder(); + + CurlHolder& operator=(CurlHolder&& old) noexcept = default; + CurlHolder& operator=(const CurlHolder& other) = default; + + /** + * Uses curl_easy_escape(...) for escaping the given string. + **/ + std::string urlEncode(const std::string& s) const; + + /** + * Uses curl_easy_unescape(...) for unescaping the given string. + **/ + std::string urlDecode(const std::string& s) const; +}; +} // namespace cpr + +#endif diff --git a/include/cpr/curlmultiholder.h b/include/cpr/curlmultiholder.h new file mode 100644 index 0000000..ccd504b --- /dev/null +++ b/include/cpr/curlmultiholder.h @@ -0,0 +1,18 @@ +#ifndef CPR_CURLMULTIHOLDER_H +#define CPR_CURLMULTIHOLDER_H + +#include + +namespace cpr { + +class CurlMultiHolder { + public: + CurlMultiHolder(); + ~CurlMultiHolder(); + + CURLM* handle{nullptr}; +}; + +} // namespace cpr + +#endif \ No newline at end of file diff --git a/include/cpr/error.h b/include/cpr/error.h new file mode 100644 index 0000000..bb59a4c --- /dev/null +++ b/include/cpr/error.h @@ -0,0 +1,53 @@ +#ifndef CPR_ERROR_H +#define CPR_ERROR_H + +#include +#include + +#include "cpr/cprtypes.h" +#include + +namespace cpr { + +enum class ErrorCode { + OK = 0, + CONNECTION_FAILURE, + EMPTY_RESPONSE, + HOST_RESOLUTION_FAILURE, + INTERNAL_ERROR, + INVALID_URL_FORMAT, + NETWORK_RECEIVE_ERROR, + NETWORK_SEND_FAILURE, + OPERATION_TIMEDOUT, + PROXY_RESOLUTION_FAILURE, + SSL_CONNECT_ERROR, + SSL_LOCAL_CERTIFICATE_ERROR, + SSL_REMOTE_CERTIFICATE_ERROR, + SSL_CACERT_ERROR, + GENERIC_SSL_ERROR, + UNSUPPORTED_PROTOCOL, + REQUEST_CANCELLED, + TOO_MANY_REDIRECTS, + UNKNOWN_ERROR = 1000, +}; + +class Error { + public: + ErrorCode code = ErrorCode::OK; + std::string message{}; + + Error() = default; + + Error(const std::int32_t& curl_code, std::string&& p_error_message) : code{getErrorCodeForCurlError(curl_code)}, message(std::move(p_error_message)) {} + + explicit operator bool() const { + return code != ErrorCode::OK; + } + + private: + static ErrorCode getErrorCodeForCurlError(std::int32_t curl_code); +}; + +} // namespace cpr + +#endif diff --git a/include/cpr/file.h b/include/cpr/file.h new file mode 100644 index 0000000..b39914a --- /dev/null +++ b/include/cpr/file.h @@ -0,0 +1,59 @@ +#ifndef CPR_FILE_H +#define CPR_FILE_H + +#include +#include +#include + +#include + +namespace cpr { + +struct File { + explicit File(std::string p_filepath, const std::string& p_overriden_filename = {}) : filepath(std::move(p_filepath)), overriden_filename(p_overriden_filename) {} + + std::string filepath; + std::string overriden_filename; + + [[nodiscard]] bool hasOverridenFilename() const noexcept { + return !overriden_filename.empty(); + }; +}; + +class Files { + public: + Files() = default; + // NOLINTNEXTLINE(google-explicit-constructor) + Files(const File& p_file) : files{p_file} {}; + + Files(const Files& other) = default; + Files(Files&& old) noexcept = default; + + Files(const std::initializer_list& p_files) : files{p_files} {}; + Files(const std::initializer_list& p_filepaths); + + ~Files() noexcept = default; + + Files& operator=(const Files& other); + Files& operator=(Files&& old) noexcept; + + using iterator = std::vector::iterator; + using const_iterator = std::vector::const_iterator; + + iterator begin(); + iterator end(); + [[nodiscard]] const_iterator begin() const; + [[nodiscard]] const_iterator end() const; + [[nodiscard]] const_iterator cbegin() const; + [[nodiscard]] const_iterator cend() const; + void emplace_back(const File& file); + void push_back(const File& file); + void pop_back(); + + private: + std::vector files; +}; + +} // namespace cpr + +#endif diff --git a/include/cpr/filesystem.h b/include/cpr/filesystem.h new file mode 100644 index 0000000..f296770 --- /dev/null +++ b/include/cpr/filesystem.h @@ -0,0 +1,26 @@ +#ifndef CPR_FILESYSTEM_H +#define CPR_FILESYSTEM_H + +// Include filesystem into the namespace "fs" from either "filesystem" or "experimental/filesystem" or "boost/filesystem" +#ifdef CPR_USE_BOOST_FILESYSTEM +#define BOOST_FILESYSTEM_VERSION 4 // Use the latest, with the closest behavior to std::filesystem. +#include +namespace cpr { +namespace fs = boost::filesystem; +} +// cppcheck-suppress preprocessorErrorDirective +#elif __has_include() +#include +namespace cpr { +namespace fs = std::filesystem; +} +#elif __has_include("experimental/filesystem") +#include +namespace cpr { +namespace fs = std::experimental::filesystem; +} +#else +#error "Failed to include header!" +#endif + +#endif diff --git a/include/cpr/http_version.h b/include/cpr/http_version.h new file mode 100644 index 0000000..45b5028 --- /dev/null +++ b/include/cpr/http_version.h @@ -0,0 +1,67 @@ +#ifndef CPR_HTTP_VERSION_H +#define CPR_HTTP_VERSION_H + +#include + +namespace cpr { +enum class HttpVersionCode { + /** + * Let libcurl decide which version is the best. + **/ + VERSION_NONE, + /** + * Enforce HTTP 1.0 requests. + **/ + VERSION_1_0, + /** + * Enforce HTTP 1.1 requests. + **/ + VERSION_1_1, +#if LIBCURL_VERSION_NUM >= 0x072100 // 7.33.0 + /** + * Attempt HTTP 2.0 requests. + * Fallback to HTTP 1.1 if negotiation fails. + **/ + VERSION_2_0, +#endif +#if LIBCURL_VERSION_NUM >= 0x072F00 // 7.47.0 + /** + * Attempt HTTP 2.0 for HTTPS requests only. + * Fallback to HTTP 1.1 if negotiation fails. + * HTTP 1.1 will be used for HTTP connections. + **/ + VERSION_2_0_TLS, +#endif +#if LIBCURL_VERSION_NUM >= 0x073100 // 7.49.0 + /** + * Start HTTP 2.0 for HTTP requests. + * Requires prior knowledge that the server supports HTTP 2.0. + * For HTTPS requests we will negotiate the protocol version in the TLS handshake. + **/ + VERSION_2_0_PRIOR_KNOWLEDGE, +#endif +#if LIBCURL_VERSION_NUM >= 0x074200 // 7.66.0 + /** + * Attempt HTTP 3.0 requests. + * Requires prior knowledge that the server supports HTTP 3.0 since there is no gracefully downgrade. + * Fallback to HTTP 1.1 if negotiation fails. + **/ + VERSION_3_0 +#endif +}; + +class HttpVersion { + public: + /** + * The HTTP version that should be used by libcurl when initiating a HTTP(S) connection. + * Default: HttpVersionCode::VERSION_NONE + **/ + HttpVersionCode code = HttpVersionCode::VERSION_NONE; + + HttpVersion() = default; + explicit HttpVersion(HttpVersionCode _code) : code(_code) {} +}; + +} // namespace cpr + +#endif diff --git a/include/cpr/interceptor.h b/include/cpr/interceptor.h new file mode 100644 index 0000000..34cfe50 --- /dev/null +++ b/include/cpr/interceptor.h @@ -0,0 +1,74 @@ +#ifndef CPR_INTERCEPTOR_H +#define CPR_INTERCEPTOR_H + +#include "cpr/multiperform.h" +#include "cpr/response.h" +#include "cpr/session.h" +#include + +namespace cpr { +class Interceptor { + public: + enum class ProceedHttpMethod { + GET_REQUEST = 0, + POST_REQUEST, + PUT_REQUEST, + DELETE_REQUEST, + PATCH_REQUEST, + HEAD_REQUEST, + OPTIONS_REQUEST, + DOWNLOAD_CALLBACK_REQUEST, + DOWNLOAD_FILE_REQUEST, + }; + + Interceptor() = default; + Interceptor(const Interceptor& other) = default; + Interceptor(Interceptor&& old) = default; + virtual ~Interceptor() = default; + + Interceptor& operator=(const Interceptor& other) = default; + Interceptor& operator=(Interceptor&& old) = default; + + virtual Response intercept(Session& session) = 0; + + protected: + static Response proceed(Session& session); + static Response proceed(Session& session, ProceedHttpMethod httpMethod); + static Response proceed(Session& session, ProceedHttpMethod httpMethod, std::ofstream& file); + static Response proceed(Session& session, ProceedHttpMethod httpMethod, const WriteCallback& write); +}; + +class InterceptorMulti { + public: + enum class ProceedHttpMethod { + GET_REQUEST = 0, + POST_REQUEST, + PUT_REQUEST, + DELETE_REQUEST, + PATCH_REQUEST, + HEAD_REQUEST, + OPTIONS_REQUEST, + DOWNLOAD_CALLBACK_REQUEST, + DOWNLOAD_FILE_REQUEST, + }; + + InterceptorMulti() = default; + InterceptorMulti(const InterceptorMulti& other) = default; + InterceptorMulti(InterceptorMulti&& old) = default; + virtual ~InterceptorMulti() = default; + + InterceptorMulti& operator=(const InterceptorMulti& other) = default; + InterceptorMulti& operator=(InterceptorMulti&& old) = default; + + virtual std::vector intercept(MultiPerform& multi) = 0; + + protected: + static std::vector proceed(MultiPerform& multi); + + static void PrepareDownloadSession(MultiPerform& multi, size_t sessions_index, const WriteCallback& write); +}; + +} // namespace cpr + + +#endif \ No newline at end of file diff --git a/include/cpr/interface.h b/include/cpr/interface.h new file mode 100644 index 0000000..b98940e --- /dev/null +++ b/include/cpr/interface.h @@ -0,0 +1,32 @@ +#ifndef CPR_INTERFACE_H +#define CPR_INTERFACE_H + +#include +#include + +#include "cpr/cprtypes.h" + +namespace cpr { + +class Interface : public StringHolder { + public: + Interface() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Interface(std::string iface) : StringHolder(std::move(iface)) {} + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Interface(std::string_view iface) : StringHolder(iface) {} + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Interface(const char* iface) : StringHolder(iface) {} + Interface(const char* str, size_t len) : StringHolder(str, len) {} + Interface(const std::initializer_list args) : StringHolder(args) {} + Interface(const Interface& other) = default; + Interface(Interface&& old) noexcept = default; + ~Interface() override = default; + + Interface& operator=(Interface&& old) noexcept = default; + Interface& operator=(const Interface& other) = default; +}; + +} // namespace cpr + +#endif diff --git a/include/cpr/limit_rate.h b/include/cpr/limit_rate.h new file mode 100644 index 0000000..2b0e8a2 --- /dev/null +++ b/include/cpr/limit_rate.h @@ -0,0 +1,18 @@ +#ifndef CPR_SPEED_LIMIT_H +#define CPR_SPEED_LIMIT_H + +#include + +namespace cpr { + +class LimitRate { + public: + LimitRate(const std::int64_t p_downrate, const std::int64_t p_uprate) : downrate(p_downrate), uprate(p_uprate) {} + + std::int64_t downrate = 0; + std::int64_t uprate = 0; +}; + +} // namespace cpr + +#endif \ No newline at end of file diff --git a/include/cpr/local_port.h b/include/cpr/local_port.h new file mode 100644 index 0000000..e853425 --- /dev/null +++ b/include/cpr/local_port.h @@ -0,0 +1,23 @@ +#ifndef CPR_LOCAL_PORT_H +#define CPR_LOCAL_PORT_H + +#include + +namespace cpr { + +class LocalPort { + public: + // NOLINTNEXTLINE(google-explicit-constructor) + LocalPort(const std::uint16_t p_localport) : localport_(p_localport) {} + + operator std::uint16_t() const { + return localport_; + } + + private: + std::uint16_t localport_ = 0; +}; + +} // namespace cpr + +#endif \ No newline at end of file diff --git a/include/cpr/local_port_range.h b/include/cpr/local_port_range.h new file mode 100644 index 0000000..cc2d7e2 --- /dev/null +++ b/include/cpr/local_port_range.h @@ -0,0 +1,23 @@ +#ifndef CPR_LOCAL_PORT_RANGE_H +#define CPR_LOCAL_PORT_RANGE_H + +#include + +namespace cpr { + +class LocalPortRange { + public: + // NOLINTNEXTLINE(google-explicit-constructor) + LocalPortRange(const std::uint16_t p_localportrange) : localportrange_(p_localportrange) {} + + operator std::uint16_t() const { + return localportrange_; + } + + private: + std::uint16_t localportrange_ = 0; +}; + +} // namespace cpr + +#endif \ No newline at end of file diff --git a/include/cpr/low_speed.h b/include/cpr/low_speed.h new file mode 100644 index 0000000..ff77fd2 --- /dev/null +++ b/include/cpr/low_speed.h @@ -0,0 +1,18 @@ +#ifndef CPR_LOW_SPEED_H +#define CPR_LOW_SPEED_H + +#include + +namespace cpr { + +class LowSpeed { + public: + LowSpeed(const std::int32_t p_limit, const std::int32_t p_time) : limit(p_limit), time(p_time) {} + + std::int32_t limit; + std::int32_t time; +}; + +} // namespace cpr + +#endif diff --git a/include/cpr/multipart.h b/include/cpr/multipart.h new file mode 100644 index 0000000..3eaaea7 --- /dev/null +++ b/include/cpr/multipart.h @@ -0,0 +1,43 @@ +#ifndef CPR_MULTIPART_H +#define CPR_MULTIPART_H + +#include +#include +#include +#include +#include + +#include "buffer.h" +#include "file.h" + +namespace cpr { + +struct Part { + Part(const std::string& p_name, const std::string& p_value, const std::string& p_content_type = {}) : name{p_name}, value{p_value}, content_type{p_content_type}, is_file{false}, is_buffer{false} {} + Part(const std::string& p_name, const std::int32_t& p_value, const std::string& p_content_type = {}) : name{p_name}, value{std::to_string(p_value)}, content_type{p_content_type}, is_file{false}, is_buffer{false} {} + Part(const std::string& p_name, const Files& p_files, const std::string& p_content_type = {}) : name{p_name}, content_type{p_content_type}, is_file{true}, is_buffer{false}, files{p_files} {} + Part(const std::string& p_name, Files&& p_files, const std::string& p_content_type = {}) : name{p_name}, content_type{p_content_type}, is_file{true}, is_buffer{false}, files{p_files} {} + Part(const std::string& p_name, const Buffer& buffer, const std::string& p_content_type = {}) : name{p_name}, value{buffer.filename.string()}, content_type{p_content_type}, data{buffer.data}, datalen{buffer.datalen}, is_file{false}, is_buffer{true} {} + + std::string name; + // We don't use fs::path here, as this leads to problems using windows + std::string value; + std::string content_type; + Buffer::data_t data{nullptr}; + size_t datalen{0}; + bool is_file; + bool is_buffer; + + Files files; +}; + +class Multipart { + public: + Multipart(const std::initializer_list& parts); + + std::vector parts; +}; + +} // namespace cpr + +#endif diff --git a/include/cpr/multiperform.h b/include/cpr/multiperform.h new file mode 100644 index 0000000..d9ab978 --- /dev/null +++ b/include/cpr/multiperform.h @@ -0,0 +1,137 @@ +#ifndef CPR_MULTIPERFORM_H +#define CPR_MULTIPERFORM_H + +#include "cpr/curlmultiholder.h" +#include "cpr/response.h" +#include "cpr/session.h" +#include +#include +#include +#include +#include + +namespace cpr { + +class InterceptorMulti; + +class MultiPerform { + public: + enum class HttpMethod { + UNDEFINED = 0, + GET_REQUEST, + POST_REQUEST, + PUT_REQUEST, + DELETE_REQUEST, + PATCH_REQUEST, + HEAD_REQUEST, + OPTIONS_REQUEST, + DOWNLOAD_REQUEST, + }; + + MultiPerform(); + MultiPerform(const MultiPerform& other) = delete; + MultiPerform(MultiPerform&& old) = default; + ~MultiPerform(); + + MultiPerform& operator=(const MultiPerform& other) = delete; + MultiPerform& operator=(MultiPerform&& old) noexcept = default; + + std::vector Get(); + std::vector Delete(); + template + std::vector Download(DownloadArgTypes... args); + std::vector Put(); + std::vector Head(); + std::vector Options(); + std::vector Patch(); + std::vector Post(); + + std::vector Perform(); + template + std::vector PerformDownload(DownloadArgTypes... args); + + void AddSession(std::shared_ptr& session, HttpMethod method = HttpMethod::UNDEFINED); + void RemoveSession(const std::shared_ptr& session); + std::vector, HttpMethod>>& GetSessions(); + [[nodiscard]] const std::vector, HttpMethod>>& GetSessions() const; + + void AddInterceptor(const std::shared_ptr& pinterceptor); + + private: + // Interceptors should be able to call the private proceed() and PrepareDownloadSessions() functions + friend InterceptorMulti; + + void SetHttpMethod(HttpMethod method); + + void PrepareSessions(); + template + void PrepareDownloadSessions(size_t sessions_index, CurrentDownloadArgType current_arg, DownloadArgTypes... args); + template + void PrepareDownloadSessions(size_t sessions_index, CurrentDownloadArgType current_arg); + void PrepareDownloadSession(size_t sessions_index, std::ofstream& file); + void PrepareDownloadSession(size_t sessions_index, const WriteCallback& write); + + void PrepareGet(); + void PrepareDelete(); + void PreparePut(); + void PreparePatch(); + void PrepareHead(); + void PrepareOptions(); + void PreparePost(); + template + void PrepareDownload(DownloadArgTypes... args); + + std::vector intercept(); + std::vector proceed(); + std::vector MakeRequest(); + std::vector MakeDownloadRequest(); + + void DoMultiPerform(); + std::vector ReadMultiInfo(std::function&& complete_function); + + std::vector, HttpMethod>> sessions_; + std::unique_ptr multicurl_; + bool is_download_multi_perform{false}; + + std::queue> interceptors_; +}; + +template +void MultiPerform::PrepareDownloadSessions(size_t sessions_index, CurrentDownloadArgType current_arg) { + PrepareDownloadSession(sessions_index, current_arg); +} + +template +void MultiPerform::PrepareDownloadSessions(size_t sessions_index, CurrentDownloadArgType current_arg, DownloadArgTypes... args) { + PrepareDownloadSession(sessions_index, current_arg); + PrepareDownloadSessions(sessions_index + 1, args...); +} + + +template +void MultiPerform::PrepareDownload(DownloadArgTypes... args) { + SetHttpMethod(HttpMethod::DOWNLOAD_REQUEST); + PrepareDownloadSessions(0, args...); +} + +template +std::vector MultiPerform::Download(DownloadArgTypes... args) { + if (sizeof...(args) != sessions_.size()) { + throw std::invalid_argument("Number of download arguments has to match the number of sessions added to the multiperform!"); + } + PrepareDownload(args...); + return MakeDownloadRequest(); +} + +template +std::vector MultiPerform::PerformDownload(DownloadArgTypes... args) { + if (sizeof...(args) != sessions_.size()) { + throw std::invalid_argument("Number of download arguments has to match the number of sessions added to the multiperform!"); + } + PrepareDownloadSessions(0, args...); + return MakeDownloadRequest(); +} + +} // namespace cpr + +#endif \ No newline at end of file diff --git a/include/cpr/parameters.h b/include/cpr/parameters.h new file mode 100644 index 0000000..0e34d4d --- /dev/null +++ b/include/cpr/parameters.h @@ -0,0 +1,18 @@ +#ifndef CPR_PARAMETERS_H +#define CPR_PARAMETERS_H + +#include + +#include "cpr/curl_container.h" + +namespace cpr { + +class Parameters : public CurlContainer { + public: + Parameters() = default; + Parameters(const std::initializer_list& parameters); +}; + +} // namespace cpr + +#endif diff --git a/include/cpr/payload.h b/include/cpr/payload.h new file mode 100644 index 0000000..686b540 --- /dev/null +++ b/include/cpr/payload.h @@ -0,0 +1,23 @@ +#ifndef CPR_PAYLOAD_H +#define CPR_PAYLOAD_H + +#include + +#include "cpr/curl_container.h" + + +namespace cpr { +class Payload : public CurlContainer { + public: + template + Payload(const It begin, const It end) { + for (It pair = begin; pair != end; ++pair) { + Add(*pair); + } + } + Payload(const std::initializer_list& pairs); +}; + +} // namespace cpr + +#endif diff --git a/include/cpr/proxies.h b/include/cpr/proxies.h new file mode 100644 index 0000000..6970442 --- /dev/null +++ b/include/cpr/proxies.h @@ -0,0 +1,23 @@ +#ifndef CPR_PROXIES_H +#define CPR_PROXIES_H + +#include +#include +#include + +namespace cpr { +class Proxies { + public: + Proxies() = default; + Proxies(const std::initializer_list>& hosts); + Proxies(const std::map& hosts); + + bool has(const std::string& protocol) const; + const std::string& operator[](const std::string& protocol); + + private: + std::map hosts_; +}; +} // namespace cpr + +#endif diff --git a/include/cpr/proxyauth.h b/include/cpr/proxyauth.h new file mode 100644 index 0000000..7e34764 --- /dev/null +++ b/include/cpr/proxyauth.h @@ -0,0 +1,43 @@ +#ifndef CPR_PROXYAUTH_H +#define CPR_PROXYAUTH_H + +#include +#include + +#include "cpr/auth.h" +#include "cpr/util.h" + +namespace cpr { +class EncodedAuthentication { + public: + EncodedAuthentication() : auth_string_{""} {} + EncodedAuthentication(std::string username, std::string password) : auth_string_{cpr::util::urlEncode(std::move(username)) + ":" + cpr::util::urlEncode(std::move(password))} {} + EncodedAuthentication(const EncodedAuthentication& other) = default; + EncodedAuthentication(EncodedAuthentication&& old) noexcept = default; + virtual ~EncodedAuthentication() noexcept; + + EncodedAuthentication& operator=(EncodedAuthentication&& old) noexcept = default; + EncodedAuthentication& operator=(const EncodedAuthentication& other) = default; + + const char* GetAuthString() const noexcept; + + protected: + std::string auth_string_; +}; + +class ProxyAuthentication { + public: + ProxyAuthentication() = default; + ProxyAuthentication(const std::initializer_list>& auths) : proxyAuth_{auths} {} + ProxyAuthentication(const std::map& auths) : proxyAuth_{auths} {} + + bool has(const std::string& protocol) const; + const char* operator[](const std::string& protocol); + + private: + std::map proxyAuth_; +}; + +} // namespace cpr + +#endif diff --git a/include/cpr/range.h b/include/cpr/range.h new file mode 100644 index 0000000..2c5a145 --- /dev/null +++ b/include/cpr/range.h @@ -0,0 +1,44 @@ +#ifndef CPR_RANGE_H +#define CPR_RANGE_H + +#include +#include + +namespace cpr { + +class Range { + public: + Range(const std::optional p_resume_from = std::nullopt, const std::optional p_finish_at = std::nullopt) { + resume_from = p_resume_from.value_or(0); + finish_at = p_finish_at.value_or(-1); + } + + std::int64_t resume_from; + std::int64_t finish_at; + + const std::string str() const { + std::string from_str = (resume_from < 0U) ? "" : std::to_string(resume_from); + std::string to_str = (finish_at < 0U) ? "" : std::to_string(finish_at); + return from_str + "-" + to_str; + } +}; + +class MultiRange { + public: + MultiRange(std::initializer_list rs) : ranges{rs} {} + + const std::string str() const { + std::string multi_range_string{}; + for (Range range : ranges) { + multi_range_string += ((multi_range_string.empty()) ? "" : ", ") + range.str(); + } + return multi_range_string; + } + + private: + std::vector ranges; +}; // namespace cpr + +} // namespace cpr + +#endif diff --git a/include/cpr/redirect.h b/include/cpr/redirect.h new file mode 100644 index 0000000..32b4372 --- /dev/null +++ b/include/cpr/redirect.h @@ -0,0 +1,84 @@ +#ifndef CPR_REDIRECT_H +#define CPR_REDIRECT_H + +#include + +namespace cpr { +enum class PostRedirectFlags : uint8_t { + /** + * Respect RFC 7231 (section 6.4.2 to 6.4.4). + * Same as CURL_REDIR_POST_301 (https://curl.se/libcurl/c/CURLOPT_POSTREDIR.html). + **/ + POST_301 = 0x1 << 0, + /** + * Maintain the request method after a 302 redirect. + * Same as CURL_REDIR_POST_302 (https://curl.se/libcurl/c/CURLOPT_POSTREDIR.html). + **/ + POST_302 = 0x1 << 1, + /** + * Maintain the request method after a 303 redirect. + * Same as CURL_REDIR_POST_303 (https://curl.se/libcurl/c/CURLOPT_POSTREDIR.html). + **/ + POST_303 = 0x1 << 2, + /** + * Default value. + * Convenience option to enable all flags. + * Same as CURL_REDIR_POST_ALL (https://curl.se/libcurl/c/CURLOPT_POSTREDIR.html). + **/ + POST_ALL = POST_301 | POST_302 | POST_303, + /** + * * Convenience option to disable all flags. + **/ + NONE = 0x0 +}; + +PostRedirectFlags operator|(PostRedirectFlags lhs, PostRedirectFlags rhs); +PostRedirectFlags operator&(PostRedirectFlags lhs, PostRedirectFlags rhs); +PostRedirectFlags operator^(PostRedirectFlags lhs, PostRedirectFlags rhs); +PostRedirectFlags operator~(PostRedirectFlags flag); +PostRedirectFlags& operator|=(PostRedirectFlags& lhs, PostRedirectFlags rhs); +PostRedirectFlags& operator&=(PostRedirectFlags& lhs, PostRedirectFlags rhs); +PostRedirectFlags& operator^=(PostRedirectFlags& lhs, PostRedirectFlags rhs); +bool any(PostRedirectFlags flag); + +class Redirect { + public: + /** + * The maximum number of redirects to follow. + * 0: Refuse any redirects. + * -1: Infinite number of redirects. + * Default: 50 + * https://curl.se/libcurl/c/CURLOPT_MAXREDIRS.html + **/ + // NOLINTNEXTLINE (google-runtime-int) + long maximum{50L}; + /** + * Follow 3xx redirects. + * Default: true + * https://curl.se/libcurl/c/CURLOPT_FOLLOWLOCATION.html + **/ + bool follow{true}; + /** + * Continue to send authentication (user+password) credentials when following locations, even when hostname changed. + * Default: false + * https://curl.se/libcurl/c/CURLOPT_UNRESTRICTED_AUTH.html + **/ + bool cont_send_cred{false}; + /** + * Flags to control how to act after a redirect for a post request. + * Default: POST_ALL + **/ + PostRedirectFlags post_flags{PostRedirectFlags::POST_ALL}; + + Redirect() = default; + // NOLINTNEXTLINE (google-runtime-int) + Redirect(long p_maximum, bool p_follow, bool p_cont_send_cred, PostRedirectFlags p_post_flags) : maximum(p_maximum), follow(p_follow), cont_send_cred(p_cont_send_cred), post_flags(p_post_flags){}; + // NOLINTNEXTLINE (google-runtime-int) + explicit Redirect(long p_maximum) : maximum(p_maximum){}; + explicit Redirect(bool p_follow) : follow(p_follow){}; + Redirect(bool p_follow, bool p_cont_send_cred) : follow(p_follow), cont_send_cred(p_cont_send_cred){}; + explicit Redirect(PostRedirectFlags p_post_flags) : post_flags(p_post_flags){}; +}; +} // namespace cpr + +#endif diff --git a/include/cpr/reserve_size.h b/include/cpr/reserve_size.h new file mode 100644 index 0000000..5eae4c8 --- /dev/null +++ b/include/cpr/reserve_size.h @@ -0,0 +1,17 @@ +#ifndef CPR_RESERVE_SIZE_H +#define CPR_RESERVE_SIZE_H + +#include + +namespace cpr { + +class ReserveSize { + public: + ReserveSize(const size_t _size) : size(_size) {} + + size_t size = 0; +}; + +} // namespace cpr + +#endif diff --git a/include/cpr/resolve.h b/include/cpr/resolve.h new file mode 100644 index 0000000..86a7c89 --- /dev/null +++ b/include/cpr/resolve.h @@ -0,0 +1,23 @@ +#ifndef CPR_RESOLVE_H +#define CPR_RESOLVE_H + +#include +#include + +namespace cpr { + class Resolve { + public: + std::string host; + std::string addr; + std::set ports; + + Resolve(const std::string& host_param, const std::string& addr_param, const std::set& ports_param = std::set{80U, 443U}): host(host_param), addr(addr_param), ports(ports_param) { + if (this->ports.empty()) { + this->ports.insert(80U); + this->ports.insert(443U); + } + } + }; +} // namespace cpr + +#endif diff --git a/include/cpr/response.h b/include/cpr/response.h new file mode 100644 index 0000000..5c296da --- /dev/null +++ b/include/cpr/response.h @@ -0,0 +1,58 @@ +#ifndef CPR_RESPONSE_H +#define CPR_RESPONSE_H + +#include +#include +#include +#include +#include +#include + +#include "cpr/cert_info.h" +#include "cpr/cookies.h" +#include "cpr/cprtypes.h" +#include "cpr/error.h" +#include "cpr/ssl_options.h" +#include "cpr/util.h" + +namespace cpr { + +class MultiPerform; + +class Response { + private: + friend MultiPerform; + std::shared_ptr curl_{nullptr}; + + public: + // Ignored here since libcurl uses a long for this. + // NOLINTNEXTLINE(google-runtime-int) + long status_code{}; + std::string text{}; + Header header{}; + Url url{}; + double elapsed{}; + Cookies cookies{}; + Error error{}; + std::string raw_header{}; + std::string status_line{}; + std::string reason{}; + cpr_off_t uploaded_bytes{}; + cpr_off_t downloaded_bytes{}; + // Ignored here since libcurl uses a long for this. + // NOLINTNEXTLINE(google-runtime-int) + long redirect_count{}; + + Response() = default; + Response(std::shared_ptr curl, std::string&& p_text, std::string&& p_header_string, Cookies&& p_cookies, Error&& p_error); + std::vector GetCertInfos(); + Response(const Response& other) = default; + Response(Response&& old) noexcept = default; + ~Response() noexcept = default; + + Response& operator=(Response&& old) noexcept = default; + Response& operator=(const Response& other) = default; +}; +} // namespace cpr + +#endif diff --git a/include/cpr/session.h b/include/cpr/session.h new file mode 100644 index 0000000..6b2b93e --- /dev/null +++ b/include/cpr/session.h @@ -0,0 +1,308 @@ +#ifndef CPR_SESSION_H +#define CPR_SESSION_H + +#include +#include +#include +#include +#include +#include + +#include "cpr/accept_encoding.h" +#include "cpr/async_wrapper.h" +#include "cpr/auth.h" +#include "cpr/bearer.h" +#include "cpr/body.h" +#include "cpr/callback.h" +#include "cpr/connect_timeout.h" +#include "cpr/cookies.h" +#include "cpr/cprtypes.h" +#include "cpr/curlholder.h" +#include "cpr/http_version.h" +#include "cpr/interface.h" +#include "cpr/limit_rate.h" +#include "cpr/local_port.h" +#include "cpr/local_port_range.h" +#include "cpr/low_speed.h" +#include "cpr/multipart.h" +#include "cpr/parameters.h" +#include "cpr/payload.h" +#include "cpr/proxies.h" +#include "cpr/proxyauth.h" +#include "cpr/range.h" +#include "cpr/redirect.h" +#include "cpr/reserve_size.h" +#include "cpr/resolve.h" +#include "cpr/response.h" +#include "cpr/ssl_options.h" +#include "cpr/timeout.h" +#include "cpr/unix_socket.h" +#include "cpr/user_agent.h" +#include "cpr/util.h" +#include "cpr/verbose.h" + +namespace cpr { + +using AsyncResponse = AsyncWrapper; + +class Interceptor; +class MultiPerform; + +class Session : public std::enable_shared_from_this { + public: + Session(); + Session(const Session& other) = delete; + Session(Session&& old) = default; + + ~Session() = default; + + Session& operator=(Session&& old) noexcept = default; + Session& operator=(const Session& other) = delete; + + void SetUrl(const Url& url); + void SetParameters(const Parameters& parameters); + void SetParameters(Parameters&& parameters); + void SetHeader(const Header& header); + void UpdateHeader(const Header& header); + void SetTimeout(const Timeout& timeout); + void SetConnectTimeout(const ConnectTimeout& timeout); + void SetAuth(const Authentication& auth); +// Only supported with libcurl >= 7.61.0. +// As an alternative use SetHeader and add the token manually. +#if LIBCURL_VERSION_NUM >= 0x073D00 + void SetBearer(const Bearer& token); +#endif + void SetUserAgent(const UserAgent& ua); + void SetPayload(Payload&& payload); + void SetPayload(const Payload& payload); + void SetProxies(Proxies&& proxies); + void SetProxies(const Proxies& proxies); + void SetProxyAuth(ProxyAuthentication&& proxy_auth); + void SetProxyAuth(const ProxyAuthentication& proxy_auth); + void SetMultipart(Multipart&& multipart); + void SetMultipart(const Multipart& multipart); + void SetRedirect(const Redirect& redirect); + void SetCookies(const Cookies& cookies); + void SetBody(Body&& body); + void SetBody(const Body& body); + void SetLowSpeed(const LowSpeed& low_speed); + void SetVerifySsl(const VerifySsl& verify); + void SetUnixSocket(const UnixSocket& unix_socket); + void SetSslOptions(const SslOptions& options); + void SetReadCallback(const ReadCallback& read); + void SetHeaderCallback(const HeaderCallback& header); + void SetWriteCallback(const WriteCallback& write); + void SetProgressCallback(const ProgressCallback& progress); + void SetDebugCallback(const DebugCallback& debug); + void SetVerbose(const Verbose& verbose); + void SetInterface(const Interface& iface); + void SetLocalPort(const LocalPort& local_port); + void SetLocalPortRange(const LocalPortRange& local_port_range); + void SetHttpVersion(const HttpVersion& version); + void SetRange(const Range& range); + void SetResolve(const Resolve& resolve); + void SetResolves(const std::vector& resolves); + void SetMultiRange(const MultiRange& multi_range); + void SetReserveSize(const ReserveSize& reserve_size); + void SetAcceptEncoding(const AcceptEncoding& accept_encoding); + void SetAcceptEncoding(AcceptEncoding&& accept_encoding); + void SetLimitRate(const LimitRate& limit_rate); + + // For cancellable requests + void SetCancellationParam(std::shared_ptr param); + + // Used in templated functions + void SetOption(const Url& url); + void SetOption(const Parameters& parameters); + void SetOption(Parameters&& parameters); + void SetOption(const Header& header); + void SetOption(const Timeout& timeout); + void SetOption(const ConnectTimeout& timeout); + void SetOption(const Authentication& auth); +// Only supported with libcurl >= 7.61.0. +// As an alternative use SetHeader and add the token manually. +#if LIBCURL_VERSION_NUM >= 0x073D00 + void SetOption(const Bearer& auth); +#endif + void SetOption(const UserAgent& ua); + void SetOption(Payload&& payload); + void SetOption(const Payload& payload); + void SetOption(const LimitRate& limit_rate); + void SetOption(Proxies&& proxies); + void SetOption(const Proxies& proxies); + void SetOption(ProxyAuthentication&& proxy_auth); + void SetOption(const ProxyAuthentication& proxy_auth); + void SetOption(Multipart&& multipart); + void SetOption(const Multipart& multipart); + void SetOption(const Redirect& redirect); + void SetOption(const Cookies& cookies); + void SetOption(Body&& body); + void SetOption(const Body& body); + void SetOption(const ReadCallback& read); + void SetOption(const HeaderCallback& header); + void SetOption(const WriteCallback& write); + void SetOption(const ProgressCallback& progress); + void SetOption(const DebugCallback& debug); + void SetOption(const LowSpeed& low_speed); + void SetOption(const VerifySsl& verify); + void SetOption(const Verbose& verbose); + void SetOption(const UnixSocket& unix_socket); + void SetOption(const SslOptions& options); + void SetOption(const Interface& iface); + void SetOption(const LocalPort& local_port); + void SetOption(const LocalPortRange& local_port_range); + void SetOption(const HttpVersion& version); + void SetOption(const Range& range); + void SetOption(const MultiRange& multi_range); + void SetOption(const ReserveSize& reserve_size); + void SetOption(const AcceptEncoding& accept_encoding); + void SetOption(AcceptEncoding&& accept_encoding); + void SetOption(const Resolve& resolve); + void SetOption(const std::vector& resolves); + + cpr_off_t GetDownloadFileLength(); + /** + * Attempt to preallocate enough memory for specified number of characters in the response string. + * Pass 0 to disable this behavior and let the response string be allocated dynamically on demand. + * + * Example: + * cpr::Session session; + * session.SetUrl(cpr::Url{"http://xxx/file"}); + * session.ResponseStringReserve(1024 * 512); // Reserve space for at least 1024 * 512 characters + * cpr::Response r = session.Get(); + **/ + void ResponseStringReserve(size_t size); + Response Delete(); + Response Download(const WriteCallback& write); + Response Download(std::ofstream& file); + Response Get(); + Response Head(); + Response Options(); + Response Patch(); + Response Post(); + Response Put(); + + AsyncResponse GetAsync(); + AsyncResponse DeleteAsync(); + AsyncResponse DownloadAsync(const WriteCallback& write); + AsyncResponse DownloadAsync(std::ofstream& file); + AsyncResponse HeadAsync(); + AsyncResponse OptionsAsync(); + AsyncResponse PatchAsync(); + AsyncResponse PostAsync(); + AsyncResponse PutAsync(); + + template + auto GetCallback(Then then); + template + auto PostCallback(Then then); + template + auto PutCallback(Then then); + template + auto HeadCallback(Then then); + template + auto DeleteCallback(Then then); + template + auto OptionsCallback(Then then); + template + auto PatchCallback(Then then); + + std::shared_ptr GetCurlHolder(); + std::string GetFullRequestUrl(); + + void PrepareDelete(); + void PrepareGet(); + void PrepareHead(); + void PrepareOptions(); + void PreparePatch(); + void PreparePost(); + void PreparePut(); + void PrepareDownload(const WriteCallback& write); + void PrepareDownload(std::ofstream& file); + Response Complete(CURLcode curl_error); + Response CompleteDownload(CURLcode curl_error); + + void AddInterceptor(const std::shared_ptr& pinterceptor); + + private: + // Interceptors should be able to call the private proceed() function + friend Interceptor; + friend MultiPerform; + + + bool hasBodyOrPayload_{false}; + bool chunkedTransferEncoding_{false}; + std::shared_ptr curl_; + Url url_; + Parameters parameters_; + Proxies proxies_; + ProxyAuthentication proxyAuth_; + Header header_; + AcceptEncoding acceptEncoding_; + /** + * Will be set by the read callback. + * Ensures that the "Transfer-Encoding" is set to "chunked", if not overriden in header_. + **/ + ReadCallback readcb_; + HeaderCallback headercb_; + WriteCallback writecb_; + ProgressCallback progresscb_; + DebugCallback debugcb_; + CancellationCallback cancellationcb_; + + size_t response_string_reserve_size_{0}; + std::string response_string_; + std::string header_string_; + std::queue> interceptors_; + bool isUsedInMultiPerform{false}; + bool isCancellable{false}; + + Response makeDownloadRequest(); + Response makeRequest(); + Response proceed(); + Response intercept(); + void prepareCommon(); + void prepareCommonDownload(); + void SetHeaderInternal(); + std::shared_ptr GetSharedPtrFromThis(); + CURLcode DoEasyPerform(); +}; + +template +auto Session::GetCallback(Then then) { + return async([shared_this = GetSharedPtrFromThis()](Then then_inner) { return then_inner(shared_this->Get()); }, std::move(then)); +} + +template +auto Session::PostCallback(Then then) { + return async([shared_this = GetSharedPtrFromThis()](Then then_inner) { return then_inner(shared_this->Post()); }, std::move(then)); +} + +template +auto Session::PutCallback(Then then) { + return async([shared_this = GetSharedPtrFromThis()](Then then_inner) { return then_inner(shared_this->Put()); }, std::move(then)); +} + +template +auto Session::HeadCallback(Then then) { + return async([shared_this = GetSharedPtrFromThis()](Then then_inner) { return then_inner(shared_this->Head()); }, std::move(then)); +} + +template +auto Session::DeleteCallback(Then then) { + return async([shared_this = GetSharedPtrFromThis()](Then then_inner) { return then_inner(shared_this->Delete()); }, std::move(then)); +} + +template +auto Session::OptionsCallback(Then then) { + return async([shared_this = GetSharedPtrFromThis()](Then then_inner) { return then_inner(shared_this->Options()); }, std::move(then)); +} + +template +auto Session::PatchCallback(Then then) { + return async([shared_this = GetSharedPtrFromThis()](Then then_inner) { return then_inner(shared_this->Patch()); }, std::move(then)); +} + +} // namespace cpr + +#endif diff --git a/include/cpr/singleton.h b/include/cpr/singleton.h new file mode 100644 index 0000000..e2ea13b --- /dev/null +++ b/include/cpr/singleton.h @@ -0,0 +1,47 @@ +#ifndef CPR_SINGLETON_H +#define CPR_SINGLETON_H + +#include + +#ifndef CPR_DISABLE_COPY +#define CPR_DISABLE_COPY(Class) \ + Class(const Class&) = delete; \ + Class& operator=(const Class&) = delete; +#endif + +#ifndef CPR_SINGLETON_DECL +#define CPR_SINGLETON_DECL(Class) \ + public: \ + static Class* GetInstance(); \ + static void ExitInstance(); \ + private: \ + CPR_DISABLE_COPY(Class) \ + static Class* s_pInstance; \ + static std::mutex s_mutex; +#endif + +#ifndef CPR_SINGLETON_IMPL +#define CPR_SINGLETON_IMPL(Class) \ + Class* Class::s_pInstance = nullptr; \ + std::mutex Class::s_mutex; \ + Class* Class::GetInstance() { \ + if (s_pInstance == nullptr) { \ + s_mutex.lock(); \ + if (s_pInstance == nullptr) { \ + s_pInstance = new Class; \ + } \ + s_mutex.unlock(); \ + } \ + return s_pInstance; \ + } \ + void Class::ExitInstance() { \ + s_mutex.lock(); \ + if (s_pInstance) { \ + delete s_pInstance; \ + s_pInstance = nullptr; \ + } \ + s_mutex.unlock(); \ + } +#endif + +#endif diff --git a/include/cpr/ssl_ctx.h b/include/cpr/ssl_ctx.h new file mode 100644 index 0000000..d495fb2 --- /dev/null +++ b/include/cpr/ssl_ctx.h @@ -0,0 +1,26 @@ +#ifndef CPR_SSL_CTX_H +#define CPR_SSL_CTX_H + +#include "cpr/ssl_options.h" +#include + +#if SUPPORT_CURLOPT_SSL_CTX_FUNCTION + +namespace cpr { + +/** + * This callback function loads a CA certificate from raw_cert_buf and gets called by libcurl + * just before the initialization of an SSL connection. + * The raw_cert_buf argument is set with the CURLOPT_SSL_CTX_DATA option and has to be a nul + * terminated buffer. + * + * Sources: https://curl.se/libcurl/c/CURLOPT_SSL_CTX_FUNCTION.html + * https://curl.se/libcurl/c/CURLOPT_SSL_CTX_DATA.html + */ +CURLcode sslctx_function_load_ca_cert_from_buffer(CURL* curl, void* sslctx, void* raw_cert_buf); + +} // Namespace cpr + +#endif + +#endif \ No newline at end of file diff --git a/include/cpr/ssl_options.h b/include/cpr/ssl_options.h new file mode 100644 index 0000000..dfd38ad --- /dev/null +++ b/include/cpr/ssl_options.h @@ -0,0 +1,622 @@ +#ifndef CPR_SSLOPTIONS_H +#define CPR_SSLOPTIONS_H + +#include +#include +#include + +#include +#include + +#include "cpr/util.h" +#include + +#define __LIBCURL_VERSION_GTE(major, minor) ((LIBCURL_VERSION_MAJOR > (major)) || ((LIBCURL_VERSION_MAJOR == (major)) && (LIBCURL_VERSION_MINOR >= (minor)))) +#define __LIBCURL_VERSION_LT(major, minor) ((LIBCURL_VERSION_MAJOR < (major)) || ((LIBCURL_VERSION_MAJOR == (major)) && (LIBCURL_VERSION_MINOR < (minor)))) + +#ifndef SUPPORT_ALPN +#define SUPPORT_ALPN __LIBCURL_VERSION_GTE(7, 36) +#endif +#ifndef SUPPORT_NPN +#define SUPPORT_NPN __LIBCURL_VERSION_GTE(7, 36) && __LIBCURL_VERSION_LT(7, 86) +#endif + +#ifndef SUPPORT_SSLv2 +#define SUPPORT_SSLv2 __LIBCURL_VERSION_LT(7, 19) +#endif +#ifndef SUPPORT_SSLv3 +#define SUPPORT_SSLv3 __LIBCURL_VERSION_LT(7, 39) +#endif +#ifndef SUPPORT_TLSv1_0 +#define SUPPORT_TLSv1_0 __LIBCURL_VERSION_GTE(7, 34) +#endif +#ifndef SUPPORT_TLSv1_1 +#define SUPPORT_TLSv1_1 __LIBCURL_VERSION_GTE(7, 34) +#endif +#ifndef SUPPORT_TLSv1_2 +#define SUPPORT_TLSv1_2 __LIBCURL_VERSION_GTE(7, 34) +#endif +#ifndef SUPPORT_TLSv1_3 +#define SUPPORT_TLSv1_3 __LIBCURL_VERSION_GTE(7, 52) +#endif +#ifndef SUPPORT_MAX_TLS_VERSION +#define SUPPORT_MAX_TLS_VERSION __LIBCURL_VERSION_GTE(7, 54) +#endif +#ifndef SUPPORT_MAX_TLSv1_1 +#define SUPPORT_MAX_TLSv1_1 __LIBCURL_VERSION_GTE(7, 54) +#endif +#ifndef SUPPORT_MAX_TLSv1_2 +#define SUPPORT_MAX_TLSv1_2 __LIBCURL_VERSION_GTE(7, 54) +#endif +#ifndef SUPPORT_MAX_TLSv1_3 +#define SUPPORT_MAX_TLSv1_3 __LIBCURL_VERSION_GTE(7, 54) +#endif +#ifndef SUPPORT_TLSv13_CIPHERS +#define SUPPORT_TLSv13_CIPHERS __LIBCURL_VERSION_GTE(7, 61) +#endif +#ifndef SUPPORT_SESSIONID_CACHE +#define SUPPORT_SESSIONID_CACHE __LIBCURL_VERSION_GTE(7, 16) +#endif +#ifndef SUPPORT_SSL_FALSESTART +#define SUPPORT_SSL_FALSESTART __LIBCURL_VERSION_GTE(7, 42) +#endif +#ifndef SUPPORT_SSL_NO_REVOKE +#define SUPPORT_SSL_NO_REVOKE __LIBCURL_VERSION_GTE(7, 44) +#endif +#ifndef SUPPORT_CURLOPT_SSLKEY_BLOB +#define SUPPORT_CURLOPT_SSLKEY_BLOB __LIBCURL_VERSION_GTE(7, 71) +#endif +#ifndef SUPPORT_CURLOPT_SSL_CTX_FUNCTION +#define SUPPORT_CURLOPT_SSL_CTX_FUNCTION __LIBCURL_VERSION_GTE(7, 11) +#endif + +namespace cpr { + +class VerifySsl { + public: + VerifySsl() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + VerifySsl(bool p_verify) : verify(p_verify) {} + + explicit operator bool() const { + return verify; + } + + bool verify = true; +}; + +namespace ssl { + +// set SSL client certificate +class CertFile { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + CertFile(fs::path&& p_filename) : filename(std::move(p_filename)) {} + + virtual ~CertFile() = default; + + const fs::path filename; + + virtual const char* GetCertType() const { + return "PEM"; + } +}; + +using PemCert = CertFile; + +class DerCert : public CertFile { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + DerCert(fs::path&& p_filename) : CertFile(std::move(p_filename)) {} + + virtual ~DerCert() = default; + + const char* GetCertType() const override { + return "DER"; + } +}; + +// specify private keyfile for TLS and SSL client cert +class KeyFile { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + KeyFile(fs::path&& p_filename) : filename(std::move(p_filename)) {} + + template + KeyFile(FileType&& p_filename, PassType p_password) : filename(std::forward(p_filename)), password(std::move(p_password)) {} + + virtual ~KeyFile() { + util::secureStringClear(password); + } + + fs::path filename; + std::string password; + + virtual const char* GetKeyType() const { + return "PEM"; + } +}; + +#if SUPPORT_CURLOPT_SSLKEY_BLOB +class KeyBlob { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + KeyBlob(std::string&& p_blob) : blob(std::move(p_blob)) {} + + template + KeyBlob(BlobType&& p_blob, PassType p_password) : blob(std::forward(p_blob)), password(std::move(p_password)) {} + + virtual ~KeyBlob() { + util::secureStringClear(password); + } + + std::string blob; + std::string password; + + virtual const char* GetKeyType() const { + return "PEM"; + } +}; +#endif + +using PemKey = KeyFile; + +class DerKey : public KeyFile { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + DerKey(fs::path&& p_filename) : KeyFile(std::move(p_filename)) {} + + template + DerKey(FileType&& p_filename, PassType p_password) : KeyFile(std::forward(p_filename), std::move(p_password)) {} + + virtual ~DerKey() = default; + + const char* GetKeyType() const override { + return "DER"; + } +}; + +class PinnedPublicKey { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + PinnedPublicKey(std::string&& p_pinned_public_key) : pinned_public_key(std::move(p_pinned_public_key)) {} + + const std::string pinned_public_key; +}; + +#if SUPPORT_ALPN +// This option enables/disables ALPN in the SSL handshake (if the SSL backend libcurl is built to +// use supports it), which can be used to negotiate http2. +class ALPN { + public: + ALPN() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + ALPN(bool p_enabled) : enabled(p_enabled) {} + + explicit operator bool() const { + return enabled; + } + + bool enabled = true; +}; +#endif // SUPPORT_ALPN + +#if SUPPORT_NPN +// This option enables/disables NPN in the SSL handshake (if the SSL backend libcurl is built to +// use supports it), which can be used to negotiate http2. +class NPN { + public: + NPN() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + NPN(bool p_enabled) : enabled(p_enabled) {} + + explicit operator bool() const { + return enabled; + } + + bool enabled = true; +}; +#endif // SUPPORT_NPN + +// This option determines whether libcurl verifies that the server cert is for the server it is +// known as. +class VerifyHost { + public: + VerifyHost() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + VerifyHost(bool p_enabled) : enabled(p_enabled) {} + + explicit operator bool() const { + return enabled; + } + + bool enabled = true; +}; + +// This option determines whether libcurl verifies the authenticity of the peer's certificate. +class VerifyPeer { + public: + VerifyPeer() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + VerifyPeer(bool p_enabled) : enabled(p_enabled) {} + + explicit operator bool() const { + return enabled; + } + + bool enabled = true; +}; + +// This option determines whether libcurl verifies the status of the server cert using the +// "Certificate Status Request" TLS extension (aka. OCSP stapling). +class VerifyStatus { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + VerifyStatus(bool p_enabled) : enabled(p_enabled) {} + + explicit operator bool() const { + return enabled; + } + + bool enabled = false; +}; + +// TLS v1.0 or later +struct TLSv1 {}; +#if SUPPORT_SSLv2 +// SSL v2 (but not SSLv3) +struct SSLv2 {}; +#endif +#if SUPPORT_SSLv3 +// SSL v3 (but not SSLv2) +struct SSLv3 {}; +#endif +#if SUPPORT_TLSv1_0 +// TLS v1.0 or later (Added in 7.34.0) +struct TLSv1_0 {}; +#endif +#if SUPPORT_TLSv1_1 +// TLS v1.1 or later (Added in 7.34.0) +struct TLSv1_1 {}; +#endif +#if SUPPORT_TLSv1_2 +// TLS v1.2 or later (Added in 7.34.0) +struct TLSv1_2 {}; +#endif +#if SUPPORT_TLSv1_3 +// TLS v1.3 or later (Added in 7.52.0) +struct TLSv1_3 {}; +#endif +#if SUPPORT_MAX_TLS_VERSION +// The flag defines the maximum supported TLS version by libcurl, or the default value from the SSL +// library is used. +struct MaxTLSVersion {}; +#endif +#if SUPPORT_MAX_TLSv1_0 +// The flag defines maximum supported TLS version as TLSv1.0. (Added in 7.54.0) +struct MaxTLSv1_0 {}; +#endif +#if SUPPORT_MAX_TLSv1_1 +// The flag defines maximum supported TLS version as TLSv1.1. (Added in 7.54.0) +struct MaxTLSv1_1 {}; +#endif +#if SUPPORT_MAX_TLSv1_2 +// The flag defines maximum supported TLS version as TLSv1.2. (Added in 7.54.0) +struct MaxTLSv1_2 {}; +#endif +#if SUPPORT_MAX_TLSv1_3 +// The flag defines maximum supported TLS version as TLSv1.3. (Added in 7.54.0) +struct MaxTLSv1_3 {}; +#endif + +// path to Certificate Authority (CA) bundle +class CaInfo { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + CaInfo(fs::path&& p_filename) : filename(std::move(p_filename)) {} + + fs::path filename; +}; + +// specify directory holding CA certificates +class CaPath { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + CaPath(fs::path&& p_filename) : filename(std::move(p_filename)) {} + + fs::path filename; +}; + +#if SUPPORT_CURLOPT_SSL_CTX_FUNCTION +class CaBuffer { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + CaBuffer(std::string&& p_buffer) : buffer(std::move(p_buffer)) {} + + const std::string buffer; +}; +#endif + +// specify a Certificate Revocation List file +class Crl { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Crl(fs::path&& p_filename) : filename(std::move(p_filename)) {} + + fs::path filename; +}; + +// specify ciphers to use for TLS +class Ciphers { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Ciphers(std::string&& p_ciphers) : ciphers(std::move(p_ciphers)) {} + + std::string ciphers; +}; + +#if SUPPORT_TLSv13_CIPHERS +// specify ciphers suites to use for TLS 1.3 +class TLS13_Ciphers { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + TLS13_Ciphers(std::string&& p_ciphers) : ciphers(std::move(p_ciphers)) {} + + std::string ciphers; +}; +#endif + +#if SUPPORT_SESSIONID_CACHE +// enable/disable use of the SSL session-ID cache +class SessionIdCache { + public: + SessionIdCache() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + SessionIdCache(bool p_enabled) : enabled(p_enabled) {} + + explicit operator bool() const { + return enabled; + } + + bool enabled = true; +}; +#endif + +#if SUPPORT_SSL_FALSESTART +class SslFastStart { + public: + SslFastStart() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + SslFastStart(bool p_enabled) : enabled(p_enabled) {} + + explicit operator bool() const { + return enabled; + } + + bool enabled = false; +}; +#endif + +class NoRevoke { + public: + NoRevoke() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + NoRevoke(bool p_enabled) : enabled(p_enabled) {} + + explicit operator bool() const { + return enabled; + } + + bool enabled = false; +}; + +} // namespace ssl + +struct SslOptions { + // We don't use fs::path here, as this leads to problems using windows + std::string cert_file; + std::string cert_type; + // We don't use fs::path here, as this leads to problems using windows + std::string key_file; +#if SUPPORT_CURLOPT_SSLKEY_BLOB + std::string key_blob; +#endif + std::string key_type; + std::string key_pass; + std::string pinned_public_key; +#if SUPPORT_ALPN + bool enable_alpn = true; +#endif // SUPPORT_ALPN +#if SUPPORT_NPN + bool enable_npn = true; +#endif // SUPPORT_ALPN + bool verify_host = true; + bool verify_peer = true; + bool verify_status = false; + int ssl_version = CURL_SSLVERSION_DEFAULT; +#if SUPPORT_SSL_NO_REVOKE + bool ssl_no_revoke = false; +#endif +#if SUPPORT_MAX_TLS_VERSION + int max_version = CURL_SSLVERSION_MAX_DEFAULT; +#endif + // We don't use fs::path here, as this leads to problems using windows + std::string ca_info; + // We don't use fs::path here, as this leads to problems using windows + std::string ca_path; +#if SUPPORT_CURLOPT_SSL_CTX_FUNCTION + std::string ca_buffer; +#endif + // We don't use fs::path here, as this leads to problems using windows + std::string crl_file; + std::string ciphers; +#if SUPPORT_TLSv13_CIPHERS + std::string tls13_ciphers; +#endif +#if SUPPORT_SESSIONID_CACHE + bool session_id_cache = true; +#endif + + ~SslOptions() noexcept { +#if SUPPORT_CURLOPT_SSLKEY_BLOB + util::secureStringClear(key_blob); +#endif + util::secureStringClear(key_pass); + } + + void SetOption(const ssl::CertFile& opt) { + cert_file = opt.filename.string(); + cert_type = opt.GetCertType(); + } + void SetOption(const ssl::KeyFile& opt) { + key_file = opt.filename.string(); + key_type = opt.GetKeyType(); + key_pass = opt.password; + } +#if SUPPORT_CURLOPT_SSLKEY_BLOB + void SetOption(const ssl::KeyBlob& opt) { + key_blob = opt.blob; + key_type = opt.GetKeyType(); + key_pass = opt.password; + } +#endif + void SetOption(const ssl::PinnedPublicKey& opt) { + pinned_public_key = opt.pinned_public_key; + } + +#if SUPPORT_ALPN + void SetOption(const ssl::ALPN& opt) { + enable_alpn = opt.enabled; + } +#endif // SUPPORT_ALPN +#if SUPPORT_NPN + void SetOption(const ssl::NPN& opt) { + enable_npn = opt.enabled; + } +#endif // SUPPORT_NPN + void SetOption(const ssl::VerifyHost& opt) { + verify_host = opt.enabled; + } + void SetOption(const ssl::VerifyPeer& opt) { + verify_peer = opt.enabled; + } + void SetOption(const ssl::VerifyStatus& opt) { + verify_status = opt.enabled; + } + void SetOption(const ssl::TLSv1& /*opt*/) { + ssl_version = CURL_SSLVERSION_TLSv1; + } +#if SUPPORT_SSL_NO_REVOKE + void SetOption(const ssl::NoRevoke& opt) { + ssl_no_revoke = opt.enabled; + } +#endif +#if SUPPORT_SSLv2 + void SetOption(const ssl::SSLv2& /*opt*/) { + ssl_version = CURL_SSLVERSION_SSLv2; + } +#endif +#if SUPPORT_SSLv3 + void SetOption(const ssl::SSLv3& /*opt*/) { + ssl_version = CURL_SSLVERSION_SSLv3; + } +#endif +#if SUPPORT_TLSv1_0 + void SetOption(const ssl::TLSv1_0& /*opt*/) { + ssl_version = CURL_SSLVERSION_TLSv1_0; + } +#endif +#if SUPPORT_TLSv1_1 + void SetOption(const ssl::TLSv1_1& /*opt*/) { + ssl_version = CURL_SSLVERSION_TLSv1_1; + } +#endif +#if SUPPORT_TLSv1_2 + void SetOption(const ssl::TLSv1_2& /*opt*/) { + ssl_version = CURL_SSLVERSION_TLSv1_2; + } +#endif +#if SUPPORT_TLSv1_3 + void SetOption(const ssl::TLSv1_3& /*opt*/) { + ssl_version = CURL_SSLVERSION_TLSv1_3; + } +#endif +#if SUPPORT_MAX_TLS_VERSION + void SetOption(const ssl::MaxTLSVersion& /*opt*/) { + max_version = CURL_SSLVERSION_DEFAULT; + } +#endif +#if SUPPORT_MAX_TLSv1_0 + void SetOption(const ssl::MaxTLSv1_0& opt) { + max_version = CURL_SSLVERSION_MAX_TLSv1_0; + } +#endif +#if SUPPORT_MAX_TLSv1_1 + void SetOption(const ssl::MaxTLSv1_1& /*opt*/) { + max_version = CURL_SSLVERSION_MAX_TLSv1_1; + } +#endif +#if SUPPORT_MAX_TLSv1_2 + void SetOption(const ssl::MaxTLSv1_2& /*opt*/) { + max_version = CURL_SSLVERSION_MAX_TLSv1_2; + } +#endif +#if SUPPORT_MAX_TLSv1_3 + void SetOption(const ssl::MaxTLSv1_3& /*opt*/) { + max_version = CURL_SSLVERSION_MAX_TLSv1_3; + } +#endif + void SetOption(const ssl::CaInfo& opt) { + ca_info = opt.filename.string(); + } + void SetOption(const ssl::CaPath& opt) { + ca_path = opt.filename.string(); + } +#if SUPPORT_CURLOPT_SSL_CTX_FUNCTION + void SetOption(const ssl::CaBuffer& opt) { + ca_buffer = opt.buffer; + } +#endif + void SetOption(const ssl::Crl& opt) { + crl_file = opt.filename.string(); + } + void SetOption(const ssl::Ciphers& opt) { + ciphers = opt.ciphers; + } +#if SUPPORT_TLSv13_CIPHERS + void SetOption(const ssl::TLS13_Ciphers& opt) { + tls13_ciphers = opt.ciphers; + } +#endif +#if SUPPORT_SESSIONID_CACHE + void SetOption(const ssl::SessionIdCache& opt) { + session_id_cache = opt.enabled; + } +#endif +}; + +namespace priv { + +template +void set_ssl_option(SslOptions& opts, T&& t) { + opts.SetOption(std::forward(t)); +} + +template +void set_ssl_option(SslOptions& opts, T&& t, Ts&&... ts) { + set_ssl_option(opts, std::forward(t)); + set_ssl_option(opts, std::move(ts)...); +} + +} // namespace priv + +template +SslOptions Ssl(Ts&&... ts) { + SslOptions opts; + priv::set_ssl_option(opts, std::move(ts)...); + return opts; +} + +} // namespace cpr + +#endif diff --git a/include/cpr/status_codes.h b/include/cpr/status_codes.h new file mode 100644 index 0000000..6c7acd6 --- /dev/null +++ b/include/cpr/status_codes.h @@ -0,0 +1,100 @@ +#ifndef _CPR_STATUS_CODES +#define _CPR_STATUS_CODES +#include +namespace cpr { +namespace status { +// Information responses +constexpr std::int32_t HTTP_CONTINUE = 100; +constexpr std::int32_t HTTP_SWITCHING_PROTOCOL = 101; +constexpr std::int32_t HTTP_PROCESSING = 102; +constexpr std::int32_t HTTP_EARLY_HINTS = 103; +// Successful responses +constexpr std::int32_t HTTP_OK = 200; +constexpr std::int32_t HTTP_CREATED = 201; +constexpr std::int32_t HTTP_ACCEPTED = 202; +constexpr std::int32_t HTTP_NON_AUTHORITATIVE_INFORMATION = 203; +constexpr std::int32_t HTTP_NO_CONTENT = 204; +constexpr std::int32_t HTTP_RESET_CONTENT = 205; +constexpr std::int32_t HTTP_PARTIAL_CONTENT = 206; +constexpr std::int32_t HTTP_MULTI_STATUS = 207; +constexpr std::int32_t HTTP_ALREADY_REPORTED = 208; +constexpr std::int32_t HTTP_IM_USED = 226; +// Redirection messages +constexpr std::int32_t HTTP_MULTIPLE_CHOICE = 300; +constexpr std::int32_t HTTP_MOVED_PERMANENTLY = 301; +constexpr std::int32_t HTTP_FOUND = 302; +constexpr std::int32_t HTTP_SEE_OTHER = 303; +constexpr std::int32_t HTTP_NOT_MODIFIED = 304; +constexpr std::int32_t HTTP_USE_PROXY = 305; +constexpr std::int32_t HTTP_UNUSED = 306; +constexpr std::int32_t HTTP_TEMPORARY_REDIRECT = 307; +constexpr std::int32_t HTTP_PERMANENT_REDIRECT = 308; +// Client error responses +constexpr std::int32_t HTTP_BAD_REQUEST = 400; +constexpr std::int32_t HTTP_UNAUTHORIZED = 401; +constexpr std::int32_t HTTP_PAYMENT_REQUIRED = 402; +constexpr std::int32_t HTTP_FORBIDDEN = 403; +constexpr std::int32_t HTTP_NOT_FOUND = 404; +constexpr std::int32_t HTTP_METHOD_NOT_ALLOWED = 405; +constexpr std::int32_t HTTP_NOT_ACCEPTABLE = 406; +constexpr std::int32_t HTTP_PROXY_AUTHENTICATION_REQUIRED = 407; +constexpr std::int32_t HTTP_REQUEST_TIMEOUT = 408; +constexpr std::int32_t HTTP_CONFLICT = 409; +constexpr std::int32_t HTTP_GONE = 410; +constexpr std::int32_t HTTP_LENGTH_REQUIRED = 411; +constexpr std::int32_t HTTP_PRECONDITION_FAILED = 412; +constexpr std::int32_t HTTP_PAYLOAD_TOO_LARGE = 413; +constexpr std::int32_t HTTP_URI_TOO_LONG = 414; +constexpr std::int32_t HTTP_UNSUPPORTED_MEDIA_TYPE = 415; +constexpr std::int32_t HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416; +constexpr std::int32_t HTTP_EXPECTATION_FAILED = 417; +constexpr std::int32_t HTTP_IM_A_TEAPOT = 418; +constexpr std::int32_t HTTP_MISDIRECTED_REQUEST = 421; +constexpr std::int32_t HTTP_UNPROCESSABLE_ENTITY = 422; +constexpr std::int32_t HTTP_LOCKED = 423; +constexpr std::int32_t HTTP_FAILED_DEPENDENCY = 424; +constexpr std::int32_t HTTP_TOO_EARLY = 425; +constexpr std::int32_t HTTP_UPGRADE_REQUIRED = 426; +constexpr std::int32_t HTTP_PRECONDITION_REQUIRED = 428; +constexpr std::int32_t HTTP_TOO_MANY_REQUESTS = 429; +constexpr std::int32_t HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431; +constexpr std::int32_t HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451; +// Server response errors +constexpr std::int32_t HTTP_INTERNAL_SERVER_ERROR = 500; +constexpr std::int32_t HTTP_NOT_IMPLEMENTED = 501; +constexpr std::int32_t HTTP_BAD_GATEWAY = 502; +constexpr std::int32_t HTTP_SERVICE_UNAVAILABLE = 503; +constexpr std::int32_t HTTP_GATEWAY_TIMEOUT = 504; +constexpr std::int32_t HTTP_HTTP_VERSION_NOT_SUPPORTED = 505; +constexpr std::int32_t HTTP_VARIANT_ALSO_NEGOTIATES = 506; +constexpr std::int32_t HTTP_INSUFFICIENT_STORAGE = 507; +constexpr std::int32_t HTTP_LOOP_DETECTED = 508; +constexpr std::int32_t HTTP_NOT_EXTENDED = 510; +constexpr std::int32_t HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511; + +constexpr std::int32_t INFO_CODE_OFFSET = 100; +constexpr std::int32_t SUCCESS_CODE_OFFSET = 200; +constexpr std::int32_t REDIRECT_CODE_OFFSET = 300; +constexpr std::int32_t CLIENT_ERROR_CODE_OFFSET = 400; +constexpr std::int32_t SERVER_ERROR_CODE_OFFSET = 500; +constexpr std::int32_t MISC_CODE_OFFSET = 600; + +constexpr bool is_informational(const std::int32_t code) { + return (code >= INFO_CODE_OFFSET && code < SUCCESS_CODE_OFFSET); +} +constexpr bool is_success(const std::int32_t code) { + return (code >= SUCCESS_CODE_OFFSET && code < REDIRECT_CODE_OFFSET); +} +constexpr bool is_redirect(const std::int32_t code) { + return (code >= REDIRECT_CODE_OFFSET && code < CLIENT_ERROR_CODE_OFFSET); +} +constexpr bool is_client_error(const std::int32_t code) { + return (code >= CLIENT_ERROR_CODE_OFFSET && code < SERVER_ERROR_CODE_OFFSET); +} +constexpr bool is_server_error(const std::int32_t code) { + return (code >= SERVER_ERROR_CODE_OFFSET && code < MISC_CODE_OFFSET); +} + +} // namespace status +} // namespace cpr +#endif \ No newline at end of file diff --git a/include/cpr/threadpool.h b/include/cpr/threadpool.h new file mode 100644 index 0000000..bb7e7f2 --- /dev/null +++ b/include/cpr/threadpool.h @@ -0,0 +1,122 @@ +#ifndef CPR_THREAD_POOL_H +#define CPR_THREAD_POOL_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CPR_DEFAULT_THREAD_POOL_MAX_THREAD_NUM std::thread::hardware_concurrency() + +constexpr size_t CPR_DEFAULT_THREAD_POOL_MIN_THREAD_NUM = 1; +constexpr std::chrono::milliseconds CPR_DEFAULT_THREAD_POOL_MAX_IDLE_TIME{60000}; + +namespace cpr { + +class ThreadPool { + public: + using Task = std::function; + + explicit ThreadPool(size_t min_threads = CPR_DEFAULT_THREAD_POOL_MIN_THREAD_NUM, size_t max_threads = CPR_DEFAULT_THREAD_POOL_MAX_THREAD_NUM, std::chrono::milliseconds max_idle_ms = CPR_DEFAULT_THREAD_POOL_MAX_IDLE_TIME); + + virtual ~ThreadPool(); + + void SetMinThreadNum(size_t min_threads) { + min_thread_num = min_threads; + } + void SetMaxThreadNum(size_t max_threads) { + max_thread_num = max_threads; + } + void SetMaxIdleTime(std::chrono::milliseconds ms) { + max_idle_time = ms; + } + size_t GetCurrentThreadNum() { + return cur_thread_num; + } + size_t GetIdleThreadNum() { + return idle_thread_num; + } + bool IsStarted() { + return status != STOP; + } + bool IsStopped() { + return status == STOP; + } + + int Start(size_t start_threads = 0); + int Stop(); + int Pause(); + int Resume(); + int Wait(); + + /** + * Return a future, calling future.get() will wait task done and return RetType. + * Submit(fn, args...) + * Submit(std::bind(&Class::mem_fn, &obj)) + * Submit(std::mem_fn(&Class::mem_fn, &obj)) + **/ + template + auto Submit(Fn&& fn, Args&&... args) { + if (status == STOP) { + Start(); + } + if (idle_thread_num <= 0 && cur_thread_num < max_thread_num) { + CreateThread(); + } + using RetType = decltype(fn(args...)); + auto task = std::make_shared >(std::bind(std::forward(fn), std::forward(args)...)); + std::future future = task->get_future(); + { + std::lock_guard locker(task_mutex); + tasks.emplace([task] { (*task)(); }); + } + + task_cond.notify_one(); + return future; + } + + private: + bool CreateThread(); + void AddThread(std::thread* thread); + void DelThread(std::thread::id id); + + public: + size_t min_thread_num; + size_t max_thread_num; + std::chrono::milliseconds max_idle_time; + + private: + enum Status { + STOP, + RUNNING, + PAUSE, + }; + + struct ThreadData { + std::shared_ptr thread; + std::thread::id id; + Status status; + time_t start_time; + time_t stop_time; + }; + + std::atomic status; + std::atomic cur_thread_num; + std::atomic idle_thread_num; + std::list threads; + std::mutex thread_mutex; + std::queue tasks; + std::mutex task_mutex; + std::condition_variable task_cond; +}; + +} // namespace cpr + +#endif diff --git a/include/cpr/timeout.h b/include/cpr/timeout.h new file mode 100644 index 0000000..83b3e68 --- /dev/null +++ b/include/cpr/timeout.h @@ -0,0 +1,27 @@ +#ifndef CPR_TIMEOUT_H +#define CPR_TIMEOUT_H + +#include +#include + +namespace cpr { + +class Timeout { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Timeout(const std::chrono::milliseconds& duration) : ms{duration} {} + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Timeout(const std::int32_t& milliseconds) : Timeout{std::chrono::milliseconds(milliseconds)} {} + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Timeout(const std::chrono::seconds& duration) : ms{std::chrono::milliseconds(duration).count()} {} + + // No way around since curl uses a long here. + // NOLINTNEXTLINE(google-runtime-int) + long Milliseconds() const; + + std::chrono::milliseconds ms; +}; + +} // namespace cpr + +#endif diff --git a/include/cpr/unix_socket.h b/include/cpr/unix_socket.h new file mode 100644 index 0000000..152caf0 --- /dev/null +++ b/include/cpr/unix_socket.h @@ -0,0 +1,21 @@ +#ifndef CPR_UNIX_SOCKET_H +#define CPR_UNIX_SOCKET_H + +#include + +namespace cpr { + +class UnixSocket { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + UnixSocket(std::string unix_socket) : unix_socket_(std::move(unix_socket)) {} + + const char* GetUnixSocketString() const noexcept; + + private: + const std::string unix_socket_; +}; + +} // namespace cpr + +#endif diff --git a/include/cpr/user_agent.h b/include/cpr/user_agent.h new file mode 100644 index 0000000..a3cc129 --- /dev/null +++ b/include/cpr/user_agent.h @@ -0,0 +1,31 @@ +#ifndef CPR_USERAGENT_H +#define CPR_USERAGENT_H + +#include +#include + +#include "cpr/cprtypes.h" + +namespace cpr { +class UserAgent : public StringHolder { + public: + UserAgent() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + UserAgent(std::string useragent) : StringHolder(std::move(useragent)) {} + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + UserAgent(std::string_view useragent) : StringHolder(useragent) {} + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + UserAgent(const char* useragent) : StringHolder(useragent) {} + UserAgent(const char* str, size_t len) : StringHolder(str, len) {} + UserAgent(const std::initializer_list args) : StringHolder(args) {} + UserAgent(const UserAgent& other) = default; + UserAgent(UserAgent&& old) noexcept = default; + ~UserAgent() override = default; + + UserAgent& operator=(UserAgent&& old) noexcept = default; + UserAgent& operator=(const UserAgent& other) = default; +}; + +} // namespace cpr + +#endif diff --git a/include/cpr/util.h b/include/cpr/util.h new file mode 100644 index 0000000..d851e23 --- /dev/null +++ b/include/cpr/util.h @@ -0,0 +1,45 @@ +#ifndef CPR_UTIL_H +#define CPR_UTIL_H + +#include +#include +#include + +#include "cpr/callback.h" +#include "cpr/cookies.h" +#include "cpr/cprtypes.h" +#include "cpr/curlholder.h" + +namespace cpr::util { + +Header parseHeader(const std::string& headers, std::string* status_line = nullptr, std::string* reason = nullptr); +Cookies parseCookies(curl_slist* raw_cookies); +size_t readUserFunction(char* ptr, size_t size, size_t nitems, const ReadCallback* read); +size_t headerUserFunction(char* ptr, size_t size, size_t nmemb, const HeaderCallback* header); +size_t writeFunction(char* ptr, size_t size, size_t nmemb, std::string* data); +size_t writeFileFunction(char* ptr, size_t size, size_t nmemb, std::ofstream* file); +size_t writeUserFunction(char* ptr, size_t size, size_t nmemb, const WriteCallback* write); + +template +int progressUserFunction(const T* progress, cpr_pf_arg_t dltotal, cpr_pf_arg_t dlnow, cpr_pf_arg_t ultotal, cpr_pf_arg_t ulnow) { + const int cancel_retval{1}; + static_assert(cancel_retval != CURL_PROGRESSFUNC_CONTINUE); + return (*progress)(dltotal, dlnow, ultotal, ulnow) ? 0 : cancel_retval; +} +int debugUserFunction(CURL* handle, curl_infotype type, char* data, size_t size, const DebugCallback* debug); +std::vector split(const std::string& to_split, char delimiter); +std::string urlEncode(const std::string& s); +std::string urlDecode(const std::string& s); + +/** + * Override the content of the provided string to hide sensitive data. The + * string content after invocation is undefined. The string size is reset to zero. + * impl. based on: + * https://github.com/ojeda/secure_clear/blob/master/example-implementation/secure_clear.h + **/ +void secureStringClear(std::string& s); +bool isTrue(const std::string& s); + +} // namespace cpr::util + +#endif diff --git a/include/cpr/verbose.h b/include/cpr/verbose.h new file mode 100644 index 0000000..2bf0df8 --- /dev/null +++ b/include/cpr/verbose.h @@ -0,0 +1,18 @@ +#ifndef CPR_VERBOSE_H_ +#define CPR_VERBOSE_H_ + +namespace cpr { + +class Verbose { + public: + Verbose() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Verbose(const bool p_verbose) : verbose{p_verbose} {} + + bool verbose = true; +}; + +} // namespace cpr + + +#endif /* CPR_VERBOSE_H_ */ diff --git a/nuget/build/native/libcpr.props b/nuget/build/native/libcpr.props new file mode 100644 index 0000000..ea6c695 --- /dev/null +++ b/nuget/build/native/libcpr.props @@ -0,0 +1,26 @@ + + + + mdd + md + + + + + + + + %(RecursiveDir)%(FileName)%(Extension) + PreserveNewest + + + + @(CprLibs) + + + + $(CprLibraries);%(AdditionalDependencies) + + + \ No newline at end of file diff --git a/nuget/build/native/libcpr.targets b/nuget/build/native/libcpr.targets new file mode 100644 index 0000000..0124891 --- /dev/null +++ b/nuget/build/native/libcpr.targets @@ -0,0 +1,11 @@ + + + + + + + + $(MSBuildThisFileDirectory)\$(Platform)\$(Configuration)\include\;%(AdditionalIncludeDirectories) + + + \ No newline at end of file diff --git a/nuget/libcpr.nuspec b/nuget/libcpr.nuspec new file mode 100644 index 0000000..e47a8c0 --- /dev/null +++ b/nuget/libcpr.nuspec @@ -0,0 +1,19 @@ + + + + libcpr + $VERSION$ + C++ Requests: Curl for People + Simon Berger + Fabian Sauter, Kilian Traub, many other libcpr contributors + false + MIT + resources/cpr.png + README.md + https://github.com/libcpr + C++ Requests: Curl for People, a spiritual port of Python Requests. + Native, native + english + + + \ No newline at end of file diff --git a/nuget/resources/cpr.png b/nuget/resources/cpr.png new file mode 100644 index 0000000000000000000000000000000000000000..61ad790107c60a623b12ecb33a01f350caae0c4d GIT binary patch literal 6047 zcmZ8_do&aN|NkU%C(8Y@E||N9a#`+ji7fZ~eaYQcVKE~^J z<}OC%mMz!0eD`^u-(SB!Ugver^YXepp0D$KKAx{f&>QBwT;f~+0D#xh!qg4`V4*U{ zc@B1_q<5Me&YUbBWteXu z%ns%ofP{aBK>z>|WlK{dhnR`g>EnAsz5A!;v{H|4PL0*PKXUT9$x?p8lnt&}UkSXy z)_3-#qG9b^Ay>W9n<%#ZbqTDX(R-gV8NCh2uuiNgB0_L&C|I8{G&HoXBAmtt*oDW& z#+o1YHpRQgr)-jsWsi=ISdUvoY^B@)ySH{TufNZ9(&xXtH7i60M2fsGvs+V5+Krr? zH3G4I3Fa_RVmWAxlV07|A?eY{bG_Om~i*v$;w^#CbF53NQ^Jkj+aqLPG z!%FGI2N%$|Xh)e~>w@6NRD9gwLPRo>b3F}j#c0HARXClBZ=6k!?FkfP+HGu`x;b_9 z1N3in^!FdeZsO~`*taU4u}ecg7Q>B0s$Sq&5K&pJg9?AtqNo`R{FvG9J@S)%RwpS9l^S1vbPN6c z5%^DAQ+yh5w`takbMdL;*Up{EmmMlgn=YorYIeoCOh%Z~{`iZ1C&EF!kgV<@5@Q&B zf`;3uWn1|WQ>z#Dt_+LO zufn^w1JOh68s)qpya8=o?)+46ca)9;FaPFqhe3HhHjV(o0euucMcVx$R=u?8XTgYW zYBmCLQ%mbIr%67ZnR+{g^90o9Bg1eAuK%6?=%u3S!eeEuZ2^IIXQwA;J-oo(jfdKJ zJ}Q|93cxOacJyLggTiYmB62%v21jPQQL==3N&9W35a5_4l_^XW9 zy-k2jZII7-+qa5uX{>rro%f~|ovGltDUexyjKog(L^@ZWi z+Xwv}3!Se1v1W#9r6j4m_nDp2dtLf{M^@^y+LAnwtcB?KpB4wyR?M0sCAW)|+p+x{ zAbx+WK=UtDglhUmS^mRVbq!)d6N=L*& zihDsqY{N*Z=epmSmEU|4y1HTfrReD}&iOxjD@TmpvrDf-JqzZVr%wKRD*8!Zv6m~w zMK-V&vs_UpW!lx0167r3GB3?$-;6fi_oNI{(;3<#Zs?DGogU5UhdiP__~NkXhdW9} zkT=}(FZFIOmJK!UtJQrUB6E&>$CSpS11qf3W#X;R7ifB1mFXQxKD?wu&oRVLg&UC4 zmT`ekeftG=R7-Z`EqL6A+)5H9oL?OZ6C~#kUZjg_Qdn}i&p>65+ha6;W8)dNHixS@ zDpwF5Rn7VhEA7P>7tYyEOx)C-{WsY>AL+&CpQO0A%vCau@&-RnJDgYw4pqTTn;((D zCHY_I-Scq{s(x+=kM$O&8636xW=j0kNREou-=-9$1X1HXGuT08WTx*LN98hAyY=Tl zi1bZa15o<6bp7yGcA19ZoBFgOFy^P&_fqaysmW|JR+h8{zuhZ*g4i+`L%G(gxKY~qU{>rA4R>~Tumeu zaYTK6zdqEg?OvN*HeAB9tMutMrE)3u6R8%r@V*5cDmC1daW(KQ-Y4^s&w}dl#rg%> zk=u{l&xVZEe0y5^z>6D)yT3je6B~S249IsPy}H|`BQyCH@6o1cBGIw9mk6TPO)GSAHfPX`w45 zo!u$k6p?NP|3pZ-&ke6juX2M1dz~2LEvbltOZO+ zNL_WlyiCOqHz!XIRKr{%j@?SUf?Cm@A)vfhdkc&51ExUkB`L@L3o6!r5#+_e7d0fi zAqa70iqqavz4NAuiI$gpXwV?RL-_0PK07GU!&|+V{{YoEeLu)1G@G3^;Ol~}DBe;u z2aYREtoc7bd}Jo&73_4f5qji>FWx%M^oVYWvwxuT&5a`OmU}T?s851}3NFrVusQ-| z*`K7t8tL@<<3Xt)SHUxo+>0|%<~`RbV{Jz(UH4v^7y4w%pkFZhRfVgrWHeXs8LS#J^?>34$`c!>R4+clzqNDfzw$^o>5%q8q_SqN;qOR_5;drC z;dDgo{pDs0iGe27;n@6SaLehwA|9%pY+xZ=N{GbsYB8@OBN?oP3_FRS;}I_aq|wh> z_A2JRktIIP+k+oo2rf&bCL%$3TD2NAGg@o5g-S0?>_-tPmfk`(pWk~%%SDA%*bRLb z^N8`PB?jxPhrn*r8^!{DiDa%{h|tFM7X^FCQjXn6uQd$`I`pT`NcJT3xR);RbuTGV zt`r+2{+AXDpv^^$ISS_C?+%gus?Dtb3^c=4L0o$n?;@J-U&Yp)F4as1WJK;a> z$KnyWKvLug^t(O=HP-=V6{Kqpw9Z@u<+T~TgUaT-cFSDO4M8tAF5|@p%xWoPldo`1 zCn?0wo)=%`_7-NbsN0%@3?ApCsEu~a5Us>eA=e2W>#i`DuIu=LPTYKJ;cQ#n>$75p zc*q^mR%eAkSCI+4GvrG*i4aEm$X|KE!MOI;hMl{2We**OQdJ5W931 zz4K(rWOm|OMX~R%ePjFpe%iQkv1C)eW*}F|0iO25(!_tMB_6I(PiegKDmu&nr!i&gB>RoLN3*6A=58wRQ(}eM zW1jUt?qs;o8)U~}(=n}Q$xOn!$vRsXcYLcbKeh6p16-LcMIfUQYgl7Wm~_QmZC$A^ZQ;{xDRsU0jXXjK zfKXg!cSC@DL)>RvRdD@VMPN=c&3`~krT#i9CPRu%`j30}__{pn`&ttsq|~Dco8k=U zvr`zCCRYalMlguAw25IYC4eH8#YI_|No)5_i0uZ%rL9^uO$C6fBFtnQ0K_GO0mjJD zH?+gecw43ut@0;|xE%)uC>Rsl#r_!Lc>tt3Tu5iBGLA)n%zA0O7!bfoePSgBa3CA> zUsPjGtndm2mcBD&t>N~P5G#NkhBhcg1M^huGzRzN@Qpspz;!@JQ6nX36hpmA#uD-g#5An*W!Oo&U)zg_)=0C<{V zj2IihSPURQW+L>T))+Zw{qKY!h`-CP;jhJ+|4;ek=0 zMZ<<80VJX12Iv?g8(ch`I`}UPav0`X5u=zd!GREWb5mcmgWa$KBEXPN zI{~x@jmLYtwlJ}TUS^d5unUsu33X=ZD9TEmNzK>La~PkQF+ z+FDgEi-i!`8NZPRWq%fI>wF}MH5-NSK-Sv|F)Fx4^9iSIkKbYL8P z#Lhjv1+xj3L=i_rlQsztHezn>+pz!aU&hrb&C1A&)TB06pAt+)tQp)U47lMey%@$~ z0%UCTuZ)|T9>g=dR!P>nP&HX4qI<#;d$H8IWg$AbbV z)g=r#fR}o42j6Wf^v&Xb%RN$}X3d{{sJ0)BEHBtQ4MX^3s1e?%ii{ZkgqFxt%tyhL zVLb6g(jsBX8);rqJfHmgmM^_*%9fqoT_JFEOr9 z!orQptq*Jd$R|8N+$;E;?Aa-M+1>}|Qfd=@QxU`y=sGt!BFEyPRVTxmZluSoM8SJ5 zEdy7+>x%tt?Tln*0}j|ISO>@5(rwk*!RhtI4J0q|!AXT)Vc9e52{K8-BU)vSt()wt z7jGVKR*!9Xj|l?Ad8v~Z8F6z&*yEs{*~52BNlezm;~V%^(Lf2?^~|akuP7#7i|bbY z8ov=OhpNh6IonhlSSTRQN9_>GkJj;8e)BH`sU%Px;0T6#R!9zIuN@cIKeKFcE-)6u zARZ5`Uf+C(YqMx^l*N`!E6pT_G}Rh`jw`Hpb#0?~Ucj`492*tIDy!}IC@FC}dc&%1 zcCxml``*L(TRW!?tU-A#QEJ&zsS6{%Iajj$?Pi0hB>30kquORU4ythNn5{}z!Nh<* zK*5A)tx3EQ1DSDkxCp~T*8ZS=e;jEE%rOGZ1{VH}`FKbremn07#FF+?YaBlVah>ie z!`V*#sVMVW)`8P~m;O9~wKj5k0dYmIbGwy>)n2M^tY0$-j`or^KZ!x)j3RcYC(EmA zvlpfy){T1hDqO~{sbB9!u-(BYN-i(Hx=6aSehDN%uBM`kk#IA|L`jjGFCRq(y>K)F zy~;h_98VAJ@~{DR+B9Dmp{|7{K;t7@;-2hko2egC(OTP` zWs{&s1;IMHS#pXjMncSQhY2@eui2OO ze!3(rZ^(fO$aqZ!!}pQ~V_08#h$k?D*$DkyZtQQe8_)ZRM2C1JLA_#IB=G|>8B+b; z*GC~yZZ%ia{om+%DQ^bm`Kzo%cn!EMh+}9ktVvT!DAb@)Ckm=n_fEItPn*c+=&k4N zFC_)Y5>>eikI^H);GRx{>ski->E?%*BcAE)Rsh@Ja{2Go^g z3}|&zP@BDYZ#@ewJaoxkPY(06S=~L>_3VzjLyyeccJ73OhxuV^1pz)+2p-Mx=)DlX zbLq2;HT0%(c)QOzd&Az?%6_w6s+D8hmzbj78c7X$A{zO2@Yn?+=Q#66V`Dbiv&7mWrR6d*oDZl`3sckanGUg^{@{|R%cOeCoVZ%Cn*}C>_6P!3*FUC zy7FM#mGX1AtWoOTH9hG+&mreI)Qh32^9{wQ11k*(*@%+M9je^D-y{&T*y%o3>+NAm z#PaZHFfmq>*LwHmj0AjDfaim^5VO%|?^yl}=;$ujxKK1DS7Ody%|W%hn$D(S`R;N9 zcV(5G@Ib&lFX2I!N_m{~1LTP7g(65I1TLDOXhLK%_^Nz1jT6i?&&qYngz_CU-5v?VUfn-g&P5d*kVBu#Vj)1@`^48(iPqmqhcx}4ma77t0S4UTtiW$%>=IVagy&4UQM|34us7)k2^KmWxc;v zKD+%?u&O&IIA5Gu6jN6%S57^%W-p3LX=!>)*}3#6L^+EEk3c!MU3x90(R!hhU-C)t z(>(Gnnc!oNvg8#>yz~$Kyaj_$F{p+|UwM7_;cKNaWI}VLO#^$G#aJgo8*8-{_x!$e zx#;r}*MF=6TKO8!m0 zndOsu((;J#n+RmbfU&&4n10kH{eRun^BnWU*pCU&Ti?#1%CnCK%sA)!1wXFyvl5kjm;kn9sP<$-n1JI-nZh!aR=VpGc11!yMnARHm GJorE8t<2N_ literal 0 HcmV?d00001 diff --git a/package-build/build-package.sh b/package-build/build-package.sh new file mode 100755 index 0000000..eef22da --- /dev/null +++ b/package-build/build-package.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +SRC_DIR=$1 + +LIB='libcpr' + +LIB_DIR="${LIB}_${VERSION}" +DEBIAN_DIR="${LIB_DIR}/debian" + +ARCHIVE_NAME="$LIB_DIR.orig.tar.gz" + +echo -e "Preparing tar archive and directory\n" +cp -r $SRC_DIR $LIB_DIR + +tar --exclude-vcs -czf $ARCHIVE_NAME $LIB_DIR +tar -xzf $ARCHIVE_NAME + +cd $LIB_DIR + +echo -e "\n\nCopying prepared debian files to directory\n" +mkdir debian +cp -r package-build/debian-libcpr/* debian/ +sed -i "s/\%VERSION/$VERSION/g" debian/changelog +sed -i "s/\%DATE/$(date -R)/g" debian/changelog + +echo -e "\n\nCalling debuild\n" +debuild diff --git a/package-build/debian-libcpr/README.Debian b/package-build/debian-libcpr/README.Debian new file mode 100644 index 0000000..e159461 --- /dev/null +++ b/package-build/debian-libcpr/README.Debian @@ -0,0 +1,5 @@ +libcpr for Debian + +A package of the libcpr library. + + -- Philip Saendig Tue, 24 May 2022 10:37:24 +0200 diff --git a/package-build/debian-libcpr/changelog b/package-build/debian-libcpr/changelog new file mode 100644 index 0000000..dbe84a9 --- /dev/null +++ b/package-build/debian-libcpr/changelog @@ -0,0 +1,6 @@ +libcpr (%VERSION-1) UNRELEASED; urgency=low + + [ Philip Saendig ] + * First package of libcpr %VERSION for debian. + + -- Philip Saendig %DATE diff --git a/package-build/debian-libcpr/control b/package-build/debian-libcpr/control new file mode 100644 index 0000000..e4526b1 --- /dev/null +++ b/package-build/debian-libcpr/control @@ -0,0 +1,39 @@ +Source: libcpr +Section: libs +Priority: optional +Maintainer: Philip Saendig +Build-Depends: + debhelper-compat (= 12), + cmake, + libcurl4-openssl-dev, + libssl-dev, +Standards-Version: 4.5.0 +Homepage: https://github.com/libcpr/cpr + +Package: libcpr-dev +Architecture: any +Multi-Arch: same +Pre-Depends: ${misc:Pre-Depends} +Depends: ${misc:Depends}, ${shlibs:Depends}, libcpr1 +Description: C++ wrapper around the libcurl library - development kit + This package contains the header files and development + libraries of cpr, Curl for People. + . + The project is inspried by the Python Request project. + Using the more expressive language facilities of C++11, + it captures the essence of making network calls into a + few concise idioms. + +Package: libcpr1 +Architecture: any +Multi-Arch: same +Pre-Depends: ${misc:Pre-Depends} +Depends: ${misc:Depends}, ${shlibs:Depends}, +Description: C++ wrapper around the libcurl library - runtime library + This package contains the runtime, shared library of cpr, + Curl for People. + . + The project is inspried by the Python Request project. + Using the more expressive language facilities of C++11, + it captures the essence of making network calls into a + few concise idioms. diff --git a/package-build/debian-libcpr/copyright b/package-build/debian-libcpr/copyright new file mode 100644 index 0000000..33b7ffc --- /dev/null +++ b/package-build/debian-libcpr/copyright @@ -0,0 +1,52 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: libcpr +Source: https://github.com/libcpr/cpr + +Files: .clang-format + .clang-tidy + .github/* + CMakeLists.txt + CODE_OF_CONDUCT.md + CONTRIBUTING.md + CppCheckSuppressions.txt + README.md + cmake/* + cpr-config.cmake + cpr/* + include/* + nuget/* + package-build/* + debian/* +Copyright: 2017-2021 Huu Nguyen + 2022 libcpr and many other contributors +License: Expat + MIT License + . + Copyright (c) 2017-2021 Huu Nguyen + Copyright (c) 2022 libcpr and many other contributors + . + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +Files: test/* +Copyright: 2022 libcpr and many other contributors +License: GPL-3 + On Debian systems, the full text of the GNU General Public + License version 3 can be found in the file + `/usr/share/common-licenses/GPL-3'. + diff --git a/package-build/debian-libcpr/libcpr-dev.install b/package-build/debian-libcpr/libcpr-dev.install new file mode 100644 index 0000000..bb72267 --- /dev/null +++ b/package-build/debian-libcpr/libcpr-dev.install @@ -0,0 +1,3 @@ +usr/include +usr/lib/*/*.so +usr/lib/*/cmake diff --git a/package-build/debian-libcpr/libcpr1.install b/package-build/debian-libcpr/libcpr1.install new file mode 100644 index 0000000..3de3b10 --- /dev/null +++ b/package-build/debian-libcpr/libcpr1.install @@ -0,0 +1 @@ +usr/lib/*/*.so.* diff --git a/package-build/debian-libcpr/rules b/package-build/debian-libcpr/rules new file mode 100755 index 0000000..11b4eb1 --- /dev/null +++ b/package-build/debian-libcpr/rules @@ -0,0 +1,14 @@ +#!/usr/bin/make -f +export DH_VERBOSE = 1 +export DEB_BUILD_MAINT_OPTIONS = hardening=+all +export DEB_CFLAGS_MAINT_APPEND = -Wall -pedantic +export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed + +%: + dh $@ + +override_dh_auto_configure: + dh_auto_configure -- \ + -DCMAKE_LIBRARY_ARCHITECTURE="$(DEB_TARGET_MULTIARCH)" -DCMAKE_BUILD_TYPE=Release \ + -DCPR_USE_SYSTEM_CURL=ON -DCURL_ZLIB=OFF -DBUILD_SHARED_LIBS=ON + diff --git a/package-build/debian-libcpr/source/format b/package-build/debian-libcpr/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/package-build/debian-libcpr/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..d4aa16e --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,82 @@ +cmake_minimum_required(VERSION 3.15) + +find_package(Threads REQUIRED) + +if (ENABLE_SSL_TESTS) + add_library(test_server STATIC + abstractServer.cpp + httpServer.cpp + httpsServer.cpp) +else () + add_library(test_server STATIC + abstractServer.cpp + httpServer.cpp) +endif() +if(WIN32) + target_link_libraries(test_server PRIVATE Threads::Threads cpr::cpr GTest::GTest + PUBLIC mongoose ws2_32 wsock32) +else() + target_link_libraries(test_server PRIVATE Threads::Threads cpr::cpr GTest::GTest + PUBLIC mongoose) +endif() + +macro(add_cpr_test _TEST_NAME) + add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests.cpp) + target_link_libraries(${_TEST_NAME}_tests PRIVATE + test_server + GTest::GTest + cpr::cpr + CURL::libcurl) + add_test(NAME cpr_${_TEST_NAME}_tests COMMAND ${_TEST_NAME}_tests) + # Group under the "tests" project folder in IDEs such as Visual Studio. + set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests") + if(WIN32 AND BUILD_SHARED_LIBS) + add_custom_command(TARGET ${_TEST_NAME}_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $ $) + add_custom_command(TARGET ${_TEST_NAME}_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $ $) + endif() +endmacro() + +add_cpr_test(get) +add_cpr_test(post) +add_cpr_test(session) +add_cpr_test(prepare) +add_cpr_test(async) +if(CPR_BUILD_TESTS_PROXY) + add_cpr_test(proxy) + add_cpr_test(proxy_auth) +endif() +add_cpr_test(head) +add_cpr_test(delete) +add_cpr_test(put) +add_cpr_test(callback) +add_cpr_test(raw_body) +add_cpr_test(options) +add_cpr_test(patch) +add_cpr_test(error) +add_cpr_test(alternating) +add_cpr_test(util) +add_cpr_test(structures) +add_cpr_test(encoded_auth) +add_cpr_test(version) +add_cpr_test(download) +add_cpr_test(interceptor) +add_cpr_test(interceptor_multi) +add_cpr_test(multiperform) +add_cpr_test(resolve) +add_cpr_test(multiasync) + +if (ENABLE_SSL_TESTS) + add_cpr_test(ssl) + + # Install all ssl keys and certs. Explicit copy for each file to prevent issues when copying on Windows. + add_custom_command(TARGET ssl_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory $/data/certificates $/data/keys) + add_custom_command(TARGET ssl_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/data/certificates/client.crt $/data/certificates/client.crt) + add_custom_command(TARGET ssl_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/data/certificates/root-ca.crt $/data/certificates/root-ca.crt) + add_custom_command(TARGET ssl_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/data/certificates/server.crt $/data/certificates/server.crt) + add_custom_command(TARGET ssl_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/data/keys/client.key $/data/keys/client.key) + add_custom_command(TARGET ssl_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/data/keys/root-ca.key $/data/keys/root-ca.key) + add_custom_command(TARGET ssl_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/data/keys/server.key $/data/keys/server.key) + add_custom_command(TARGET ssl_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/data/keys/server.pub $/data/keys/server.pub) +endif() + +file(INSTALL data DESTINATION data) diff --git a/test/LICENSE b/test/LICENSE new file mode 100644 index 0000000..4d188aa --- /dev/null +++ b/test/LICENSE @@ -0,0 +1,677 @@ +This license applies to everything inside this directory and all +subdirectories. + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/test/abstractServer.cpp b/test/abstractServer.cpp new file mode 100644 index 0000000..bb8eaeb --- /dev/null +++ b/test/abstractServer.cpp @@ -0,0 +1,143 @@ +#include "abstractServer.hpp" + +namespace cpr { +void AbstractServer::SetUp() { + Start(); +} + +void AbstractServer::TearDown() { + Stop(); +} + +void AbstractServer::Start() { + should_run = true; + serverThread = std::make_shared(&AbstractServer::Run, this); + serverThread->detach(); + std::unique_lock server_lock(server_mutex); + server_start_cv.wait(server_lock); +} + +void AbstractServer::Stop() { + should_run = false; + std::unique_lock server_lock(server_mutex); + server_stop_cv.wait(server_lock); +} + +static void EventHandler(mg_connection* conn, int event, void* event_data, void* context) { + switch (event) { + case MG_EV_READ: + case MG_EV_WRITE: + /** Do nothing. Just for housekeeping. **/ + break; + case MG_EV_POLL: + /** Do nothing. Just for housekeeping. **/ + break; + case MG_EV_CLOSE: + /** Do nothing. Just for housekeeping. **/ + break; + case MG_EV_ACCEPT: + /* Initialize HTTPS connection if Server is an HTTPS Server */ + static_cast(context)->acceptConnection(conn); + break; + case MG_EV_CONNECT: + /** Do nothing. Just for housekeeping. **/ + break; + + case MG_EV_HTTP_CHUNK: { + /** Do nothing. Just for housekeeping. **/ + } break; + + case MG_EV_HTTP_MSG: { + AbstractServer* server = static_cast(context); + server->OnRequest(conn, static_cast(event_data)); + } break; + + default: + break; + } +} + +void AbstractServer::Run() { + // Setup a new mongoose http server. + memset(&mgr, 0, sizeof(mg_mgr)); + initServer(&mgr, EventHandler); + + // Notify the main thread that the server is up and runing: + server_start_cv.notify_all(); + + // Main server loop: + while (should_run) { + // NOLINTNEXTLINE (cppcoreguidelines-avoid-magic-numbers) + mg_mgr_poll(&mgr, 100); + } + + // Shutdown and cleanup: + timer_args.clear(); + mg_mgr_free(&mgr); + + // Notify the main thread that we have shut down everything: + server_stop_cv.notify_all(); +} + +static const std::string base64_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; +/** + * Decodes the given BASE64 string to a normal string. + * Source: https://gist.github.com/williamdes/308b95ac9ef1ee89ae0143529c361d37 + **/ +std::string AbstractServer::Base64Decode(const std::string& in) { + std::string out; + + std::vector T(256, -1); + for (size_t i = 0; i < 64; i++) + T[base64_chars[i]] = static_cast(i); + + int val = 0; + int valb = -8; + for (unsigned char c : in) { + if (T[c] == -1) { + break; + } + val = (val << 6) + T[c]; + valb += 6; + if (valb >= 0) { + out.push_back(char((val >> valb) & 0xFF)); + valb -= 8; + } + } + return out; +} + +// Sends error similar like in mongoose 6 method mg_http_send_error +// https://github.com/cesanta/mongoose/blob/6.18/mongoose.c#L7081-L7089 +void AbstractServer::SendError(mg_connection* conn, int code, std::string& reason) { + std::string headers{"Content-Type: text/plain\r\nConnection: close\r\n"}; + mg_http_reply(conn, code, headers.c_str(), reason.c_str()); +} + +// Checks whether a pointer to a connection is still managed by a mg_mgr. +// This check tells whether it is still possible to send a message via the given connection +// Note that it is still possible that the pointer of an old connection object may be reused by mongoose. +// In this case, the active connection might refer to a different connection than the one the caller refers to +bool AbstractServer::IsConnectionActive(mg_mgr* mgr, mg_connection* conn) { + mg_connection* c{mgr->conns}; + while (c) { + if (c == conn) { + return true; + } + c = c->next; + } + return false; +} + +uint16_t AbstractServer::GetRemotePort(const mg_connection* conn) { + return (conn->rem.port >> 8) | (conn->rem.port << 8); +} + +uint16_t AbstractServer::GetLocalPort(const mg_connection* conn) { + return (conn->loc.port >> 8) | (conn->loc.port << 8); +} + +} // namespace cpr diff --git a/test/abstractServer.hpp b/test/abstractServer.hpp new file mode 100644 index 0000000..d2daec2 --- /dev/null +++ b/test/abstractServer.hpp @@ -0,0 +1,70 @@ +#ifndef CPR_TEST_ABSTRACT_SERVER_SERVER_H +#define CPR_TEST_ABSTRACT_SERVER_SERVER_H + +#include +#include +#include +#include +#include +#include + +#include "cpr/cpr.h" +#include "mongoose.h" + +namespace cpr { + +// Helper struct for functions using timers to simulate slow connections +struct TimerArg { + mg_mgr* mgr; + mg_connection* connection; + unsigned long connection_id; + mg_timer timer; + unsigned counter; + + explicit TimerArg(mg_mgr* m, mg_connection* c, mg_timer&& t) : mgr{m}, connection{c}, connection_id{0}, timer{t}, counter{0} {} + + ~TimerArg() { + mg_timer_free(&mgr->timers, &timer); + } +}; + +class AbstractServer : public testing::Environment { + public: + ~AbstractServer() override = default; + + void SetUp() override; + void TearDown() override; + + void Start(); + void Stop(); + + virtual std::string GetBaseUrl() = 0; + virtual uint16_t GetPort() = 0; + + virtual void acceptConnection(mg_connection* conn) = 0; + virtual void OnRequest(mg_connection* conn, mg_http_message* msg) = 0; + + private: + std::shared_ptr serverThread{nullptr}; + std::mutex server_mutex; + std::condition_variable server_start_cv; + std::condition_variable server_stop_cv; + std::atomic should_run{false}; + + void Run(); + + protected: + mg_mgr mgr{}; + std::vector> timer_args{}; + virtual mg_connection* initServer(mg_mgr* mgr, mg_event_handler_t event_handler) = 0; + + static std::string Base64Decode(const std::string& in); + static void SendError(mg_connection* conn, int code, std::string& reason); + static bool IsConnectionActive(mg_mgr* mgr, mg_connection* conn); + + static uint16_t GetRemotePort(const mg_connection* conn); + static uint16_t GetLocalPort(const mg_connection* conn); +}; +} // namespace cpr + +#endif diff --git a/test/alternating_tests.cpp b/test/alternating_tests.cpp new file mode 100644 index 0000000..d0466f6 --- /dev/null +++ b/test/alternating_tests.cpp @@ -0,0 +1,163 @@ +#include + +#include + +#include + +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +TEST(AlternatingTests, PutGetTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + + { + Payload payload{{"x", "5"}}; + Response response = cpr::Put(url, payload); + std::string expected_text{"Header reflect PUT"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + + { + Response response = cpr::Get(url); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(AlternatingTests, PutGetPutGetTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + + { + Payload payload{{"x", "5"}}; + Response response = cpr::Put(url, payload); + std::string expected_text{"Header reflect PUT"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + + { + Response response = cpr::Get(url); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + + { + Payload payload{{"x", "5"}}; + Response response = cpr::Put(url, payload); + std::string expected_text{"Header reflect PUT"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + + { + Response response = cpr::Get(url); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(AlternatingTests, HeadGetTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + + { + // Head shouldn't return a body + Response response = cpr::Head(url); + std::string expected_text{""}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + + { + Response response = cpr::Get(url); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(AlternatingTests, PutHeadTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + + { + Payload payload{{"x", "5"}}; + Response response = cpr::Put(url, payload); + std::string expected_text{"Header reflect PUT"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + + { + // Head shouldn't return a body + Response response = cpr::Head(url); + std::string expected_text{""}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(AlternatingTests, PutPostTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + + { + Payload payload{{"x", "5"}}; + Response response = cpr::Put(url, payload); + std::string expected_text{"Header reflect PUT"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + + { + Payload payload{{"x", "5"}}; + Response response = cpr::Post(url, payload); + std::string expected_text{"Header reflect POST"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/test/async_tests.cpp b/test/async_tests.cpp new file mode 100644 index 0000000..026bdf0 --- /dev/null +++ b/test/async_tests.cpp @@ -0,0 +1,81 @@ +#include + +#include +#include + +#include +#include + +#include "cpr/api.h" +#include "cpr/response.h" +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +bool write_data(std::string /*data*/, intptr_t /*userdata*/) { + return true; +} + +TEST(AsyncTests, AsyncGetTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + cpr::AsyncResponse future = cpr::GetAsync(url); + std::string expected_text{"Hello world!"}; + cpr::Response response = future.get(); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); +} + +TEST(AsyncTests, AsyncGetMultipleTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + responses.emplace_back(cpr::GetAsync(url)); + } + for (cpr::AsyncResponse& future : responses) { + std::string expected_text{"Hello world!"}; + cpr::Response response = future.get(); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + } +} + +TEST(AsyncTests, AsyncGetMultipleReflectTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::vector responses; + for (size_t i = 0; i < 100; ++i) { + Parameters p{{"key", std::to_string(i)}}; + responses.emplace_back(cpr::GetAsync(url, p)); + } + int i = 0; + for (cpr::AsyncResponse& future : responses) { + std::string expected_text{"Hello world!"}; + cpr::Response response = future.get(); + EXPECT_EQ(expected_text, response.text); + Url expected_url{url + "?key=" + std::to_string(i)}; + EXPECT_EQ(expected_url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + ++i; + } +} + +TEST(AsyncTests, AsyncDownloadTest) { + cpr::Url url{server->GetBaseUrl() + "/download_gzip.html"}; + cpr::AsyncResponse future = cpr::DownloadAsync(fs::path{"/tmp/aync_download"}, url, cpr::Header{{"Accept-Encoding", "gzip"}}, cpr::WriteCallback{write_data, 0}); + cpr::Response response = future.get(); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/test/callback_tests.cpp b/test/callback_tests.cpp new file mode 100644 index 0000000..834f960 --- /dev/null +++ b/test/callback_tests.cpp @@ -0,0 +1,931 @@ +#include +#include + +#include +#include +#include +#include + +#include + +#include "cpr/cprtypes.h" +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); +std::chrono::milliseconds sleep_time{50}; +std::chrono::seconds zero{0}; + +int status_callback(int& status_code, Response r) { + status_code = r.status_code; + return r.status_code; +} + +int status_callback_ref(int& status_code, const Response& r) { + status_code = r.status_code; + return r.status_code; +} + +std::string text_callback(std::string& expected_text, Response r) { + expected_text = r.text; + return r.text; +} + +std::string text_callback_ref(std::string& expected_text, const Response& r) { + expected_text = r.text; + return r.text; +} + +TEST(CallbackGetTests, CallbackGetLambdaStatusTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + int status_code = 0; + auto future = cpr::GetCallback( + [&status_code](Response r) { + status_code = r.status_code; + return r.status_code; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackGetTests, CallbackGetLambdaTextTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string expected_text{}; + auto future = cpr::GetCallback( + [&expected_text](Response r) { + expected_text = r.text; + return r.text; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackGetTests, CallbackGetLambdaStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + int status_code = 0; + auto future = cpr::GetCallback( + [&status_code](const Response& r) { + status_code = r.status_code; + return r.status_code; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackGetTests, CallbackGetLambdaTextReferenceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string expected_text{}; + auto future = cpr::GetCallback( + [&expected_text](const Response& r) { + expected_text = r.text; + return r.text; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackGetTests, CallbackGetFunctionStatusTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::GetCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackGetTests, CallbackGetFunctionTextTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::GetCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackGetTests, CallbackGetFunctionStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback_ref, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::GetCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackGetTests, CallbackGetFunctionTextReferenceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback_ref, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::GetCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackDeleteTests, CallbackDeleteLambdaStatusTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + int status_code = 0; + auto future = cpr::DeleteCallback( + [&status_code](Response r) { + status_code = r.status_code; + return r.status_code; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackDeleteTests, CallbackDeleteLambdaTextTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + std::string expected_text{}; + auto future = cpr::DeleteCallback( + [&expected_text](Response r) { + expected_text = r.text; + return r.text; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackDeleteTests, CallbackDeleteLambdaStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + int status_code = 0; + auto future = cpr::DeleteCallback( + [&status_code](const Response& r) { + status_code = r.status_code; + return r.status_code; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackDeleteTests, CallbackDeleteLambdaTextReferenceTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + std::string expected_text{}; + auto future = cpr::DeleteCallback( + [&expected_text](const Response& r) { + expected_text = r.text; + return r.text; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackDeleteTests, CallbackDeleteFunctionStatusTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::DeleteCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackDeleteTests, CallbackDeleteFunctionTextTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::DeleteCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackDeleteTests, CallbackDeleteFunctionStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback_ref, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::DeleteCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackDeleteTests, CallbackDeleteFunctionTextReferenceTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback_ref, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::DeleteCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackHeadTests, CallbackHeadLambdaStatusTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + int status_code = 0; + auto future = cpr::HeadCallback( + [&status_code](Response r) { + status_code = r.status_code; + return r.status_code; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackHeadTests, CallbackHeadLambdaTextTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string expected_text{}; + auto future = cpr::HeadCallback( + [&expected_text](Response r) { + expected_text = r.text; + return r.text; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackHeadTests, CallbackHeadLambdaStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + int status_code = 0; + auto future = cpr::HeadCallback( + [&status_code](const Response& r) { + status_code = r.status_code; + return r.status_code; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackHeadTests, CallbackHeadLambdaTextReferenceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string expected_text{}; + auto future = cpr::HeadCallback( + [&expected_text](const Response& r) { + expected_text = r.text; + return r.text; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackHeadTests, CallbackHeadFunctionStatusTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::HeadCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackHeadTests, CallbackHeadFunctionTextTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::HeadCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackHeadTests, CallbackHeadFunctionStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback_ref, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::HeadCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackHeadTests, CallbackHeadFunctionTextReferenceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback_ref, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::HeadCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackPostTests, CallbackPostLambdaStatusTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + int status_code = 0; + auto future = cpr::PostCallback( + [&status_code](Response r) { + status_code = r.status_code; + return r.status_code; + }, + url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackPostTests, CallbackPostLambdaTextTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::string expected_text{}; + auto future = cpr::PostCallback( + [&expected_text](Response r) { + expected_text = r.text; + return r.text; + }, + url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackPostTests, CallbackPostLambdaStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + int status_code = 0; + auto future = cpr::PostCallback( + [&status_code](const Response& r) { + status_code = r.status_code; + return r.status_code; + }, + url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackPostTests, CallbackPostLambdaTextReferenceTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::string expected_text{}; + auto future = cpr::PostCallback( + [&expected_text](const Response& r) { + expected_text = r.text; + return r.text; + }, + url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackPostTests, CallbackPostFunctionStatusTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::PostCallback(callback, url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackPostTests, CallbackPostFunctionTextTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::PostCallback(callback, url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackPostTests, CallbackPostFunctionStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback_ref, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::PostCallback(callback, url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackPostTests, CallbackPostFunctionTextReferenceTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback_ref, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::PostCallback(callback, url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackPutTests, CallbackPutLambdaStatusTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + int status_code = 0; + auto future = cpr::PutCallback( + [&status_code](Response r) { + status_code = r.status_code; + return r.status_code; + }, + url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackPutTests, CallbackPutLambdaTextTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::string expected_text{}; + auto future = cpr::PutCallback( + [&expected_text](Response r) { + expected_text = r.text; + return r.text; + }, + url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackPutTests, CallbackPutLambdaStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + int status_code = 0; + auto future = cpr::PutCallback( + [&status_code](const Response& r) { + status_code = r.status_code; + return r.status_code; + }, + url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackPutTests, CallbackPutLambdaTextReferenceTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::string expected_text{}; + auto future = cpr::PutCallback( + [&expected_text](const Response& r) { + expected_text = r.text; + return r.text; + }, + url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackPutTests, CallbackPutFunctionStatusTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::PutCallback(callback, url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackPutTests, CallbackPutFunctionTextTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::PutCallback(callback, url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackPutTests, CallbackPutFunctionStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback_ref, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::PutCallback(callback, url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackPutTests, CallbackPutFunctionTextReferenceTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback_ref, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::PutCallback(callback, url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackOptionsTests, CallbackOptionsLambdaStatusTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + int status_code = 0; + auto future = cpr::OptionsCallback( + [&status_code](Response r) { + status_code = r.status_code; + return r.status_code; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackOptionsTests, CallbackOptionsLambdaTextTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string expected_text{}; + auto future = cpr::OptionsCallback( + [&expected_text](Response r) { + expected_text = r.text; + return r.text; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackOptionsTests, CallbackOptionsLambdaStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + int status_code = 0; + auto future = cpr::OptionsCallback( + [&status_code](const Response& r) { + status_code = r.status_code; + return r.status_code; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackOptionsTests, CallbackOptionsLambdaTextReferenceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string expected_text{}; + auto future = cpr::OptionsCallback( + [&expected_text](const Response& r) { + expected_text = r.text; + return r.text; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackOptionsTests, CallbackOptionsFunctionStatusTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::OptionsCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackOptionsTests, CallbackOptionsFunctionTextTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::OptionsCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackOptionsTests, CallbackOptionsFunctionStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback_ref, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::OptionsCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackOptionsTests, CallbackOptionsFunctionTextReferenceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback_ref, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::OptionsCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackPatchTests, CallbackPatchLambdaStatusTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + int status_code = 0; + auto future = cpr::PatchCallback( + [&status_code](Response r) { + status_code = r.status_code; + return r.status_code; + }, + url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackPatchTests, CallbackPatchLambdaTextTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::string expected_text{}; + auto future = cpr::PatchCallback( + [&expected_text](Response r) { + expected_text = r.text; + return r.text; + }, + url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackPatchTests, CallbackPatchLambdaStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + int status_code = 0; + auto future = cpr::PatchCallback( + [&status_code](const Response& r) { + status_code = r.status_code; + return r.status_code; + }, + url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackPatchTests, CallbackPatchLambdaTextReferenceTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::string expected_text{}; + auto future = cpr::PatchCallback( + [&expected_text](const Response& r) { + expected_text = r.text; + return r.text; + }, + url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackPatchTests, CallbackPatchFunctionStatusTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::PatchCallback(callback, url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackPatchTests, CallbackPatchFunctionTextTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::PatchCallback(callback, url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackPatchTests, CallbackPatchFunctionStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback_ref, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::PatchCallback(callback, url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackPatchTests, CallbackPatchFunctionTextReferenceTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback_ref, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::PatchCallback(callback, url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackDataTests, CallbackReadFunctionCancelTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Response response = cpr::Post(url, cpr::ReadCallback([](char* /*buffer*/, size_t& /*size*/, intptr_t /*userdata*/) -> size_t { return false; })); + EXPECT_EQ(response.error.code, ErrorCode::REQUEST_CANCELLED); +} + +TEST(CallbackDataTests, CallbackReadFunctionTextTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + unsigned count = 0; + Response response = cpr::Post(url, cpr::ReadCallback{3, [&](char* buffer, size_t& size, intptr_t /*userdata*/) -> size_t { + std::string data; + ++count; + switch (count) { + case 1: + data = "x="; + break; + case 2: + data = "5"; + break; + default: + return false; + } + std::copy(data.begin(), data.end(), buffer); + size = data.size(); + return true; + }}); + EXPECT_EQ(2, count); + EXPECT_EQ(expected_text, response.text); +} + +TEST(CallbackDataTests, CallbackReadFunctionTextTestPut) { + Url url{server->GetBaseUrl() + "/put.html"}; + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + unsigned count = 0; + Response response = cpr::Put(url, cpr::ReadCallback{3, [&](char* buffer, size_t& size, intptr_t /*userdata*/) -> size_t { + std::string data; + ++count; + switch (count) { + case 1: + data = "x="; + break; + case 2: + data = "5"; + break; + default: + return false; + } + std::copy(data.begin(), data.end(), buffer); + size = data.size(); + return true; + }}); + EXPECT_EQ(2, count); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +/** + * Checks if the "Transfer-Encoding" header will be kept when using headers and a read callback. + * Issue: https://github.com/whoshuu/cpr/issues/517 + **/ +TEST(CallbackDataTests, CallbackReadFunctionHeaderTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + std::string data = "Test"; + Response response = cpr::Post(url, + cpr::ReadCallback{-1, + [&](char* /*buffer*/, size_t& size, intptr_t /*userdata*/) -> size_t { + size = 0; + return true; + }}, + Header{{"TestHeader", "42"}}); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + + // Check Header: + EXPECT_EQ(std::string{"42"}, response.header["TestHeader"]); // Set by us + EXPECT_TRUE(response.header.find("TestHeader") != response.header.end()); + EXPECT_EQ(std::string{"chunked"}, response.header["Transfer-Encoding"]); // Set by the read callback + EXPECT_TRUE(response.header.find("Transfer-Encoding") != response.header.end()); +} + +/* cesanta mongoose doesn't support chunked requests yet +TEST(CallbackDataTests, CallbackReadFunctionChunkedTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + unsigned count = 0; + Response response = cpr::Post(url, cpr::ReadCallback{[&count](char* buffer, size_t & size) -> size_t { + std::string data; + ++ count; + switch (count) { + case 1: + data = "x="; + break; + case 2: + data = "5"; + break; + default: + data = ""; + break; + } + std::copy(data.begin(), data.end(), buffer); + size = data.size(); + return true; + }}); + EXPECT_EQ(3, count); + EXPECT_EQ(expected_text, response.text); +} +*/ + +TEST(CallbackDataTests, CallbackHeaderFunctionCancelTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Response response = Post(url, HeaderCallback{[](std::string /*header*/, intptr_t /*userdata*/) -> bool { return false; }}); + EXPECT_EQ(response.error.code, ErrorCode::REQUEST_CANCELLED); +} + +TEST(CallbackDataTests, CallbackHeaderFunctionTextTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + std::vector expected_headers{"HTTP/1.1 201 Created\r\n", "Content-Type: application/json\r\n", "\r\n"}; + std::set response_headers; + Post(url, HeaderCallback{[&response_headers](std::string header, intptr_t /*userdata*/) -> bool { + response_headers.insert(header); + return true; + }}); + for (std::string& header : expected_headers) { + std::cout << header << std::endl; + EXPECT_TRUE(response_headers.count(header)); + } +} + +TEST(CallbackDataTests, CallbackWriteFunctionCancelTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Response response = Post(url, WriteCallback{[](std::string /*header*/, intptr_t /*userdata*/) -> bool { return false; }}); + EXPECT_EQ(response.error.code, ErrorCode::REQUEST_CANCELLED); +} + +TEST(CallbackDataTests, CallbackWriteFunctionTextTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + std::string response_text; + Post(url, Payload{{"x", "5"}}, WriteCallback{[&response_text](std::string header, intptr_t /*userdata*/) -> bool { + response_text.append(header); + return true; + }}); + EXPECT_EQ(expected_text, response_text); +} + +TEST(CallbackDataTests, CallbackProgressFunctionCancelTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Response response = Post(url, ProgressCallback{[](size_t /*downloadTotal*/, size_t /*downloadNow*/, size_t /*uploadTotal*/, size_t /*uploadNow*/, intptr_t /*userdata*/) -> bool { return false; }}); + EXPECT_EQ(response.error.code, ErrorCode::REQUEST_CANCELLED); +} + +TEST(CallbackDataTests, CallbackProgressFunctionTotalTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Body body{"x=5"}; + size_t response_upload = 0, response_download = 0; + Response response = Post(url, body, ProgressCallback{[&](size_t downloadTotal, size_t /*downloadNow*/, size_t uploadTotal, size_t /*uploadNow*/, intptr_t /*userdata*/) -> bool { + response_upload = uploadTotal; + response_download = downloadTotal; + return true; + }}); + EXPECT_EQ(body.str().length(), response_upload); + EXPECT_EQ(response.text.length(), response_download); +} + +TEST(CallbackDataTests, CallbackDebugFunctionTextTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Body body{"x=5"}; + std::string debug_body; + Response response = Post(url, body, DebugCallback{[&](DebugCallback::InfoType type, std::string data, intptr_t /*userdata*/) { + if (type == DebugCallback::InfoType::DATA_OUT) { + debug_body = data; + } + }}); + EXPECT_EQ(body.str(), debug_body); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/test/data/certificates/client.crt b/test/data/certificates/client.crt new file mode 100644 index 0000000..0583f54 --- /dev/null +++ b/test/data/certificates/client.crt @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBejCCASygAwIBAgIQKMJShx7GKmJqmABrC/KIkDAFBgMrZXAwMTELMAkGA1UE +BhMCR0IxEDAOBgNVBAoMB0V4YW1wbGUxEDAOBgNVBAMMB1Jvb3QgQ0EwHhcNMjIw +NjI5MTEzMzA3WhcNMjcwNjI4MTEzMzA3WjAWMRQwEgYDVQQDDAt0ZXN0LWNsaWVu +dDAqMAUGAytlcAMhAOGArRN1SIicY6uB/2CRB668fBEDTQb1oLcCoTsYQetho3Uw +czAfBgNVHSMEGDAWgBTk8vOFDreFdYR240PRtp0UuOKktzAMBgNVHRMBAf8EAjAA +MBMGA1UdJQQMMAoGCCsGAQUFBwMCMA4GA1UdDwEB/wQEAwIHgDAdBgNVHQ4EFgQU +a5RqAAt7DpJN8iHcLvTjH2TIKtowBQYDK2VwA0EApzcNlIuTMToyqyWZ0FhxikP/ +c2TS6u5qkP+YHgcJJkvJ0rRTXs164k4LpvlMG0gNxle4zfoAJQ8mAAMZcQKyAg== +-----END CERTIFICATE----- diff --git a/test/data/certificates/root-ca.crt b/test/data/certificates/root-ca.crt new file mode 100644 index 0000000..32d7ba9 --- /dev/null +++ b/test/data/certificates/root-ca.crt @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBrjCCAWCgAwIBAgIQKMJShx7GKmJqmABrC/KIjjAFBgMrZXAwMTELMAkGA1UE +BhMCR0IxEDAOBgNVBAoMB0V4YW1wbGUxEDAOBgNVBAMMB1Jvb3QgQ0EwHhcNMjIw +NjI5MTEzMzA3WhcNMzIwNjI2MTEzMzA3WjAxMQswCQYDVQQGEwJHQjEQMA4GA1UE +CgwHRXhhbXBsZTEQMA4GA1UEAwwHUm9vdCBDQTAqMAUGAytlcAMhAJqzaumMKuMm +htBGbS+UCrCmXbGb+lRcuO71mPRey7HXo4GNMIGKMA8GA1UdEwEB/wQFMAMBAf8w +DgYDVR0PAQH/BAQDAgIEMB0GA1UdDgQWBBTk8vOFDreFdYR240PRtp0UuOKktzBI +BgNVHR4EQTA/oD0wC4IJbG9jYWxob3N0MAqHCH8AAAH/AAAAMCKHIAAAAAAAAAAA +AAAAAAAAAAH/////////////////////MAUGAytlcANBAESQBu1/oyaeYouu3q+h +VbIDkQiyZT4sPRYautZZ+xrN4MkNWDtwLeVJ+a9N0YU9vDpOviJpvXN4H/EEBwBF +3AA= +-----END CERTIFICATE----- diff --git a/test/data/certificates/server.crt b/test/data/certificates/server.crt new file mode 100644 index 0000000..da57202 --- /dev/null +++ b/test/data/certificates/server.crt @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBdTCCASegAwIBAgIQKMJShx7GKmJqmABrC/KIjzAFBgMrZXAwMTELMAkGA1UE +BhMCR0IxEDAOBgNVBAoMB0V4YW1wbGUxEDAOBgNVBAMMB1Jvb3QgQ0EwHhcNMjIw +NjI5MTEzMzA3WhcNMjcwNjI4MTEzMzA3WjAWMRQwEgYDVQQDDAt0ZXN0LXNlcnZl +cjAqMAUGAytlcAMhAI64JU5RjfdEG1KQMxS5DQWkiGlKIQO7ye4mNFq9QleTo3Aw +bjAsBgNVHREEJTAjgglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEw +HQYDVR0OBBYEFDnBgTgB3FU45S9OetBMhHu3J9OvMB8GA1UdIwQYMBaAFOTy84UO +t4V1hHbjQ9G2nRS44qS3MAUGAytlcANBAC4NoQ31kHfp64R9gGNjTYrr2SNXHyEq +7YG0qFi5ABvLXJAbM2v27EIgY1TWYO43FBsclQsz6mcp1MzZfjT9RwQ= +-----END CERTIFICATE----- diff --git a/test/data/client.cnf b/test/data/client.cnf new file mode 100644 index 0000000..d387d39 --- /dev/null +++ b/test/data/client.cnf @@ -0,0 +1,8 @@ +# Based on https://www.feistyduck.com/library/openssl-cookbook/online/openssl-command-line/private-ca-create-subordinate.html +[req] +prompt = no +distinguished_name = dn + +[dn] +CN = test-client + diff --git a/test/data/generate-certificates.sh b/test/data/generate-certificates.sh new file mode 100755 index 0000000..f20d772 --- /dev/null +++ b/test/data/generate-certificates.sh @@ -0,0 +1,76 @@ +#!/bin/sh + +# Generate a CA with a self-signed root certificate that then signs the server certificate +# Based on the OpenSSL Cookbook by Ivan Ristic: +# https://www.feistyduck.com/library/openssl-cookbook/online/ +# +# Especially, see chapter 1.5. Creating a private Certification Authority: +# https://www.feistyduck.com/library/openssl-cookbook/online/openssl-command-line/private-ca.html + +export KEY_PATH=keys +export CRT_PATH=certificates +export CA_PATH=ca + +# Create environment. +# $CA_PATH is deleted in the end. +# If new certificates need to be issued, this needs to be done before the cleanup in the end. +mkdir -p $KEY_PATH $CRT_PATH $CA_PATH/db $CA_PATH/private $CA_PATH/certificates +touch $CA_PATH/db/index +openssl rand -hex 16 > $CA_PATH/db/serial + + +# Generate all private keys +openssl genpkey -algorithm ed25519 -out $KEY_PATH/root-ca.key +openssl genpkey -algorithm ed25519 -out $KEY_PATH/server.key +openssl genpkey -algorithm ed25519 -out $KEY_PATH/client.key + +# For the server, we also need the public key +openssl pkey -in $KEY_PATH/server.key -pubout -out $KEY_PATH/server.pub + + +# Generate a Certificate Signing Request for the Root CA based on a config file +openssl req -new \ + -config root-ca.cnf -out root-ca.csr \ + -key $KEY_PATH/root-ca.key + +# Self-sign the root certificate +openssl ca -batch \ + -selfsign -config root-ca.cnf \ + -extensions ca_ext \ + -in root-ca.csr -out $CRT_PATH/root-ca.crt -notext + + +# Create a Certificate Signing request for the server certificate +openssl req -new \ + -config server.cnf -out server.csr \ + -key $KEY_PATH/server.key +openssl req -text -in server.csr -noout + +# Issue the server certificate +openssl ca -batch \ + -config root-ca.cnf \ + -extensions server_ext \ + -extfile server.cnf -extensions ext \ + -in server.csr -out $CRT_PATH/server.crt -notext \ + -days 1825 + + +# Create a Certificate Signing request for the client certificate +openssl req -new \ + -config client.cnf -out client.csr \ + -key $KEY_PATH/client.key + +# Issue the client certificate +openssl ca -batch \ + -config root-ca.cnf \ + -extensions client_ext \ + -in client.csr -out $CRT_PATH/client.crt -notext \ + -days 1825 + + + +# Clean up +# IMPORTANT: If new certificates should be issued, $CA_PATH and its files MUST NOT be deleted! +# New certificates can be created in this script before cleaning up. +rm -rf *.csr $CA_PATH + diff --git a/test/data/keys/client.key b/test/data/keys/client.key new file mode 100644 index 0000000..120f9e0 --- /dev/null +++ b/test/data/keys/client.key @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIPTCPxm8reXOE2aIrafTcibvg4f6Rg1/F2LVk12EILzJ +-----END PRIVATE KEY----- diff --git a/test/data/keys/root-ca.key b/test/data/keys/root-ca.key new file mode 100644 index 0000000..a574c0b --- /dev/null +++ b/test/data/keys/root-ca.key @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIHbCvDGMRz5Ky+7gJvQYZ5t+5sZyHI+UcAKWvS20CoLU +-----END PRIVATE KEY----- diff --git a/test/data/keys/server.key b/test/data/keys/server.key new file mode 100644 index 0000000..bfdefcb --- /dev/null +++ b/test/data/keys/server.key @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIGVXwKYyi/u52mmDVC56TSorC/GGNqgyiW4+jsDno81i +-----END PRIVATE KEY----- diff --git a/test/data/keys/server.pub b/test/data/keys/server.pub new file mode 100644 index 0000000..715576a --- /dev/null +++ b/test/data/keys/server.pub @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAjrglTlGN90QbUpAzFLkNBaSIaUohA7vJ7iY0Wr1CV5M= +-----END PUBLIC KEY----- diff --git a/test/data/root-ca.cnf b/test/data/root-ca.cnf new file mode 100644 index 0000000..9a1fd65 --- /dev/null +++ b/test/data/root-ca.cnf @@ -0,0 +1,69 @@ +# Based on: https://www.feistyduck.com/library/openssl-cookbook/online/openssl-command-line/private-ca-creating-root.html +[default] +name = root-ca +default_ca = ca_default +name_opt = utf8,esc_ctrl,multiline,lname,align + +[ca_dn] +countryName = "GB" +organizationName = "Example" +commonName = "Root CA" + +[ca_default] +home = ./${ENV::CA_PATH} +database = $home/db/index +serial = $home/db/serial +certificate = ./${ENV::CRT_PATH}/$name.crt +private_key = ./${ENV::KEY_PATH}/$name.key +RANDFILE = $home/private/random +new_certs_dir = $home/certificates +unique_subject = no +copy_extensions = none +default_days = 3650 +default_md = sha256 +policy = policy_cn_supplied + +[policy_cn_supplied] +countryName = optional +stateOrProvinceName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +[req] +default_bits = 4096 +encrypt_key = yes +default_md = sha256 +utf8 = yes +string_mask = utf8only +prompt = no +distinguished_name = ca_dn +req_extensions = ca_ext + +[ca_ext] +basicConstraints = critical,CA:true +keyUsage = critical,keyCertSign +subjectKeyIdentifier = hash +nameConstraints = @name_constraints + + +[server_ext] +authorityKeyIdentifier = keyid:always +basicConstraints = critical,CA:false +extendedKeyUsage = clientAuth,serverAuth +keyUsage = critical,digitalSignature,keyEncipherment +subjectKeyIdentifier = hash + +[client_ext] +authorityKeyIdentifier = keyid:always +basicConstraints = critical,CA:false +extendedKeyUsage = clientAuth +keyUsage = critical,digitalSignature +subjectKeyIdentifier = hash + +[name_constraints] +permitted;DNS.0=localhost +permitted;IP.0=127.0.0.1/255.0.0.0 +permitted;IP.1=::1/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff + diff --git a/test/data/server.cnf b/test/data/server.cnf new file mode 100644 index 0000000..a67fe34 --- /dev/null +++ b/test/data/server.cnf @@ -0,0 +1,12 @@ +# Based on https://www.feistyduck.com/library/openssl-cookbook/online/openssl-command-line/private-ca-create-subordinate.html +[req] +prompt = no +distinguished_name = dn +req_extensions = ext + +[dn] +CN = test-server + +[ext] +subjectAltName = DNS:localhost,IP:127.0.0.1,IP:::1 + diff --git a/test/delete_tests.cpp b/test/delete_tests.cpp new file mode 100644 index 0000000..50856df --- /dev/null +++ b/test/delete_tests.cpp @@ -0,0 +1,259 @@ +#include + +#include + +#include + +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +TEST(DeleteTests, DeleteTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + Response response = cpr::Delete(url); + std::string expected_text{"Delete success"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, DeleteUnallowedTest) { + Url url{server->GetBaseUrl() + "/delete_unallowed.html"}; + Response response = cpr::Delete(url); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, DeleteJsonBodyTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + Response response = cpr::Delete(url, cpr::Body{"'foo': 'bar'"}, cpr::Header{{"Content-Type", "application/json"}}); + std::string expected_text{"'foo': 'bar'"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, SessionDeleteTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + Session session; + session.SetUrl(url); + Response response = session.Delete(); + std::string expected_text{"Delete success"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, SessionDeleteUnallowedTest) { + Url url{server->GetBaseUrl() + "/delete_unallowed.html"}; + Session session; + session.SetUrl(url); + Response response = session.Delete(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, SessionDeleteJsonBodyTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + Session session; + session.SetUrl(url); + session.SetHeader(cpr::Header{{"Content-Type", "application/json"}}); + session.SetBody(cpr::Body{"{'foo': 'bar'}"}); + Response response = session.Delete(); + std::string expected_text{"{'foo': 'bar'}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, SessionDeleteAfterGetTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/get.html"}; + session.SetUrl(url); + Response response = session.Get(); + } + Url url{server->GetBaseUrl() + "/delete.html"}; + session.SetUrl(url); + Response response = session.Delete(); + std::string expected_text{"Delete success"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, SessionDeleteUnallowedAfterGetTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/get.html"}; + session.SetUrl(url); + Response response = session.Get(); + } + Url url{server->GetBaseUrl() + "/delete_unallowed.html"}; + session.SetUrl(url); + Response response = session.Delete(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, SessionDeleteAfterHeadTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/get.html"}; + session.SetUrl(url); + Response response = session.Head(); + } + Url url{server->GetBaseUrl() + "/delete.html"}; + session.SetUrl(url); + Response response = session.Delete(); + std::string expected_text{"Delete success"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, SessionDeleteUnallowedAfterHeadTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/get.html"}; + session.SetUrl(url); + Response response = session.Head(); + } + Url url{server->GetBaseUrl() + "/delete_unallowed.html"}; + session.SetUrl(url); + Response response = session.Delete(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, SessionDeleteAfterPostTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + Response response = session.Post(); + } + Url url{server->GetBaseUrl() + "/patch_unallowed.html"}; + session.SetUrl(url); + Response response = session.Delete(); + std::string expected_text{"Delete success"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, SessionDeleteUnallowedAfterPostTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + Response response = session.Post(); + } + Url url{server->GetBaseUrl() + "/delete_unallowed.html"}; + session.SetUrl(url); + Response response = session.Delete(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, AsyncDeleteTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + cpr::AsyncResponse future_response = cpr::DeleteAsync(url); + cpr::Response response = future_response.get(); + std::string expected_text{"Delete success"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, AsyncDeleteUnallowedTest) { + Url url{server->GetBaseUrl() + "/delete_unallowed.html"}; + cpr::AsyncResponse future_response = cpr::DeleteAsync(url); + cpr::Response response = future_response.get(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, AsyncMultipleDeleteTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + responses.emplace_back(cpr::DeleteAsync(url)); + } + for (cpr::AsyncResponse& future_response : responses) { + cpr::Response response = future_response.get(); + std::string expected_text{"Delete success"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(DeleteTests, AsyncMultipleDeleteUnallowedTest) { + Url url{server->GetBaseUrl() + "/delete_unallowed.html"}; + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + responses.emplace_back(cpr::DeleteAsync(url)); + } + for (cpr::AsyncResponse& future_response : responses) { + cpr::Response response = future_response.get(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/test/download_tests.cpp b/test/download_tests.cpp new file mode 100644 index 0000000..a1ee6df --- /dev/null +++ b/test/download_tests.cpp @@ -0,0 +1,140 @@ +#include +#include + +#include + +#include + +#include "cpr/api.h" +#include "cpr/callback.h" +#include "cpr/cprtypes.h" +#include "cpr/session.h" +#include "httpServer.hpp" + + +static cpr::HttpServer* server = new cpr::HttpServer(); + +bool write_data(std::string /*data*/, intptr_t /*userdata*/) { + return true; +} + +TEST(DownloadTests, DownloadGzip) { + cpr::Url url{server->GetBaseUrl() + "/download_gzip.html"}; + cpr::Session session; + session.SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + session.SetUrl(url); + cpr::Response response = session.Download(cpr::WriteCallback{write_data, 0}); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); +} + +TEST(DownloadTests, RangeTestWholeFile) { + const int64_t download_size = 9; + cpr::Url url{server->GetBaseUrl() + "/download_gzip.html"}; + cpr::Session session; + session.SetUrl(url); + session.SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + session.SetRange(cpr::Range{std::nullopt, std::nullopt}); + cpr::Response response = session.Download(cpr::WriteCallback{write_data, 0}); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); + EXPECT_EQ(download_size, response.downloaded_bytes); +} + +TEST(DownloadTests, RangeTestLowerLimit) { + const int64_t download_size = 8; + cpr::Url url{server->GetBaseUrl() + "/download_gzip.html"}; + cpr::Session session; + session.SetUrl(url); + session.SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + session.SetRange(cpr::Range{1, std::nullopt}); + cpr::Response response = session.Download(cpr::WriteCallback{write_data, 0}); + EXPECT_EQ(206, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); + EXPECT_EQ(download_size, response.downloaded_bytes); +} + +TEST(DownloadTests, RangeTestUpperLimit) { + const int64_t download_size = 6; + cpr::Url url{server->GetBaseUrl() + "/download_gzip.html"}; + cpr::Session session; + session.SetUrl(url); + session.SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + session.SetRange(cpr::Range{std::nullopt, download_size - 1}); + cpr::Response response = session.Download(cpr::WriteCallback{write_data, 0}); + EXPECT_EQ(206, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); + EXPECT_EQ(download_size, response.downloaded_bytes); +} + +TEST(DownloadTests, RangeTestLowerAndUpperLimit) { + const int64_t download_size = 2; + const int64_t start_from = 2; + const int64_t finish_at = start_from + download_size - 1; + cpr::Url url{server->GetBaseUrl() + "/download_gzip.html"}; + cpr::Session session; + session.SetUrl(url); + session.SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + session.SetRange(cpr::Range{start_from, finish_at}); + cpr::Response response = session.Download(cpr::WriteCallback{write_data, 0}); + EXPECT_EQ(206, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); + EXPECT_EQ(download_size, response.downloaded_bytes); +} + +TEST(DownloadTests, RangeTestMultipleRangesSet) { + const int64_t num_parts = 2; + const int64_t download_size = num_parts * (26 /*content range*/ + 4 /*\n*/) + ((num_parts + 1) * 15 /*boundary*/) + 2 /*--*/ + 6 /*data*/; + cpr::Url url{server->GetBaseUrl() + "/download_gzip.html"}; + cpr::Session session; + session.SetUrl(url); + session.SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + session.SetMultiRange(cpr::MultiRange{cpr::Range{std::nullopt, 3}, cpr::Range{5, 6}}); + cpr::Response response = session.Download(cpr::WriteCallback{write_data, 0}); + EXPECT_EQ(206, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); + EXPECT_EQ(download_size, response.downloaded_bytes); +} + +TEST(DownloadTests, RangeTestMultipleRangesOption) { + const int64_t num_parts = 3; + const int64_t download_size = num_parts * (26 /*content range*/ + 4 /*\n*/) + ((num_parts + 1) * 15 /*boundary*/) + 2 /*--*/ + 7 /*data*/; + cpr::Url url{server->GetBaseUrl() + "/download_gzip.html"}; + cpr::Session session; + session.SetUrl(url); + session.SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + session.SetOption(cpr::MultiRange{cpr::Range{std::nullopt, 2}, cpr::Range{4, 5}, cpr::Range{7, 8}}); + cpr::Response response = session.Download(cpr::WriteCallback{write_data, 0}); + EXPECT_EQ(206, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); + EXPECT_EQ(download_size, response.downloaded_bytes); +} + +bool real_write_data(std::string data, intptr_t userdata) { + // NOLINTNEXTLINE (cppcoreguidelines-pro-type-reinterpret-cast) + std::string* dst = reinterpret_cast(userdata); + *dst += data; + return true; +} + +TEST(DownloadTests, GetDownloadFileLength) { + cpr::Url url{server->GetBaseUrl() + "/get_download_file_length.html"}; + cpr::Session session; + session.SetUrl(url); + auto len = session.GetDownloadFileLength(); + EXPECT_EQ(len, -1); + + std::string strFileData; + cpr::Response response = session.Download(cpr::WriteCallback{real_write_data, (intptr_t) &strFileData}); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); + EXPECT_EQ(strFileData, "this is a file content."); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/test/encoded_auth_tests.cpp b/test/encoded_auth_tests.cpp new file mode 100644 index 0000000..c8b89ce --- /dev/null +++ b/test/encoded_auth_tests.cpp @@ -0,0 +1,20 @@ +#include + +#include + +#include + +using namespace cpr; + +TEST(EncodedAuthenticationTests, UnicodeEncoderTest) { + std::string user = "一二三"; + std::string pass = "Hello World!"; + EncodedAuthentication pa{user, pass}; + std::string expected = "%E4%B8%80%E4%BA%8C%E4%B8%89:Hello%20World%21"; + EXPECT_EQ(pa.GetAuthString(), expected); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/error_tests.cpp b/test/error_tests.cpp new file mode 100644 index 0000000..13831ef --- /dev/null +++ b/test/error_tests.cpp @@ -0,0 +1,97 @@ +#include + +#include +#include + +#include +#include + +#include "httpServer.hpp" +#include "httpsServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +TEST(ErrorTests, UnsupportedProtocolFailure) { + Url url{"urk://wat.is.this"}; + Response response = cpr::Get(url); + EXPECT_EQ(0, response.status_code); + EXPECT_EQ(ErrorCode::UNSUPPORTED_PROTOCOL, response.error.code); +} + +TEST(ErrorTests, InvalidURLFailure) { + Url url{"???"}; + Response response = cpr::Get(url); + EXPECT_EQ(0, response.status_code); + EXPECT_EQ(ErrorCode::INVALID_URL_FORMAT, response.error.code); +} + +TEST(ErrorTests, TimeoutFailure) { + Url url{server->GetBaseUrl() + "/timeout.html"}; + Response response = cpr::Get(url, cpr::Timeout{1}); + EXPECT_EQ(0, response.status_code); + EXPECT_EQ(ErrorCode::OPERATION_TIMEDOUT, response.error.code); +} + +TEST(ErrorTests, ChronoTimeoutFailure) { + Url url{server->GetBaseUrl() + "/timeout.html"}; + Response response = cpr::Get(url, cpr::Timeout{std::chrono::milliseconds{1}}); + EXPECT_EQ(0, response.status_code); + EXPECT_EQ(ErrorCode::OPERATION_TIMEDOUT, response.error.code); +} + +TEST(ErrorTests, ConnectTimeoutFailure) { + Url url{"http://localhost:67"}; + Response response = cpr::Get(url, cpr::ConnectTimeout{1}); + EXPECT_EQ(0, response.status_code); + // Sometimes a CONNECTION_FAILURE happens before the OPERATION_TIMEDOUT: + EXPECT_TRUE(response.error.code == ErrorCode::OPERATION_TIMEDOUT || response.error.code == ErrorCode::CONNECTION_FAILURE); +} + +TEST(ErrorTests, ChronoConnectTimeoutFailure) { + Url url{"http://localhost:67"}; + Response response = cpr::Get(url, cpr::ConnectTimeout{std::chrono::milliseconds{1}}); + EXPECT_EQ(0, response.status_code); + // Sometimes a CONNECTION_FAILURE happens before the OPERATION_TIMEDOUT: + EXPECT_TRUE(response.error.code == ErrorCode::OPERATION_TIMEDOUT || response.error.code == ErrorCode::CONNECTION_FAILURE); +} + +TEST(ErrorTests, LowSpeedTimeFailure) { + Url url{server->GetBaseUrl() + "/low_speed.html"}; + Response response = cpr::Get(url, cpr::LowSpeed{1000, 1}); + // Do not check for the HTTP status code, since libcurl always provides the status code of the header if it was received + EXPECT_EQ(ErrorCode::OPERATION_TIMEDOUT, response.error.code); +} + +TEST(ErrorTests, LowSpeedBytesFailure) { + Url url{server->GetBaseUrl() + "/low_speed_bytes.html"}; + Response response = cpr::Get(url, cpr::LowSpeed{1000, 1}); + // Do not check for the HTTP status code, since libcurl always provides the status code of the header if it was received + EXPECT_EQ(ErrorCode::OPERATION_TIMEDOUT, response.error.code); +} + +TEST(ErrorTests, ProxyFailure) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Response response = cpr::Get(url, cpr::Proxies{{"http", "http://bad_host/"}}); + EXPECT_EQ(url, response.url); + EXPECT_EQ(0, response.status_code); + EXPECT_EQ(ErrorCode::PROXY_RESOLUTION_FAILURE, response.error.code); +} + +TEST(ErrorTests, BoolFalseTest) { + Error error; + EXPECT_FALSE(error); +} + +TEST(ErrorTests, BoolTrueTest) { + Error error; + error.code = ErrorCode::UNSUPPORTED_PROTOCOL; + EXPECT_TRUE(error); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/test/get_tests.cpp b/test/get_tests.cpp new file mode 100644 index 0000000..d93bd17 --- /dev/null +++ b/test/get_tests.cpp @@ -0,0 +1,1305 @@ +#include + +#include +#include + +#include "cpr/cpr.h" +#include "cpr/cprtypes.h" +#include "cpr/redirect.h" +#include "cpr/session.h" +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +TEST(BasicTests, HelloWorldTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Response response = cpr::Get(url); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicTests, HelloWorldStringViewUrlTest) { + Url url{static_cast(server->GetBaseUrl() + "/hello.html")}; + Response response = cpr::Get(url); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicTests, HelloWorldNoInterfaceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Interface iface{""}; // Do not specify any specific interface + Response response = cpr::Get(url, iface); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicTests, HelloWorldNoInterfaceStringViewTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Interface iface{std::string_view{}}; // Do not specify any specific interface + Response response = cpr::Get(url, iface); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicTests, TimeoutTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Response response = cpr::Get(url, Timeout{0L}); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicTests, BasicJsonTest) { + Url url{server->GetBaseUrl() + "/basic.json"}; + Response response = cpr::Get(url); + std::string expected_text{ + "[\n" + " {\n" + " \"first_key\": \"first_value\",\n" + " \"second_key\": \"second_value\"\n" + " }\n" + "]"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicTests, ResourceNotFoundTest) { + Url url{server->GetBaseUrl() + "/error.html"}; + Response response = cpr::Get(url); + std::string expected_text{"Not Found"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(404, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicTests, BadHostTest) { + Url url{"http://bad_host/"}; + Response response = cpr::Get(url); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(0, response.status_code); + EXPECT_EQ(ErrorCode::HOST_RESOLUTION_FAILURE, response.error.code); +} + +TEST(CookiesTests, BasicCookiesTest) { + Url url{server->GetBaseUrl() + "/basic_cookies.html"}; + Response response = cpr::Get(url); + cpr::Cookies res_cookies{response.cookies}; + std::string expected_text{"Basic Cookies"}; + cpr::Cookies expectedCookies{ + {"SID", "31d4d96e407aad42", "127.0.0.1", false, "/", true, std::chrono::system_clock::from_time_t(3905119080)}, + {"lang", "en-US", "127.0.0.1", false, "/", true, std::chrono::system_clock::from_time_t(3905119080)}, + }; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + for (auto cookie = res_cookies.begin(), expectedCookie = expectedCookies.begin(); cookie != res_cookies.end() && expectedCookie != expectedCookies.end(); cookie++, expectedCookie++) { + EXPECT_EQ(expectedCookie->GetName(), cookie->GetName()); + EXPECT_EQ(expectedCookie->GetValue(), cookie->GetValue()); + EXPECT_EQ(expectedCookie->GetDomain(), cookie->GetDomain()); + EXPECT_EQ(expectedCookie->IsIncludingSubdomains(), cookie->IsIncludingSubdomains()); + EXPECT_EQ(expectedCookie->GetPath(), cookie->GetPath()); + EXPECT_EQ(expectedCookie->IsHttpsOnly(), cookie->IsHttpsOnly()); + EXPECT_EQ(expectedCookie->GetExpires(), cookie->GetExpires()); + } +} + +TEST(CookiesTests, EmptyCookieTest) { + Url url{server->GetBaseUrl() + "/empty_cookies.html"}; + Response response = cpr::Get(url); + cpr::Cookies res_cookies{response.cookies}; + std::string expected_text{"Empty Cookies"}; + cpr::Cookies expectedCookies{ + {"SID", "", "127.0.0.1", false, "/", true, std::chrono::system_clock::from_time_t(3905119080)}, + {"lang", "", "127.0.0.1", false, "/", true, std::chrono::system_clock::from_time_t(3905119080)}, + }; + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + EXPECT_EQ(expected_text, response.text); + for (auto cookie = res_cookies.begin(), expectedCookie = expectedCookies.begin(); cookie != res_cookies.end() && expectedCookie != expectedCookies.end(); cookie++, expectedCookie++) { + EXPECT_EQ(expectedCookie->GetName(), cookie->GetName()); + EXPECT_EQ(expectedCookie->GetValue(), cookie->GetValue()); + EXPECT_EQ(expectedCookie->GetDomain(), cookie->GetDomain()); + EXPECT_EQ(expectedCookie->IsIncludingSubdomains(), cookie->IsIncludingSubdomains()); + EXPECT_EQ(expectedCookie->GetPath(), cookie->GetPath()); + EXPECT_EQ(expectedCookie->IsHttpsOnly(), cookie->IsHttpsOnly()); + EXPECT_EQ(expectedCookie->GetExpires(), cookie->GetExpires()); + } +} + +TEST(CookiesTests, ClientSetCookiesTest) { + Url url{server->GetBaseUrl() + "/cookies_reflect.html"}; + Cookies cookies{ + {"SID", "31d4d96e407aad42", "127.0.0.1", false, "/", true, std::chrono::system_clock::from_time_t(3905119080)}, + {"lang", "en-US", "127.0.0.1", false, "/", true, std::chrono::system_clock::from_time_t(3905119080)}, + }; + Response response = cpr::Get(url, cookies); + std::string expected_text{"SID=31d4d96e407aad42; lang=en-US;"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterTests, SingleParameterTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Parameters parameters{{"key", "value"}}; + Response response = cpr::Get(url, parameters); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?key=value"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterTests, SingleParameterOnlyKeyTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Parameters parameters{{"key", ""}}; + Response response = cpr::Get(url, parameters); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?key"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); +} + +TEST(ParameterTests, MultipleParametersTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Response response = cpr::Get(url, Parameters{{"key", "value"}, {"hello", "world"}, {"test", "case"}}); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?key=value&hello=world&test=case"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterTests, MultipleDynamicParametersTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Parameters parameters{{"key", "value"}}; + parameters.Add({"hello", "world"}); + parameters.Add({"test", "case"}); + Response response = cpr::Get(url, parameters); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?key=value&hello=world&test=case"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationTests, BasicAuthenticationSuccessTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationTests, BasicBearerSuccessTest) { + Url url{server->GetBaseUrl() + "/bearer_token.html"}; +#if CPR_LIBCURL_VERSION_NUM >= 0x073D00 + Response response = cpr::Get(url, Bearer{"the_token"}); +#else + Response response = cpr::Get(url, Header{{"Authorization", "Bearer the_token"}}); +#endif + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationTests, BasicDigestSuccessTest) { + Url url{server->GetBaseUrl() + "/digest_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::DIGEST}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAthenticationParameterTests, BasicAuthenticationSuccessSingleParameterTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Parameters{{"hello", "world"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?hello=world"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterTests, BasicAuthenticationSuccessMultipleParametersTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Parameters{{"key", "value"}, {"hello", "world"}, {"test", "case"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?key=value&hello=world&test=case"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterTests, BasicAuthenticationSuccessSingleParameterReverseTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{{"hello", "world"}}, Authentication{"user", "password", AuthMode::BASIC}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?hello=world"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterTests, BasicAuthenticationSuccessMultipleParametersReverseTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{{"key", "value"}, {"hello", "world"}, {"test", "case"}}, Authentication{"user", "password", AuthMode::BASIC}); + + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?key=value&hello=world&test=case"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationHeaderTests, BasicAuthenticationSuccessSingleHeaderTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Header{{"hello", "world"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationHeaderTests, BasicAuthenticationSuccessMultipleHeadersTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Header{{"key", "value"}, {"hello", "world"}, {"test", "case"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(std::string{"value"}, response.header["key"]); + EXPECT_EQ(std::string{"case"}, response.header["test"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationHeaderTests, BasicAuthenticationSuccessSingleHeaderReverseTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}}, Authentication{"user", "password", AuthMode::BASIC}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationHeaderTests, BasicAuthenticationSuccessMultipleHeadersReverseTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{{"key", "value"}, {"hello", "world"}, {"test", "case"}}, Authentication{"user", "password", AuthMode::BASIC}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(std::string{"value"}, response.header["key"]); + EXPECT_EQ(std::string{"case"}, response.header["test"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationTests, BasicAuthenticationNullFailureTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationTests, BasicAuthenticationFailureTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "bad_password", AuthMode::BASIC}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterTests, BasicAuthenticationFailureSingleParameterTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "bad_password", AuthMode::BASIC}, Parameters{{"hello", "world"}}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?hello=world"}, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterTests, BasicAuthenticationFailureMultipleParametersTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "bad_password", AuthMode::BASIC}, Parameters{{"key", "value"}, {"hello", "world"}, {"test", "case"}}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?key=value&hello=world&test=case"}, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeaderTests, HeaderJsonTest) { + Url url{server->GetBaseUrl() + "/basic.json"}; + Response response = cpr::Get(url, Header{{"content-type", "application/json"}}); + std::string expected_text{ + "[\n" + " {\n" + " \"first_key\": \"first_value\",\n" + " \"second_key\": \"second_value\"\n" + " }\n" + "]"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeaderTests, HeaderReflectNoneTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeaderTests, HeaderReflectUpdateHeaderAddSessionTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetHeader(Header{{"Header1", "Value1"}}); + session.SetUrl(url); + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"Value1"}, response.header["Header1"]); + EXPECT_EQ(std::string{}, response.header["Header2"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + + session.UpdateHeader(Header{{"Header2", "Value2"}}); + response = session.Get(); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"Value1"}, response.header["Header1"]); + EXPECT_EQ(std::string{"Value2"}, response.header["Header2"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +/** + * Test case for #532 + * https://github.com/whoshuu/cpr/issues/532 + **/ +TEST(HeaderTests, SessionHeaderReflectTest) { + std::unique_ptr session(new cpr::Session()); + session->SetUrl({server->GetBaseUrl() + "/header_reflect.html"}); + session->SetBody("Some Body to post"); + session->SetHeader({{"Content-Type", "application/json"}}); + cpr::Response response = session->Post(); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + EXPECT_EQ(std::string{"Header reflect POST"}, response.text); + EXPECT_EQ(std::string{"application/json"}, response.header["Content-Type"]); +} + +TEST(HeaderTests, HeaderReflectUpdateHeaderUpdateSessionTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetHeader(Header{{"Header1", "Value1"}}); + session.SetUrl(url); + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"Value1"}, response.header["Header1"]); + EXPECT_EQ(std::string{}, response.header["Header2"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + + session.UpdateHeader(Header{{"Header1", "Value2"}}); + response = session.Get(); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"Value2"}, response.header["Header1"]); + EXPECT_EQ(std::string{}, response.header["Header2"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeaderTests, HeaderReflectEmptyTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Header{}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeaderTests, HeaderReflectSingleTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeaderTests, HeaderReflectMultipleTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}, {"key", "value"}, {"test", "case"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(std::string{"value"}, response.header["key"]); + EXPECT_EQ(std::string{"case"}, response.header["test"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeaderTests, HeaderReflectCaseInsensitiveTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Header{{"HeLlO", "wOrLd"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"wOrLd"}, response.header["hello"]); + EXPECT_EQ(std::string{"wOrLd"}, response.header["HELLO"]); + EXPECT_EQ(std::string{"wOrLd"}, response.header["hElLo"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeaderTests, SetEmptyHeaderTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Header{{"hello", ""}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterHeaderTests, HeaderReflectNoneParametersTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}, {"three", "four"}, {"five", "six"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two&three=four&five=six"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterHeaderTests, HeaderReflectEmptyParametersTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Header{}, Parameters{{"one", "two"}, {"three", "four"}, {"five", "six"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two&three=four&five=six"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterHeaderTests, HeaderReflectSingleParametersTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}}, Parameters{{"one", "two"}, {"three", "four"}, {"five", "six"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two&three=four&five=six"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterHeaderTests, HeaderReflectMultipleParametersTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}, {"key", "value"}, {"test", "case"}}, Parameters{{"one", "two"}, {"three", "four"}, {"five", "six"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two&three=four&five=six"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(std::string{"value"}, response.header["key"]); + EXPECT_EQ(std::string{"case"}, response.header["test"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterHeaderTests, HeaderReflectCaseInsensitiveParametersTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Header{{"HeLlO", "wOrLd"}}, Parameters{{"one", "two"}, {"three", "four"}, {"five", "six"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two&three=four&five=six"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"wOrLd"}, response.header["hello"]); + EXPECT_EQ(std::string{"wOrLd"}, response.header["HELLO"]); + EXPECT_EQ(std::string{"wOrLd"}, response.header["hElLo"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterHeaderTests, HeaderReflectEmptyParametersReverseTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}, {"three", "four"}, {"five", "six"}}, Header{}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two&three=four&five=six"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterHeaderTests, HeaderReflectSingleParametersReverseTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}, {"three", "four"}, {"five", "six"}}, Header{{"hello", "world"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two&three=four&five=six"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterHeaderTests, HeaderReflectMultipleParametersReverseTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}, {"three", "four"}, {"five", "six"}}, Header{{"hello", "world"}, {"key", "value"}, {"test", "case"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two&three=four&five=six"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(std::string{"value"}, response.header["key"]); + EXPECT_EQ(std::string{"case"}, response.header["test"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterHeaderTests, HeaderReflectCaseInsensitiveParametersReverseTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}, {"three", "four"}, {"five", "six"}}, Header{{"HeLlO", "wOrLd"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two&three=four&five=six"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"wOrLd"}, response.header["hello"]); + EXPECT_EQ(std::string{"wOrLd"}, response.header["HELLO"]); + EXPECT_EQ(std::string{"wOrLd"}, response.header["hElLo"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderAATest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Parameters{}, Header{}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderABTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "bad_password", AuthMode::BASIC}, Parameters{}, Header{}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderACTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Parameters{{"one", "two"}}, Header{}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderADTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "bad_password", AuthMode::BASIC}, Parameters{{"one", "two"}}, Header{}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderAETest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Parameters{}, Header{{"hello", "world"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderAFTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "bad_password", AuthMode::BASIC}, Parameters{}, Header{{"hello", "world"}}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderAGTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Parameters{{"one", "two"}}, Header{{"hello", "world"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderAHTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "bad_password", AuthMode::BASIC}, Parameters{{"one", "two"}}, Header{{"hello", "world"}}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderBATest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{}, Header{}, Authentication{"user", "password", AuthMode::BASIC}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderBBTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{}, Header{}, Authentication{"user", "bad_password", AuthMode::BASIC}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderBCTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}}, Header{}, Authentication{"user", "password", AuthMode::BASIC}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderBDTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}}, Header{}, Authentication{"user", "bad_password", AuthMode::BASIC}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderBETest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{}, Header{{"hello", "world"}}, Authentication{"user", "password", AuthMode::BASIC}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderBFTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{}, Header{{"hello", "world"}}, Authentication{"user", "bad_password", AuthMode::BASIC}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderBGTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}}, Header{{"hello", "world"}}, Authentication{"user", "password", AuthMode::BASIC}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderBHTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}}, Header{{"hello", "world"}}, Authentication{"user", "bad_password", AuthMode::BASIC}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderCATest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{}, Authentication{"user", "password", AuthMode::BASIC}, Parameters{}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderCBTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{}, Authentication{"user", "bad_password", AuthMode::BASIC}, Parameters{}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderCCTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{}, Authentication{"user", "password", AuthMode::BASIC}, Parameters{{"one", "two"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderCDTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{}, Authentication{"user", "bad_password", AuthMode::BASIC}, Parameters{{"one", "two"}}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderCETest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}}, Authentication{"user", "password", AuthMode::BASIC}, Parameters{}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderCFTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}}, Authentication{"user", "bad_password", AuthMode::BASIC}, Parameters{}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderCGTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}}, Authentication{"user", "password", AuthMode::BASIC}, Parameters{{"one", "two"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderCHTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}}, Authentication{"user", "bad_password", AuthMode::BASIC}, Parameters{{"one", "two"}}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderDATest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Header{}, Parameters{}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderDBTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "bad_password", AuthMode::BASIC}, Header{}, Parameters{}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderDCTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Header{}, Parameters{{"one", "two"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderDDTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "bad_password", AuthMode::BASIC}, Header{}, Parameters{{"one", "two"}}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderDETest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Header{{"hello", "world"}}, Parameters{}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderDFTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "bad_password", AuthMode::BASIC}, Header{{"hello", "world"}}, Parameters{}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderDGTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Header{{"hello", "world"}}, Parameters{{"one", "two"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderDHTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "bad_password", AuthMode::BASIC}, Header{{"hello", "world"}}, Parameters{{"one", "two"}}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderEATest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{}, Parameters{}, Authentication{"user", "password", AuthMode::BASIC}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderEBTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{}, Parameters{}, Authentication{"user", "bad_password", AuthMode::BASIC}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderECTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{}, Parameters{{"one", "two"}}, Authentication{"user", "password", AuthMode::BASIC}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderEDTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{}, Parameters{{"one", "two"}}, Authentication{"user", "bad_password", AuthMode::BASIC}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderEETest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}}, Parameters{}, Authentication{"user", "password", AuthMode::BASIC}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderEFTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}}, Parameters{}, Authentication{"user", "bad_password", AuthMode::BASIC}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderEGTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}}, Parameters{{"one", "two"}}, Authentication{"user", "password", AuthMode::BASIC}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderEHTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}}, Parameters{{"one", "two"}}, Authentication{"user", "bad_password", AuthMode::BASIC}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderFATest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{}, Authentication{"user", "password", AuthMode::BASIC}, Header{}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderFBTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{}, Authentication{"user", "bad_password", AuthMode::BASIC}, Header{}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderFCTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}}, Authentication{"user", "password", AuthMode::BASIC}, Header{}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderFDTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}}, Authentication{"user", "bad_password", AuthMode::BASIC}, Header{}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderFETest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{}, Authentication{"user", "password", AuthMode::BASIC}, Header{{"hello", "world"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderFFTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{}, Authentication{"user", "bad_password", AuthMode::BASIC}, Header{{"hello", "world"}}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderFGTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}}, Authentication{"user", "password", AuthMode::BASIC}, Header{{"hello", "world"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderFHTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}}, Authentication{"user", "bad_password", AuthMode::BASIC}, Header{{"hello", "world"}}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(GetRedirectTests, RedirectTest) { + Url url{server->GetBaseUrl() + "/temporary_redirect.html"}; + Response response = cpr::Get(url, Redirect(false)); + std::string expected_text{"Moved Temporarily"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{}, response.header["content-type"]); + EXPECT_EQ(302, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(GetRedirectTests, ZeroMaxRedirectsSuccessTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Response response = cpr::Get(url, Redirect(0L)); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(GetRedirectTests, ZeroMaxRedirectsFailureTest) { + Url url{server->GetBaseUrl() + "/permanent_redirect.html"}; + Response response = cpr::Get(url, Redirect(0L)); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{}, response.header["content-type"]); + EXPECT_EQ(301, response.status_code); + EXPECT_EQ(ErrorCode::TOO_MANY_REDIRECTS, response.error.code); +} + +TEST(GetRedirectTests, BasicAuthenticationRedirectSuccessTest) { + Url url{server->GetBaseUrl() + "/temporary_redirect.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Header{{"RedirectLocation", "basic_auth.html"}}, Redirect(true, true)); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + std::string resultUrl = "http://user:password@127.0.0.1:" + std::to_string(server->GetPort()) + "/basic_auth.html"; + EXPECT_EQ(response.url, resultUrl); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicTests, RequestBodyTest) { + Url url{server->GetBaseUrl() + "/body_get.html"}; + Body body{"message=abc123"}; + Response response = cpr::Get(url, body); + std::string expected_text{"abc123"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicTests, RequestBodyStringViewTest) { + Url url{server->GetBaseUrl() + "/body_get.html"}; + Body body{static_cast("message=abc123")}; + Response response = cpr::Get(url, body); + std::string expected_text{"abc123"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(LimitRateTests, HelloWorldTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Response response = cpr::Get(url, LimitRate(1024, 1024)); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/test/head_tests.cpp b/test/head_tests.cpp new file mode 100644 index 0000000..43c1831 --- /dev/null +++ b/test/head_tests.cpp @@ -0,0 +1,226 @@ +#include +#include + +#include + +#include + +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +TEST(HeadTests, BasicHeadTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Response response = cpr::Head(url); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, ComplexHeadTest) { + Url url{server->GetBaseUrl() + "/basic.json"}; + Response response = cpr::Head(url); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, ResourceNotFoundHeadTest) { + Url url{server->GetBaseUrl() + "/error.html"}; + Response response = cpr::Head(url); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(404, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, BadHostHeadTest) { + Url url{"http://bad_host/"}; + Response response = cpr::Head(url); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(0, response.status_code); + EXPECT_EQ(ErrorCode::HOST_RESOLUTION_FAILURE, response.error.code); +} + +TEST(HeadTests, CookieHeadTest) { + Url url{server->GetBaseUrl() + "/basic_cookies.html"}; + Response response = cpr::Head(url); + cpr::Cookies expectedCookies{ + {"SID", "31d4d96e407aad42", "127.0.0.1", false, "/", true, std::chrono::system_clock::from_time_t(3905119080)}, + {"lang", "en-US", "127.0.0.1", false, "/", true, std::chrono::system_clock::from_time_t(3905119080)}, + }; + cpr::Cookies res_cookies{response.cookies}; + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + for (auto cookie = res_cookies.begin(), expectedCookie = expectedCookies.begin(); cookie != res_cookies.end() && expectedCookie != expectedCookies.end(); cookie++, expectedCookie++) { + EXPECT_EQ(expectedCookie->GetName(), cookie->GetName()); + EXPECT_EQ(expectedCookie->GetValue(), cookie->GetValue()); + EXPECT_EQ(expectedCookie->GetDomain(), cookie->GetDomain()); + EXPECT_EQ(expectedCookie->IsIncludingSubdomains(), cookie->IsIncludingSubdomains()); + EXPECT_EQ(expectedCookie->GetPath(), cookie->GetPath()); + EXPECT_EQ(expectedCookie->IsHttpsOnly(), cookie->IsHttpsOnly()); + EXPECT_EQ(expectedCookie->GetExpires(), cookie->GetExpires()); + } +} + +TEST(HeadTests, ParameterHeadTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Parameters parameters{{"key", "value"}}; + Response response = cpr::Head(url, parameters); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(Url{url + "?key=value"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, AuthenticationSuccessHeadTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Head(url, Authentication{"user", "password", AuthMode::BASIC}); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, AuthenticationNullFailureHeadTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Head(url); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, AuthenticationFailureHeadTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Head(url, Authentication{"user", "bad_password", AuthMode::BASIC}); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, BearerSuccessHeadTest) { + Url url{server->GetBaseUrl() + "/bearer_token.html"}; +#if CPR_LIBCURL_VERSION_NUM >= 0x073D00 + Response response = cpr::Get(url, Bearer{"the_token"}); +#else + Response response = cpr::Get(url, Header{{"Authorization", "Bearer the_token"}}); +#endif + EXPECT_EQ(std::string{"Header reflect GET"}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, DigestSuccessHeadTest) { + Url url{server->GetBaseUrl() + "/digest_auth.html"}; + Response response = cpr::Head(url, Authentication{"user", "password", AuthMode::DIGEST}); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, HeaderReflectNoneHeadTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Head(url); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, HeaderReflectEmptyHeadTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Head(url, Header{}); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, HeaderReflectHeadTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Head(url, Header{{"hello", "world"}}); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, SetEmptyHeaderHeadTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Head(url, Header{{"hello", ""}}); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, RedirectHeadTest) { + Url url{server->GetBaseUrl() + "/temporary_redirect.html"}; + Response response = cpr::Head(url, Redirect(false)); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{}, response.header["content-type"]); + EXPECT_EQ(302, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, ZeroMaxRedirectsHeadTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Response response = cpr::Head(url, Redirect(0L)); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, BasicHeadAsyncTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + responses.emplace_back(cpr::HeadAsync(url)); + } + for (cpr::AsyncResponse& future_response : responses) { + cpr::Response response = future_response.get(); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/test/httpServer.cpp b/test/httpServer.cpp new file mode 100644 index 0000000..9514de5 --- /dev/null +++ b/test/httpServer.cpp @@ -0,0 +1,927 @@ +#include "httpServer.hpp" +#include +#include +#include +#include + +namespace cpr { + +std::string HttpServer::GetBaseUrl() { + return "http://127.0.0.1:" + std::to_string(GetPort()); +} + +uint16_t HttpServer::GetPort() { + // Unassigned port number in the ephemeral range + return 61936; +} + +mg_connection* HttpServer::initServer(mg_mgr* mgr, mg_event_handler_t event_handler) { + // Based on: https://mongoose.ws/tutorials/http-server/ + mg_mgr_init(mgr); + std::string port = std::to_string(GetPort()); + mg_connection* c = mg_http_listen(mgr, GetBaseUrl().c_str(), event_handler, this); + if (!c) { + throw std::system_error(errno, std::system_category(), "Failed to listen at port " + port); + } + return c; +} + +void HttpServer::acceptConnection(mg_connection* /* conn */) {} + +void HttpServer::OnRequestHello(mg_connection* conn, mg_http_message* msg) { + if (std::string{msg->method.ptr, msg->method.len} == std::string{"OPTIONS"}) { + OnRequestOptions(conn, msg); + } else { + std::string response{"Hello world!"}; + std::string headers = "Content-Type: text/html\r\n"; + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); + } +} + +void HttpServer::OnRequestRoot(mg_connection* conn, mg_http_message* msg) { + if (std::string{msg->method.ptr, msg->method.len} == std::string{"OPTIONS"}) { + OnRequestOptions(conn, msg); + } else { + std::string errorMessage{"Method Not Allowed"}; + SendError(conn, 405, errorMessage); + } +} + +void HttpServer::OnRequestNotFound(mg_connection* conn, mg_http_message* msg) { + if (std::string{msg->method.ptr, msg->method.len} == std::string{"OPTIONS"}) { + OnRequestOptions(conn, msg); + } else { + std::string errorMessage{"Not Found"}; + SendError(conn, 404, errorMessage); + } +} + +void HttpServer::OnRequestOptions(mg_connection* conn, mg_http_message* /*msg*/) { + std::string headers = + "Content-Type: text/plain\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Access-Control-Allow-Credentials: true\r\n" + "Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS\r\n" + "Access-Control-Max-Age: 3600\r\n"; + + std::string response; + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestTimeout(mg_connection* conn, mg_http_message* msg) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + OnRequestHello(conn, msg); +} + +void HttpServer::OnRequestLongTimeout(mg_connection* conn, mg_http_message* msg) { + std::this_thread::sleep_for(std::chrono::seconds(2)); + OnRequestHello(conn, msg); +} + +// Send the header, then send "Hello world!" every 100ms +// For this, we use a mongoose timer +void HttpServer::OnRequestLowSpeedTimeout(mg_connection* conn, mg_http_message* /* msg */, TimerArg* timer_arg) { + std::string response{"Hello world!"}; + mg_printf(conn, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: %d\r\n\r\n", response.length() * 20); + timer_arg->connection_id = conn->id; + mg_timer_init( + &timer_arg->mgr->timers, &timer_arg->timer, 100, MG_TIMER_REPEAT, + // The following lambda function gets executed each time the timer is called. + // It sends "Hello world!" to the client each 100ms at most 20 times. + [](void* arg) { + TimerArg* timer_arg = static_cast(arg); + if (timer_arg->counter < 20 && IsConnectionActive(timer_arg->mgr, timer_arg->connection) && timer_arg->connection->id == timer_arg->connection_id) { + std::string response{"Hello world!"}; + mg_send(timer_arg->connection, response.c_str(), response.length()); + ++timer_arg->counter; + } else { + timer_arg->counter = 20; // Make sure that this timer is never called again + } + }, + timer_arg); +} + +// Before and after calling an endpoint that calls this method, the test needs to wait until all previous connections are closed +// The nested call to mg_mgr_poll can lead to problems otherwise +void HttpServer::OnRequestLowSpeed(mg_connection* conn, mg_http_message* /*msg*/, mg_mgr* mgr) { + std::string response{"Hello world!"}; + mg_printf(conn, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: %d\r\n\r\n", response.length()); + mg_timer_add( + mgr, 2000, MG_TIMER_ONCE, + [](void* connection) { + std::string response{"Hello world!"}; + mg_send(static_cast(connection), response.c_str(), response.length()); + }, + conn); +} + +// Before and after calling an endpoint that calls this method, the test needs to wait until all previous connections are closed +// The nested call to mg_mgr_poll can lead to problems otherwise +void HttpServer::OnRequestLowSpeedBytes(mg_connection* conn, mg_http_message* /*msg*/, TimerArg* timer_arg) { + std::string response{'a'}; + mg_printf(conn, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: %d\r\n\r\n", response.length() * 20); + + mg_timer_init( + &timer_arg->mgr->timers, &timer_arg->timer, 100, MG_TIMER_REPEAT, + // The following lambda function gets executed each time the timer is called. + // It first waits for 2 seconds, then sends "a" to the client each 100ms at most 20 times. + [](void* arg) { + TimerArg* timer_arg = static_cast(arg); + if (timer_arg->counter == 0) { + std::this_thread::sleep_for(std::chrono::seconds(2)); + } + if (timer_arg->counter < 20 && IsConnectionActive(timer_arg->mgr, timer_arg->connection) && timer_arg->connection->id == timer_arg->connection_id) { + std::string response{'a'}; + mg_send(timer_arg->connection, response.c_str(), response.length()); + ++timer_arg->counter; + } else { + timer_arg->counter = 20; // Make sure that this timer is never called again + } + }, + timer_arg); +} + +void HttpServer::OnRequestBasicCookies(mg_connection* conn, mg_http_message* /*msg*/) { + time_t expires_time = 3905119080; // Expires=Wed, 30 Sep 2093 03:18:00 GMT + std::array expires_string; + std::strftime(expires_string.data(), expires_string.size(), "%a, %d %b %Y %H:%M:%S GMT", gmtime(&expires_time)); + + std::string cookie1{"SID=31d4d96e407aad42; Expires=" + std::string(expires_string.data()) + "; Secure"}; + std::string cookie2{"lang=en-US; Expires=" + std::string(expires_string.data()) + "; Secure"}; + std::string headers = + "Content-Type: text/html\r\n" + "Set-Cookie: " + + cookie1 + + "\r\n" + "Set-Cookie: " + + cookie2 + "\r\n"; + std::string response{"Basic Cookies"}; + + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestEmptyCookies(mg_connection* conn, mg_http_message* /*msg*/) { + time_t expires_time = 3905119080; // Expires=Wed, 30 Sep 2093 03:18:00 GMT + std::array expires_string; + std::strftime(expires_string.data(), sizeof(expires_string), "%a, %d %b %Y %H:%M:%S GMT", gmtime(&expires_time)); + + std::string cookie1{"SID=; Expires=" + std::string(expires_string.data()) + "; Secure"}; + std::string cookie2{"lang=; Expires=" + std::string(expires_string.data()) + "; Secure"}; + std::string headers = + "Content-Type: text/html\r\n" + "Set-Cookie: " + + cookie1 + + "\r\n" + "Set-Cookie: " + + cookie2 + "\r\n"; + std::string response{"Empty Cookies"}; + + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestCookiesReflect(mg_connection* conn, mg_http_message* msg) { + mg_str* request_cookies; + if ((request_cookies = mg_http_get_header(msg, "Cookie")) == nullptr) { + std::string errorMessage{"Cookie not found"}; + SendError(conn, 400, errorMessage); + return; + } + std::string cookie_str{request_cookies->ptr, request_cookies->len}; + std::string headers = "Content-Type: text/html\r\n"; + mg_http_reply(conn, 200, headers.c_str(), cookie_str.c_str()); +} + +void HttpServer::OnRequestRedirectionWithChangingCookies(mg_connection* conn, mg_http_message* msg) { + time_t expires_time = 3905119080; // Expires=Wed, 30 Sep 2093 03:18:00 GMT + std::array expires_string; + std::strftime(expires_string.data(), sizeof(expires_string), "%a, %d %b %Y %H:%M:%S GMT", gmtime(&expires_time)); + + mg_str* request_cookies; + std::string cookie_str; + if ((request_cookies = mg_http_get_header(msg, "Cookie")) != nullptr) { + cookie_str = std::string{request_cookies->ptr, request_cookies->len}; + } + + if (cookie_str.find("SID=31d4d96e407aad42") == std::string::npos) { + std::string cookie1{"SID=31d4d96e407aad42; Expires=" + std::string(expires_string.data()) + "; Secure"}; + std::string cookie2{"lang=en-US; Expires=" + std::string(expires_string.data()) + "; Secure"}; + std::string headers = + "Content-Type: text/html\r\n" + "Location: http://127.0.0.1:61936/redirection_with_changing_cookies.html\r\n" + "Set-Cookie: " + + cookie1 + + "\r\n" + "Set-Cookie: " + + cookie2 + "\r\n"; + + mg_http_reply(conn, 302, headers.c_str(), ""); + } else { + cookie_str = "Received cookies are: " + cookie_str; + std::string headers = "Content-Type: text/html\r\n"; + mg_http_reply(conn, 200, headers.c_str(), cookie_str.c_str()); + } +} + +void HttpServer::OnRequestBasicAuth(mg_connection* conn, mg_http_message* msg) { + mg_str* requested_auth; + std::string auth{"Basic"}; + if ((requested_auth = mg_http_get_header(msg, "Authorization")) == nullptr || mg_ncasecmp(requested_auth->ptr, auth.c_str(), auth.length()) != 0) { + std::string errorMessage{"Unauthorized"}; + SendError(conn, 401, errorMessage); + return; + } + std::string auth_string{requested_auth->ptr, requested_auth->len}; + size_t basic_token = auth_string.find(' ') + 1; + auth_string = auth_string.substr(basic_token, auth_string.length() - basic_token); + auth_string = Base64Decode(auth_string); + size_t colon = auth_string.find(':'); + std::string username = auth_string.substr(0, colon); + std::string password = auth_string.substr(colon + 1, auth_string.length() - colon - 1); + if (username == "user" && password == "password") { + OnRequestHeaderReflect(conn, msg); + } else { + std::string errorMessage{"Unauthorized"}; + SendError(conn, 401, errorMessage); + } +} + +void HttpServer::OnRequestBearerAuth(mg_connection* conn, mg_http_message* msg) { + mg_str* requested_auth; + std::string auth{"Bearer"}; + if ((requested_auth = mg_http_get_header(msg, "Authorization")) == nullptr || mg_ncasecmp(requested_auth->ptr, auth.c_str(), auth.length()) != 0) { + std::string errorMessage{"Unauthorized"}; + SendError(conn, 401, errorMessage); + return; + } + std::string auth_string{requested_auth->ptr, requested_auth->len}; + size_t basic_token = auth_string.find(' ') + 1; + auth_string = auth_string.substr(basic_token, auth_string.length() - basic_token); + if (auth_string == "the_token") { + OnRequestHeaderReflect(conn, msg); + } else { + std::string errorMessage{"Unauthorized"}; + SendError(conn, 401, errorMessage); + } +} + +void HttpServer::OnRequestBasicJson(mg_connection* conn, mg_http_message* /*msg*/) { + std::string response = + "[\n" + " {\n" + " \"first_key\": \"first_value\",\n" + " \"second_key\": \"second_value\"\n" + " }\n" + "]"; + std::string headers = "Content-Type: application/json\r\n"; + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestHeaderReflect(mg_connection* conn, mg_http_message* msg) { + std::string response = "Header reflect " + std::string{msg->method.ptr, msg->method.len}; + std::string headers; + bool hasContentTypeHeader = false; + for (const mg_http_header& header : msg->headers) { + if (!header.name.ptr) { + continue; + } + + std::string name = std::string(header.name.ptr, header.name.len); + if (std::string{"Content-Type"} == name) { + hasContentTypeHeader = true; + } + + if (std::string{"Host"} != name && std::string{"Accept"} != name) { + if (header.value.ptr) { + headers.append(name + ": " + std::string(header.value.ptr, header.value.len) + "\r\n"); + } + } + } + + if (!hasContentTypeHeader) { + headers.append("Content-Type: text/html\r\n"); + } + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestTempRedirect(mg_connection* conn, mg_http_message* msg) { + // Get the requested target location: + std::string location; + for (mg_http_header& header : msg->headers) { + if (!header.name.ptr) { + continue; + } + + std::string name = std::string(header.name.ptr, header.name.len); + if (std::string{"RedirectLocation"} == name) { + location = std::string(header.value.ptr, header.value.len); + break; + } + } + + // Check if the request contains a valid location, else default to 'hello.html': + if (location.empty()) { + location = "hello.html"; + } + std::string headers = "Location: " + location + "\r\n"; + std::string response = "Moved Temporarily"; + mg_http_reply(conn, 302, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestPermRedirect(mg_connection* conn, mg_http_message* msg) { + // Get the requested target location: + std::string location; + for (mg_http_header& header : msg->headers) { + if (!header.name.ptr) { + continue; + } + + std::string name = std::string(header.name.ptr, header.name.len); + if (std::string{"RedirectLocation"} == name) { + location = std::string(header.value.ptr, header.value.len); + break; + } + } + + // Check if the request contains a valid location, else default to 'hello.html': + if (location.empty()) { + location = "hello.html"; + } + std::string headers = "Location: " + location + "\r\n"; + std::string response = "Moved Permanently"; + + mg_http_reply(conn, 301, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestResolvePermRedirect(mg_connection* conn, mg_http_message* msg) { + // Get the requested target location: + std::string location; + for (mg_http_header& header : msg->headers) { + if (!header.name.ptr) { + continue; + } + + std::string name = std::string(header.name.ptr, header.name.len); + if (std::string{"RedirectLocation"} == name) { + location = std::string(header.value.ptr, header.value.len); + break; + } + } + + if (location.empty()) { + std::string errorMessage{"Redirect location missing"}; + SendError(conn, 405, errorMessage); + return; + } + + std::string headers = "Location: " + location + "\r\n"; + std::string response = "Moved Permanently"; + + mg_http_reply(conn, 301, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestTwoRedirects(mg_connection* conn, mg_http_message* /*msg*/) { + std::string response = "Moved Permanently"; + std::string headers = "Location: permanent_redirect.html\r\n"; + mg_http_reply(conn, 301, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestUrlPost(mg_connection* conn, mg_http_message* msg) { + if (std::string{msg->method.ptr, msg->method.len} != std::string{"POST"}) { + std::string errorMessage{"Method Not Allowed"}; + SendError(conn, 405, errorMessage); + return; + } + + std::string headers = "Content-Type: application/json\r\n"; + + char x[100]; + char y[100]; + mg_http_get_var(&(msg->body), "x", x, sizeof(x)); + mg_http_get_var(&(msg->body), "y", y, sizeof(y)); + std::string x_string{x}; + std::string y_string{y}; + std::string response; + if (y_string.empty()) { + response = std::string{ + "{\n" + " \"x\": " + + x_string + + "\n" + "}"}; + } else { + response = std::string{ + "{\n" + " \"x\": " + + x_string + + ",\n" + " \"y\": " + + y_string + + ",\n" + " \"sum\": " + + std::to_string(atoi(x) + atoi(y)) + + "\n" + "}"}; + } + mg_http_reply(conn, 201, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestBodyGet(mg_connection* conn, mg_http_message* msg) { + if (std::string{msg->method.ptr, msg->method.len} != std::string{"GET"}) { + std::string errorMessage{"Method Not Allowed"}; + SendError(conn, 405, errorMessage); + return; + } + std::array message{}; + mg_http_get_var(&(msg->body), "message", message.data(), message.size()); + if (msg->body.len <= 0) { + std::string errorMessage{"No Content"}; + SendError(conn, 405, errorMessage); + return; + } + std::string response = message.data(); + std::string headers = "Content-Type: text/html\r\n"; + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestJsonPost(mg_connection* conn, mg_http_message* msg) { + mg_str* content_type{nullptr}; + if ((content_type = mg_http_get_header(msg, "Content-Type")) == nullptr || std::string{content_type->ptr, content_type->len} != "application/json") { + std::string errorMessage{"Unsupported Media Type"}; + SendError(conn, 415, errorMessage); + return; + } + + std::string headers = "Content-Type: application/json\r\n"; + mg_http_reply(conn, 201, headers.c_str(), msg->body.ptr); +} + +void HttpServer::OnRequestFormPost(mg_connection* conn, mg_http_message* msg) { + size_t pos{0}; + mg_http_part part{}; + + std::string headers = "Content-Type: application/json\r\n"; + std::string response{}; + response += "{\n"; + while ((pos = mg_http_next_multipart(msg->body, pos, &part)) > 0) { + response += " \"" + std::string(part.name.ptr, part.name.len) + "\": \""; + if (!std::string(part.filename.ptr, part.filename.len).empty()) { + response += std::string(part.filename.ptr, part.filename.len) + "="; + } + response += std::string(part.body.ptr, part.body.len) + "\",\n"; + } + response.erase(response.find_last_not_of(",\n") + 1); + response += "\n}"; + + mg_http_reply(conn, 201, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestDelete(mg_connection* conn, mg_http_message* msg) { + bool has_json_header = false; + for (mg_http_header& header : msg->headers) { + if (!header.name.ptr) { + continue; + } + + std::string name = std::string(header.name.ptr, header.name.len); + std::string value = std::string(header.value.ptr, header.value.len); + if (std::string{"Content-Type"} == name && std::string{"application/json"} == value) { + has_json_header = true; + break; + } + } + if (std::string{msg->method.ptr, msg->method.len} == std::string{"DELETE"}) { + std::string headers; + std::string response = "Patch success"; + if (!has_json_header) { + headers = "Content-Type: text/html\r\n"; + response = "Delete success"; + } else { + headers = "Content-Type: application/json\r\n"; + response = std::string{msg->body.ptr, msg->body.len}; + } + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); + } else { + std::string errorMessage{"Method Not Allowed"}; + SendError(conn, 405, errorMessage); + } +} + +void HttpServer::OnRequestDeleteNotAllowed(mg_connection* conn, mg_http_message* msg) { + if (std::string{msg->method.ptr, msg->method.len} == std::string{"DELETE"}) { + std::string errorMessage{"Method Not Allowed"}; + SendError(conn, 405, errorMessage); + } else { + std::string headers = "Content-Type: text/html\r\n"; + std::string response = "Delete success"; + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); + } +} + +void HttpServer::OnRequestPut(mg_connection* conn, mg_http_message* msg) { + if (std::string{msg->method.ptr, msg->method.len} == std::string{"PUT"}) { + char x[100]; + char y[100]; + mg_http_get_var(&(msg->body), "x", x, sizeof(x)); + mg_http_get_var(&(msg->body), "y", y, sizeof(y)); + std::string x_string = std::string{x}; + std::string y_string = std::string{y}; + std::string headers = "Content-Type: application/json\r\n"; + std::string response; + if (y_string.empty()) { + response = std::string{ + "{\n" + " \"x\": " + + x_string + + "\n" + "}"}; + } else { + response = std::string{ + "{\n" + " \"x\": " + + x_string + + ",\n" + " \"y\": " + + y_string + + ",\n" + " \"sum\": " + + std::to_string(atoi(x) + atoi(y)) + + "\n" + "}"}; + } + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); + } else { + std::string errorMessage{"Method Not Allowed"}; + SendError(conn, 405, errorMessage); + } +} + +void HttpServer::OnRequestPostReflect(mg_connection* conn, mg_http_message* msg) { + if (std::string{msg->method.ptr, msg->method.len} != std::string{"POST"}) { + std::string errorMessage{"Method Not Allowed"}; + SendError(conn, 405, errorMessage); + } + + std::string response = std::string{msg->body.ptr, msg->body.len}; + std::string headers; + for (mg_http_header& header : msg->headers) { + if (!header.name.ptr) { + continue; + } + + std::string name{header.name.ptr, header.name.len}; + if (std::string{"Host"} != name && std::string{"Accept"} != name) { + if (header.value.ptr) { + headers.append(name + ": " + std::string(header.value.ptr, header.value.len) + "\r\n"); + } + } + } + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestPutNotAllowed(mg_connection* conn, mg_http_message* msg) { + if (std::string{msg->method.ptr, msg->method.len} == std::string{"PUT"}) { + std::string errorMessage{"Method Not Allowed"}; + SendError(conn, 405, errorMessage); + } else { + std::string headers = "Content-Type: text/html\r\n"; + std::string response = "Delete success"; + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); + } +} + +void HttpServer::OnRequestPatch(mg_connection* conn, mg_http_message* msg) { + if (std::string{msg->method.ptr, msg->method.len} == std::string{"PATCH"}) { + char x[100]; + char y[100]; + mg_http_get_var(&(msg->body), "x", x, sizeof(x)); + mg_http_get_var(&(msg->body), "y", y, sizeof(y)); + std::string x_string = std::string{x}; + std::string y_string = std::string{y}; + std::string headers = "Content-Type: application/json\r\n"; + std::string response; + if (y_string.empty()) { + response = std::string{ + "{\n" + " \"x\": " + + x_string + + "\n" + "}"}; + } else { + response = std::string{ + "{\n" + " \"x\": " + + x_string + + ",\n" + " \"y\": " + + y_string + + ",\n" + " \"sum\": " + + std::to_string(atoi(x) + atoi(y)) + + "\n" + "}"}; + } + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); + } else { + std::string errorMessage{"Method Not Allowed"}; + SendError(conn, 405, errorMessage); + } +} + +void HttpServer::OnRequestPatchNotAllowed(mg_connection* conn, mg_http_message* msg) { + if (std::string{msg->method.ptr, msg->method.len} == std::string{"PATCH"}) { + std::string errorMessage{"Method Not Allowed"}; + SendError(conn, 405, errorMessage); + } else { + std::string headers = "Content-Type: text/html\r\n"; + std::string response = "Delete success"; + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); + } +} + +void HttpServer::OnRequestDownloadGzip(mg_connection* conn, mg_http_message* msg) { + if (std::string{msg->method.ptr, msg->method.len} == std::string{"DOWNLOAD"}) { + std::string errorMessage{"Method Not Allowed"}; + SendError(conn, 405, errorMessage); + } else { + std::string encoding; + std::string range; + std::vector> ranges; + + for (mg_http_header& header : msg->headers) { + if (!header.name.ptr) { + continue; + } + + std::string name = std::string(header.name.ptr, header.name.len); + if (std::string{"Accept-Encoding"} == name) { + encoding = std::string(header.value.ptr, header.value.len); + } else if (std::string{"Range"} == name) { + range = std::string(header.value.ptr, header.value.len); + } + } + if (encoding.find("gzip") == std::string::npos) { + std::string errorMessage{"Invalid encoding: " + encoding}; + SendError(conn, 405, errorMessage); + return; + } + if (!range.empty()) { + std::string::size_type eq_pos = range.find('='); + if (eq_pos == std::string::npos) { + std::string errorMessage{"Invalid range header: " + range}; + SendError(conn, 405, errorMessage); + return; + } + + int64_t current_start_index = eq_pos + 1; + int64_t current_end_index; + std::string::size_type range_len = range.length(); + std::string::size_type com_pos; + std::string::size_type sep_pos; + bool more_ranges_exists; + + do { + com_pos = range.find(',', current_start_index); + if (com_pos < range_len) { + current_end_index = com_pos - 1; + } else { + current_end_index = range_len - 1; + } + + std::pair current_range{0, -1}; + + sep_pos = range.find('-', current_start_index); + if (sep_pos == std::string::npos) { + std::string errorMessage{"Invalid range format " + range.substr(current_start_index, current_end_index)}; + SendError(conn, 405, errorMessage); + return; + } + if (sep_pos == eq_pos + 1) { + std::string errorMessage{"Suffix ranage not supported: " + range.substr(current_start_index, current_end_index)}; + SendError(conn, 405, errorMessage); + return; + } + + current_range.first = std::strtoll(range.substr(current_start_index, sep_pos - 1).c_str(), nullptr, 10); + if (current_range.first == LLONG_MAX || current_range.first == LLONG_MIN) { + std::string errorMessage{"Start range is invalid number: " + range.substr(current_start_index, current_end_index)}; + SendError(conn, 405, errorMessage); + return; + } + + std::string er_str = range.substr(sep_pos + 1, current_end_index); + if (!er_str.empty()) { + current_range.second = std::strtoll(er_str.c_str(), nullptr, 10); + if (current_range.second == 0 || current_range.second == LLONG_MAX || current_range.second == LLONG_MIN) { + std::string errorMessage{"End range is invalid number: " + range.substr(current_start_index, current_end_index)}; + SendError(conn, 405, errorMessage); + return; + } + } + + ranges.push_back(current_range); + + if (current_end_index >= static_cast(range.length() - 1)) { + more_ranges_exists = false; + } else { + // Multiple ranges are separated by ', ' + more_ranges_exists = true; + current_start_index = current_end_index + 3; + } + } while (more_ranges_exists); + } + + std::string response = "Download!"; + int status_code = 200; + std::string headers; + + if (!ranges.empty()) { + // Create response parts + std::vector responses; + for (std::pair local_range : ranges) { + if (local_range.first >= 0) { + if (local_range.first >= (int64_t) response.length()) { + responses.push_back(""); + } else if (local_range.second == -1 || local_range.second >= (int64_t) response.length()) { + responses.push_back(response.substr(local_range.first)); + } else { + responses.push_back(response.substr(local_range.first, local_range.second - local_range.first + 1)); + } + } + } + + if (responses.size() > 1) { + // Create mime multipart response + std::string boundary = "3d6b6a416f9b5"; + status_code = 206; + response.clear(); + + for (size_t i{0}; i < responses.size(); ++i) { + response += "--" + boundary + "\n"; + response += "Content-Range: bytes " + std::to_string(ranges.at(i).first) + "-"; + if (ranges.at(i).second > 0) { + response += std::to_string(ranges.at(i).second); + } else { + response += std::to_string(responses.at(i).length()); + } + response += "/" + std::to_string(responses.at(i).length()) + "\n\n"; + response += responses.at(i) + "\n"; + } + response += "--" + boundary + "--"; + } else { + if (ranges.at(0).second == -1 || ranges.at(0).second >= (int64_t) response.length()) { + status_code = ranges.at(0).first > 0 ? 206 : 200; + } else { + status_code = 206; + } + response = responses.at(0); + + if (status_code == 206) { + headers = "Content-Range: bytes " + std::to_string(ranges.at(0).first) + "-"; + if (ranges.at(0).second > 0) { + headers += std::to_string(ranges.at(0).second); + } else { + headers += std::to_string(response.length()); + } + headers += "/" + std::to_string(response.length()); + } + } + } + if (!headers.empty()) { + headers += "\r\n"; + } + + mg_http_reply(conn, status_code, headers.c_str(), response.c_str()); + } +} + +void HttpServer::OnRequestCheckAcceptEncoding(mg_connection* conn, mg_http_message* msg) { + std::string response; + for (mg_http_header& header : msg->headers) { + if (!header.name.ptr) { + continue; + } + std::string name = std::string(header.name.ptr, header.name.len); + if (std::string{"Accept-Encoding"} == name) { + response = std::string(header.value.ptr, header.value.len); + } + } + std::string headers = "Content-Type: text/html\r\n"; + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestCheckExpect100Continue(mg_connection* conn, mg_http_message* msg) { + std::string response; + for (mg_http_header& header : msg->headers) { + if (!header.name.ptr) { + continue; + } + std::string name = std::string(header.name.ptr, header.name.len); + if (std::string{"Expect"} == name) { + response = std::string(header.value.ptr, header.value.len); + } + } + std::string headers = "Content-Type: text/html\r\n"; + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestGetDownloadFileLength(mg_connection* conn, mg_http_message* msg) { + auto method = std::string{msg->method.ptr, msg->method.len}; + if (method == std::string{"HEAD"}) { + mg_http_reply(conn, 405, nullptr, ""); + } else { + std::string response("this is a file content."); + std::string headers = "Content-Type: text/plain\r\n"; + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); + } +} + +void HttpServer::OnRequest(mg_connection* conn, mg_http_message* msg) { + std::string uri = std::string(msg->uri.ptr, msg->uri.len); + if (uri == "/") { + OnRequestRoot(conn, msg); + } else if (uri == "/hello.html") { + OnRequestHello(conn, msg); + } else if (uri == "/timeout.html") { + OnRequestTimeout(conn, msg); + } else if (uri == "/long_timeout.html") { + OnRequestLongTimeout(conn, msg); + } else if (uri == "/low_speed_timeout.html") { + timer_args.emplace_back(std::make_unique(&mgr, conn, mg_timer{})); + OnRequestLowSpeedTimeout(conn, msg, timer_args.back().get()); + } else if (uri == "/low_speed.html") { + OnRequestLowSpeed(conn, msg, &mgr); + } else if (uri == "/low_speed_bytes.html") { + timer_args.emplace_back(std::make_unique(&mgr, conn, mg_timer{})); + OnRequestLowSpeedBytes(conn, msg, timer_args.back().get()); + } else if (uri == "/basic_cookies.html") { + OnRequestBasicCookies(conn, msg); + } else if (uri == "/empty_cookies.html") { + OnRequestEmptyCookies(conn, msg); + } else if (uri == "/cookies_reflect.html") { + OnRequestCookiesReflect(conn, msg); + } else if (uri == "/redirection_with_changing_cookies.html") { + OnRequestRedirectionWithChangingCookies(conn, msg); + } else if (uri == "/basic_auth.html") { + OnRequestBasicAuth(conn, msg); + } else if (uri == "/bearer_token.html") { + OnRequestBearerAuth(conn, msg); + } else if (uri == "/digest_auth.html") { + OnRequestHeaderReflect(conn, msg); + } else if (uri == "/basic.json") { + OnRequestBasicJson(conn, msg); + } else if (uri == "/header_reflect.html") { + OnRequestHeaderReflect(conn, msg); + } else if (uri == "/temporary_redirect.html") { + OnRequestTempRedirect(conn, msg); + } else if (uri == "/permanent_redirect.html") { + OnRequestPermRedirect(conn, msg); + } else if (uri == "/resolve_permanent_redirect.html") { + OnRequestResolvePermRedirect(conn, msg); + } else if (uri == "/two_redirects.html") { + OnRequestTwoRedirects(conn, msg); + } else if (uri == "/url_post.html") { + OnRequestUrlPost(conn, msg); + } else if (uri == "/body_get.html") { + OnRequestBodyGet(conn, msg); + } else if (uri == "/json_post.html") { + OnRequestJsonPost(conn, msg); + } else if (uri == "/form_post.html") { + OnRequestFormPost(conn, msg); + } else if (uri == "/post_reflect.html") { + OnRequestPostReflect(conn, msg); + } else if (uri == "/delete.html") { + OnRequestDelete(conn, msg); + } else if (uri == "/delete_unallowed.html") { + OnRequestDeleteNotAllowed(conn, msg); + } else if (uri == "/put.html") { + OnRequestPut(conn, msg); + } else if (uri == "/put_unallowed.html") { + OnRequestPutNotAllowed(conn, msg); + } else if (uri == "/patch.html") { + OnRequestPatch(conn, msg); + } else if (uri == "/patch_unallowed.html") { + OnRequestPatchNotAllowed(conn, msg); + } else if (uri == "/download_gzip.html") { + OnRequestDownloadGzip(conn, msg); + } else if (uri == "/local_port.html") { + OnRequestLocalPort(conn, msg); + } else if (uri == "/check_accept_encoding.html") { + OnRequestCheckAcceptEncoding(conn, msg); + } else if (uri == "/check_expect_100_continue.html") { + OnRequestCheckExpect100Continue(conn, msg); + } else if (uri == "/get_download_file_length.html") { + OnRequestGetDownloadFileLength(conn, msg); + } else { + OnRequestNotFound(conn, msg); + } +} + +void HttpServer::OnRequestLocalPort(mg_connection* conn, mg_http_message* /*msg*/) { + // send source port number as response for checking SetLocalPort/SetLocalPortRange + std::string headers = "Content-Type: text/plain\r\n"; + // Convert from big endian to little endian + std::string response = std::to_string(AbstractServer::GetRemotePort(conn)); + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); +} + +} // namespace cpr diff --git a/test/httpServer.hpp b/test/httpServer.hpp new file mode 100644 index 0000000..735ff40 --- /dev/null +++ b/test/httpServer.hpp @@ -0,0 +1,66 @@ +#ifndef CPR_TEST_HTTP_SERVER_H +#define CPR_TEST_HTTP_SERVER_H + +#include +#include + +#include "abstractServer.hpp" +#include "cpr/cpr.h" +#include "mongoose.h" + +namespace cpr { +class HttpServer : public AbstractServer { + public: + ~HttpServer() override = default; + + std::string GetBaseUrl() override; + uint16_t GetPort() override; + + void OnRequest(mg_connection* conn, mg_http_message* msg) override; + + private: + static void OnRequestHello(mg_connection* conn, mg_http_message* msg); + static void OnRequestRoot(mg_connection* conn, mg_http_message* msg); + static void OnRequestOptions(mg_connection* conn, mg_http_message* msg); + static void OnRequestNotFound(mg_connection* conn, mg_http_message* msg); + static void OnRequestTimeout(mg_connection* conn, mg_http_message* msg); + static void OnRequestLongTimeout(mg_connection* conn, mg_http_message* msg); + static void OnRequestLowSpeedTimeout(mg_connection* conn, mg_http_message* msg, TimerArg* arg); + static void OnRequestLowSpeed(mg_connection* conn, mg_http_message* msg, mg_mgr* mgr); + static void OnRequestLowSpeedBytes(mg_connection* conn, mg_http_message* msg, TimerArg* arg); + static void OnRequestBasicCookies(mg_connection* conn, mg_http_message* msg); + static void OnRequestEmptyCookies(mg_connection* conn, mg_http_message* msg); + static void OnRequestCookiesReflect(mg_connection* conn, mg_http_message* msg); + static void OnRequestRedirectionWithChangingCookies(mg_connection* conn, mg_http_message* msg); + static void OnRequestBasicAuth(mg_connection* conn, mg_http_message* msg); + static void OnRequestBearerAuth(mg_connection* conn, mg_http_message* msg); + static void OnRequestBasicJson(mg_connection* conn, mg_http_message* msg); + static void OnRequestHeaderReflect(mg_connection* conn, mg_http_message* msg); + static void OnRequestTempRedirect(mg_connection* conn, mg_http_message* msg); + static void OnRequestPermRedirect(mg_connection* conn, mg_http_message* msg); + static void OnRequestResolvePermRedirect(mg_connection* conn, mg_http_message* msg); + static void OnRequestTwoRedirects(mg_connection* conn, mg_http_message* msg); + static void OnRequestUrlPost(mg_connection* conn, mg_http_message* msg); + static void OnRequestPostReflect(mg_connection* conn, mg_http_message* msg); + static void OnRequestBodyGet(mg_connection* conn, mg_http_message* msg); + static void OnRequestJsonPost(mg_connection* conn, mg_http_message* msg); + static void OnRequestFormPost(mg_connection* conn, mg_http_message* msg); + static void OnRequestDelete(mg_connection* conn, mg_http_message* msg); + static void OnRequestDeleteNotAllowed(mg_connection* conn, mg_http_message* msg); + static void OnRequestPut(mg_connection* conn, mg_http_message* msg); + static void OnRequestPutNotAllowed(mg_connection* conn, mg_http_message* msg); + static void OnRequestPatch(mg_connection* conn, mg_http_message* msg); + static void OnRequestPatchNotAllowed(mg_connection* conn, mg_http_message* msg); + static void OnRequestDownloadGzip(mg_connection* conn, mg_http_message* msg); + static void OnRequestLocalPort(mg_connection* conn, mg_http_message* msg); + static void OnRequestCheckAcceptEncoding(mg_connection* conn, mg_http_message* msg); + static void OnRequestCheckExpect100Continue(mg_connection* conn, mg_http_message* msg); + static void OnRequestGetDownloadFileLength(mg_connection* conn, mg_http_message* msg); + + protected: + mg_connection* initServer(mg_mgr* mgr, mg_event_handler_t event_handler) override; + void acceptConnection(mg_connection* conn) override; +}; +} // namespace cpr + +#endif diff --git a/test/httpsServer.cpp b/test/httpsServer.cpp new file mode 100644 index 0000000..401a470 --- /dev/null +++ b/test/httpsServer.cpp @@ -0,0 +1,65 @@ +#include "httpsServer.hpp" +#include + +namespace cpr { +HttpsServer::HttpsServer(fs::path&& baseDirPath, fs::path&& sslCertFileName, fs::path&& sslKeyFileName) : baseDirPath(baseDirPath.make_preferred().string()), sslCertFileName(sslCertFileName.make_preferred().string()), sslKeyFileName(sslKeyFileName.make_preferred().string()) { + // See https://mongoose.ws/tutorials/tls/ + memset(static_cast(&tlsOpts), 0, sizeof(tlsOpts)); + tlsOpts.cert = this->sslCertFileName.c_str(); + tlsOpts.certkey = this->sslKeyFileName.c_str(); +} + +std::string HttpsServer::GetBaseUrl() { + return "https://127.0.0.1:" + std::to_string(GetPort()); +} + +uint16_t HttpsServer::GetPort() { + // Unassigned port in the ephemeral range + return 61937; +} + +mg_connection* HttpsServer::initServer(mg_mgr* mgr, mg_event_handler_t event_handler) { + mg_mgr_init(mgr); + + std::string port = std::to_string(GetPort()); + mg_connection* c = mg_http_listen(mgr, GetBaseUrl().c_str(), event_handler, this); + return c; +} + +void HttpsServer::acceptConnection(mg_connection* conn) { + // See https://mongoose.ws/tutorials/tls/ + mg_tls_init(conn, &tlsOpts); +} + +void HttpsServer::OnRequest(mg_connection* conn, mg_http_message* msg) { + std::string uri = std::string(msg->uri.ptr, msg->uri.len); + if (uri == "/hello.html") { + OnRequestHello(conn, msg); + } else { + OnRequestNotFound(conn, msg); + } +} + +void HttpsServer::OnRequestNotFound(mg_connection* conn, mg_http_message* /*msg*/) { + mg_http_reply(conn, 404, nullptr, "Not Found"); +} + +void HttpsServer::OnRequestHello(mg_connection* conn, mg_http_message* /*msg*/) { + std::string response{"Hello world!"}; + std::string headers{"Content-Type: text/html\r\n"}; + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); +} + +const std::string& HttpsServer::getBaseDirPath() const { + return baseDirPath; +} + +const std::string& HttpsServer::getSslCertFileName() const { + return sslCertFileName; +} + +const std::string& HttpsServer::getSslKeyFileName() const { + return sslKeyFileName; +} + +} // namespace cpr diff --git a/test/httpsServer.hpp b/test/httpsServer.hpp new file mode 100644 index 0000000..cea4d34 --- /dev/null +++ b/test/httpsServer.hpp @@ -0,0 +1,42 @@ +#ifndef CPR_TEST_HTTPS_SERVER_H +#define CPR_TEST_HTTPS_SERVER_H + +#include +#include + +#include "abstractServer.hpp" +#include "cpr/cpr.h" +#include "mongoose.h" +#include + +namespace cpr { +class HttpsServer : public AbstractServer { + private: + // We don't use fs::path here, as this leads to problems using windows + const std::string baseDirPath; + const std::string sslCertFileName; + const std::string sslKeyFileName; + struct mg_tls_opts tlsOpts; + + public: + explicit HttpsServer(fs::path&& baseDirPath, fs::path&& sslCertFileName, fs::path&& sslKeyFileName); + ~HttpsServer() override = default; + + std::string GetBaseUrl() override; + uint16_t GetPort() override; + + void OnRequest(mg_connection* conn, mg_http_message* msg) override; + static void OnRequestHello(mg_connection* conn, mg_http_message* msg); + static void OnRequestNotFound(mg_connection* conn, mg_http_message* msg); + + const std::string& getBaseDirPath() const; + const std::string& getSslCertFileName() const; + const std::string& getSslKeyFileName() const; + + protected: + mg_connection* initServer(mg_mgr* mgr, mg_event_handler_t event_handler) override; + void acceptConnection(mg_connection* conn) override; +}; +} // namespace cpr + +#endif diff --git a/test/interceptor_multi_tests.cpp b/test/interceptor_multi_tests.cpp new file mode 100644 index 0000000..28a0af5 --- /dev/null +++ b/test/interceptor_multi_tests.cpp @@ -0,0 +1,470 @@ +#include +#include +#include + +#include "cpr/cpr.h" +#include "cpr/interceptor.h" +#include "cpr/response.h" +#include "cpr/session.h" +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +class HiddenHelloWorldRedirectInterceptorMulti : public InterceptorMulti { + public: + std::vector intercept(MultiPerform& multi) override { + EXPECT_FALSE(multi.GetSessions().empty()); + std::shared_ptr session = multi.GetSessions().front().first; + + // Save original url + Url old_url = session->GetFullRequestUrl(); + + // Rewrite the url + Url url{server->GetBaseUrl() + "/hello.html"}; + session->SetUrl(url); + + // Proceed the chain + std::vector response = proceed(multi); + EXPECT_FALSE(response.empty()); + + // Restore the url again + response.front().url = old_url; + return response; + } +}; + +class ChangeStatusCodeInterceptorMulti : public InterceptorMulti { + public: + std::vector intercept(MultiPerform& multi) override { + EXPECT_FALSE(multi.GetSessions().empty()); + std::shared_ptr session = multi.GetSessions().front().first; + + // Proceed the chain + std::vector response = proceed(multi); + EXPECT_FALSE(response.empty()); + + // Change the status code + response.front().status_code = 12345; + return response; + } +}; + +class SetBasicAuthInterceptorMulti : public InterceptorMulti { + public: + std::vector intercept(MultiPerform& multi) override { + EXPECT_FALSE(multi.GetSessions().empty()); + std::shared_ptr session = multi.GetSessions().front().first; + + // Set authentication + session->SetAuth(Authentication{"user", "password", AuthMode::BASIC}); + + // Proceed the chain + return proceed(multi); + } +}; + +class SetUnsupportedProtocolErrorInterceptorMulti : public InterceptorMulti { + public: + std::vector intercept(MultiPerform& multi) override { + EXPECT_FALSE(multi.GetSessions().empty()); + std::shared_ptr session = multi.GetSessions().front().first; + + // Proceed the chain + std::vector response = proceed(multi); + EXPECT_FALSE(response.empty()); + + // Set unsupported protocol error + response.front().error = Error{CURLE_UNSUPPORTED_PROTOCOL, "SetErrorInterceptorMulti"}; + + // Return response + return response; + } +}; + +class ChangeRequestMethodToGetInterceptorMulti : public InterceptorMulti { + public: + std::vector intercept(MultiPerform& multi) override { + EXPECT_FALSE(multi.GetSessions().empty()); + multi.GetSessions().front().second = MultiPerform::HttpMethod::GET_REQUEST; + + return proceed(multi); + } +}; + +class ChangeRequestMethodToPostInterceptorMulti : public InterceptorMulti { + public: + std::vector intercept(MultiPerform& multi) override { + EXPECT_FALSE(multi.GetSessions().empty()); + multi.GetSessions().front().second = MultiPerform::HttpMethod::POST_REQUEST; + multi.GetSessions().front().first->SetOption(Payload{{"x", "5"}}); + return proceed(multi); + } +}; + +class ChangeRequestMethodToPutInterceptorMulti : public InterceptorMulti { + public: + std::vector intercept(MultiPerform& multi) override { + EXPECT_FALSE(multi.GetSessions().empty()); + multi.GetSessions().front().second = MultiPerform::HttpMethod::PUT_REQUEST; + multi.GetSessions().front().first->SetOption(Payload{{"x", "5"}}); + return proceed(multi); + } +}; + +class ChangeRequestMethodToDeleteInterceptorMulti : public InterceptorMulti { + public: + std::vector intercept(MultiPerform& multi) override { + EXPECT_FALSE(multi.GetSessions().empty()); + multi.GetSessions().front().second = MultiPerform::HttpMethod::DELETE_REQUEST; + return proceed(multi); + } +}; + +bool write_data(std::string /*data*/, intptr_t /*userdata*/) { + return true; +} + +class ChangeRequestMethodToDownloadCallbackInterceptorMulti : public InterceptorMulti { + public: + std::vector intercept(MultiPerform& multi) override { + EXPECT_FALSE(multi.GetSessions().empty()); + multi.GetSessions().front().second = MultiPerform::HttpMethod::DOWNLOAD_REQUEST; + PrepareDownloadSession(multi, 0, WriteCallback{write_data, 0}); + return proceed(multi); + } +}; + +class ChangeRequestMethodToHeadInterceptorMulti : public InterceptorMulti { + public: + std::vector intercept(MultiPerform& multi) override { + EXPECT_FALSE(multi.GetSessions().empty()); + multi.GetSessions().front().second = MultiPerform::HttpMethod::HEAD_REQUEST; + return proceed(multi); + } +}; + +class ChangeRequestMethodToOptionsInterceptorMulti : public InterceptorMulti { + public: + std::vector intercept(MultiPerform& multi) override { + EXPECT_FALSE(multi.GetSessions().empty()); + multi.GetSessions().front().second = MultiPerform::HttpMethod::OPTIONS_REQUEST; + return proceed(multi); + } +}; + +class ChangeRequestMethodToPatchInterceptorMulti : public InterceptorMulti { + public: + std::vector intercept(MultiPerform& multi) override { + EXPECT_FALSE(multi.GetSessions().empty()); + multi.GetSessions().front().second = MultiPerform::HttpMethod::PATCH_REQUEST; + multi.GetSessions().front().first->SetOption(Payload{{"x", "5"}}); + return proceed(multi); + } +}; + +TEST(InterceptorMultiTest, HiddenUrlRewriteInterceptorMultiTest) { + Url url{server->GetBaseUrl() + "/basic.json"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Get(); + EXPECT_EQ(response.size(), 1); + + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.front().text); + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, ChangeStatusCodeInterceptorMultiTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Get(); + EXPECT_EQ(response.size(), 1); + + long expected_status_code{12345}; + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(expected_status_code, response.front().status_code); + EXPECT_EQ(ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, DownloadChangeStatusCodeInterceptorMultiTest) { + cpr::Url url{server->GetBaseUrl() + "/download_gzip.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + session->SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Download(cpr::WriteCallback{write_data, 0}); + EXPECT_EQ(response.size(), 1); + + long expected_status_code{12345}; + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(expected_status_code, response.front().status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, SetBasicAuthInterceptorMultiTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Get(); + EXPECT_EQ(response.size(), 1); + + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.front().text); + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(std::string{"text/html"}, response.front().header["content-type"]); + EXPECT_EQ(200, response.front().status_code); + EXPECT_EQ(ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, SetUnsupportedProtocolErrorInterceptorMultiTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Get(); + EXPECT_EQ(response.size(), 1); + + std::string expected_error_message{"SetErrorInterceptorMulti"}; + ErrorCode expected_error_code{ErrorCode::UNSUPPORTED_PROTOCOL}; + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(std::string{"text/html"}, response.front().header["content-type"]); + EXPECT_EQ(200, response.front().status_code); + EXPECT_EQ(expected_error_message, response.front().error.message); + EXPECT_EQ(expected_error_code, response.front().error.code); +} + +TEST(InterceptorMultiTest, ChangeRequestMethodToGetInterceptorMultiTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Head(); + EXPECT_EQ(response.size(), 1); + + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.front().text); + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(std::string{"text/html"}, response.front().header["content-type"]); + EXPECT_EQ(200, response.front().status_code); + EXPECT_EQ(ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, ChangeRequestMethodToPostInterceptorMultiTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Head(); + EXPECT_EQ(response.size(), 1); + + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.front().text); + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(std::string{"application/json"}, response.front().header["content-type"]); + EXPECT_EQ(201, response.front().status_code); + EXPECT_EQ(ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, ChangeRequestMethodToPutInterceptorMultiTest) { + Url url{server->GetBaseUrl() + "/put.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Get(); + EXPECT_EQ(response.size(), 1); + + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.front().text); + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(std::string{"application/json"}, response.front().header["content-type"]); + EXPECT_EQ(200, response.front().status_code); + EXPECT_EQ(ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, ChangeRequestMethodToPatchInterceptorMultiTest) { + Url url{server->GetBaseUrl() + "/patch.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Get(); + EXPECT_EQ(response.size(), 1); + + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.front().text); + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(std::string{"application/json"}, response.front().header["content-type"]); + EXPECT_EQ(200, response.front().status_code); + EXPECT_EQ(ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, ChangeRequestMethodToOptionsInterceptorMultiTest) { + Url url{server->GetBaseUrl() + "/"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Get(); + EXPECT_EQ(response.size(), 1); + + std::string expected_text{""}; + EXPECT_EQ(expected_text, response.front().text); + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(std::string{"GET, POST, PUT, DELETE, PATCH, OPTIONS"}, response.front().header["Access-Control-Allow-Methods"]); + EXPECT_EQ(200, response.front().status_code); + EXPECT_EQ(ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, ChangeRequestMethodToHeadInterceptorMultiTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Get(); + EXPECT_EQ(response.size(), 1); + + EXPECT_EQ(std::string{}, response.front().text); + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(std::string{"text/html"}, response.front().header["content-type"]); + EXPECT_EQ(200, response.front().status_code); + EXPECT_EQ(ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, ChangeRequestMethodToDownloadCallbackInterceptorMultiTest) { + Url url{server->GetBaseUrl() + "/download_gzip.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + session->SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + session->SetTimeout(Timeout{2000}); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Put(); + EXPECT_EQ(response.size(), 1); + + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(200, response.front().status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, ChangeRequestMethodToDownloadCallbackInterceptorMultiMixTest) { + Url url{server->GetBaseUrl() + "/download_gzip.html"}; + std::shared_ptr session1 = std::make_shared(); + session1->SetUrl(url); + session1->SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + session1->SetTimeout(Timeout{2000}); + + std::shared_ptr session2 = std::make_shared(); + session2->SetUrl(url); + session2->SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + session2->SetTimeout(Timeout{2000}); + + MultiPerform multi; + multi.AddSession(session1); + multi.AddSession(session2); + // Changes only one of two sessions to download, so it is expected to throw an exception here since we can not mix them. + multi.AddInterceptor(std::make_shared()); + EXPECT_THROW(multi.Put(), std::invalid_argument); +} + +TEST(InterceptorMultiTest, ChangeRequestMethodToDeleteInterceptorMultiTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Get(); + EXPECT_EQ(response.size(), 1); + + std::string expected_text{"Delete success"}; + EXPECT_EQ(expected_text, response.front().text); + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(std::string{"text/html"}, response.front().header["content-type"]); + EXPECT_EQ(200, response.front().status_code); + EXPECT_EQ(ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, TwoInterceptorMultisTest) { + Url url{server->GetBaseUrl() + "/basic.json"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Get(); + EXPECT_EQ(response.size(), 1); + + std::string expected_text{"Hello world!"}; + long expected_status_code{12345}; + EXPECT_EQ(expected_text, response.front().text); + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(expected_status_code, response.front().status_code); + EXPECT_EQ(ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, ThreeInterceptorMultisTest) { + Url url{server->GetBaseUrl() + "/basic.json"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + multi.AddInterceptor(std::make_shared()); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Get(); + EXPECT_EQ(response.size(), 1); + + std::string expected_text{"Hello world!"}; + long expected_status_code{12345}; + std::string expected_error_message{"SetErrorInterceptorMulti"}; + ErrorCode expected_error_code{ErrorCode::UNSUPPORTED_PROTOCOL}; + EXPECT_EQ(expected_text, response.front().text); + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(expected_status_code, response.front().status_code); + EXPECT_EQ(expected_error_message, response.front().error.message); + EXPECT_EQ(expected_error_code, response.front().error.code); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/test/interceptor_tests.cpp b/test/interceptor_tests.cpp new file mode 100644 index 0000000..8bdc06b --- /dev/null +++ b/test/interceptor_tests.cpp @@ -0,0 +1,369 @@ +#include +#include + +#include "cpr/cpr.h" +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +class HiddenHelloWorldRedirectInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + // Save original url + Url old_url = session.GetFullRequestUrl(); + + // Rewrite the url + Url url{server->GetBaseUrl() + "/hello.html"}; + session.SetUrl(url); + + // Proceed the chain + Response response = proceed(session); + + // Restore the url again + response.url = old_url; + return response; + } +}; + +class ChangeStatusCodeInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + // Proceed the chain + Response response = proceed(session); + + // Change the status code + response.status_code = 12345; + return response; + } +}; + +class SetBasicAuthInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + // Set authentication + session.SetAuth(Authentication{"user", "password", AuthMode::BASIC}); + + // Proceed the chain + return proceed(session); + } +}; + +class SetUnsupportedProtocolErrorInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + // Proceed the chain + Response response = proceed(session); + + // Set unsupported protocol error + response.error = Error{CURLE_UNSUPPORTED_PROTOCOL, "SetErrorInterceptor"}; + + // Return response + return response; + } +}; + +class ChangeRequestMethodToGetInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + return proceed(session, Interceptor::ProceedHttpMethod::GET_REQUEST); + } +}; + +class ChangeRequestMethodToPostInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + session.SetOption(Payload{{"x", "5"}}); + return proceed(session, Interceptor::ProceedHttpMethod::POST_REQUEST); + } +}; + +class ChangeRequestMethodToPutInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + session.SetOption(Payload{{"x", "5"}}); + return proceed(session, Interceptor::ProceedHttpMethod::PUT_REQUEST); + } +}; + +class ChangeRequestMethodToDeleteInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + return proceed(session, Interceptor::ProceedHttpMethod::DELETE_REQUEST); + } +}; + +bool write_data(std::string /*data*/, intptr_t /*userdata*/) { + return true; +} + +class ChangeRequestMethodToDownloadCallbackInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + return proceed(session, Interceptor::ProceedHttpMethod::DOWNLOAD_CALLBACK_REQUEST, WriteCallback{write_data, 0}); + } +}; + +class ChangeRequestMethodToHeadInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + return proceed(session, Interceptor::ProceedHttpMethod::HEAD_REQUEST); + } +}; + +class ChangeRequestMethodToOptionsInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + return proceed(session, Interceptor::ProceedHttpMethod::OPTIONS_REQUEST); + } +}; + +class ChangeRequestMethodToPatchInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + session.SetOption(Payload{{"x", "5"}}); + return proceed(session, Interceptor::ProceedHttpMethod::PATCH_REQUEST); + } +}; + +TEST(InterceptorTest, HiddenUrlRewriteInterceptorTest) { + Url url{server->GetBaseUrl() + "/basic.json"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + Response response = session.Get(); + + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, ChangeStatusCodeInterceptorTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + Response response = session.Get(); + + long expected_status_code{12345}; + EXPECT_EQ(url, response.url); + EXPECT_EQ(expected_status_code, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, DownloadChangeStatusCodeInterceptorTest) { + cpr::Url url{server->GetBaseUrl() + "/download_gzip.html"}; + cpr::Session session; + session.SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + Response response = session.Download(cpr::WriteCallback{write_data, 0}); + + long expected_status_code{12345}; + EXPECT_EQ(url, response.url); + EXPECT_EQ(expected_status_code, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, SetBasicAuthInterceptorTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, SetUnsupportedProtocolErrorInterceptorTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + + Response response = session.Get(); + std::string expected_error_message{"SetErrorInterceptor"}; + ErrorCode expected_error_code{ErrorCode::UNSUPPORTED_PROTOCOL}; + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(expected_error_message, response.error.message); + EXPECT_EQ(expected_error_code, response.error.code); +} + +TEST(InterceptorTest, ChangeRequestMethodToGetInterceptorTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + Response response = session.Head(); + + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, ChangeRequestMethodToPostInterceptorTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + Response response = session.Head(); + + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, ChangeRequestMethodToPutInterceptorTest) { + Url url{server->GetBaseUrl() + "/put.html"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + Response response = session.Get(); + + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, ChangeRequestMethodToPatchInterceptorTest) { + Url url{server->GetBaseUrl() + "/patch.html"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + Response response = session.Get(); + + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, ChangeRequestMethodToOptionsInterceptorTest) { + Url url{server->GetBaseUrl() + "/"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + Response response = session.Get(); + + std::string expected_text{""}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"GET, POST, PUT, DELETE, PATCH, OPTIONS"}, response.header["Access-Control-Allow-Methods"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, ChangeRequestMethodToHeadInterceptorTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + Response response = session.Get(); + + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, ChangeRequestMethodToDownloadCallbackInterceptorTest) { + Url url{server->GetBaseUrl() + "/download_gzip.html"}; + Session session; + session.SetUrl(url); + session.SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + session.SetTimeout(Timeout{2000}); + session.AddInterceptor(std::make_shared()); + Response response = session.Put(); + + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, ChangeRequestMethodToDeleteInterceptorTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + Response response = session.Get(); + + + std::string expected_text{"Delete success"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, TwoInterceptorsTest) { + Url url{server->GetBaseUrl() + "/basic.json"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + session.AddInterceptor(std::make_shared()); + Response response = session.Get(); + + std::string expected_text{"Hello world!"}; + long expected_status_code{12345}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(expected_status_code, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, ThreeInterceptorsTest) { + Url url{server->GetBaseUrl() + "/basic.json"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + session.AddInterceptor(std::make_shared()); + session.AddInterceptor(std::make_shared()); + Response response = session.Get(); + + std::string expected_text{"Hello world!"}; + long expected_status_code{12345}; + std::string expected_error_message{"SetErrorInterceptor"}; + ErrorCode expected_error_code{ErrorCode::UNSUPPORTED_PROTOCOL}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(expected_status_code, response.status_code); + EXPECT_EQ(expected_error_message, response.error.message); + EXPECT_EQ(expected_error_code, response.error.code); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/test/multiasync_tests.cpp b/test/multiasync_tests.cpp new file mode 100644 index 0000000..1e39f73 --- /dev/null +++ b/test/multiasync_tests.cpp @@ -0,0 +1,437 @@ +#include +#include + +#include + +#include "httpServer.hpp" +#include "multiasync_tests.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +// A cancellable AsyncResponse +using AsyncResponseC = AsyncWrapper; + +/** This property is tested at compile-time, so if compilation succeeds, it has already been verified. It is, however, useful to structure it as a test for semantic purposes. + */ +TEST(AsyncWrapperTests, TestConstructorDeductions) { + auto wrapper_non_cancellable{AsyncWrapper{std::future{}}}; + auto wrapper_cancellable{AsyncWrapper{std::future{}, std::make_shared(false)}}; + + static_assert(std::is_same, decltype(wrapper_non_cancellable)>::value); + static_assert(std::is_same, decltype(wrapper_cancellable)>::value); + SUCCEED(); +} + +/** These tests aim to set a point of reference for AsyncWrapper behavior. + * Those functions that replicate std::future member functions should behave in a way that is in all ways compatible. + * Others should behave as expected by the below test set. + */ +TEST(AsyncWrapperNonCancellableTests, TestGetNoError) { + const Url hello_url{server->GetBaseUrl() + "/hello.html"}; + const std::string expected_hello{"Hello world!"}; + const Response resp{GetAsync(hello_url).get()}; + EXPECT_EQ(expected_hello, resp.text); +} + +TEST(AsyncWrapperNonCancellableTests, TestExceptionsNoSharedState) { + const std::chrono::duration five_secs{std::chrono::seconds(1)}; + const std::chrono::time_point in_five_s{std::chrono::steady_clock::now() + five_secs}; + + // We create an AsyncWrapper for a future without a shared state (default-initialized) + AsyncWrapper test_wrapper{std::future{}}; + + + ASSERT_FALSE(test_wrapper.valid()); + ASSERT_FALSE(test_wrapper.IsCancelled()); + + // Trying to get or wait for a future that doesn't have a shared state should result to an exception + // It should be noted that there is a divergence from std::future behavior here: calling wait* on the original std::future is undefined behaviour, according to cppreference.com . We find it preferrable to throw an exception. + EXPECT_THROW(std::ignore = test_wrapper.get(), std::exception); + EXPECT_THROW(test_wrapper.wait(), std::exception); + EXPECT_THROW(test_wrapper.wait_for(five_secs), std::exception); + EXPECT_THROW(test_wrapper.wait_until(in_five_s), std::exception); +} + +TEST(AsyncWrapperCancellableTests, TestExceptionsNoSharedState) { + const std::chrono::duration five_secs{std::chrono::seconds(5)}; + const std::chrono::time_point in_five_s{std::chrono::steady_clock::now() + five_secs}; + + AsyncWrapper test_wrapper{std::future{}, std::make_shared(false)}; + + static_assert(std::is_same, decltype(test_wrapper)>::value); + + ASSERT_FALSE(test_wrapper.valid()); + ASSERT_FALSE(test_wrapper.IsCancelled()); + + EXPECT_THROW(std::ignore = test_wrapper.get(), std::exception); + EXPECT_THROW(test_wrapper.wait(), std::exception); + EXPECT_THROW(test_wrapper.wait_for(five_secs), std::exception); + EXPECT_THROW(test_wrapper.wait_until(in_five_s), std::exception); +} + +TEST(AsyncWrapperCancellableTests, TestExceptionsCancelledRequest) { + const Url call_url{server->GetBaseUrl() + "/low_speed_bytes.html"}; + const std::chrono::duration five_secs{std::chrono::seconds(5)}; + const std::chrono::time_point in_five_s{std::chrono::steady_clock::now() + five_secs}; + + AsyncResponseC test_wrapper{std::move(MultiGetAsync(std::tuple{call_url}).at(0))}; + EXPECT_EQ(CancellationResult::success, test_wrapper.Cancel()); + EXPECT_EQ(CancellationResult::invalid_operation, test_wrapper.Cancel()); + ASSERT_TRUE(test_wrapper.IsCancelled()); + + EXPECT_THROW(std::ignore = test_wrapper.get(), std::exception); + EXPECT_THROW(test_wrapper.wait(), std::exception); + EXPECT_THROW(test_wrapper.wait_for(five_secs), std::exception); + EXPECT_THROW(test_wrapper.wait_until(in_five_s), std::exception); +} + +TEST(AsyncWrapperCancellableTests, TestWaitFor) { + constexpr std::chrono::duration wait_for_time{std::chrono::milliseconds(100)}; + constexpr std::chrono::duration teardown_time{std::chrono::milliseconds(10)}; + + const Url call_url{server->GetBaseUrl() + "/low_speed_bytes.html"}; + + AsyncResponseC test_wrapper{std::move(MultiGetAsync(std::tuple{call_url}).at(0))}; + + EXPECT_EQ(std::future_status::timeout, test_wrapper.wait_for(wait_for_time)); + + ASSERT_TRUE(test_wrapper.valid()); + ASSERT_FALSE(test_wrapper.IsCancelled()); + + EXPECT_EQ(CancellationResult::success, test_wrapper.Cancel()); + + std::this_thread::sleep_for(teardown_time); +} +/** The group MultiAsyncBasicTests executes multiple tests from the test sources associated with every Http action in parallel. + * These tests are reproductions of tests from the appropriate test suites, but they guarantee that the multiasync function template produces correctly working instantiations for every Http action. + */ +TEST(MultiAsyncBasicTests, MultiAsyncGetTest) { + const Url hello_url{server->GetBaseUrl() + "/hello.html"}; + const std::string expected_hello{"Hello world!"}; + std::vector resps{MultiGetAsync(std::tuple{hello_url}, std::tuple{hello_url}, std::tuple{hello_url})}; + + for (AsyncResponseC& resp : resps) { + EXPECT_EQ(expected_hello, resp.get().text); + } +} + +TEST(MultiAsyncBasicTests, MultiAsyncDeleteTest) { + const std::string server_base{server->GetBaseUrl()}; + const Url delete_allowed{server_base + "/delete.html"}; + const Url delete_unallowed{server_base + "/delete_unallowed.html"}; + const std::tuple del_json_params{delete_allowed, Body{"'foo':'bar'"}, Header{{"Content-Type", "application/json"}}}; + const std::string expected_text_success{"Delete success"}; + const std::string expected_text_fail{"Method Not Allowed"}; + const std::string expected_text_json{"'foo':'bar'"}; + + std::vector resps{MultiDeleteAsync(std::tuple{delete_allowed}, std::tuple{delete_unallowed}, del_json_params)}; + + Response del_success{resps.at(0).get()}; + Response del_fail{resps.at(1).get()}; + Response del_json{resps.at(2).get()}; + + EXPECT_EQ(expected_text_success, del_success.text); + EXPECT_EQ(delete_allowed, del_success.url); + EXPECT_EQ(std::string{"text/html"}, del_success.header["content-type"]); + EXPECT_EQ(200, del_success.status_code); + EXPECT_EQ(ErrorCode::OK, del_success.error.code); + + EXPECT_EQ(expected_text_fail, del_fail.text); + EXPECT_EQ(delete_unallowed, del_fail.url); + EXPECT_EQ(std::string{"text/plain"}, del_fail.header["content-type"]); + EXPECT_EQ(405, del_fail.status_code); + EXPECT_EQ(ErrorCode::OK, del_fail.error.code); + + EXPECT_EQ(expected_text_json, del_json.text); + EXPECT_EQ(delete_allowed, del_json.url); + EXPECT_EQ(std::string{"application/json"}, del_json.header["content-type"]); + EXPECT_EQ(200, del_json.status_code); + EXPECT_EQ(ErrorCode::OK, del_json.error.code); +} + +TEST(MultiAsyncBasicTests, MultiAsyncHeadTest) { + const std::string server_base{server->GetBaseUrl()}; + const Url hello_url{server_base + "/hello.html"}; + const Url json_url{server_base + "/basic.json"}; + const Url notfound_url{server_base + "/error.html"}; + const Url digest_url{server_base + "/digest_auth.html"}; + const Authentication digest_auth{"user", "password", AuthMode::DIGEST}; + + std::vector resps{MultiHeadAsync(std::tuple{hello_url}, std::tuple{json_url}, std::tuple{notfound_url}, std::tuple{digest_url, digest_auth})}; + Response hello_resp{resps.at(0).get()}; + Response json_resp{resps.at(1).get()}; + Response notfound_resp{resps.at(2).get()}; + Response digest_resp{resps.at(3).get()}; + + EXPECT_EQ(std::string{}, hello_resp.text); + EXPECT_EQ(hello_url, hello_resp.url); + EXPECT_EQ(std::string{"text/html"}, hello_resp.header["content-type"]); + EXPECT_EQ(200, hello_resp.status_code); + EXPECT_EQ(ErrorCode::OK, hello_resp.error.code); + + EXPECT_EQ(std::string{}, json_resp.text); + EXPECT_EQ(json_url, json_resp.url); + EXPECT_EQ(std::string{"application/json"}, json_resp.header["content-type"]); + EXPECT_EQ(200, json_resp.status_code); + EXPECT_EQ(ErrorCode::OK, json_resp.error.code); + + EXPECT_EQ(std::string{}, notfound_resp.text); + EXPECT_EQ(notfound_url, notfound_resp.url); + EXPECT_EQ(std::string{"text/plain"}, notfound_resp.header["content-type"]); + EXPECT_EQ(404, notfound_resp.status_code); + EXPECT_EQ(ErrorCode::OK, notfound_resp.error.code); + + EXPECT_EQ(std::string{}, digest_resp.text); + EXPECT_EQ(digest_url, digest_resp.url); + EXPECT_EQ(std::string{"text/html"}, digest_resp.header["content-type"]); + EXPECT_EQ(200, digest_resp.status_code); + EXPECT_EQ(ErrorCode::OK, digest_resp.error.code); +} + +TEST(MultiAsyncBasicTests, MultiAsyncOptionsTest) { + const std::string server_base{server->GetBaseUrl()}; + const Url root_url{server_base + "/"}; + const Url hello_url{server_base + "/hello.html"}; + + std::vector resps{MultiOptionsAsync(std::tuple{root_url}, std::tuple{hello_url})}; + + Response root_resp{resps.at(0).get()}; + Response hello_resp{resps.at(1).get()}; + + EXPECT_EQ(std::string{}, root_resp.text); + EXPECT_EQ(root_url, root_resp.url); + EXPECT_EQ(std::string{"GET, POST, PUT, DELETE, PATCH, OPTIONS"}, root_resp.header["Access-Control-Allow-Methods"]); + EXPECT_EQ(200, root_resp.status_code); + EXPECT_EQ(ErrorCode::OK, root_resp.error.code); + + EXPECT_EQ(std::string{}, hello_resp.text); + EXPECT_EQ(hello_url, hello_resp.url); + EXPECT_EQ(std::string{"GET, POST, PUT, DELETE, PATCH, OPTIONS"}, hello_resp.header["Access-Control-Allow-Methods"]); + EXPECT_EQ(200, hello_resp.status_code); + EXPECT_EQ(ErrorCode::OK, hello_resp.error.code); +} + +TEST(MultiAsyncBasicTests, MultiAsyncPatchTest) { + const std::string server_base{server->GetBaseUrl()}; + const Url patch_url{server_base + "/patch.html"}; + const Url patch_not_allowed_url{server_base + "/patch_unallowed.html"}; + const Payload pl{{"x", "10"}, {"y", "1"}}; + const std::string expected_text{ + "{\n" + " \"x\": 10,\n" + " \"y\": 1,\n" + " \"sum\": 11\n" + "}"}; + const std::string notallowed_text{"Method Not Allowed"}; + std::vector resps{MultiPatchAsync(std::tuple{patch_url, pl}, std::tuple{patch_not_allowed_url, pl})}; + const Response success{resps.at(0).get()}; + const Response fail{resps.at(1).get()}; + EXPECT_EQ(expected_text, success.text); + EXPECT_EQ(200, success.status_code); + EXPECT_EQ(patch_url, success.url); + + EXPECT_EQ(notallowed_text, fail.text); + EXPECT_EQ(405, fail.status_code); + EXPECT_EQ(ErrorCode::OK, fail.error.code); +} + +TEST(MultiAsyncBasicTests, MultiAsyncPostTest) { + const std::string server_base{server->GetBaseUrl()}; + const Url post_url{server_base + "/url_post.html"}; + const Url form_post_url{server_base + "/form_post.html"}; + + const Payload post_data{{"x", "5"}, {"y", "15"}}; + const Multipart form_data{{"x", 5}}; + + const std::string post_text{ + "{\n" + " \"x\": 5,\n" + " \"y\": 15,\n" + " \"sum\": 20\n" + "}"}; + const std::string form_text{ + "{\n" + " \"x\": \"5\"\n" + "}"}; + + std::vector resps{MultiPostAsync(std::tuple{post_url, post_data}, std::tuple{form_post_url, form_data})}; + + Response post_resp{resps.at(0).get()}; + Response form_resp{resps.at(1).get()}; + + EXPECT_EQ(post_text, post_resp.text); + EXPECT_EQ(post_url, post_resp.url); + EXPECT_EQ(std::string{"application/json"}, post_resp.header["content-type"]); + EXPECT_EQ(201, post_resp.status_code); + EXPECT_EQ(ErrorCode::OK, post_resp.error.code); + + EXPECT_EQ(form_text, form_resp.text); + EXPECT_EQ(form_post_url, form_resp.url); + EXPECT_EQ(std::string{"application/json"}, form_resp.header["content-type"]); + EXPECT_EQ(201, form_resp.status_code); + EXPECT_EQ(ErrorCode::OK, form_resp.error.code); +} + +TEST(MultiAsyncBasicTests, MultiAsyncPutTest) { + const std::string server_base{server->GetBaseUrl()}; + const Url put_url{server_base + "/put.html"}; + const Url put_failure_url{server_base + "/put_unallowed.html"}; + const Payload pl{{"x", "7"}}; + const std::string success_text{ + "{\n" + " \"x\": 7\n" + "}"}; + const std::string failure_text{"Method Not Allowed"}; + + std::vector resps{MultiPutAsync(std::tuple{put_url, pl}, std::tuple{put_failure_url, pl})}; + Response success_resp{resps.at(0).get()}; + Response failure_resp{resps.at(1).get()}; + + EXPECT_EQ(success_text, success_resp.text); + EXPECT_EQ(put_url, success_resp.url); + EXPECT_EQ(std::string{"application/json"}, success_resp.header["content-type"]); + EXPECT_EQ(200, success_resp.status_code); + EXPECT_EQ(ErrorCode::OK, success_resp.error.code); + + EXPECT_EQ(failure_text, failure_resp.text); + EXPECT_EQ(put_failure_url, failure_resp.url); + EXPECT_EQ(std::string{"text/plain"}, failure_resp.header["content-type"]); + EXPECT_EQ(405, failure_resp.status_code); + EXPECT_EQ(ErrorCode::OK, failure_resp.error.code); +} + +static TestSynchronizationEnv* synchro_env = new TestSynchronizationEnv(); + +/** + * We test that cancellation on queue, works, ie libcurl does not get engaged at all + * To do this, we plant an observer function in the progress call sequence, which + * will set an atomic boolean to true. The objective is to verify that within 500ms, + * the function is never called. + */ +TEST(MultiAsyncCancelTests, CancellationOnQueue) { + synchro_env->Reset(); + const Url hello_url{server->GetBaseUrl() + "/hello.html"}; + const std::function observer_fn{[](cpr_pf_arg_t, cpr_pf_arg_t, cpr_pf_arg_t, cpr_pf_arg_t, intptr_t) -> bool { + synchro_env->fn_called.store(true); + return true; + }}; + + GlobalThreadPool::GetInstance()->Pause(); + std::vector resps{MultiGetAsync(std::tuple{hello_url, ProgressCallback{observer_fn}})}; + EXPECT_EQ(CancellationResult::success, resps.at(0).Cancel()); + GlobalThreadPool::GetInstance()->Resume(); + const bool was_called{synchro_env->fn_called}; + EXPECT_EQ(false, was_called); +} + +/** + * We test that cancellation works as intended while the request is being processed by the server. + * To achieve this we use a condition variable to ensure that the observer function, wrapped in a + * cpr::ProgressCallback, is called at least once, and then no further calls are made for half a + * second after cancellation. + * + * The usage of the condition variable and mutex to synchronize this procedure is analogous to the section "Example" in https://en.cppreference.com/w/cpp/thread/condition_variable + * We use the condition variable in our synchronization environment to ensure that the transfer has + * started at the time of cancellation, ie the observer function has been called at least once. + */ +TEST(MultiAsyncCancelTests, TestCancellationInTransit) { + const Url call_url{server->GetBaseUrl() + "/low_speed_bytes.html"}; + synchro_env->Reset(); + + // 1. Thread running the test acquires the condition variable's mutex + std::unique_lock setup_lock{synchro_env->test_cv_mutex}; + const std::function observer_fn{[](cpr_pf_arg_t, cpr_pf_arg_t, cpr_pf_arg_t, cpr_pf_arg_t, intptr_t) -> bool { + if (synchro_env->counter == 0) { + // 3. in Threadpool, the cv mutex is obtained by the worker thread + const std::unique_lock l{synchro_env->test_cv_mutex}; + synchro_env->counter++; + // 4. the cv is notified + synchro_env->test_cv.notify_all(); + } else { + synchro_env->counter++; + } + return true; + }}; + std::vector res{cpr::MultiGetAsync(std::tuple{call_url, cpr::ProgressCallback{observer_fn}})}; + // 2. cv mutex is released, thread waits for notification on cv + // see https://en.cppreference.com/w/cpp/thread/condition_variable/wait + synchro_env->test_cv.wait(setup_lock); + // 5. execution continues after notification + const size_t init_calls{synchro_env->counter}; + EXPECT_LT(0, init_calls); + EXPECT_EQ(cpr::CancellationResult::success, res.at(0).Cancel()); + const size_t calls{synchro_env->counter}; + std::this_thread::sleep_for(std::chrono::milliseconds{101}); + const size_t calls_post{synchro_env->counter}; + EXPECT_LT(calls_post, calls + 2); +} + +/** Checks that the request is cancelled when the corresponding AsyncResponseC is desturcted + */ +TEST(MultiAsyncCancelTests, TestCancellationOnResponseWrapperDestruction) { + const Url call_url{server->GetBaseUrl() + "/hello.html"}; + synchro_env->Reset(); + std::unique_lock setup_lock{synchro_env->test_cv_mutex}; + const std::function observer_fn{[](cpr_pf_arg_t, cpr_pf_arg_t, cpr_pf_arg_t, cpr_pf_arg_t, intptr_t) -> bool { + const std::unique_lock l{synchro_env->test_cv_mutex}; + synchro_env->counter++; + synchro_env->test_cv.notify_all(); + return true; + }}; + + // We construct a Request that will not terminate, wait until it is being processed by a thread, and destruct the AsyncResponseC + { + AsyncResponseC resp{std::move(MultiGetAsync(std::tuple{call_url, ProgressCallback{observer_fn}}).at(0))}; + synchro_env->test_cv.wait(setup_lock); + const size_t init_calls{synchro_env->counter}; + EXPECT_LT(0, init_calls); + } + + const size_t calls{synchro_env->counter}; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + const size_t post_calls{synchro_env->counter}; + EXPECT_EQ(calls, post_calls); +} + +/** + * This test checks if the interval of calls to the progress function is + * acceptable during a low-speed transaction. The server's low_speed_bytes + * uri sends 1 Byte/second, and we aim to evaluate that 15 calls to the + * progress function happen within 5 seconds. This would indicate that + * the user can realistically expect to have their request cancelled within + * ~1s on a bad case (low network speed). + * INFO this test is not, strictly speaking, deterministic. It depends at the + * least on scheduler behaviour. We have tried, however, to set a boundary that + * is permissive enough to ensure consistency. + */ + +TEST(MultiAsyncCancelTests, TestIntervalOfProgressCallsLowSpeed) { + const Url call_url{server->GetBaseUrl() + "/low_speed_bytes.html"}; + + synchro_env->Reset(); + size_t N{15}; + // This variable will be used to cancel the transaction at the point of the Nth call. + const std::chrono::time_point start{std::chrono::steady_clock::now()}; + + const std::function observer_fn{[N](cpr_pf_arg_t, cpr_pf_arg_t, cpr_pf_arg_t, cpr_pf_arg_t, intptr_t) -> bool { + const size_t current_iteration{++(synchro_env->counter)}; + return current_iteration <= N; + }}; + const ProgressCallback pcall{observer_fn}; + + std::vector resp{MultiGetAsync(std::tuple{call_url, pcall})}; + resp.at(0).wait(); + + const std::chrono::duration elapsed_time{std::chrono::steady_clock::now() - start}; + EXPECT_GT(std::chrono::seconds(N), elapsed_time); + std::this_thread::sleep_for(std::chrono::milliseconds{101}); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + ::testing::AddGlobalTestEnvironment(synchro_env); + return RUN_ALL_TESTS(); +} diff --git a/test/multiasync_tests.hpp b/test/multiasync_tests.hpp new file mode 100644 index 0000000..ee1273d --- /dev/null +++ b/test/multiasync_tests.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include + +class TestSynchronizationEnv : public testing::Environment { + public: + std::atomic_size_t counter{0}; + std::atomic_bool fn_called{false}; + std::condition_variable test_cv{}; + std::mutex test_cv_mutex{}; + + void Reset() { + counter = 0; + fn_called = false; + } +}; diff --git a/test/multiperform_tests.cpp b/test/multiperform_tests.cpp new file mode 100644 index 0000000..6d6dcdf --- /dev/null +++ b/test/multiperform_tests.cpp @@ -0,0 +1,673 @@ +#include +#include + +#include +#include + +#include +#include +#include + +#include "httpServer.hpp" +#include "httpsServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +bool write_data(std::string /*data*/, intptr_t /*userdata*/) { + return true; +} + +TEST(MultiperformAddSessionTests, MultiperformAddSingleSessionTest) { + std::shared_ptr session = std::make_shared(); + MultiPerform multiperform; + multiperform.AddSession(session); + + EXPECT_EQ(2, session.use_count()); +} + +TEST(MultiperformAddSessionTests, MultiperformAddMultipleSessionsTest) { + MultiPerform multiperform; + for (int i = 0; i < 10; ++i) { + std::shared_ptr session = std::make_shared(); + multiperform.AddSession(session); + EXPECT_EQ(2, session.use_count()); + } +} + +TEST(MultiperformAddSessionTests, MultiperformAddMultipleNonDownloadSessionsTest) { + MultiPerform multiperform; + for (int i = 0; i < 10; ++i) { + std::shared_ptr session = std::make_shared(); + multiperform.AddSession(session, MultiPerform::HttpMethod::GET_REQUEST); + EXPECT_EQ(2, session.use_count()); + } +} + +TEST(MultiperformAddSessionTests, MultiperformAddMultipleDownloadSessionsTest) { + MultiPerform multiperform; + for (int i = 0; i < 10; ++i) { + std::shared_ptr session = std::make_shared(); + multiperform.AddSession(session, MultiPerform::HttpMethod::DOWNLOAD_REQUEST); + EXPECT_EQ(2, session.use_count()); + } +} + +TEST(MultiperformAddSessionTests, MultiperformAddTwoMixedTypeSessionsTest) { + std::shared_ptr session_1 = std::make_shared(); + std::shared_ptr session_2 = std::make_shared(); + MultiPerform multiperform; + multiperform.AddSession(session_1, MultiPerform::HttpMethod::GET_REQUEST); + EXPECT_EQ(2, session_1.use_count()); + EXPECT_THROW(multiperform.AddSession(session_2, MultiPerform::HttpMethod::DOWNLOAD_REQUEST), std::invalid_argument); +} + +TEST(MultiperformAddSessionTests, MultiperformAddTwoMixedTypeSessionsReversTest) { + std::shared_ptr session_1 = std::make_shared(); + std::shared_ptr session_2 = std::make_shared(); + MultiPerform multiperform; + multiperform.AddSession(session_1, MultiPerform::HttpMethod::DOWNLOAD_REQUEST); + EXPECT_EQ(2, session_1.use_count()); + EXPECT_THROW(multiperform.AddSession(session_2, MultiPerform::HttpMethod::GET_REQUEST), std::invalid_argument); +} + +TEST(MultiperformRemoveSessionTests, MultiperformRemoveSingleSessionTest) { + std::shared_ptr session = std::make_shared(); + MultiPerform multiperform; + multiperform.AddSession(session); + EXPECT_EQ(2, session.use_count()); + multiperform.RemoveSession(session); + EXPECT_EQ(1, session.use_count()); +} + +TEST(MultiperformRemoveSessionTests, MultiperformRemoveMultipleSessionsTest) { + MultiPerform multiperform; + for (int i = 0; i < 10; ++i) { + std::shared_ptr session = std::make_shared(); + multiperform.AddSession(session); + EXPECT_EQ(2, session.use_count()); + multiperform.RemoveSession(session); + EXPECT_EQ(1, session.use_count()); + } +} + +TEST(MultiperformRemoveSessionTests, MultiperformRemoveNonExistingSessionTest) { + std::shared_ptr session = std::make_shared(); + MultiPerform multiperform; + EXPECT_THROW(multiperform.RemoveSession(session), std::invalid_argument); +} + +TEST(MultiperformGetTests, MultiperformSingleSessionGetTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multiperform; + multiperform.AddSession(session); + std::vector responses = multiperform.Get(); + + EXPECT_EQ(responses.size(), 1); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(url, responses.at(0).url); + EXPECT_EQ(std::string{"text/html"}, responses.at(0).header["content-type"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformGetTests, MultiperformTwoSessionsGetTest) { + MultiPerform multiperform; + std::vector urls; + urls.push_back({server->GetBaseUrl() + "/hello.html"}); + urls.push_back({server->GetBaseUrl() + "/error.html"}); + + std::vector> sessions; + sessions.push_back(std::make_shared()); + sessions.push_back(std::make_shared()); + + + for (size_t i = 0; i < sessions.size(); ++i) { + sessions.at(i)->SetUrl(urls.at(i)); + multiperform.AddSession(sessions.at(i)); + } + + std::vector responses = multiperform.Get(); + + EXPECT_EQ(responses.size(), sessions.size()); + std::vector expected_texts; + expected_texts.emplace_back("Hello world!"); + expected_texts.emplace_back("Not Found"); + + std::vector expected_content_types; + expected_content_types.emplace_back("text/html"); + expected_content_types.emplace_back("text/plain"); + + std::vector expected_status_codes; + expected_status_codes.push_back(200); + expected_status_codes.push_back(404); + + for (size_t i = 0; i < responses.size(); ++i) { + EXPECT_EQ(expected_texts.at(i), responses.at(i).text); + EXPECT_EQ(urls.at(i), responses.at(i).url); + EXPECT_EQ(expected_content_types.at(i), responses.at(i).header["content-type"]); + EXPECT_EQ(expected_status_codes.at(i), responses.at(i).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(i).error.code); + } +} + +TEST(MultiperformGetTests, MultiperformRemoveSessionGetTest) { + MultiPerform multiperform; + std::vector urls; + urls.push_back({server->GetBaseUrl() + "/hello.html"}); + urls.push_back({server->GetBaseUrl() + "/hello.html"}); + + std::vector> sessions; + sessions.push_back(std::make_shared()); + sessions.push_back(std::make_shared()); + + + for (size_t i = 0; i < sessions.size(); ++i) { + sessions.at(i)->SetUrl(urls.at(i)); + multiperform.AddSession(sessions.at(i)); + } + + multiperform.RemoveSession(sessions.at(0)); + + std::vector responses = multiperform.Get(); + EXPECT_EQ(responses.size(), 1); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(urls.at(0), responses.at(0).url); + EXPECT_EQ(std::string{"text/html"}, responses.at(0).header["content-type"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +#ifndef __APPLE__ +/** + * This test case is currently disabled for macOS/Apple systems since it fails in an nondeterministic manner. + * It is probably caused by a bug inside curl_multi_perform on macOS. + * Needs further investigation. + * Issue: https://github.com/libcpr/cpr/issues/841 + **/ +TEST(MultiperformGetTests, MultiperformTenSessionsGetTest) { + const size_t sessionCount = 10; + + MultiPerform multiperform; + Url url{server->GetBaseUrl() + "/hello.html"}; + for (size_t i = 0; i < sessionCount; ++i) { + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + multiperform.AddSession(session); + } + + std::vector responses = multiperform.Get(); + + EXPECT_EQ(responses.size(), sessionCount); + for (Response& response : responses) { + EXPECT_EQ(std::string{"Hello world!"}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} +#endif + +TEST(MultiperformDeleteTests, MultiperformSingleSessionDeleteTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multiperform; + multiperform.AddSession(session); + std::vector responses = multiperform.Delete(); + + EXPECT_EQ(responses.size(), 1); + std::string expected_text{"Delete success"}; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(url, responses.at(0).url); + EXPECT_EQ(std::string{"text/html"}, responses.at(0).header["content-type"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformDownloadTests, MultiperformSingleSessionDownloadTest) { + Url url{server->GetBaseUrl() + "/download_gzip.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + session->SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + MultiPerform multiperform; + multiperform.AddSession(session); + std::vector responses = multiperform.Download(WriteCallback{write_data, 0}); + + EXPECT_EQ(responses.size(), 1); + EXPECT_EQ(url, responses.at(0).url); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(cpr::ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformDownloadTests, MultiperformSingleSessionDownloadNonMatchingArgumentsNumberTest) { + std::shared_ptr session = std::make_shared(); + MultiPerform multiperform; + multiperform.AddSession(session); + EXPECT_THROW(std::vector responses = multiperform.Download(WriteCallback{write_data, 0}, WriteCallback{write_data, 0}), std::invalid_argument); +} + +TEST(MultiperformDownloadTests, MultiperformTwoSessionsDownloadTest) { + MultiPerform multiperform; + std::vector urls; + urls.push_back({server->GetBaseUrl() + "/download_gzip.html"}); + urls.push_back({server->GetBaseUrl() + "/download_gzip.html"}); + + std::vector> sessions; + sessions.push_back(std::make_shared()); + sessions.push_back(std::make_shared()); + + + for (size_t i = 0; i < sessions.size(); ++i) { + sessions.at(i)->SetUrl(urls.at(i)); + sessions.at(i)->SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + + multiperform.AddSession(sessions.at(i)); + } + + std::vector responses = multiperform.Download(WriteCallback{write_data, 0}, WriteCallback{write_data, 0}); + + EXPECT_EQ(responses.size(), sessions.size()); + for (size_t i = 0; i < responses.size(); ++i) { + EXPECT_EQ(urls.at(i), responses.at(i).url); + EXPECT_EQ(200, responses.at(i).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(i).error.code); + } +} + +TEST(MultiperformPutTests, MultiperformSingleSessionPutTest) { + Url url{server->GetBaseUrl() + "/put.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + session->SetPayload(Payload{{"x", "5"}}); + MultiPerform multiperform; + multiperform.AddSession(session); + std::vector responses = multiperform.Put(); + + EXPECT_EQ(responses.size(), 1); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(url, responses.at(0).url); + EXPECT_EQ(std::string{"application/json"}, responses.at(0).header["content-type"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformHeadTests, MultiperformSingleSessionHeadTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multiperform; + multiperform.AddSession(session); + std::vector responses = multiperform.Head(); + + EXPECT_EQ(responses.size(), 1); + std::string expected_text; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(url, responses.at(0).url); + EXPECT_EQ(std::string{"text/html"}, responses.at(0).header["content-type"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformOptionsTests, MultiperformSingleSessionOptionsTest) { + Url url{server->GetBaseUrl() + "/"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multiperform; + multiperform.AddSession(session); + std::vector responses = multiperform.Options(); + + EXPECT_EQ(responses.size(), 1); + std::string expected_text; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(url, responses.at(0).url); + EXPECT_EQ(std::string{"GET, POST, PUT, DELETE, PATCH, OPTIONS"}, responses.at(0).header["Access-Control-Allow-Methods"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformPatchTests, MultiperformSingleSessionPatchTest) { + Url url{server->GetBaseUrl() + "/patch.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + session->SetPayload(Payload{{"x", "5"}}); + MultiPerform multiperform; + multiperform.AddSession(session); + std::vector responses = multiperform.Patch(); + + EXPECT_EQ(responses.size(), 1); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(url, responses.at(0).url); + EXPECT_EQ(std::string{"application/json"}, responses.at(0).header["content-type"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformPostTests, MultiperformSingleSessionPostTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + session->SetPayload(Payload{{"x", "5"}}); + MultiPerform multiperform; + multiperform.AddSession(session); + std::vector responses = multiperform.Post(); + + EXPECT_EQ(responses.size(), 1); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(url, responses.at(0).url); + EXPECT_EQ(std::string{"application/json"}, responses.at(0).header["content-type"]); + EXPECT_EQ(201, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformPerformTests, MultiperformSingleGetPerformTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multiperform; + multiperform.AddSession(session, MultiPerform::HttpMethod::GET_REQUEST); + std::vector responses = multiperform.Perform(); + + EXPECT_EQ(responses.size(), 1); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(url, responses.at(0).url); + EXPECT_EQ(std::string{"text/html"}, responses.at(0).header["content-type"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformPerformTests, MultiperformTwoGetPerformTest) { + MultiPerform multiperform; + std::vector urls; + urls.push_back({server->GetBaseUrl() + "/hello.html"}); + urls.push_back({server->GetBaseUrl() + "/error.html"}); + + std::vector> sessions; + sessions.push_back(std::make_shared()); + sessions.push_back(std::make_shared()); + + + for (size_t i = 0; i < sessions.size(); ++i) { + sessions.at(i)->SetUrl(urls.at(i)); + multiperform.AddSession(sessions.at(i), MultiPerform::HttpMethod::GET_REQUEST); + } + + std::vector responses = multiperform.Perform(); + + EXPECT_EQ(responses.size(), sessions.size()); + std::vector expected_texts; + expected_texts.emplace_back("Hello world!"); + expected_texts.emplace_back("Not Found"); + + std::vector expected_content_types; + expected_content_types.emplace_back("text/html"); + expected_content_types.emplace_back("text/plain"); + + std::vector expected_status_codes; + expected_status_codes.push_back(200); + expected_status_codes.push_back(404); + + for (size_t i = 0; i < responses.size(); ++i) { + EXPECT_EQ(expected_texts.at(i), responses.at(i).text); + EXPECT_EQ(urls.at(i), responses.at(i).url); + EXPECT_EQ(expected_content_types.at(i), responses.at(i).header["content-type"]); + EXPECT_EQ(expected_status_codes.at(i), responses.at(i).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(i).error.code); + } +} + +TEST(MultiperformPerformTests, MultiperformMixedMethodsPerformTest) { + MultiPerform multiperform; + std::vector urls; + urls.push_back({server->GetBaseUrl() + "/hello.html"}); + urls.push_back({server->GetBaseUrl() + "/delete.html"}); + urls.push_back({server->GetBaseUrl() + "/error.html"}); + urls.push_back({server->GetBaseUrl() + "/url_post.html"}); + + std::vector> sessions; + sessions.push_back(std::make_shared()); + sessions.push_back(std::make_shared()); + sessions.push_back(std::make_shared()); + sessions.push_back(std::make_shared()); + + std::vector methods; + methods.push_back(MultiPerform::HttpMethod::GET_REQUEST); + methods.push_back(MultiPerform::HttpMethod::DELETE_REQUEST); + methods.push_back(MultiPerform::HttpMethod::GET_REQUEST); + methods.push_back(MultiPerform::HttpMethod::POST_REQUEST); + + for (size_t i = 0; i < sessions.size(); ++i) { + sessions.at(i)->SetUrl(urls.at(i)); + if (methods.at(i) == MultiPerform::HttpMethod::POST_REQUEST) { + sessions.at(i)->SetPayload(Payload{{"x", "5"}}); + } + multiperform.AddSession(sessions.at(i), methods.at(i)); + } + + std::vector responses = multiperform.Perform(); + + EXPECT_EQ(responses.size(), sessions.size()); + + std::vector expected_texts; + expected_texts.emplace_back("Hello world!"); + expected_texts.emplace_back("Delete success"); + expected_texts.emplace_back("Not Found"); + expected_texts.emplace_back( + "{\n" + " \"x\": 5\n" + "}"); + + std::vector expected_content_types; + expected_content_types.emplace_back("text/html"); + expected_content_types.emplace_back("text/html"); + expected_content_types.emplace_back("text/plain"); + expected_content_types.emplace_back("application/json"); + + std::vector expected_status_codes; + expected_status_codes.push_back(200); + expected_status_codes.push_back(200); + expected_status_codes.push_back(404); + expected_status_codes.push_back(201); + + for (size_t i = 0; i < responses.size(); ++i) { + EXPECT_EQ(expected_texts.at(i), responses.at(i).text); + EXPECT_EQ(urls.at(i), responses.at(i).url); + EXPECT_EQ(expected_content_types.at(i), responses.at(i).header["content-type"]); + EXPECT_EQ(expected_status_codes.at(i), responses.at(i).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(i).error.code); + } +} + +TEST(MultiperformPerformDownloadTests, MultiperformSinglePerformDownloadTest) { + Url url{server->GetBaseUrl() + "/download_gzip.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + session->SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + MultiPerform multiperform; + multiperform.AddSession(session, MultiPerform::HttpMethod::DOWNLOAD_REQUEST); + std::vector responses = multiperform.PerformDownload(WriteCallback{write_data, 0}); + + EXPECT_EQ(responses.size(), 1); + EXPECT_EQ(url, responses.at(0).url); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(cpr::ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformDownloadTests, MultiperformSinglePerformDownloadNonMatchingArgumentsNumberTest) { + std::shared_ptr session = std::make_shared(); + MultiPerform multiperform; + multiperform.AddSession(session, MultiPerform::HttpMethod::DOWNLOAD_REQUEST); + EXPECT_THROW(std::vector responses = multiperform.PerformDownload(WriteCallback{write_data, 0}, WriteCallback{write_data, 0}), std::invalid_argument); +} + +TEST(MultiperformPerformDownloadTests, MultiperformTwoPerformDownloadTest) { + MultiPerform multiperform; + std::vector urls; + urls.push_back({server->GetBaseUrl() + "/download_gzip.html"}); + urls.push_back({server->GetBaseUrl() + "/download_gzip.html"}); + + std::vector> sessions; + sessions.push_back(std::make_shared()); + sessions.push_back(std::make_shared()); + + + for (size_t i = 0; i < sessions.size(); ++i) { + sessions.at(i)->SetUrl(urls.at(i)); + sessions.at(i)->SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + + multiperform.AddSession(sessions.at(i), MultiPerform::HttpMethod::DOWNLOAD_REQUEST); + } + + std::vector responses = multiperform.PerformDownload(WriteCallback{write_data, 0}, WriteCallback{write_data, 0}); + + EXPECT_EQ(responses.size(), sessions.size()); + for (size_t i = 0; i < responses.size(); ++i) { + EXPECT_EQ(urls.at(i), responses.at(i).url); + EXPECT_EQ(200, responses.at(i).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(i).error.code); + } +} + +TEST(MultiperformAPITests, MultiperformApiSingleGetTest) { + std::vector responses = MultiGet(std::tuple{Url{server->GetBaseUrl() + "/hello.html"}}); + EXPECT_EQ(responses.size(), 1); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(Url{server->GetBaseUrl() + "/hello.html"}, responses.at(0).url); + EXPECT_EQ(std::string{"text/html"}, responses.at(0).header["content-type"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformAPITests, MultiperformApiTwoGetsTest) { + std::vector responses = MultiGet(std::tuple{Url{server->GetBaseUrl() + "/long_timeout.html"}, Timeout{1000}}, std::tuple{Url{server->GetBaseUrl() + "/error.html"}}); + + EXPECT_EQ(responses.size(), 2); + std::vector urls; + urls.push_back({server->GetBaseUrl() + "/long_timeout.html"}); + urls.push_back({server->GetBaseUrl() + "/error.html"}); + + std::vector expected_texts; + expected_texts.emplace_back(""); + expected_texts.emplace_back("Not Found"); + + std::vector expected_content_types; + expected_content_types.emplace_back(""); + expected_content_types.emplace_back("text/plain"); + + std::vector expected_status_codes; + expected_status_codes.push_back(0); + expected_status_codes.push_back(404); + + std::vector expected_error_codes; + expected_error_codes.push_back(ErrorCode::OPERATION_TIMEDOUT); + expected_error_codes.push_back(ErrorCode::OK); + + for (size_t i = 0; i < responses.size(); ++i) { + EXPECT_EQ(expected_texts.at(i), responses.at(i).text); + EXPECT_EQ(urls.at(i), responses.at(i).url); + EXPECT_EQ(expected_content_types.at(i), responses.at(i).header["content-type"]); + EXPECT_EQ(expected_status_codes.at(i), responses.at(i).status_code); + EXPECT_EQ(expected_error_codes.at(i), responses.at(i).error.code); + } +} + +TEST(MultiperformAPITests, MultiperformApiSingleDeleteTest) { + std::vector responses = MultiDelete(std::tuple{Url{server->GetBaseUrl() + "/delete.html"}}); + EXPECT_EQ(responses.size(), 1); + std::string expected_text{"Delete success"}; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(Url{server->GetBaseUrl() + "/delete.html"}, responses.at(0).url); + EXPECT_EQ(std::string{"text/html"}, responses.at(0).header["content-type"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformAPITests, MultiperformApiSinglePutTest) { + std::vector responses = MultiPut(std::tuple{Url{server->GetBaseUrl() + "/put.html"}, Payload{{"x", "5"}}}); + EXPECT_EQ(responses.size(), 1); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(Url{server->GetBaseUrl() + "/put.html"}, responses.at(0).url); + EXPECT_EQ(std::string{"application/json"}, responses.at(0).header["content-type"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformAPITests, MultiperformApiSingleHeadTest) { + std::vector responses = MultiHead(std::tuple{Url{server->GetBaseUrl() + "/hello.html"}}); + EXPECT_EQ(responses.size(), 1); + std::string expected_text; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(Url{server->GetBaseUrl() + "/hello.html"}, responses.at(0).url); + EXPECT_EQ(std::string{"text/html"}, responses.at(0).header["content-type"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformAPITests, MultiperformApiSingleOptionsTest) { + std::vector responses = MultiOptions(std::tuple{Url{server->GetBaseUrl() + "/"}}); + EXPECT_EQ(responses.size(), 1); + std::string expected_text; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(Url{server->GetBaseUrl() + "/"}, responses.at(0).url); + EXPECT_EQ(std::string{"GET, POST, PUT, DELETE, PATCH, OPTIONS"}, responses.at(0).header["Access-Control-Allow-Methods"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformAPITests, MultiperformApiSinglePatchTest) { + std::vector responses = MultiPatch(std::tuple{Url{server->GetBaseUrl() + "/patch.html"}, Payload{{"x", "5"}}}); + EXPECT_EQ(responses.size(), 1); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(Url{server->GetBaseUrl() + "/patch.html"}, responses.at(0).url); + EXPECT_EQ(std::string{"application/json"}, responses.at(0).header["content-type"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformAPITests, MultiperformApiSinglePostTest) { + std::vector responses = MultiPost(std::tuple{Url{server->GetBaseUrl() + "/url_post.html"}, Payload{{"x", "5"}}}); + EXPECT_EQ(responses.size(), 1); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(Url{server->GetBaseUrl() + "/url_post.html"}, responses.at(0).url); + EXPECT_EQ(std::string{"application/json"}, responses.at(0).header["content-type"]); + EXPECT_EQ(201, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/test/options_tests.cpp b/test/options_tests.cpp new file mode 100644 index 0000000..ee3176f --- /dev/null +++ b/test/options_tests.cpp @@ -0,0 +1,73 @@ +#include + +#include + +#include + +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +TEST(OptionsTests, BaseUrlTest) { + Url url{server->GetBaseUrl() + "/"}; + Response response = cpr::Options(url); + std::string expected_text{""}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"GET, POST, PUT, DELETE, PATCH, OPTIONS"}, response.header["Access-Control-Allow-Methods"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(OptionsTests, SpecificUrlTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Response response = cpr::Options(url); + std::string expected_text{""}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"GET, POST, PUT, DELETE, PATCH, OPTIONS"}, response.header["Access-Control-Allow-Methods"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(OptionsTests, AsyncBaseUrlTest) { + Url url{server->GetBaseUrl() + "/"}; + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + responses.emplace_back(cpr::OptionsAsync(url)); + } + for (cpr::AsyncResponse& future_response : responses) { + cpr::Response response = future_response.get(); + std::string expected_text{""}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"GET, POST, PUT, DELETE, PATCH, OPTIONS"}, response.header["Access-Control-Allow-Methods"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(OptionsTests, AsyncSpecificUrlTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + responses.emplace_back(cpr::OptionsAsync(url)); + } + for (cpr::AsyncResponse& future_response : responses) { + cpr::Response response = future_response.get(); + std::string expected_text{""}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"GET, POST, PUT, DELETE, PATCH, OPTIONS"}, response.header["Access-Control-Allow-Methods"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/test/patch_tests.cpp b/test/patch_tests.cpp new file mode 100644 index 0000000..22d0b4f --- /dev/null +++ b/test/patch_tests.cpp @@ -0,0 +1,276 @@ +#include + +#include + +#include + +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +TEST(PatchTests, PatchTest) { + Url url{server->GetBaseUrl() + "/patch.html"}; + Payload payload{{"x", "5"}}; + Response response = cpr::Patch(url, payload); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PatchTests, PatchUnallowedTest) { + Url url{server->GetBaseUrl() + "/patch_unallowed.html"}; + Payload payload{{"x", "5"}}; + Response response = cpr::Patch(url, payload); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PatchTests, SessionPatchTest) { + Url url{server->GetBaseUrl() + "/patch.html"}; + Payload payload{{"x", "5"}}; + Session session; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Patch(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PatchTests, SessionPatchUnallowedTest) { + Url url{server->GetBaseUrl() + "/patch_unallowed.html"}; + Payload payload{{"x", "5"}}; + Session session; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Patch(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PatchTests, SessionPatchAfterGetTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/get.html"}; + session.SetUrl(url); + Response response = session.Get(); + } + Url url{server->GetBaseUrl() + "/patch.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Patch(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PatchTests, SessionPatchUnallowedAfterGetTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/get.html"}; + session.SetUrl(url); + Response response = session.Get(); + } + Url url{server->GetBaseUrl() + "/patch_unallowed.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Patch(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PatchTests, SessionPatchAfterHeadTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/get.html"}; + session.SetUrl(url); + Response response = session.Head(); + } + Url url{server->GetBaseUrl() + "/patch.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Patch(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PatchTests, SessionPatchUnallowedAfterHeadTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/get.html"}; + session.SetUrl(url); + Response response = session.Head(); + } + Url url{server->GetBaseUrl() + "/patch_unallowed.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Patch(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PatchTests, SessionPatchAfterPostTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + Response response = session.Post(); + } + Url url{server->GetBaseUrl() + "/patch.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Patch(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PatchTests, SessionPatchUnallowedAfterPostTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + Response response = session.Post(); + } + Url url{server->GetBaseUrl() + "/patch_unallowed.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Patch(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PatchTests, AsyncPatchTest) { + Url url{server->GetBaseUrl() + "/patch.html"}; + Payload payload{{"x", "5"}}; + cpr::AsyncResponse future_response = cpr::PatchAsync(url, payload); + cpr::Response response = future_response.get(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PatchTests, AsyncPatchUnallowedTest) { + Url url{server->GetBaseUrl() + "/patch_unallowed.html"}; + Payload payload{{"x", "5"}}; + cpr::AsyncResponse future_response = cpr::PatchAsync(url, payload); + cpr::Response response = future_response.get(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PatchTests, AsyncMultiplePatchTest) { + Url url{server->GetBaseUrl() + "/patch.html"}; + Payload payload{{"x", "5"}}; + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + responses.emplace_back(cpr::PatchAsync(url, payload)); + } + for (cpr::AsyncResponse& future_response : responses) { + cpr::Response response = future_response.get(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(PatchTests, AsyncMultiplePatchUnallowedTest) { + Url url{server->GetBaseUrl() + "/patch_unallowed.html"}; + Payload payload{{"x", "5"}}; + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + responses.emplace_back(cpr::PatchAsync(url, payload)); + } + for (cpr::AsyncResponse& future_response : responses) { + cpr::Response response = future_response.get(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/test/post_tests.cpp b/test/post_tests.cpp new file mode 100644 index 0000000..14603a4 --- /dev/null +++ b/test/post_tests.cpp @@ -0,0 +1,756 @@ +#include + +#include +#include +#include +#include + +#include +#include + +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +TEST(UrlEncodedPostTests, UrlPostSingleTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Response response = cpr::Post(url, Payload{{"x", "5"}}); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, UrlPostAddPayloadPair) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "1"}}; + payload.Add({"y", "2"}); + Response response = cpr::Post(url, payload); + std::string expected_text{ + "{\n" + " \"x\": 1,\n" + " \"y\": 2,\n" + " \"sum\": 3\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); +} + +TEST(UrlEncodedPostTests, UrlPostPayloadIteratorTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + std::vector payloadData; + payloadData.emplace_back("x", "1"); + payloadData.emplace_back("y", "2"); + Response response = cpr::Post(url, Payload(payloadData.begin(), payloadData.end())); + std::string expected_text{ + "{\n" + " \"x\": 1,\n" + " \"y\": 2,\n" + " \"sum\": 3\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); +} + +TEST(UrlEncodedPostTests, UrlPostEncodeTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Response response = cpr::Post(url, Payload{{"x", "hello world!!~"}}); + std::string expected_text{ + "{\n" + " \"x\": hello world!!~\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, UrlPostEncodeNoCopyTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "hello world!!~"}}; + // payload lives through the lifetime of Post, so it doesn't need to be copied + Response response = cpr::Post(url, payload); + std::string expected_text{ + "{\n" + " \"x\": hello world!!~\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, UrlPostManyTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Response response = cpr::Post(url, Payload{{"x", "5"}, {"y", "13"}}); + std::string expected_text{ + "{\n" + " \"x\": 5,\n" + " \"y\": 13,\n" + " \"sum\": 18\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, UrlPostBadHostTest) { + Url url{"http://bad_host/"}; + Response response = cpr::Post(url, Payload{{"hello", "world"}}); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{}, response.header["content-type"]); + EXPECT_EQ(0, response.status_code); + EXPECT_EQ(ErrorCode::HOST_RESOLUTION_FAILURE, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostSingleTest) { + Url url{server->GetBaseUrl() + "/form_post.html"}; + Response response = cpr::Post(url, Multipart{{"x", 5}}); + std::string expected_text{ + "{\n" + " \"x\": \"5\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostFileTest) { + std::string filename{"test_file"}; + std::string content{"hello world"}; + std::ofstream test_file; + test_file.open(filename); + test_file << content; + test_file.close(); + Url url{server->GetBaseUrl() + "/form_post.html"}; + Response response = cpr::Post(url, Multipart{{"x", File{filename}}}); + std::string expected_text{ + "{\n" + " \"x\": \"test_file=" + + content + + "\"\n" + "}"}; + std::remove(filename.c_str()); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostMultipleFilesTestLvalue) { + Url url{server->GetBaseUrl() + "/form_post.html"}; + std::string filename1{"file1"}; + std::string content1{"apple"}; + std::ofstream file1; + file1.open(filename1); + file1 << content1; + file1.close(); + std::string filename2{"file2"}; + std::string content2{"banana"}; + std::ofstream file2; + file2.open(filename2); + file2 << content2; + file2.close(); + File singleFile{"file1"}; + File singleFileWithOverridenFilename{"file1", "applefile"}; + Files multipleFiles{"file1", "file2"}; + Files multipleFilesWithOverridenFilename{ + File{"file1", "applefile"}, + File{"file2", "bananafile"}, + }; + { + Response response = cpr::Post(url, Multipart{{"files", singleFile}}); + std::string expected_text{ + "{\n" + " \"files\": \"file1=" + + content1 + "\"\n}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = cpr::Post(url, Multipart{{"singleFile", singleFileWithOverridenFilename}}); + std::string expected_text{ + "{\n" + " \"singleFile\": \"applefile=" + + content1 + "\"\n}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = cpr::Post(url, Multipart{{"files", multipleFiles}}); + std::string expected_text{ + "{\n" + " \"files\": \"file1=" + + content1 + + "\",\n" + " \"files\": \"file2=" + + content2 + "\"\n}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = cpr::Post(url, Multipart{{"files", multipleFilesWithOverridenFilename}}); + std::string expected_text{ + "{\n" + " \"files\": \"applefile=" + + content1 + + "\",\n" + " \"files\": \"bananafile=" + + content2 + "\"\n}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + std::remove(filename1.c_str()); + std::remove(filename2.c_str()); +} + +TEST(UrlEncodedPostTests, FormPostMultipleFilesTestRvalue) { + Url url{server->GetBaseUrl() + "/form_post.html"}; + std::string filename1{"file1"}; + std::string content1{"apple"}; + std::ofstream file1; + file1.open(filename1); + file1 << content1; + file1.close(); + std::string filename2{"file2"}; + std::string content2{"banana"}; + std::ofstream file2; + file2.open(filename2); + file2 << content2; + file2.close(); + { + Response response = cpr::Post(url, Multipart{{"files", File{"file1"}}}); + std::string expected_text{ + "{\n" + " \"files\": \"file1=" + + content1 + "\"\n}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = cpr::Post(url, Multipart{{"files", File{"file1", "applefile"}}}); + std::string expected_text{ + "{\n" + " \"files\": \"applefile=" + + content1 + "\"\n}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = cpr::Post(url, Multipart{{"files", Files{"file1", "file2"}}}); + std::string expected_text{ + "{\n" + " \"files\": \"file1=" + + content1 + + "\",\n" + " \"files\": \"file2=" + + content2 + "\"\n}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = cpr::Post(url, Multipart{{"files", Files{ + File{"file1", "applefile"}, + File{"file2", "bananafile"}, + }}}); + std::string expected_text{ + "{\n" + " \"files\": \"applefile=" + + content1 + + "\",\n" + " \"files\": \"bananafile=" + + content2 + "\"\n}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + std::remove(filename1.c_str()); + std::remove(filename2.c_str()); +} + +TEST(UrlEncodedPostTests, FormPostFileTestWithOverridenFilename) { + std::string filename{"test_file"}; + std::string overided_filename{"overided_filename"}; + std::string content{"hello world"}; + std::ofstream test_file; + test_file.open(filename); + test_file << content; + test_file.close(); + Url url{server->GetBaseUrl() + "/form_post.html"}; + + Response response = cpr::Post(url, Multipart{{"x", File{filename, overided_filename}}}); + std::string expected_text{ + "{\n" + " \"x\": \"" + + overided_filename + "=" + content + + "\"\n" + "}"}; + std::remove(filename.c_str()); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostFileNoCopyTest) { + std::string filename{"./test_file"}; + std::string content{"hello world"}; + std::ofstream test_file; + test_file.open(filename); + test_file << content; + test_file.close(); + Url url{server->GetBaseUrl() + "/form_post.html"}; + Multipart multipart{{"x", File{filename}}}; + Response response = cpr::Post(url, multipart); + std::string expected_text{ + "{\n" + " \"x\": \"test_file=" + + content + + "\"\n" + "}"}; + std::remove(filename.c_str()); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostFileNoCopyTestWithOverridenFilename) { + std::string filename{"test_file"}; + std::string overriden_filename{"overided_filename"}; + std::string content{"hello world"}; + std::ofstream test_file; + test_file.open(filename); + test_file << content; + test_file.close(); + Url url{server->GetBaseUrl() + "/form_post.html"}; + Multipart multipart{{"x", File{filename, overriden_filename}}}; + Response response = cpr::Post(url, multipart); + std::string expected_text{ + "{\n" + " \"x\": \"" + + overriden_filename + "=" + content + + "\"\n" + "}"}; + std::remove(filename.c_str()); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, TimeoutPostTest) { + Url url{server->GetBaseUrl() + "/json_post.html"}; + std::string body{R"({"RegisterObject": {"DeviceID": "65010000005030000001"}})"}; + cpr::Response response = cpr::Post(url, cpr::Header{{"Content-Type", "application/json"}}, cpr::Body{body}, cpr::ConnectTimeout{3000}, cpr::Timeout{3000}); + std::string expected_text{R"({"RegisterObject": {"DeviceID": "65010000005030000001"}})"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostFileBufferTest) { + std::string content{"hello world"}; + Url url{server->GetBaseUrl() + "/form_post.html"}; + Response response = cpr::Post(url, Multipart{{"x", Buffer{content.begin(), content.end(), "test_file"}}}); + std::string expected_text{ + "{\n" + " \"x\": \"test_file=" + + content + + "\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostFileBufferNoCopyTest) { + std::string content{"hello world"}; + Url url{server->GetBaseUrl() + "/form_post.html"}; + Multipart multipart{{"x", Buffer{content.begin(), content.end(), "test_file"}}}; + Response response = cpr::Post(url, multipart); + std::string expected_text{ + "{\n" + " \"x\": \"test_file=" + + content + + "\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostFileBufferPointerTest) { + const char* content = "hello world"; + Url url{server->GetBaseUrl() + "/form_post.html"}; + Response response = cpr::Post(url, Multipart{{"x", Buffer{content, 11 + content, "test_file"}}}); + std::string expected_text{ + "{\n" + " \"x\": \"test_file=" + + std::string(content) + + "\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostFileBufferArrayTest) { + const char content[] = "hello world"; + Url url{server->GetBaseUrl() + "/form_post.html"}; + // We subtract 1 from std::end() because we don't want to include the terminating null + Response response = cpr::Post(url, Multipart{{"x", Buffer{std::begin(content), std::end(content) - 1, "test_file"}}}); + std::string expected_text{ + "{\n" + " \"x\": \"test_file=" + + std::string(content) + + "\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostFileBufferVectorTest) { + std::vector content{'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'}; + Url url{server->GetBaseUrl() + "/form_post.html"}; + Response response = cpr::Post(url, Multipart{{"x", Buffer{content.begin(), content.end(), "test_file"}}}); + std::string expected_text{ + "{\n" + " \"x\": \"test_file=hello world\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostFileBufferStdArrayTest) { + std::array content{{'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'}}; + Url url{server->GetBaseUrl() + "/form_post.html"}; + Response response = cpr::Post(url, Multipart{{"x", Buffer{content.begin(), content.end(), "test_file"}}}); + std::string expected_text{ + "{\n" + " \"x\": \"test_file=hello world\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostBufferRvalueTest) { + std::vector content{'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'}; + Url url{server->GetBaseUrl() + "/form_post.html"}; + Response response = cpr::Post(url, Multipart{{"x", Buffer{content.begin(), content.end(), "test_file"}}}); + std::string expected_text{ + "{\n" + " \"x\": \"test_file=hello world\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, ReflectPostBufferLvalueTest) { + std::vector content{'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'}; + Url url{server->GetBaseUrl() + "/form_post.html"}; + Buffer buff{content.begin(), content.end(), "test_file"}; + Response response = cpr::Post(url, Multipart{{"x", buff}}); + std::string expected_text{ + "{\n" + " \"x\": \"test_file=hello world\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostManyTest) { + Url url{server->GetBaseUrl() + "/form_post.html"}; + Response response = cpr::Post(url, Multipart{{"x", 5}, {"y", 13}}); + std::string expected_text{ + "{\n" + " \"x\": \"5\",\n" + " \"y\": \"13\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostManyNoCopyTest) { + Url url{server->GetBaseUrl() + "/form_post.html"}; + Multipart multipart{{"x", 5}, {"y", 13}}; + Response response = cpr::Post(url, multipart); + std::string expected_text{ + "{\n" + " \"x\": \"5\",\n" + " \"y\": \"13\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostContentTypeTest) { + Url url{server->GetBaseUrl() + "/form_post.html"}; + Response response = cpr::Post(url, Multipart{{"x", 5, "application/number"}}); + std::string expected_text{ + "{\n" + " \"x\": \"5\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostContentTypeLValueTest) { + Url url{server->GetBaseUrl() + "/form_post.html"}; + Multipart multipart{{"x", 5, "application/number"}}; + Response response = cpr::Post(url, multipart); + std::string expected_text{ + "{\n" + " \"x\": \"5\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, UrlPostAsyncSingleTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + responses.emplace_back(cpr::PostAsync(url, payload)); + } + for (cpr::AsyncResponse& future_response : responses) { + cpr::Response response = future_response.get(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(UrlEncodedPostTests, UrlReflectTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Post(url, Payload{{"x", "5"}}); + std::string expected_text{"Header reflect POST"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, PostWithNoBodyTest) { + Url url{server->GetBaseUrl() + "/form_post.html"}; + Response response = cpr::Post(url); + std::string expected_text{"{\n}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +static std::string getTimestamp() { + char buf[1000]; + time_t now = time(0); + struct tm* tm = gmtime(&now); + strftime(buf, sizeof buf, "%a, %d %b %Y %H:%M:%S GMT", tm); + return buf; +} + +TEST(UrlEncodedPostTests, PostReflectTest) { + std::string uri = server->GetBaseUrl() + "/post_reflect.html"; + std::string body = R"({"property1": "value1"})"; + std::string contentType = "application/json"; + std::string signature = "x-ms-date: something"; + std::string logType = "LoggingTest"; + std::string date = getTimestamp(); + Response response = cpr::Post(cpr::Url(uri), cpr::Header{{"content-type", contentType}, {"Authorization", signature}, {"log-type", logType}, {"x-ms-date", date}, {"content-length", std::to_string(body.length())}}, cpr::Body(body)); + EXPECT_EQ(ErrorCode::OK, response.error.code); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(body, response.text); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(signature, response.header["Authorization"]); + EXPECT_EQ(logType, response.header["log-type"]); + EXPECT_EQ(date, response.header["x-ms-date"]); + EXPECT_EQ(std::to_string(body.length()), response.header["content-length"]); +} + +TEST(UrlEncodedPostTests, PostReflectPayloadTest) { + std::string uri = server->GetBaseUrl() + "/header_reflect.html"; + cpr::Payload payload = cpr::Payload{{"email", ""}, {"password", ""}, {"devicetoken", ""}}; + cpr::Response response = cpr::Post(cpr::Url(uri), cpr::Timeout{10000}, payload); + + EXPECT_EQ(ErrorCode::OK, response.error.code); + EXPECT_EQ(200, response.status_code); +} + +TEST(UrlEncodedPostTests, InjectMultipleHeadersTest) { + std::string uri = server->GetBaseUrl() + "/post_reflect.html"; + std::string key_1 = "key_1"; + std::string val_1 = "value_1"; + std::string key_2 = "key_2"; + std::string val_2 = "value_2"; + cpr::Response response = cpr::Post(cpr::Url{uri}, cpr::Header{{key_1, val_1}}, cpr::Header{{key_2, val_2}}); + + EXPECT_EQ(ErrorCode::OK, response.error.code); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(val_1, response.header[key_1]); + EXPECT_EQ(val_2, response.header[key_2]); +} + +TEST(UrlEncodedPostTests, PostBodyWithFile) { + std::string filename{"test_file"}; + std::string expected_text(R"({"property1": "value1"})"); + std::ofstream test_file; + test_file.open(filename); + test_file << expected_text; + test_file.close(); + Url url{server->GetBaseUrl() + "/post_reflect.html"}; + cpr::Response response = Post(url, cpr::Header({{"Content-Type", "application/octet-stream"}}), cpr::Body(File("test_file"))); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(ErrorCode::OK, response.error.code); + EXPECT_EQ(std::string{"application/octet-stream"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); +} + +TEST(UrlEncodedPostTests, PostBodyWithBuffer) { + Url url{server->GetBaseUrl() + "/post_reflect.html"}; + std::string expected_text(R"({"property1": "value1"})"); + cpr::Response response = Post(url, cpr::Header({{"Content-Type", "application/octet-stream"}}), cpr::Body(Buffer{expected_text.begin(), expected_text.end(), "test_file"})); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/octet-stream"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PostRedirectTests, TempRedirectTest) { + Url url{server->GetBaseUrl() + "/temporary_redirect.html"}; + Response response = cpr::Post(url, Payload{{"x", "5"}}, Header{{"RedirectLocation", "url_post.html"}}); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(response.url, server->GetBaseUrl() + "/url_post.html"); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PostRedirectTests, TempRedirectNoneTest) { + Url url{server->GetBaseUrl() + "/temporary_redirect.html"}; + Response response = cpr::Post(url, Payload{{"x", "5"}}, Header{{"RedirectLocation", "url_post.html"}}, Redirect(PostRedirectFlags::NONE)); + EXPECT_EQ(response.url, server->GetBaseUrl() + "/url_post.html"); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PostRedirectTests, PermRedirectTest) { + Url url{server->GetBaseUrl() + "/permanent_redirect.html"}; + Response response = cpr::Post(url, Payload{{"x", "5"}}, Header{{"RedirectLocation", "url_post.html"}}); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(response.url, server->GetBaseUrl() + "/url_post.html"); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PostRedirectTests, PermRedirectNoneTest) { + Url url{server->GetBaseUrl() + "/permanent_redirect.html"}; + Response response = cpr::Post(url, Payload{{"x", "5"}}, Header{{"RedirectLocation", "url_post.html"}}, Redirect(PostRedirectFlags::NONE)); + EXPECT_EQ(response.url, server->GetBaseUrl() + "/url_post.html"); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/test/prepare_tests.cpp b/test/prepare_tests.cpp new file mode 100644 index 0000000..d7332c9 --- /dev/null +++ b/test/prepare_tests.cpp @@ -0,0 +1,138 @@ +#include + +#include +#include + +#include + +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +TEST(PrepareTests, GetTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.PrepareGet(); + CURLcode curl_result = curl_easy_perform(session.GetCurlHolder()->handle); + Response response = session.Complete(curl_result); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PrepareTests, OptionsTests) { + Url url{server->GetBaseUrl() + "/"}; + Session session; + session.SetUrl(url); + session.PrepareOptions(); + CURLcode curl_result = curl_easy_perform(session.GetCurlHolder()->handle); + Response response = session.Complete(curl_result); + std::string expected_text{""}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"GET, POST, PUT, DELETE, PATCH, OPTIONS"}, response.header["Access-Control-Allow-Methods"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PrepareTests, PatchTest) { + Url url{server->GetBaseUrl() + "/patch.html"}; + Payload payload{{"x", "5"}}; + Session session; + session.SetUrl(url); + session.SetPayload(payload); + session.PreparePatch(); + CURLcode curl_result = curl_easy_perform(session.GetCurlHolder()->handle); + Response response = session.Complete(curl_result); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PrepareTests, MultipleDeleteHeadPutGetPostTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Url urlPost{server->GetBaseUrl() + "/post_reflect.html"}; + Url urlPut{server->GetBaseUrl() + "/put.html"}; + Session session; + for (size_t i = 0; i < 3; ++i) { + { + session.SetUrl(url); + session.PrepareDelete(); + CURLcode curl_result = curl_easy_perform(session.GetCurlHolder()->handle); + Response response = session.Complete(curl_result); + std::string expected_text{"Header reflect DELETE"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + session.SetUrl(urlPost); + std::string expectedBody = "a1b2c3Post"; + session.SetBody(expectedBody); + session.PreparePost(); + CURLcode curl_result = curl_easy_perform(session.GetCurlHolder()->handle); + Response response = session.Complete(curl_result); + EXPECT_EQ(expectedBody, response.text); + EXPECT_EQ(urlPost, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + session.SetUrl(url); + session.PrepareGet(); + CURLcode curl_result = curl_easy_perform(session.GetCurlHolder()->handle); + Response response = session.Complete(curl_result); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + session.SetUrl(urlPut); + session.SetPayload({{"x", "5"}}); + session.PreparePut(); + CURLcode curl_result = curl_easy_perform(session.GetCurlHolder()->handle); + Response response = session.Complete(curl_result); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(urlPut, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + session.SetUrl(url); + session.PrepareHead(); + CURLcode curl_result = curl_easy_perform(session.GetCurlHolder()->handle); + Response response = session.Complete(curl_result); + std::string expected_text{"Header reflect HEAD"}; + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + } +} + + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/test/proxy_auth_tests.cpp b/test/proxy_auth_tests.cpp new file mode 100644 index 0000000..f618ba2 --- /dev/null +++ b/test/proxy_auth_tests.cpp @@ -0,0 +1,94 @@ +#include + +#include + +#include "cpr/cpr.h" +#include "httpServer.hpp" + +// TODO: This requires a local proxy server (squid). This should be replaced with a source +// code implementation. + +#define HTTP_PROXY "127.0.0.1:3128" +#define HTTPS_PROXY "127.0.0.1:3128" +#define PROXY_USER "u$er" +#define PROXY_PASS "p@ss" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +// TODO: These should be fixed after a source code implementation of a proxy +#if defined(false) +TEST(ProxyAuthTests, SingleProxyTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Response response = cpr::Get(url, Proxies{{"http", HTTP_PROXY}}, ProxyAuthentication{{"http", EncodedAuthentication{PROXY_USER, PROXY_PASS}}}); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ProxyAuthTests, MultipleProxyHttpTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Response response = cpr::Get(url, Proxies{{"https", HTTPS_PROXY}, {"http", HTTP_PROXY}}, ProxyAuthentication{{"http", EncodedAuthentication{PROXY_USER, PROXY_PASS}}, {"https", EncodedAuthentication{PROXY_USER, PROXY_PASS}}}); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ProxyAuthTests, CopyProxyTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Proxies proxies{{"http", HTTP_PROXY}}; + ProxyAuthentication proxy_auth{{"http", EncodedAuthentication{PROXY_USER, PROXY_PASS}}}; + Response response = cpr::Get(url, proxies, proxy_auth); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ProxyAuthTests, ProxySessionTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetProxies(Proxies{{"http", HTTP_PROXY}}); + session.SetProxyAuth(ProxyAuthentication{{"http", EncodedAuthentication{PROXY_USER, PROXY_PASS}}}); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ProxyAuthTests, ReferenceProxySessionTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Proxies proxies{{"http", HTTP_PROXY}}; + ProxyAuthentication proxy_auth{{"http", EncodedAuthentication{PROXY_USER, PROXY_PASS}}}; + Session session; + session.SetUrl(url); + session.SetProxies(proxies); + session.SetProxyAuth(proxy_auth); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} +#endif + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/test/proxy_tests.cpp b/test/proxy_tests.cpp new file mode 100644 index 0000000..f43c4c2 --- /dev/null +++ b/test/proxy_tests.cpp @@ -0,0 +1,92 @@ +#include + +#include + +#include + +// TODO: This uses public servers for proxies and endpoints. This should be replaced with a source +// code implementation inside server.cpp + +#define HTTP_PROXY "51.159.4.98:80" +#define HTTPS_PROXY "51.104.53.182:8000" + +using namespace cpr; + +TEST(ProxyTests, SingleProxyTest) { + Url url{"http://www.httpbin.org/get"}; + Response response = cpr::Get(url, Proxies{{"http", HTTP_PROXY}}); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ProxyTests, MultipleProxyHttpTest) { + Url url{"http://www.httpbin.org/get"}; + Response response = cpr::Get(url, Proxies{{"http", HTTP_PROXY}, {"https", HTTPS_PROXY}}); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +// TODO: These should be fixed after a source code implementation of an HTTPS proxy +#if defined(false) +TEST(ProxyTests, ProxyHttpsTest) { + Url url{"https://www.httpbin.org/get"}; + Response response = cpr::Get(url, Proxies{{"https", HTTPS_PROXY}}); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ProxyTests, MultipleProxyHttpsTest) { + Url url{"https://www.httpbin.org/get"}; + Response response = cpr::Get(url, Proxies{{"http", HTTP_PROXY}, {"https", HTTPS_PROXY}}); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} +#endif + +TEST(ProxyTests, CopyProxyTest) { + Url url{"http://www.httpbin.org/get"}; + Proxies proxies{{"http", HTTP_PROXY}}; + Response response = cpr::Get(url, proxies); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ProxyTests, ProxySessionTest) { + Url url{"http://www.httpbin.org/get"}; + Session session; + session.SetUrl(url); + session.SetProxies(Proxies{{"http", HTTP_PROXY}}); + Response response = session.Get(); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ProxyTests, ReferenceProxySessionTest) { + Url url{"http://www.httpbin.org/get"}; + Proxies proxies{{"http", HTTP_PROXY}}; + Session session; + session.SetUrl(url); + session.SetProxies(proxies); + Response response = session.Get(); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/put_tests.cpp b/test/put_tests.cpp new file mode 100644 index 0000000..34e083f --- /dev/null +++ b/test/put_tests.cpp @@ -0,0 +1,276 @@ +#include + +#include + +#include + +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +TEST(PutTests, PutTest) { + Url url{server->GetBaseUrl() + "/put.html"}; + Payload payload{{"x", "5"}}; + Response response = cpr::Put(url, payload); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PutTests, PutUnallowedTest) { + Url url{server->GetBaseUrl() + "/put_unallowed.html"}; + Payload payload{{"x", "5"}}; + Response response = cpr::Put(url, payload); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PutTests, SessionPutTest) { + Url url{server->GetBaseUrl() + "/put.html"}; + Payload payload{{"x", "5"}}; + Session session; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Put(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PutTests, SessionPutUnallowedTest) { + Url url{server->GetBaseUrl() + "/put_unallowed.html"}; + Payload payload{{"x", "5"}}; + Session session; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Put(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PutTests, SessionPutAfterGetTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/get.html"}; + session.SetUrl(url); + Response response = session.Get(); + } + Url url{server->GetBaseUrl() + "/put.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Put(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PutTests, SessionPutUnallowedAfterGetTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/get.html"}; + session.SetUrl(url); + Response response = session.Get(); + } + Url url{server->GetBaseUrl() + "/put_unallowed.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Put(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PutTests, SessionPutAfterHeadTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/get.html"}; + session.SetUrl(url); + Response response = session.Head(); + } + Url url{server->GetBaseUrl() + "/put.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Put(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PutTests, SessionPutUnallowedAfterHeadTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/get.html"}; + session.SetUrl(url); + Response response = session.Head(); + } + Url url{server->GetBaseUrl() + "/put_unallowed.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Put(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PutTests, SessionPutAfterPostTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + Response response = session.Post(); + } + Url url{server->GetBaseUrl() + "/put.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Put(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PutTests, SessionPutUnallowedAfterPostTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + Response response = session.Post(); + } + Url url{server->GetBaseUrl() + "/put_unallowed.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Put(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PutTests, AsyncPutTest) { + Url url{server->GetBaseUrl() + "/put.html"}; + Payload payload{{"x", "5"}}; + cpr::AsyncResponse future_response = cpr::PutAsync(url, payload); + cpr::Response response = future_response.get(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PutTests, AsyncPutUnallowedTest) { + Url url{server->GetBaseUrl() + "/put_unallowed.html"}; + Payload payload{{"x", "5"}}; + cpr::AsyncResponse future_response = cpr::PutAsync(url, payload); + cpr::Response response = future_response.get(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PutTests, AsyncMultiplePutTest) { + Url url{server->GetBaseUrl() + "/put.html"}; + Payload payload{{"x", "5"}}; + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + responses.emplace_back(cpr::PutAsync(url, payload)); + } + for (cpr::AsyncResponse& future_response : responses) { + cpr::Response response = future_response.get(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(PutTests, AsyncMultiplePutUnallowedTest) { + Url url{server->GetBaseUrl() + "/put_unallowed.html"}; + Payload payload{{"x", "5"}}; + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + responses.emplace_back(cpr::PutAsync(url, payload)); + } + for (cpr::AsyncResponse& future_response : responses) { + cpr::Response response = future_response.get(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/test/raw_body_tests.cpp b/test/raw_body_tests.cpp new file mode 100644 index 0000000..2416e4e --- /dev/null +++ b/test/raw_body_tests.cpp @@ -0,0 +1,134 @@ +#include + +#include +#include +#include + +#include +#include + +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +TEST(BodyPostTests, DefaultUrlEncodedPostTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Response response = cpr::Post(url, Body{"x=5"}); + std::string expected_text = "{\n \"x\": 5\n}"; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BodyPostTests, TextUrlEncodedPostTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Response response = cpr::Post(url, Body{"x=hello world!!~"}); + std::string expected_text{ + "{\n" + " \"x\": hello world!!~\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BodyPostTests, TextUrlEncodedNoCopyPostTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Body body{"x=hello world!!~"}; + // body lives through the lifetime of Post, so it doesn't need to be copied + Response response = cpr::Post(url, body); + std::string expected_text{ + "{\n" + " \"x\": hello world!!~\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BodyPostTests, UrlEncodedManyPostTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Response response = cpr::Post(url, Body{"x=5&y=13"}); + std::string expected_text{ + "{\n" + " \"x\": 5,\n" + " \"y\": 13,\n" + " \"sum\": 18\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BodyPostTests, CustomHeaderNumberPostTest) { + Url url{server->GetBaseUrl() + "/json_post.html"}; + Response response = cpr::Post(url, Body{"{\"x\":5}"}, Header{{"Content-Type", "application/json"}}); + std::string expected_text{"{\"x\":5}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BodyPostTests, CustomHeaderTextPostTest) { + Url url{server->GetBaseUrl() + "/json_post.html"}; + Response response = cpr::Post(url, Body{"{\"x\":\"hello world!!~\"}"}, Header{{"Content-Type", "application/json"}}); + std::string expected_text{"{\"x\":\"hello world!!~\"}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BodyPostTests, CustomWrongHeaderPostTest) { + Url url{server->GetBaseUrl() + "/json_post.html"}; + Response response = cpr::Post(url, Body{"{\"x\":5}"}, Header{{"Content-Type", "text/plain"}}); + std::string expected_text{"Unsupported Media Type"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(415, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BodyPostTests, UrlPostBadHostTest) { + Url url{"http://bad_host/"}; + Response response = cpr::Post(url, Body{"hello=world"}); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{}, response.header["content-type"]); + EXPECT_EQ(0, response.status_code); + EXPECT_EQ(ErrorCode::HOST_RESOLUTION_FAILURE, response.error.code); +} + +TEST(BodyPostTests, StringMoveBodyTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Response response = cpr::Post(url, Body{std::string{"x=5"}}); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/test/resolve_tests.cpp b/test/resolve_tests.cpp new file mode 100644 index 0000000..bf67541 --- /dev/null +++ b/test/resolve_tests.cpp @@ -0,0 +1,44 @@ +#include + +#include + +#include "cpr/cpr.h" +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +TEST(ResolveTests, HelloWorldTest) { + Url url{"http://www.example.com:" + std::to_string(server->GetPort()) + "/hello.html"}; + Resolve resolve{"www.example.com", "127.0.0.1", {server->GetPort()}}; + Response response = cpr::Get(url, resolve); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ResolveTests, RedirectMultiple) { + Url url1{"http://www.example0.com:" + std::to_string(server->GetPort()) + "/resolve_permanent_redirect.html"}; + Url url2{"http://www.example1.com:" + std::to_string(server->GetPort()) + "/hello.html"}; + Resolve resolve1{"www.example0.com", "127.0.0.1", {server->GetPort()}}; + Resolve resolve2{"www.example1.com", "127.0.0.1", {server->GetPort()}}; + + Response response = cpr::Get(url1, std::vector{resolve1, resolve2}, Header{{"RedirectLocation", url2.str()}}); + + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url2, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/test/session_tests.cpp b/test/session_tests.cpp new file mode 100644 index 0000000..f298c88 --- /dev/null +++ b/test/session_tests.cpp @@ -0,0 +1,1510 @@ +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include "cpr/accept_encoding.h" +#include "httpServer.hpp" + +using namespace cpr; +using namespace std::chrono_literals; + +static HttpServer* server = new HttpServer(); +std::chrono::milliseconds sleep_time{50}; +std::chrono::seconds zero{0}; + +bool write_data(std::string /*data*/, intptr_t /*userdata*/) { + return true; +} + +TEST(RedirectTests, TemporaryDefaultRedirectTest) { + Url url{server->GetBaseUrl() + "/temporary_redirect.html"}; + Session session; + session.SetUrl(url); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{server->GetBaseUrl() + "/hello.html"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(RedirectTests, NoTemporaryRedirectTest) { + Url url{server->GetBaseUrl() + "/temporary_redirect.html"}; + Session session; + session.SetUrl(url); + session.SetRedirect(Redirect(false)); + Response response = session.Get(); + std::string expected_text{"Moved Temporarily"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{}, response.header["content-type"]); + EXPECT_EQ(302, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(RedirectTests, PermanentDefaultRedirectTest) { + Url url{server->GetBaseUrl() + "/permanent_redirect.html"}; + Session session; + session.SetUrl(url); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{server->GetBaseUrl() + "/hello.html"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(RedirectTests, NoPermanentRedirectTest) { + Url url{server->GetBaseUrl() + "/permanent_redirect.html"}; + Session session; + session.SetUrl(url); + session.SetRedirect(Redirect(false)); + Response response = session.Get(); + std::string expected_text{"Moved Permanently"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{}, response.header["content-type"]); + EXPECT_EQ(301, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(MaxRedirectsTests, ZeroMaxRedirectsSuccessTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetRedirect(Redirect(0L)); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(MaxRedirectsTests, ZeroMaxRedirectsFailureTest) { + Url url{server->GetBaseUrl() + "/permanent_redirect.html"}; + Session session; + session.SetUrl(url); + session.SetRedirect(Redirect(0L)); + Response response = session.Get(); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{}, response.header["content-type"]); + EXPECT_EQ(301, response.status_code); + EXPECT_EQ(ErrorCode::TOO_MANY_REDIRECTS, response.error.code); +} + +TEST(MaxRedirectsTests, OneMaxRedirectsSuccessTest) { + Url url{server->GetBaseUrl() + "/permanent_redirect.html"}; + Session session; + session.SetUrl(url); + session.SetRedirect(Redirect(1L)); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{server->GetBaseUrl() + "/hello.html"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(MaxRedirectsTests, OneMaxRedirectsFailureTest) { + Url url{server->GetBaseUrl() + "/two_redirects.html"}; + Session session; + session.SetUrl(url); + session.SetRedirect(Redirect(1L)); + Response response = session.Get(); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(Url{server->GetBaseUrl() + "/permanent_redirect.html"}, response.url); + EXPECT_EQ(std::string{}, response.header["content-type"]); + EXPECT_EQ(301, response.status_code); + EXPECT_EQ(ErrorCode::TOO_MANY_REDIRECTS, response.error.code); +} + +TEST(MaxRedirectsTests, TwoMaxRedirectsSuccessTest) { + Url url{server->GetBaseUrl() + "/two_redirects.html"}; + Session session; + session.SetUrl(url); + session.SetRedirect(Redirect(2L)); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{server->GetBaseUrl() + "/hello.html"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(MultipleGetTests, BasicMultipleGetTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + for (size_t i = 0; i < 100; ++i) { + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(MultipleGetTests, UrlChangeMultipleGetTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/hello.html"}; + session.SetUrl(url); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Url url{server->GetBaseUrl() + "/basic.json"}; + session.SetUrl(url); + Response response = session.Get(); + std::string expected_text{ + "[\n" + " {\n" + " \"first_key\": \"first_value\",\n" + " \"second_key\": \"second_value\"\n" + " }\n" + "]"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(MultipleGetTests, HeaderMultipleGetTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + session.SetHeader(Header{{"hello", "world"}}); + for (size_t i = 0; i < 100; ++i) { + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(MultipleGetTests, HeaderChangeMultipleGetTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + session.SetHeader(Header{{"hello", "world"}}); + { + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + session.SetHeader(Header{{"key", "value"}}); + { + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"value"}, response.header["key"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(MultipleGetTests, ParameterMultipleGetTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetParameters({{"hello", "world"}}); + for (size_t i = 0; i < 100; ++i) { + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?hello=world"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(MultipleGetTests, ParameterChangeMultipleGetTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetParameters({{"hello", "world"}}); + { + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?hello=world"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + session.SetUrl(url); + session.SetParameters({{"key", "value"}}); + { + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?key=value"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(MultipleGetTests, BasicAuthenticationMultipleGetTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Session session; + session.SetUrl(url); + session.SetAuth(Authentication{"user", "password", AuthMode::BASIC}); + for (size_t i = 0; i < 100; ++i) { + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(MultipleGetTests, BasicAuthenticationChangeMultipleGetTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Session session; + session.SetUrl(url); + session.SetAuth(Authentication{"user", "password", AuthMode::BASIC}); + { + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + session.SetAuth(Authentication{"user", "bad_password", AuthMode::BASIC}); + { + Response response = session.Get(); + EXPECT_EQ(std::string{"Unauthorized"}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + session.SetAuth(Authentication{"bad_user", "password", AuthMode::BASIC}); + { + Response response = session.Get(); + EXPECT_EQ(std::string{"Unauthorized"}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(ParameterTests, ParameterSingleTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + Parameters parameters{{"hello", "world"}}; + session.SetParameters(parameters); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?hello=world"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterTests, ParameterMultipleTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + Parameters parameters{{"hello", "world"}, {"key", "value"}}; + session.SetParameters(parameters); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?hello=world&key=value"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(FullRequestUrlTest, GetFullRequestUrlNoParametersTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + std::string expected_text{server->GetBaseUrl() + "/hello.html"}; + EXPECT_EQ(expected_text, session.GetFullRequestUrl()); +} + +TEST(FullRequestUrlTest, GetFullRequestUrlOneParameterTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + Parameters parameters{{"hello", "world"}}; + session.SetParameters(parameters); + std::string expected_text{server->GetBaseUrl() + "/hello.html" + "?hello=world"}; + EXPECT_EQ(expected_text, session.GetFullRequestUrl()); +} + +TEST(FullRequestUrlTest, GetFullRequestUrlMultipleParametersTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + Parameters parameters{{"hello", "world"}, {"key", "value"}}; + session.SetParameters(parameters); + std::string expected_text{server->GetBaseUrl() + "/hello.html" + "?hello=world&key=value"}; + EXPECT_EQ(expected_text, session.GetFullRequestUrl()); +} + +TEST(TimeoutTests, SetTimeoutTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetTimeout(0L); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(TimeoutTests, SetTimeoutLongTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetTimeout(10000L); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(TimeoutTests, SetTimeoutLowSpeed) { + Url url{server->GetBaseUrl() + "/low_speed_timeout.html"}; + Session session; + session.SetUrl(url); + session.SetTimeout(1000); + Response response = session.Get(); + EXPECT_EQ(url, response.url); + // Do not check for the HTTP status code, since libcurl always provides the status code of the header if it was received + EXPECT_EQ(ErrorCode::OPERATION_TIMEDOUT, response.error.code); +} + +TEST(TimeoutTests, SetChronoTimeoutTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetTimeout(std::chrono::milliseconds{0}); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(TimeoutTests, SetChronoTimeoutLongTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetTimeout(std::chrono::milliseconds{10000}); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + // Do not check for the HTTP status code, since libcurl always provides the status code of the header if it was received + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(TimeoutTests, SetChronoLiteralTimeoutTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetTimeout(2s); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(TimeoutTests, SetChronoLiteralTimeoutLowSpeed) { + Url url{server->GetBaseUrl() + "/low_speed_timeout.html"}; + Session session; + session.SetUrl(url); + session.SetTimeout(1000ms); + Response response = session.Get(); + EXPECT_EQ(url, response.url); + // Do not check for the HTTP status code, since libcurl always provides the status code of the header if it was received + EXPECT_EQ(ErrorCode::OPERATION_TIMEDOUT, response.error.code); +} + +TEST(ConnectTimeoutTests, SetConnectTimeoutTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetConnectTimeout(0L); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ConnectTimeoutTests, SetConnectTimeoutLongTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetConnectTimeout(10000L); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ConnectTimeoutTests, SetChronoConnectTimeoutTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetConnectTimeout(std::chrono::milliseconds{0}); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ConnectTimeoutTests, SetChronoConnectTimeoutLongTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetConnectTimeout(std::chrono::milliseconds{10000}); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(LowSpeedTests, SetLowSpeedTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetLowSpeed({1, 1}); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PayloadTests, SetPayloadTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Session session; + session.SetUrl(url); + session.SetPayload({{"x", "5"}}); + Response response = session.Post(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PayloadTests, SetPayloadLValueTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Session session; + session.SetUrl(url); + Payload payload{{"x", "5"}}; + session.SetPayload(payload); + Response response = session.Post(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(MultipartTests, SetMultipartTest) { + Url url{server->GetBaseUrl() + "/form_post.html"}; + Session session; + session.SetUrl(url); + session.SetMultipart({{"x", "5"}}); + Response response = session.Post(); + std::string expected_text{ + "{\n" + " \"x\": \"5\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(MultipartTests, SetMultipartValueTest) { + Url url{server->GetBaseUrl() + "/form_post.html"}; + Session session; + session.SetUrl(url); + Multipart multipart{{"x", "5"}}; + session.SetMultipart(multipart); + Response response = session.Post(); + std::string expected_text{ + "{\n" + " \"x\": \"5\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BodyTests, SetBodyTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Session session; + session.SetUrl(url); + session.SetBody(Body{"x=5"}); + Response response = session.Post(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BodyTests, SetBodyValueTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Session session; + session.SetUrl(url); + Body body{"x=5"}; + session.SetBody(body); + Response response = session.Post(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DigestTests, SetDigestTest) { + Url url{server->GetBaseUrl() + "/digest_auth.html"}; + Session session; + session.SetUrl(url); + session.SetAuth({"user", "password", AuthMode::DIGEST}); + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UserAgentTests, SetUserAgentTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + UserAgent userAgent{"Test User Agent"}; + Session session; + session.SetUrl(url); + session.SetUserAgent(userAgent); + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(userAgent, response.header["User-Agent"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UserAgentTests, SetUserAgentStringViewTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + UserAgent userAgent{std::string_view{"Test User Agent"}}; + Session session; + session.SetUrl(url); + session.SetUserAgent(userAgent); + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(userAgent, response.header["User-Agent"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(CookiesTests, BasicCookiesTest) { + Url url{server->GetBaseUrl() + "/basic_cookies.html"}; + Session session{}; + session.SetUrl(url); + Response response = session.Get(); + Cookies res_cookies{response.cookies}; + std::string expected_text{"Basic Cookies"}; + cpr::Cookies expectedCookies{ + {"SID", "31d4d96e407aad42", "127.0.0.1", false, "/", true, std::chrono::system_clock::from_time_t(3905119080)}, + {"lang", "en-US", "127.0.0.1", false, "/", true, std::chrono::system_clock::from_time_t(3905119080)}, + }; + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + EXPECT_EQ(expected_text, response.text); + for (auto cookie = res_cookies.begin(), expectedCookie = expectedCookies.begin(); cookie != res_cookies.end() && expectedCookie != expectedCookies.end(); cookie++, expectedCookie++) { + EXPECT_EQ(expectedCookie->GetName(), cookie->GetName()); + EXPECT_EQ(expectedCookie->GetValue(), cookie->GetValue()); + EXPECT_EQ(expectedCookie->GetDomain(), cookie->GetDomain()); + EXPECT_EQ(expectedCookie->IsIncludingSubdomains(), cookie->IsIncludingSubdomains()); + EXPECT_EQ(expectedCookie->GetPath(), cookie->GetPath()); + EXPECT_EQ(expectedCookie->IsHttpsOnly(), cookie->IsHttpsOnly()); + EXPECT_EQ(expectedCookie->GetExpires(), cookie->GetExpires()); + } +} + +TEST(CookiesTests, ClientSetCookiesTest) { + Url url{server->GetBaseUrl() + "/cookies_reflect.html"}; + { + Session session{}; + session.SetUrl(url); + session.SetCookies(Cookies{ + {"SID", "31d4d96e407aad42"}, + {"lang", "en-US"}, + }); + Response response = session.Get(); + std::string expected_text{"SID=31d4d96e407aad42; lang=en-US;"}; + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + EXPECT_EQ(expected_text, response.text); + } + { + Session session{}; + session.SetUrl(url); + Cookies cookie{ + {"SID", "31d4d96e407aad42"}, + {"lang", "en-US"}, + }; + session.SetCookies(cookie); + Response response = session.Get(); + std::string expected_text{"SID=31d4d96e407aad42; lang=en-US;"}; + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + EXPECT_EQ(expected_text, response.text); + } +} + +TEST(CookiesTests, RedirectionWithChangingCookiesTest) { + Url url{server->GetBaseUrl() + "/redirection_with_changing_cookies.html"}; + { + Session session{}; + session.SetUrl(url); + session.SetCookies(Cookies{ + {"SID", "31d4d96e407aad42"}, + {"lang", "en-US"}, + }); + session.SetRedirect(Redirect(0L)); + Response response = session.Get(); + std::string expected_text{"Received cookies are: SID=31d4d96e407aad42; lang=en-US;"}; + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + EXPECT_EQ(expected_text, response.text); + } + { + Session session{}; + session.SetUrl(url); + session.SetRedirect(Redirect(1L)); + Response response = session.Get(); + std::string expected_text{"Received cookies are: lang=en-US; SID=31d4d96e407aad42"}; + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + EXPECT_EQ(expected_text, response.text); + } + { + Session session{}; + session.SetUrl(url); + session.SetCookies(Cookies{ + {"SID", "empty_sid"}, + }); + session.SetRedirect(Redirect(1L)); + Response response = session.Get(); + std::string expected_text{"Received cookies are: lang=en-US; SID=31d4d96e407aad42; SID=empty_sid;"}; + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + EXPECT_EQ(expected_text, response.text); + } +} + +TEST(DifferentMethodTests, GetPostTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + { + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = session.Post(); + std::string expected_text{"Header reflect POST"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(DifferentMethodTests, PostGetTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + { + Response response = session.Post(); + std::string expected_text{"Header reflect POST"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(DifferentMethodTests, GetPostGetTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + { + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = session.Post(); + std::string expected_text{"Header reflect POST"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(DifferentMethodTests, PostGetPostTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + { + Response response = session.Post(); + std::string expected_text{"Header reflect POST"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = session.Post(); + std::string expected_text{"Header reflect POST"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(DifferentMethodTests, MultipleGetPostTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + for (size_t i = 0; i < 100; ++i) { + { + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = session.Post(); + std::string expected_text{"Header reflect POST"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + } +} + +TEST(DifferentMethodTests, MultipleDeleteHeadPutGetPostTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Url urlPost{server->GetBaseUrl() + "/post_reflect.html"}; + Url urlPut{server->GetBaseUrl() + "/put.html"}; + Session session; + for (size_t i = 0; i < 10; ++i) { + { + session.SetUrl(url); + Response response = session.Delete(); + std::string expected_text{"Header reflect DELETE"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + session.SetUrl(urlPost); + std::string expectedBody = "a1b2c3Post"; + session.SetBody(expectedBody); + Response response = session.Post(); + EXPECT_EQ(expectedBody, response.text); + EXPECT_EQ(urlPost, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + session.SetUrl(url); + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + session.SetUrl(urlPut); + session.SetPayload({{"x", "5"}}); + Response response = session.Put(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(urlPut, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + session.SetUrl(url); + Response response = session.Head(); + std::string expected_text{"Header reflect HEAD"}; + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + } +} + +TEST(CurlHolderManipulateTests, CustomOptionTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + curl_easy_setopt(session.GetCurlHolder()->handle, CURLOPT_SSL_OPTIONS, CURLSSLOPT_ALLOW_BEAST | CURLSSLOPT_NO_REVOKE); + { + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = session.Post(); + std::string expected_text{"Header reflect POST"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(LocalPortTests, SetLocalPortTest) { + Url url{server->GetBaseUrl() + "/local_port.html"}; + Session session; + session.SetUrl(url); + std::uint16_t const local_port = 60252; // beware of HttpServer::GetPort when changing + std::uint16_t const local_port_range = 5000; + session.SetLocalPort(local_port); + session.SetLocalPortRange(local_port_range); + // expected response: body contains port number in specified range + // NOTE: even when trying up to 5000 ports there is the chance that all of them are occupied. + // It would be possible to also check here for ErrorCode::INTERNAL_ERROR but that somehow seems + // wrong as then this test would pass in case SetLocalPort does not work at all + // or in other words: we have to assume that at least one port in the specified range is free. + Response response = session.Get(); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + std::uint16_t port_from_response = std::strtoul(response.text.c_str(), nullptr, 10); + EXPECT_EQ(errno, 0); + EXPECT_GE(port_from_response, local_port); + EXPECT_LE(port_from_response, local_port + local_port_range); +} + +TEST(LocalPortTests, SetOptionTest) { + Url url{server->GetBaseUrl() + "/local_port.html"}; + Session session; + session.SetUrl(url); + std::uint16_t const local_port = 60551; // beware of HttpServer::GetPort when changing + std::uint16_t const local_port_range = 5000; + session.SetOption(LocalPort(local_port)); + session.SetOption(LocalPortRange(local_port_range)); + // expected response: body contains port number in specified range + // NOTE: even when trying up to 5000 ports there is the chance that all of them are occupied. + // It would be possible to also check here for ErrorCode::INTERNAL_ERROR but that somehow seems + // wrong as then this test would pass in case SetOption(LocalPort) does not work at all + // or in other words: we have to assume that at least one port in the specified range is free. + Response response = session.Get(); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + unsigned long port_from_response = std::strtoul(response.text.c_str(), nullptr, 10); + EXPECT_EQ(errno, 0); + EXPECT_GE(port_from_response, local_port); + EXPECT_LE(port_from_response, local_port + local_port_range); +} + +// The tests using the port of the server as a source port for curl fail for windows. +// The reason probably is that Windows allows two sockets to bind to the same port if the full hostname is different. +// In these tests, mongoose binds to http://127.0.0.1:61936, while libcurl binds to a different hostname, but still port 61936. +// This seems to be okay for Windows, however, these tests expect an error like on Linux and MacOS +// We therefore, simply skip the tests if Windows is used +#ifndef _WIN32 +TEST(LocalPortTests, SetLocalPortTestOccupied) { + Url url{server->GetBaseUrl() + "/local_port.html"}; + Session session; + session.SetUrl(url); + session.SetLocalPort(server->GetPort()); + // expected response: request cannot be made as port is already occupied + Response response = session.Get(); + EXPECT_EQ(ErrorCode::INTERNAL_ERROR, response.error.code); +} + +TEST(LocalPortTests, SetOptionTestOccupied) { + Url url{server->GetBaseUrl() + "/local_port.html"}; + Session session; + session.SetUrl(url); + session.SetOption(LocalPort(server->GetPort())); + // expected response: request cannot be made as port is already occupied + Response response = session.Get(); + EXPECT_EQ(ErrorCode::INTERNAL_ERROR, response.error.code); +} +#endif // _WIN32 + +TEST(BasicTests, ReserveResponseString) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetReserveSize(4096); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_GE(response.text.capacity(), 4096); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +std::vector Split(const std::string& s) { + std::vector encodings; + std::stringstream ss(s); + std::string encoding; + + while (std::getline(ss, encoding, ',')) { + encoding.erase(std::remove_if(encoding.begin(), encoding.end(), isspace), encoding.end()); // Trim + encodings.push_back(encoding); + } + + return encodings; +} + +void CompareEncodings(const std::string& response, const std::vector& expected) { + const std::vector responseVec = Split(response); + + EXPECT_EQ(responseVec.size(), expected.size()); + for (const std::string& encoding : expected) { + EXPECT_TRUE(std::find(responseVec.begin(), responseVec.end(), encoding) != responseVec.end()); + } +} + +TEST(BasicTests, AcceptEncodingTestWithMethodsStringMap) { + Url url{server->GetBaseUrl() + "/check_accept_encoding.html"}; + Session session; + session.SetUrl(url); + session.SetAcceptEncoding({{AcceptEncodingMethods::deflate, AcceptEncodingMethods::gzip, AcceptEncodingMethods::zlib}}); + Response response = session.Get(); + + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + CompareEncodings(response.text, std::vector{"deflate", "gzip", "zlib"}); +} + +TEST(BasicTests, AcceptEncodingTestWithMethodsStringMapLValue) { + Url url{server->GetBaseUrl() + "/check_accept_encoding.html"}; + Session session; + session.SetUrl(url); + AcceptEncoding accept_encoding{{AcceptEncodingMethods::deflate, AcceptEncodingMethods::gzip, AcceptEncodingMethods::zlib}}; + session.SetAcceptEncoding(accept_encoding); + Response response = session.Get(); + + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + CompareEncodings(response.text, std::vector{"deflate", "gzip", "zlib"}); +} + +TEST(BasicTests, AcceptEncodingTestWithCostomizedString) { + Url url{server->GetBaseUrl() + "/check_accept_encoding.html"}; + Session session; + session.SetUrl(url); + session.SetAcceptEncoding({{"deflate", "gzip", "zlib"}}); + Response response = session.Get(); + + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + CompareEncodings(response.text, std::vector{"deflate", "gzip", "zlib"}); +} + +TEST(BasicTests, AcceptEncodingTestWithCostomizedStringLValue) { + Url url{server->GetBaseUrl() + "/check_accept_encoding.html"}; + Session session; + session.SetUrl(url); + AcceptEncoding accept_encoding{{"deflate", "gzip", "zlib"}}; + session.SetAcceptEncoding(accept_encoding); + Response response = session.Get(); + + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + CompareEncodings(response.text, std::vector{"deflate", "gzip", "zlib"}); +} + +TEST(BasicTests, AcceptEncodingTestDisabled) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + session.SetAcceptEncoding({AcceptEncodingMethods::disabled}); + Response response = session.Get(); + + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + // Ensure no 'Accept-Encoding' header got added + EXPECT_TRUE(response.header.find("Accept-Encoding") == response.header.end()); +} + +TEST(BasicTests, AcceptEncodingTestDisabledMultipleThrow) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + session.SetAcceptEncoding({AcceptEncodingMethods::disabled, AcceptEncodingMethods::deflate}); + EXPECT_THROW(session.Get(), std::invalid_argument); +} + +TEST(BasicTests, DisableHeaderExpect100ContinueTest) { + Url url{server->GetBaseUrl() + "/check_expect_100_continue.html"}; + std::string filename{"test_file"}; + std::string content{std::string(1024 * 1024, 'a')}; + std::ofstream test_file; + test_file.open(filename); + test_file << content; + test_file.close(); + Session session{}; + session.SetUrl(url); + session.SetMultipart({{"file", File{"test_file"}}}); + Response response = session.Post(); + std::string expected_text{""}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(AsyncRequestsTests, AsyncGetTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + cpr::AsyncResponse future = session->GetAsync(); + std::string expected_text{"Hello world!"}; + cpr::Response response = future.get(); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); +} + +TEST(AsyncRequestsTests, AsyncGetMultipleTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + + std::vector responses; + std::vector> sessions; + for (size_t i = 0; i < 10; ++i) { + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + sessions.emplace_back(session); + responses.emplace_back(session->GetAsync()); + } + + for (cpr::AsyncResponse& future : responses) { + std::string expected_text{"Hello world!"}; + cpr::Response response = future.get(); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + } +} + +TEST(AsyncRequestsTests, AsyncGetMultipleTemporarySessionTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + responses.emplace_back(session->GetAsync()); + } + + for (cpr::AsyncResponse& future : responses) { + std::string expected_text{"Hello world!"}; + cpr::Response response = future.get(); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + } +} + +TEST(AsyncRequestsTests, AsyncGetMultipleReflectTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::vector responses; + for (size_t i = 0; i < 100; ++i) { + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + session->SetParameters({{"key", std::to_string(i)}}); + responses.emplace_back(session->GetAsync()); + } + int i = 0; + for (cpr::AsyncResponse& future : responses) { + cpr::Response response = future.get(); + std::string expected_text{"Hello world!"}; + Url expected_url{url + "?key=" + std::to_string(i)}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(expected_url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + ++i; + } +} + +TEST(AsyncRequestsTests, AsyncWritebackDownloadTest) { + std::shared_ptr session = std::make_shared(); + cpr::Url url{server->GetBaseUrl() + "/download_gzip.html"}; + session->SetUrl(url); + session->SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + cpr::AsyncResponse future = session->DownloadAsync(cpr::WriteCallback{write_data, 0}); + cpr::Response response = future.get(); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); +} + +TEST(AsyncRequestsTests, AsyncPostTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + session->SetPayload({{"x", "5"}}); + cpr::AsyncResponse future = session->PostAsync(); + cpr::Response response = future.get(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(AsyncRequestsTests, AsyncPutTest) { + Url url{server->GetBaseUrl() + "/put.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + session->SetPayload({{"x", "5"}}); + cpr::AsyncResponse future = session->PutAsync(); + cpr::Response response = future.get(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(AsyncRequestsTests, AsyncHeadTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + cpr::AsyncResponse future = session->HeadAsync(); + cpr::Response response = future.get(); + std::string expected_text{""}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(AsyncRequestsTests, AsyncDeleteTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + cpr::AsyncResponse future = session->DeleteAsync(); + cpr::Response response = future.get(); + std::string expected_text{"Header reflect DELETE"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(AsyncRequestsTests, AsyncOptionsTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + cpr::AsyncResponse future = session->OptionsAsync(); + cpr::Response response = future.get(); + std::string expected_text{"Header reflect OPTIONS"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(AsyncRequestsTests, AsyncPatchTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + cpr::AsyncResponse future = session->PatchAsync(); + cpr::Response response = future.get(); + std::string expected_text{"Header reflect PATCH"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(CallbackTests, GetCallbackTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + auto future = session->GetCallback([](Response r) { return r; }); + std::this_thread::sleep_for(sleep_time); + cpr::Response response = future.get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(CallbackTests, PostCallbackTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + auto future = session->PostCallback([](Response r) { return r; }); + std::this_thread::sleep_for(sleep_time); + cpr::Response response = future.get(); + std::string expected_text{"Header reflect POST"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(CallbackTests, PutCallbackTest) { + Url url{server->GetBaseUrl() + "/put.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + session->SetPayload({{"x", "5"}}); + auto future = session->PutCallback([](Response r) { return r; }); + std::this_thread::sleep_for(sleep_time); + cpr::Response response = future.get(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(CallbackTests, HeadCallbackTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + auto future = session->HeadCallback([](Response r) { return r; }); + std::this_thread::sleep_for(sleep_time); + cpr::Response response = future.get(); + std::string expected_text{""}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(CallbackTests, DeleteCallbackTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + auto future = session->DeleteCallback([](Response r) { return r; }); + std::this_thread::sleep_for(sleep_time); + cpr::Response response = future.get(); + std::string expected_text{"Header reflect DELETE"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(CallbackTests, OptionsCallbackTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + auto future = session->OptionsCallback([](Response r) { return r; }); + std::this_thread::sleep_for(sleep_time); + cpr::Response response = future.get(); + std::string expected_text{"Header reflect OPTIONS"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(CallbackTests, PatchCallbackTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + auto future = session->PatchCallback([](Response r) { return r; }); + std::this_thread::sleep_for(sleep_time); + cpr::Response response = future.get(); + std::string expected_text{"Header reflect PATCH"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/test/ssl_tests.cpp b/test/ssl_tests.cpp new file mode 100644 index 0000000..9213188 --- /dev/null +++ b/test/ssl_tests.cpp @@ -0,0 +1,170 @@ +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include "httpsServer.hpp" + + +using namespace cpr; + +static HttpsServer* server; + +static std::string caCertPath; +static std::string serverPubKeyPath; +static std::string clientKeyPath; +static std::string clientCertPath; + +std::string loadCertificateFromFile(const std::string certPath) { + std::ifstream certFile(certPath); + std::stringstream buffer; + buffer << certFile.rdbuf(); + return buffer.str(); +} + +TEST(SslTests, HelloWorldTestSimpel) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string baseDirPath{server->getBaseDirPath()}; + std::string crtPath{baseDirPath + "certificates/"}; + std::string keyPath{baseDirPath + "keys/"}; + + SslOptions sslOpts = Ssl(ssl::CaPath{crtPath + "root-ca.crt"}, ssl::CertFile{crtPath + "client.crt"}, ssl::KeyFile{keyPath + "client.key"}, ssl::VerifyPeer{false}, ssl::PinnedPublicKey{keyPath + "server.pub"}, ssl::VerifyHost{false}, ssl::VerifyStatus{false}); + Response response = cpr::Get(url, sslOpts, Timeout{5000}, Verbose{}); + std::string expected_text = "Hello world!"; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code) << response.error.message; +} + +TEST(SslTests, HelloWorldTestFull) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string baseDirPath{server->getBaseDirPath()}; + std::string crtPath{baseDirPath + "certificates/"}; + std::string keyPath{baseDirPath + "keys/"}; + + SslOptions sslOpts = Ssl(ssl::TLSv1{}, ssl::ALPN{false}, +#if SUPPORT_NPN + ssl::NPN{false}, +#endif // DEBUG + ssl::CaPath{crtPath + "root-ca.crt"}, ssl::CertFile{crtPath + "client.crt"}, ssl::KeyFile{keyPath + "client.key"}, ssl::PinnedPublicKey{keyPath + "server.pub"}, ssl::VerifyPeer{false}, ssl::VerifyHost{false}, ssl::VerifyStatus{false}); + Response response = cpr::Get(url, sslOpts, Timeout{5000}, Verbose{}); + std::string expected_text = "Hello world!"; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code) << response.error.message; +} + +TEST(SslTests, GetCertInfos) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string baseDirPath{server->getBaseDirPath()}; + std::string crtPath{baseDirPath + "certificates/"}; + std::string keyPath{baseDirPath + "keys/"}; + + SslOptions sslOpts = Ssl(ssl::CaPath{crtPath + "root-ca.crt"}, ssl::CertFile{crtPath + "client.crt"}, ssl::KeyFile{keyPath + "client.key"}, ssl::VerifyPeer{false}, ssl::VerifyHost{false}, ssl::VerifyStatus{false}); + + Response response = cpr::Get(url, sslOpts, Timeout{5000}, Verbose{}); + std::vector certInfos = response.GetCertInfos(); + + std::string expected_text = "Hello world!"; + std::vector expectedCertInfos{ + CertInfo{ + "Subject:CN = test-server", + "Issuer:C = GB, O = Example, CN = Root CA", + "Version:2", + "Serial Number:28c252871ec62a626a98006b0bf2888f", + "Signature Algorithm:ED25519", + "Public Key Algorithm:ED25519", + "X509v3 Subject Alternative Name:DNS:localhost, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1", + "X509v3 Subject Key Identifier:39:C1:81:38:01:DC:55:38:E5:2F:4E:7A:D0:4C:84:7B:B7:27:D3:AF", + "X509v3 Authority Key Identifier:E4:F2:F3:85:0E:B7:85:75:84:76:E3:43:D1:B6:9D:14:B8:E2:A4:B7", + "Start date:Jun 29 11:33:07 2022 GMT", + "Expire date:Jun 28 11:33:07 2027 GMT", + "Signature:2e:0d:a1:0d:f5:90:77:e9:eb:84:7d:80:63:63:4d:8a:eb:d9:23:57:1f:21:2a:ed:81:b4:a8:58:b9:00:1b:cb:5c:90:1b:33:6b:f6:ec:42:20:63:54:d6:60:ee:37:14:1b:1c:95:0b:33:ea:67:29:d4:cc:d9:7e:34:fd:47:04:", + R"(Cert:-----BEGIN CERTIFICATE----- +MIIBdTCCASegAwIBAgIQKMJShx7GKmJqmABrC/KIjzAFBgMrZXAwMTELMAkGA1UE +BhMCR0IxEDAOBgNVBAoMB0V4YW1wbGUxEDAOBgNVBAMMB1Jvb3QgQ0EwHhcNMjIw +NjI5MTEzMzA3WhcNMjcwNjI4MTEzMzA3WjAWMRQwEgYDVQQDDAt0ZXN0LXNlcnZl +cjAqMAUGAytlcAMhAI64JU5RjfdEG1KQMxS5DQWkiGlKIQO7ye4mNFq9QleTo3Aw +bjAsBgNVHREEJTAjgglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEw +HQYDVR0OBBYEFDnBgTgB3FU45S9OetBMhHu3J9OvMB8GA1UdIwQYMBaAFOTy84UO +t4V1hHbjQ9G2nRS44qS3MAUGAytlcANBAC4NoQ31kHfp64R9gGNjTYrr2SNXHyEq +7YG0qFi5ABvLXJAbM2v27EIgY1TWYO43FBsclQsz6mcp1MzZfjT9RwQ= +-----END CERTIFICATE----- +)", + }, + }; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code) << response.error.message; + EXPECT_EQ(1, certInfos.size()); + for (auto certInfo_it = certInfos.begin(), expectedCertInfo_it = expectedCertInfos.begin(); certInfo_it != certInfos.end() && expectedCertInfo_it != expectedCertInfos.end(); certInfo_it++, expectedCertInfo_it++) { + for (auto entry_it = (*certInfo_it).begin(), expectedEntry_it = (*expectedCertInfo_it).begin(); entry_it != (*certInfo_it).end() && expectedEntry_it != (*expectedCertInfo_it).end(); entry_it++, expectedEntry_it++) { + std::string search_string = "Identifier:keyid:"; + std::size_t search_index = (*entry_it).find(search_string); + if (search_index != std::string::npos) { + (*entry_it).replace(search_index, search_string.length(), "Identifier:"); + search_string = "\n"; + search_index = (*entry_it).find(search_string); + if (search_index != std::string::npos) { + (*entry_it).replace(search_index, search_string.length(), ""); + } + } + EXPECT_EQ(*expectedEntry_it, *entry_it); + } + std::cout << std::endl; + } +} + +#if SUPPORT_CURLOPT_SSL_CTX_FUNCTION +TEST(SslTests, LoadCertFromBufferTestSimpel) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + + Url url{server->GetBaseUrl() + "/hello.html"}; + + std::string baseDirPath{server->getBaseDirPath()}; + std::string crtPath{baseDirPath + "certificates/"}; + std::string keyPath{baseDirPath + "keys/"}; + std::string certBuffer = loadCertificateFromFile(crtPath + "root-ca.crt"); + SslOptions sslOpts = Ssl(ssl::CaBuffer{std::move(certBuffer)}, ssl::CertFile{crtPath + "client.crt"}, ssl::KeyFile{keyPath + "client.key"}, ssl::VerifyPeer{false}, ssl::VerifyHost{false}, ssl::VerifyStatus{false}); + Response response = cpr::Get(url, sslOpts, Timeout{5000}, Verbose{}); + std::string expected_text = "Hello world!"; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code) << response.error.message; +} +#endif + +fs::path getBasePath(const std::string& execPath) { + return fs::path(fs::path{execPath}.parent_path().string() + "/").make_preferred(); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + fs::path baseDirPath = fs::path{getBasePath(argv[0]).string() + "data/"}; + fs::path serverCertPath = fs::path{baseDirPath}.append("certificates/server.crt"); + fs::path serverKeyPath = fs::path{baseDirPath}.append("keys/server.key"); + server = new HttpsServer(std::move(baseDirPath), std::move(serverCertPath), std::move(serverKeyPath)); + ::testing::AddGlobalTestEnvironment(server); + + return RUN_ALL_TESTS(); +} diff --git a/test/structures_tests.cpp b/test/structures_tests.cpp new file mode 100644 index 0000000..026362e --- /dev/null +++ b/test/structures_tests.cpp @@ -0,0 +1,62 @@ +#include "cpr/cprtypes.h" +#include + +#include + +#include +#include + +using namespace cpr; + +TEST(PayloadTests, UseStringVariableTest) { + std::string value1 = "hello"; + std::string key2 = "key2"; + Payload payload{{"key1", value1}, {key2, "world"}}; + + std::string expected = "key1=hello&key2=world"; + EXPECT_EQ(payload.GetContent(CurlHolder()), expected); +} + +TEST(PayloadTests, DisableEncodingTest) { + std::string key1 = "key1"; + std::string key2 = "key2§$%&/"; + std::string value1 = "hello.,.,"; + std::string value2 = "hello"; + Payload payload{{key1, value1}, {key2, value2}}; + payload.encode = false; + + std::string expected = key1 + '=' + value1 + '&' + key2 + '=' + value2; + EXPECT_EQ(payload.GetContent(CurlHolder()), expected); +} + +TEST(ParametersTests, UseStringVariableTest) { + std::string value1 = "hello"; + std::string key2 = "key2"; + Parameters parameters{{"key1", value1}, {key2, "world"}}; + + std::string expected = "key1=hello&key2=world"; + EXPECT_EQ(parameters.GetContent(CurlHolder()), expected); +} + +TEST(ParametersTests, DisableEncodingTest) { + std::string key1 = "key1"; + std::string key2 = "key2§$%&/"; + std::string value1 = "hello.,.,"; + std::string value2 = "hello"; + Parameters parameters{{key1, value1}, {key2, value2}}; + parameters.encode = false; + + std::string expected = key1 + '=' + value1 + '&' + key2 + '=' + value2; + EXPECT_EQ(parameters.GetContent(CurlHolder()), expected); +} + +TEST(UrlToAndFromString, UrlTests) { + std::string s{"https://github.com/whoshuu/cpr"}; + cpr::Url url = s; + EXPECT_EQ(s, url.str()); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/util_tests.cpp b/test/util_tests.cpp new file mode 100644 index 0000000..3397753 --- /dev/null +++ b/test/util_tests.cpp @@ -0,0 +1,237 @@ +#include + +#include + +#include +#include + +using namespace cpr; + +TEST(UtilParseCookiesTests, BasicParseTest) { + Cookies expectedCookies{{Cookie("status", "on", "127.0.0.1", false, "/", false, std::chrono::system_clock::from_time_t(1656908640)), Cookie("name", "debug", "127.0.0.1", false, "/", false, std::chrono::system_clock::from_time_t(0))}}; + curl_slist* raw_cookies = new curl_slist{ + (char*) "127.0.0.1\tFALSE\t/\tFALSE\t1656908640\tstatus\ton", + new curl_slist{ + (char*) "127.0.0.1\tFALSE\t/\tFALSE\t0\tname\tdebug", + nullptr, + }, + }; + Cookies cookies = util::parseCookies(raw_cookies); + for (auto cookie = cookies.begin(), expectedCookie = expectedCookies.begin(); cookie != cookies.end() && expectedCookie != expectedCookies.end(); cookie++, expectedCookie++) { + EXPECT_EQ(expectedCookie->GetName(), cookie->GetName()); + EXPECT_EQ(expectedCookie->GetValue(), cookie->GetValue()); + EXPECT_EQ(expectedCookie->GetDomain(), cookie->GetDomain()); + EXPECT_EQ(expectedCookie->IsIncludingSubdomains(), cookie->IsIncludingSubdomains()); + EXPECT_EQ(expectedCookie->GetPath(), cookie->GetPath()); + EXPECT_EQ(expectedCookie->IsHttpsOnly(), cookie->IsHttpsOnly()); + EXPECT_EQ(expectedCookie->GetExpires(), cookie->GetExpires()); + } + delete raw_cookies->next; + delete raw_cookies; +} + +TEST(UtilParseHeaderTests, BasicParseTest) { + std::string header_string{ + "HTTP/1.1 200 OK\r\n" + "Server: nginx\r\n" + "Date: Sun, 05 Mar 2017 00:34:54 GMT\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 351\r\n" + "Connection: keep-alive\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Access-Control-Allow-Credentials: true\r\n" + "\r\n"}; + Header header = util::parseHeader(header_string); + EXPECT_EQ(std::string{"nginx"}, header["Server"]); + EXPECT_EQ(std::string{"Sun, 05 Mar 2017 00:34:54 GMT"}, header["Date"]); + EXPECT_EQ(std::string{"application/json"}, header["Content-Type"]); + EXPECT_EQ(std::string{"351"}, header["Content-Length"]); + EXPECT_EQ(std::string{"keep-alive"}, header["Connection"]); + EXPECT_EQ(std::string{"*"}, header["Access-Control-Allow-Origin"]); + EXPECT_EQ(std::string{"true"}, header["Access-Control-Allow-Credentials"]); +} + +TEST(UtilParseHeaderTests, NewlineTest) { + std::string header_string{ + "HTTP/1.1 200 OK\r\n" + "Auth:\n" + "Access-Control-Allow-Credentials: true\r\n" + "\r\n"}; + Header header = util::parseHeader(header_string); + EXPECT_EQ(std::string{""}, header["Server"]); + EXPECT_EQ(std::string{"true"}, header["Access-Control-Allow-Credentials"]); +} + +TEST(UtilParseHeaderTests, SpaceNewlineTest) { + std::string header_string{ + "HTTP/1.1 200 OK\r\n" + "Auth: \n" + "Access-Control-Allow-Credentials: true\r\n" + "\r\n"}; + Header header = util::parseHeader(header_string); + EXPECT_EQ(std::string{""}, header["Server"]); + EXPECT_EQ(std::string{"true"}, header["Access-Control-Allow-Credentials"]); +} + +TEST(UtilParseHeaderTests, CarriageReturnNewlineTest) { + std::string header_string{ + "HTTP/1.1 200 OK\n" + "Auth:\r\n" + "Access-Control-Allow-Credentials: true\r\n" + "\r\n"}; + Header header = util::parseHeader(header_string); + EXPECT_EQ(std::string{""}, header["Server"]); + EXPECT_EQ(std::string{"true"}, header["Access-Control-Allow-Credentials"]); +} + +TEST(UtilParseHeaderTests, SpaceCarriageReturnNewlineTest) { + std::string header_string{ + "HTTP/1.1 200 OK\n" + "Auth: \r\n" + "Access-Control-Allow-Credentials: true\r\n" + "\r\n"}; + Header header = util::parseHeader(header_string); + EXPECT_EQ(std::string{""}, header["Server"]); + EXPECT_EQ(std::string{"true"}, header["Access-Control-Allow-Credentials"]); +} + +TEST(UtilParseHeaderTests, BasicStatusLineTest) { + std::string header_string{ + "HTTP/1.1 200 OK\r\n" + "Server: nginx\r\n" + "Content-Type: application/json\r\n" + "\r\n"}; + std::string status_line; + std::string reason; + Header header = util::parseHeader(header_string, &status_line, &reason); + EXPECT_EQ(std::string{"HTTP/1.1 200 OK"}, status_line); + EXPECT_EQ(std::string{"OK"}, reason); + EXPECT_EQ(std::string{"nginx"}, header["Server"]); + EXPECT_EQ(std::string{"application/json"}, header["Content-Type"]); +} + +TEST(UtilParseHeaderTests, NewlineStatusLineTest) { + std::string header_string{ + "HTTP/1.1 407 Proxy Authentication Required\n" + "Server: nginx\r\n" + "Content-Type: application/json\r\n" + "\r\n"}; + std::string status_line; + std::string reason; + Header header = util::parseHeader(header_string, &status_line, &reason); + EXPECT_EQ(std::string{"HTTP/1.1 407 Proxy Authentication Required"}, status_line); + EXPECT_EQ(std::string{"Proxy Authentication Required"}, reason); + EXPECT_EQ(std::string{"nginx"}, header["Server"]); + EXPECT_EQ(std::string{"application/json"}, header["Content-Type"]); +} + +TEST(UtilParseHeaderTests, NoReasonSpaceTest) { + std::string header_string{ + "HTTP/1.1 200 \n" + "Server: nginx\r\n" + "Content-Type: application/json\r\n" + "\r\n"}; + std::string status_line; + std::string reason; + Header header = util::parseHeader(header_string, &status_line, &reason); + EXPECT_EQ(std::string{"HTTP/1.1 200"}, status_line); + EXPECT_EQ(std::string{""}, reason); + EXPECT_EQ(std::string{"nginx"}, header["Server"]); + EXPECT_EQ(std::string{"application/json"}, header["Content-Type"]); +} + +TEST(UtilParseHeaderTests, NoReasonTest) { + std::string header_string{ + "HTTP/1.1 200\n" + "Server: nginx\r\n" + "Content-Type: application/json\r\n" + "\r\n"}; + std::string status_line; + std::string reason; + Header header = util::parseHeader(header_string, &status_line, &reason); + EXPECT_EQ(std::string{"HTTP/1.1 200"}, status_line); + EXPECT_EQ(std::string{""}, reason); + EXPECT_EQ(std::string{"nginx"}, header["Server"]); + EXPECT_EQ(std::string{"application/json"}, header["Content-Type"]); +} + +TEST(UtilUrlEncodeTests, UnicodeEncoderTest) { + std::string input = "一二三"; + std::string result = util::urlEncode(input); + std::string expected = "%E4%B8%80%E4%BA%8C%E4%B8%89"; + EXPECT_EQ(result, expected); +} + +TEST(UtilUrlEncodeTests, AsciiEncoderTest) { + std::string input = "Hello World!"; + std::string result = util::urlEncode(input); + std::string expected = "Hello%20World%21"; + EXPECT_EQ(result, expected); +} + +TEST(UtilUrlDecodeTests, UnicodeDecoderTest) { + std::string input = "%E4%B8%80%E4%BA%8C%E4%B8%89"; + std::string result = util::urlDecode(input); + std::string expected = "一二三"; + EXPECT_EQ(result, expected); +} + +TEST(UtilUrlDecodeTests, AsciiDecoderTest) { + std::string input = "Hello%20World%21"; + std::string result = util::urlDecode(input); + std::string expected = "Hello World!"; + EXPECT_EQ(result, expected); +} + +TEST(UtilSecureStringClearTests, EmptyStringTest) { + std::string input; + util::secureStringClear(input); + EXPECT_TRUE(input.empty()); +} + +TEST(UtilSecureStringClearTests, NotEmptyStringTest) { + std::string input = "Hello World!"; + util::secureStringClear(input); + EXPECT_TRUE(input.empty()); +} + +TEST(UtilIsTrueTests, TrueTest) { + { + std::string input = "TRUE"; + bool output = util::isTrue(input); + EXPECT_TRUE(output); + } + { + std::string input = "True"; + bool output = util::isTrue(input); + EXPECT_TRUE(output); + } + { + std::string input = "true"; + bool output = util::isTrue(input); + EXPECT_TRUE(output); + } +} + +TEST(UtilIsTrueTests, FalseTest) { + { + std::string input = "FALSE"; + bool output = util::isTrue(input); + EXPECT_FALSE(output); + } + { + std::string input = "False"; + bool output = util::isTrue(input); + EXPECT_FALSE(output); + } + { + std::string input = "false"; + bool output = util::isTrue(input); + EXPECT_FALSE(output); + } +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/version_tests.cpp b/test/version_tests.cpp new file mode 100644 index 0000000..4c589d2 --- /dev/null +++ b/test/version_tests.cpp @@ -0,0 +1,65 @@ +#include +#include +#include +#include +#include + + +TEST(VersionTests, StringVersionExists) { +#ifndef CPR_VERSION + EXPECT_TRUE(false); +#endif // CPR_VERSION +} + +TEST(VersionTests, StringVersionValid) { + EXPECT_TRUE(CPR_VERSION != nullptr); + std::string version = CPR_VERSION; + + // Check if the version string is: '\d+\.\d+\.\d+' + bool digit = true; + size_t dotCount = 0; + for (size_t i = 0; i < version.size(); i++) { + if (i == 0) { + EXPECT_TRUE(std::isdigit(version[i])); + } else if (digit) { + if (version[i] == '.') { + digit = false; + dotCount++; + continue; + } + } + EXPECT_TRUE(std::isdigit(version[i])); + digit = true; + } + EXPECT_EQ(dotCount, 2); +} + +TEST(VersionTests, VersionMajorExists) { +#ifndef CPR_VERSION_MAJOR + EXPECT_TRUE(false); +#endif // CPR_VERSION_MAJOR +} + +TEST(VersionTests, VersionMinorExists) { +#ifndef CPR_VERSION_MINOR + EXPECT_TRUE(false); +#endif // CPR_VERSION_MINOR +} + +TEST(VersionTests, VersionPatchExists) { +#ifndef CPR_VERSION_PATCH + EXPECT_TRUE(false); +#endif // CPR_VERSION_PATCH +} + +TEST(VersionTests, VersionNumExists) { +#ifndef CPR_VERSION_NUM + EXPECT_TRUE(false); +#endif // CPR_VERSION_NUM +} + + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}