├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── cmake ├── Cpp20HttpClientConfig.cmake.in ├── GccClang.cmake └── Msvc.cmake ├── examples ├── CMakeLists.txt ├── async_get_request.cpp ├── async_simple.cpp ├── get_request.cpp ├── get_request_simple.cpp ├── post_request.cpp └── socket.cpp ├── include └── cpp20_http_client.hpp ├── source └── cpp20_http_client.cpp └── tests ├── CMakeLists.txt ├── chunky_body_parser.cpp ├── concatenate_byte_data.cpp ├── extract_filename.cpp ├── http_response_parser.cpp ├── http_response_parser_callbacks.cpp ├── parse_headers_string.cpp ├── parse_status_line.cpp ├── split_url.cpp ├── testing_header.hpp └── uri_encode.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !*.hpp 3 | !*.cpp 4 | !CMakeLists.txt 5 | !*.md 6 | !sources 7 | !*.cmake 8 | !*.cmake.in 9 | !*.yaml 10 | !*/ 11 | 12 | build 13 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.18.0) 2 | project(Cpp20HttpClient VERSION 2.1.1 LANGUAGES CXX) 3 | 4 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/lib/) 5 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin/) 6 | 7 | # Project specific options 8 | option(CPP20_HTTP_CLIENT_BUILD_EXAMPLES "Set to OFF to not build examples" ON) 9 | option(CPP20_HTTP_CLIENT_BUILD_TESTS "Set to OFF to not build tests" ON) 10 | option(CPP20_HTTP_CLIENT_ENABLE_INSTALL "Generate the install target" ON) 11 | 12 | #----------------------------- 13 | # Library target. 14 | 15 | add_library(cpp20_http_client STATIC source/cpp20_http_client.cpp) 16 | add_library(Cpp20HttpClient::cpp20_http_client ALIAS cpp20_http_client) 17 | 18 | set_target_properties(cpp20_http_client PROPERTIES CXX_EXTENSIONS off) 19 | 20 | if (${CMAKE_CXX_COMPILER_ID} STREQUAL MSVC) 21 | add_compile_options(/utf-8) 22 | # Concepts are (or were) only available in /std:c++latest, not in /std:c++20 23 | target_compile_options(cpp20_http_client PUBLIC /std:c++latest) 24 | else () 25 | target_compile_features(cpp20_http_client PUBLIC cxx_std_20) 26 | endif () 27 | 28 | target_include_directories(cpp20_http_client PUBLIC 29 | # When using the library from the install tree, relative paths can be used. 30 | $ 31 | $ 32 | ) 33 | 34 | if (WIN32) 35 | # Windows sockets 2 and Schannel. 36 | target_link_libraries(cpp20_http_client PRIVATE Ws2_32 crypt32) 37 | else () 38 | # OpenSSL. 39 | find_package(OpenSSL REQUIRED) 40 | target_include_directories(cpp20_http_client PRIVATE "${OPENSSL_INCLUDE_DIR}/../") 41 | target_link_libraries(cpp20_http_client PRIVATE OpenSSL::SSL OpenSSL::Crypto pthread) 42 | endif () 43 | 44 | #----------------------------- 45 | 46 | if (CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) 47 | include(CTest) 48 | if (BUILD_TESTING AND CPP20_HTTP_CLIENT_BUILD_TESTS) 49 | add_subdirectory(tests) 50 | endif () 51 | endif () 52 | 53 | if (CPP20_HTTP_CLIENT_BUILD_EXAMPLES) 54 | add_subdirectory(examples) 55 | endif () 56 | 57 | #----------------------------- 58 | # Set up installation. 59 | 60 | include(CMakePackageConfigHelpers) 61 | 62 | if (CPP20_HTTP_CLIENT_ENABLE_INSTALL) 63 | # Create a file that contains information about package versioning. 64 | # It will be placed in CMAKE_CURRENT_BINARY_DIR. 65 | write_basic_package_version_file( 66 | ${PROJECT_NAME}ConfigVersion.cmake 67 | VERSION ${PROJECT_VERSION} 68 | COMPATIBILITY AnyNewerVersion 69 | ) 70 | # During installation, the version file will be installed. 71 | install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake" 72 | DESTINATION lib/cmake/${PROJECT_NAME}) # Relative to the installation path. 73 | 74 | set(TARGET_EXPORT_NAME ${PROJECT_NAME}Targets) 75 | 76 | # Specifies the target(s) that will be installed, and where to install 77 | # the compiled library (relative to package installation path ${CMAKE_INSTALL_PREFIX}). 78 | install( 79 | TARGETS cpp20_http_client 80 | EXPORT ${TARGET_EXPORT_NAME} 81 | ARCHIVE DESTINATION lib 82 | ) 83 | 84 | # During installation, a target configuration file will be exported to a *Targets.cmake file 85 | # that is included by the *Config.cmake.in file which finds the dependencies of the library. 86 | install( 87 | EXPORT ${TARGET_EXPORT_NAME} 88 | FILE ${TARGET_EXPORT_NAME}.cmake 89 | NAMESPACE ${PROJECT_NAME}:: 90 | DESTINATION lib/cmake/${PROJECT_NAME} # Relative to installation path 91 | ) 92 | 93 | # This uses the *Config.cmake.in file to generate a *Config.cmake file with 94 | # the variables specified by PATH_VARS inserted. 95 | configure_package_config_file( 96 | cmake/${PROJECT_NAME}Config.cmake.in 97 | ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake 98 | INSTALL_DESTINATION lib/cmake/${PROJECT_NAME} 99 | PATH_VARS TARGET_EXPORT_NAME 100 | ) 101 | 102 | # Install the config file 103 | install( 104 | FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake" 105 | DESTINATION lib/cmake/${PROJECT_NAME} 106 | ) 107 | 108 | install(DIRECTORY include DESTINATION .) 109 | endif () 110 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2023 Björn Sundin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # C++20 HTTP client 2 | 3 | C++20 HTTP client is an HTTP/HTTPS client library written in C++20. 4 | 5 | As of now, only GCC and MSVC support all of the C++20 features used in this library. Additionally, there are some C++20 features that are not used in the library because no compiler or standard library yet supports them. However the library will be updated over time as compilers start implementing more of C++20. 6 | 7 | ## Aims and features 8 | * User friendly, functional design. 9 | * An API that is hard to misuse. 10 | * Library code follows C++ core guidelines. 11 | * Safe and easy to use TCP Socket abstraction with support for TLS encryption. 12 | * HTTP requests, both unsecured and over TLS. 13 | * Asynchronous requests. 14 | * Callbacks for inspecting and/or cancelling responses while being received. 15 | * Support for Windows, Linux and MacOS. 16 | * Free from warnings with all useful warning flags turned on. 17 | * Modern CMake integration. 18 | * UTF-8 support. 19 | 20 | ## Simple "GET" request example 21 | Note that the fmt library is not a dependency of this library, it's just to simplify the example. 22 | 23 | See the **examples** directory for more examples. 24 | ```cpp 25 | #include 26 | #include 27 | 28 | int main() { 29 | try { 30 | auto const response = http_client::get("https://www.google.com") 31 | .add_header({.name="HeaderName", .value="header value"}) 32 | .send(); 33 | fmt::print("Date from server: {}.\n", response.get_header_value("date").value_or("Unknown")); 34 | http_client::utils::write_to_file(response.get_body(), "index.html"); 35 | } 36 | catch (http_client::errors::ConnectionFailed const& error) { 37 | fmt::print("The connection failed - \"{}\"\n", error.what()); 38 | } 39 | } 40 | ``` 41 | 42 | ## Dependencies 43 | The only non-native dependency is OpenSSL on Linux and MacOS. It is recommended to use a package manager like VCPKG to install the OpenSSL libraries, especially on MacOS. 44 | 45 | ## CMake usage 46 | ### Building and installing 47 | You can download, build and install the library as shown below. You only need to do this if you want to use the library as an installation. 48 | ```shell 49 | git clone https://github.com/avocadoboi/cpp20-http-client.git 50 | cd cpp20-http-client 51 | mkdir build 52 | cd build 53 | cmake .. 54 | cmake --build . --target cpp20_http_client --config Release 55 | cmake --install . 56 | ``` 57 | You may want to add some flags to the cmake commands, for example the VCPKG toolchain file or a cmake prefix path for OpenSSL on Linux and MacOS. Use the latest GCC or MSVC compiler to build. You may need to add `sudo` to the install command, or run the command prompt as administrator on Windows. 58 | 59 | If you are making changes to the code then use one of the toolchain files in the `cmake` directory to add warning flags. Do this by adding `-DCMAKE_TOOLCHAIN_FILE=cmake/Msvc.cmake` or `-DCMAKE_TOOLCHAIN_FILE=cmake/GccClang.cmake` to the CMake build generation command. These include the VCPKG toolchain file if a `VCPKG_ROOT` environment variable is available. 60 | 61 | ### Using the installed library 62 | To include the installed library in a CMake project, use find_package like so: 63 | ```cmake 64 | find_package(Cpp20HttpClient CONFIG REQUIRED) 65 | target_link_libraries(target_name PRIVATE Cpp20HttpClient::cpp20_http_client) 66 | ``` 67 | Where target_name is the name of the target to link the library to. 68 | 69 | ### Using the library as a subproject 70 | You can clone the library into your own project and then use it like so: 71 | ```cmake 72 | add_subdirectory(external/cpp20-http-client) 73 | target_link_libraries(target_name PRIVATE Cpp20HttpClient::cpp20_http_client) 74 | ``` 75 | Where target_name is the name of the target to link the library to. "external/cpp20-http-client" is just an example of where you could put the library in your project. 76 | 77 | ### Using CMake to download and include the library 78 | You can use the built-in FetchContent CMake module to directly fetch the repository at configure time and link to it: 79 | ```cmake 80 | include(FetchContent) 81 | 82 | FetchContent_Declare( 83 | Cpp20HttpClient 84 | GIT_REPOSITORY https://github.com/avocadoboi/cpp20-http-client.git 85 | ) 86 | FetchContent_MakeAvailable(Cpp20HttpClient) 87 | 88 | target_link_libraries(target_name PRIVATE Cpp20HttpClient::cpp20_http_client) 89 | ``` 90 | 91 | ## Development status 92 | All planned functionality has been implemented and tested. There are some improvements left that are possible and quite big things which may be seen as missing like response caching. These things can easily be extended to the library in the future if there's any need or demand for them. The library will also be updated as more C++20 features become available. 93 | -------------------------------------------------------------------------------- /cmake/Cpp20HttpClientConfig.cmake.in: -------------------------------------------------------------------------------- 1 | @PACKAGE_INIT@ 2 | 3 | include(CMakeFindDependencyMacro) 4 | if (NOT WIN32) 5 | find_dependency(OpenSSL REQUIRED) 6 | endif () 7 | 8 | include("${CMAKE_CURRENT_LIST_DIR}/@TARGET_EXPORT_NAME@.cmake") 9 | -------------------------------------------------------------------------------- /cmake/GccClang.cmake: -------------------------------------------------------------------------------- 1 | # Toolchain file for GCC and Clang. 2 | 3 | add_compile_options( 4 | -Werror 5 | -Wall 6 | -Wpedantic 7 | -Wextra 8 | -Wduplicated-branches 9 | -Wduplicated-cond 10 | -Wcast-qual 11 | -Wcast-align 12 | -Wconversion 13 | ) 14 | 15 | add_compile_options( 16 | -fconcepts-diagnostics-depth=2 17 | -fmax-errors=5 18 | ) 19 | 20 | if (DEFINED ENV{VCPKG_ROOT}) 21 | include($ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake) 22 | endif () 23 | -------------------------------------------------------------------------------- /cmake/Msvc.cmake: -------------------------------------------------------------------------------- 1 | # Toolchain file for MSVC. 2 | 3 | add_compile_options( 4 | /experimental:external 5 | /external:anglebrackets 6 | /external:W0 7 | /WX 8 | /W4 9 | ) 10 | 11 | if (DEFINED ENV{VCPKG_ROOT}) 12 | include($ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake) 13 | endif () 14 | -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | add_executable(example_get_request get_request.cpp) 3 | target_link_libraries(example_get_request PRIVATE cpp20_http_client) 4 | 5 | add_executable(example_get_request_simple get_request_simple.cpp) 6 | target_link_libraries(example_get_request_simple PRIVATE cpp20_http_client) 7 | 8 | add_executable(example_async_get_request async_get_request.cpp) 9 | target_link_libraries(example_async_get_request PRIVATE cpp20_http_client) 10 | 11 | add_executable(example_post_request post_request.cpp) 12 | target_link_libraries(example_post_request PRIVATE cpp20_http_client) 13 | 14 | add_executable(example_socket socket.cpp) 15 | target_link_libraries(example_socket PRIVATE cpp20_http_client) 16 | 17 | add_executable(example_async_simple async_simple.cpp) 18 | target_link_libraries(example_async_simple PRIVATE cpp20_http_client) 19 | 20 | # Not a dependency, but for emergency debugging... 21 | # find_package(fmt CONFIG) 22 | # target_link_libraries(example_async_simple PRIVATE fmt::fmt-header-only) 23 | -------------------------------------------------------------------------------- /examples/async_get_request.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | using namespace std::chrono_literals; 4 | 5 | int main() { 6 | auto response = http_client::get("https://www.youtube.com") 7 | .set_raw_progress_callback([](http_client::ResponseProgressRaw const& progress) { 8 | std::cout << "Got " << progress.data.size() << " bytes so far.\n"; 9 | }) 10 | .set_headers_callback([](http_client::ResponseProgressHeaders& headers) { 11 | std::cout << "Got headers.\n"; 12 | 13 | std::cout << "Status code: " << static_cast(headers.get_status_code()) << '\n'; 14 | if (auto const date = headers.get_header_value("date")) { 15 | std::cout << "\"date\" header: " << *date << '\n'; 16 | } 17 | 18 | std::cout << "Stopped reading response after headers.\n"; 19 | // Don't continue with reading the body, stop after headers 20 | headers.stop(); 21 | }) 22 | .send_async<1024>(); 23 | 24 | // Do stuff that takes time here, while waiting for response... 25 | while (response.wait_for(20ms) != std::future_status::ready) { 26 | std::cout << "(Waiting for response...)\n"; 27 | } 28 | 29 | std::cout << "Got response!\n\n"; 30 | 31 | // Do anything with the Response object 32 | http_client::Response const result = response.get(); 33 | 34 | for (auto const [name, value] : result.get_headers()) { 35 | std::cout << '\"' << name << "\" header has value \"" << value << "\".\n"; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/async_simple.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | void handle_progress(http_client::ResponseProgressRaw const& progress) { 4 | std::cout << "Received " << progress.data.size() << " bytes.\n"; 5 | } 6 | 7 | int main() { 8 | auto response_future = 9 | http_client::make_request(http_client::RequestMethod::Delete, "https://httpbin.org/delete") 10 | .add_headers("accept: application/json") 11 | .set_raw_progress_callback(handle_progress) 12 | .send_async<256>(); 13 | 14 | std::cout << "Waiting...\n"; 15 | 16 | http_client::Response const result = response_future.get(); 17 | std::cout << "Got response!\n"; 18 | 19 | std::cout << "The content type is: " << 20 | result.get_header_value("content-type").value_or("Unknown") << ".\n"; 21 | 22 | std::cout << "Response body:\n" << result.get_body_string() << "\n"; 23 | } 24 | -------------------------------------------------------------------------------- /examples/get_request.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | using namespace std::string_view_literals; 4 | 5 | std::string read_url() { 6 | std::cout << "Please enter a url: "; 7 | 8 | auto url = std::string{}; 9 | std::cin >> url; 10 | 11 | std::cout << '\n'; 12 | 13 | return url; 14 | } 15 | 16 | http_client::Response send_request(std::string_view const url) { 17 | return http_client::get(url) 18 | .add_header({.name="One", .value="aaa"}) // Header struct. 19 | .add_headers("Two: bbb") // Can be multiple lines for more than one header. 20 | .add_headers( // Variadic template 21 | http_client::Header{.name="Three", .value="ccc"}, 22 | http_client::Header{.name="Four", .value="ddd"}, 23 | http_client::Header{.name="Five", .value="eee"} 24 | ).add_headers({ // Initializer list 25 | {.name="Six", .value="fff"}, 26 | {.name="Four", .value="ggg"}, 27 | }).send(); 28 | } 29 | 30 | void use_response(http_client::Response const& response) { 31 | auto const response_headers = response.get_headers_string(); 32 | std::cout << "Response headers below.\n\n" << response_headers << "\n\n"; 33 | 34 | if (auto const last_modified = response.get_header_value("last-modified")) { // Case insensitive 35 | std::cout << "The resource was last modified " << *last_modified << '\n'; 36 | } 37 | else { 38 | std::cout << "No last-modified header.\n"; 39 | } 40 | 41 | if (auto const content_type = response.get_header_value("content-type")) { 42 | std::cout << "The content type is " << *content_type << '\n'; 43 | } 44 | else { 45 | std::cout << "No content-type header.\n"; 46 | } 47 | 48 | auto const url = response.get_url(); 49 | 50 | auto const filename = [&]{ 51 | if (auto const filename = http_client::utils::extract_filename(url); filename.empty()) { 52 | return http_client::utils::split_url(url).host; 53 | } 54 | else { 55 | return filename; 56 | } 57 | }(); 58 | 59 | std::cout << "Writing body to file with name: " << filename << '\n'; 60 | http_client::utils::write_to_file(response.get_body(), std::string{filename}); 61 | } 62 | 63 | http_client::Response do_request() { 64 | auto url = read_url(); 65 | 66 | while (true) { 67 | auto response = send_request(url); 68 | 69 | // Handle possible TLS redirect 70 | if (response.get_status_code() == http_client::StatusCode::MovedPermanently|| 71 | response.get_status_code() == http_client::StatusCode::Found) 72 | { 73 | if (auto const new_url = response.get_header_value("location")) { 74 | std::cout << "Got status code moved permanently or found, redirecting to " << *new_url << "...\n\n"; 75 | url = *new_url; 76 | continue; 77 | } 78 | } 79 | return response; 80 | } 81 | } 82 | 83 | int main() { 84 | try { 85 | auto const response = do_request(); 86 | use_response(response); 87 | } 88 | catch (http_client::errors::ConnectionFailed const& error) { 89 | std::cout << "The connection failed with error \"" << error.what() << '"'; 90 | } 91 | 92 | std::cout << "\n\n"; 93 | } 94 | -------------------------------------------------------------------------------- /examples/get_request_simple.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | auto url = std::string{}; 5 | std::cout << "Please enter a URL: "; 6 | std::cin >> url; 7 | 8 | auto response_count = 0; 9 | 10 | while (true) { 11 | auto const response = http_client::get(url).send(); 12 | 13 | std::cout << "\nHeaders " << response_count++ << ": \n" << response.get_headers_string() << '\n'; 14 | // TODO: replace with this when GCC supports std::format 15 | // utils::println("\nHeaders {}: \n{}", response_count++, response.get_headers_string()); 16 | 17 | if (response.get_status_code() == http_client::StatusCode::MovedPermanently || 18 | response.get_status_code() == http_client::StatusCode::Found) 19 | { 20 | if (auto const new_url = response.get_header_value("location")) { 21 | url = *new_url; 22 | continue; 23 | } 24 | } 25 | else { 26 | std::cout << "\nBody:\n" << response.get_body_string() << '\n'; 27 | } 28 | break; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/post_request.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | auto const response = http_client::post("https://postman-echo.com/post?one=A&two=B") 5 | .add_header({.name="Content-Type", .value="application/json"}) 6 | .set_body(R"({"numbers": [1, 2, 3, 4, 5]})") 7 | .send(); 8 | std::cout << "Response: \n" << response.get_body_string() << '\n'; 9 | } 10 | -------------------------------------------------------------------------------- /examples/socket.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | /* 4 | This example is only meant to compile and demonstrate the socket abstraction. 5 | You'll have to implement the protocol if it's not http. 6 | */ 7 | 8 | int main() { 9 | // If the last parameter is true, the data is sent and received over TLS. 10 | auto socket = http_client::open_socket("wss://something", http_client::Port{443}, true); 11 | 12 | // Could also use read_available to only read what is currently 13 | // received and ready, and do other stuff while waiting for response data. 14 | // std::async could be used as well, together with the read function 15 | // that takes no parameters and returns a data vector. 16 | // The result cannot be ignored as it contains the size of data that was actually 17 | // read OR http_client::ConnectionClosed if the peer closed the connection. 18 | auto buffer = std::array{}; 19 | if (auto const result = socket.read(buffer); std::holds_alternative(result)) 20 | { 21 | // This is the data that we got from this call. 22 | auto const received_data = std::span{buffer}.first(std::get(result)); 23 | } 24 | else { 25 | // Peer closed the connection! 26 | } 27 | 28 | // Process buffer, in practice probably in a while loop that 29 | // continues until we reached the end of our expected data. 30 | 31 | // Or any contiguous range of std::byte 32 | socket.write("Some response data"); 33 | 34 | // etc... 35 | 36 | } 37 | -------------------------------------------------------------------------------- /include/cpp20_http_client.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021-2023 Björn Sundin 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | #pragma once 26 | 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | 46 | #include 47 | #ifdef __cpp_lib_source_location 48 | # include 49 | #endif 50 | 51 | /* 52 | Namespaces: 53 | 54 | http_client { 55 | utils 56 | errors 57 | algorithms 58 | } 59 | */ 60 | 61 | namespace http_client { 62 | 63 | using Port = int; 64 | 65 | /* 66 | An enumeration of the transfer protocols that are supported by the library. 67 | */ 68 | enum class Protocol : Port { 69 | Http = 80, 70 | Https = 443, 71 | Unknown = -1, 72 | }; 73 | 74 | /* 75 | This is everything that doesn't have anything to do with the core functionality, 76 | but are utilities that are used within the library. 77 | */ 78 | namespace utils { 79 | 80 | /* 81 | IsAnyOf is true if T is the same type as one or more of U, V, W, ... 82 | */ 83 | template 84 | concept IsAnyOf = (std::same_as || ...); 85 | 86 | //--------------------------------------------------------- 87 | 88 | template 89 | concept IsTrivial = std::is_trivial_v; 90 | 91 | /* 92 | Aliasing with types for which IsByte is true is allowed and does not invoke undefined behavior. 93 | https://en.cppreference.com/w/cpp/language/reinterpret_cast 94 | */ 95 | template 96 | concept IsByte = IsAnyOf, std::byte, char, unsigned char>; 97 | 98 | //--------------------------------------------------------- 99 | 100 | /* 101 | Used to invoke a lambda at the end of a scope. 102 | */ 103 | template 104 | class [[nodiscard]] Cleanup { 105 | public: 106 | [[nodiscard]] 107 | Cleanup(T&& callable) : 108 | callable_{std::forward(callable)} 109 | {} 110 | 111 | Cleanup() = delete; 112 | ~Cleanup() { 113 | callable_(); 114 | } 115 | 116 | Cleanup(Cleanup&&) noexcept = delete; 117 | Cleanup& operator=(Cleanup&&) noexcept = delete; 118 | 119 | Cleanup(Cleanup const&) = delete; 120 | Cleanup& operator=(Cleanup const&) = delete; 121 | 122 | private: 123 | T callable_; 124 | }; 125 | 126 | //--------------------------------------------------------- 127 | 128 | /* 129 | Similar to std::unique_ptr except that non-pointer types can be held 130 | and that a custom deleter must be specified. 131 | 132 | This is useful for OS handles that are integer types, for example a native socket handle. 133 | Use C++20 lambdas in unevaluated contexts to specify a deleter, or use an already defined 134 | functor type. 135 | 136 | Example: 137 | using DllHandle = utils::UniqueHandle; 138 | */ 139 | template Deleter_, T invalid_handle = T{}> 140 | class UniqueHandle { 141 | public: 142 | [[nodiscard]] 143 | constexpr explicit operator T() const noexcept { 144 | return handle_; 145 | } 146 | [[nodiscard]] 147 | constexpr T get() const noexcept { 148 | return handle_; 149 | } 150 | [[nodiscard]] 151 | constexpr T& get() noexcept { 152 | return handle_; 153 | } 154 | 155 | [[nodiscard]] 156 | constexpr T const* operator->() const noexcept { 157 | return &handle_; 158 | } 159 | [[nodiscard]] 160 | constexpr T* operator->() noexcept { 161 | return &handle_; 162 | } 163 | 164 | [[nodiscard]] 165 | constexpr T const* operator&() const noexcept { 166 | return &handle_; 167 | } 168 | [[nodiscard]] 169 | constexpr T* operator&() noexcept { 170 | return &handle_; 171 | } 172 | 173 | [[nodiscard]] 174 | constexpr explicit operator bool() const noexcept { 175 | return handle_ != invalid_handle; 176 | } 177 | [[nodiscard]] 178 | constexpr bool operator!() const noexcept { 179 | return handle_ == invalid_handle; 180 | } 181 | 182 | [[nodiscard]] 183 | constexpr bool operator==(UniqueHandle const&) const noexcept 184 | requires std::equality_comparable 185 | = default; 186 | 187 | constexpr explicit UniqueHandle(T const handle) noexcept : 188 | handle_{handle} 189 | {} 190 | constexpr UniqueHandle& operator=(T const handle) { 191 | close_(); 192 | handle_ = handle; 193 | return *this; 194 | } 195 | 196 | constexpr UniqueHandle() = default; 197 | constexpr ~UniqueHandle() { 198 | close_(); 199 | } 200 | 201 | constexpr UniqueHandle(UniqueHandle&& handle) noexcept : 202 | handle_{handle.handle_} 203 | { 204 | handle.handle_ = invalid_handle; 205 | } 206 | constexpr UniqueHandle& operator=(UniqueHandle&& handle) noexcept { 207 | handle_ = handle.handle_; 208 | handle.handle_ = invalid_handle; 209 | return *this; 210 | } 211 | 212 | constexpr UniqueHandle(UniqueHandle const&) = delete; 213 | constexpr UniqueHandle& operator=(UniqueHandle const&) = delete; 214 | 215 | private: 216 | T handle_{invalid_handle}; 217 | 218 | constexpr void close_() { 219 | if (handle_ != invalid_handle) { 220 | Deleter_{}(handle_); 221 | handle_ = invalid_handle; 222 | } 223 | } 224 | }; 225 | 226 | //--------------------------------------------------------- 227 | 228 | /* 229 | This can be called when the program reaches a path that should never be reachable. 230 | It prints error output and exits the program. 231 | */ 232 | #ifdef __cpp_lib_source_location 233 | [[noreturn]] 234 | inline void unreachable(std::source_location const& source_location = std::source_location::current()) { 235 | std::cerr << std::format("Reached an unreachable code path in file {}, in function {}, on line {}.\n", 236 | source_location.file_name(), source_location.function_name(), source_location.line()); 237 | std::exit(1); 238 | } 239 | #else 240 | [[noreturn]] 241 | inline void unreachable() { 242 | std::cerr << "Reached an unreachable code path, exiting.\n"; 243 | std::exit(1); 244 | } 245 | #endif 246 | 247 | /* 248 | Prints an error message to the error output stream and exits the program. 249 | */ 250 | [[noreturn]] 251 | inline void panic(std::string_view const message) { 252 | std::cerr << message << '\n'; 253 | std::exit(1); 254 | } 255 | 256 | //--------------------------------------------------------- 257 | 258 | template 259 | concept IsInputRangeOf = std::ranges::input_range && std::same_as, Value_>; 260 | 261 | template 262 | concept IsSizedRangeOf = IsInputRangeOf && std::ranges::sized_range; 263 | 264 | /* 265 | Converts a range of contiguous characters to a std::basic_string_view. 266 | 267 | TODO: Remove this in C++23; std::views::split will return contiguous ranges and std::basic_string_view will have a range constructor. 268 | */ 269 | constexpr auto range_to_string_view = []< 270 | /* 271 | std::views::split returns a range of ranges. 272 | The ranges unfortunately are not std::ranges::contiguous_range 273 | even when the base type is contiguous, so we can't use that constraint. 274 | 275 | This will be fixed :^D 276 | http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2210r2.html 277 | */ 278 | IsInputRangeOf Range_ 279 | > (Range_&& range) { 280 | return std::string_view{ 281 | &*std::ranges::begin(range), 282 | static_cast(std::ranges::distance(range)) 283 | }; 284 | }; 285 | 286 | //--------------------------------------------------------- 287 | 288 | void enable_utf8_console(); 289 | 290 | //--------------------------------------------------------- 291 | 292 | /* 293 | Copies a sized range to a std::basic_string of any type. 294 | */ 295 | template Range_> 296 | [[nodiscard]] 297 | inline std::string range_to_string(Range_ const& range) { 298 | auto result = std::string(range.size(), char{}); 299 | std::ranges::copy(range, std::ranges::begin(result)); 300 | return result; 301 | } 302 | 303 | /* 304 | Copies a range of unknown size to a std::basic_string of any type. 305 | */ 306 | template Range_> 307 | [[nodiscard]] 308 | inline std::string range_to_string(Range_ const& range) { 309 | auto result = std::string(); 310 | std::ranges::copy(range, std::back_inserter(result)); 311 | return result; 312 | } 313 | 314 | /* 315 | Reinterprets a span of any byte-sized trivial type as a string view of a specified byte-sized character type. 316 | */ 317 | template 318 | [[nodiscard]] 319 | std::string_view data_to_string(std::span const data) { 320 | return std::string_view{reinterpret_cast(data.data()), data.size()}; 321 | } 322 | /* 323 | Reinterprets a string view of any byte-sized character type as a span of any byte-sized trivial type. 324 | */ 325 | template 326 | [[nodiscard]] 327 | std::span string_to_data(std::string_view const string) { 328 | return std::span{reinterpret_cast(string.data()), string.size()}; 329 | } 330 | 331 | //--------------------------------------------------------- 332 | 333 | using DataVector = std::vector; 334 | 335 | //--------------------------------------------------------- 336 | 337 | template 338 | void append_to_vector(std::vector& vector, std::span const data) { 339 | vector.insert(vector.end(), data.begin(), data.end()); 340 | } 341 | 342 | //--------------------------------------------------------- 343 | 344 | template 345 | concept IsByteData = IsByte || std::ranges::range && IsByte>; 346 | 347 | /* 348 | Returns the size of any trivial byte-sized element or range of trivial byte-sized elements. 349 | */ 350 | template 351 | [[nodiscard]] 352 | std::size_t size_of_byte_data(T const& data) { 353 | if constexpr (std::ranges::range) { 354 | return std::ranges::distance(data); 355 | } 356 | else { 357 | return sizeof(data); 358 | } 359 | } 360 | 361 | /* 362 | Copies any type of trivial byte-sized element(s) from data to range. 363 | */ 364 | template> 365 | [[nodiscard]] 366 | auto copy_byte_data(Data_ const& data, Range_& range) 367 | -> std::ranges::iterator_t 368 | { 369 | if constexpr (IsByte) { 370 | *std::ranges::begin(range) = *reinterpret_cast(&data); 371 | return std::ranges::begin(range) + 1; 372 | } 373 | else { 374 | return std::ranges::copy(std::span{ 375 | reinterpret_cast(std::ranges::data(data)), 376 | std::ranges::size(data) 377 | }, std::ranges::begin(range)).out; 378 | } 379 | } 380 | 381 | /* 382 | Concatenates any kind of sequence of trivial byte-sized elements like char and std::byte. 383 | The arguments can be individual bytes and/or ranges of bytes. 384 | Returns a utils::DataVector (std::vector). 385 | */ 386 | template 387 | [[nodiscard]] 388 | DataVector concatenate_byte_data(T const& ... arguments) { 389 | auto buffer = DataVector((size_of_byte_data(arguments) + ...)); 390 | auto buffer_span = std::span{buffer}; 391 | ((buffer_span = std::span{copy_byte_data(arguments, buffer_span), buffer_span.end()}), ...); 392 | return buffer; 393 | } 394 | 395 | //--------------------------------------------------------- 396 | 397 | /* 398 | Parses a string as an integer type in a given base. 399 | For more details, see std::from_chars. This is just an abstraction layer on top of it. 400 | */ 401 | template 402 | [[nodiscard]] 403 | std::optional string_to_integral(std::string_view const string, int const base = 10) 404 | { 405 | auto number_result = T{}; 406 | if (std::from_chars(string.data(), string.data() + string.size(), number_result, base).ec == std::errc{}) { 407 | return number_result; 408 | } 409 | return {}; 410 | } 411 | 412 | //--------------------------------------------------------- 413 | 414 | template requires IsByte> 415 | void write_to_file(DataRange_ const& data, std::string const& file_name) { 416 | // std::string because std::ofstream does not take std::string_view. 417 | auto file_stream = std::ofstream{file_name, std::ios::binary}; 418 | file_stream.write(reinterpret_cast(std::ranges::data(data)), std::ranges::size(data)); 419 | } 420 | 421 | //--------------------------------------------------------- 422 | 423 | constexpr auto filter_true = std::views::filter([](auto const& x){ return static_cast(x); }); 424 | constexpr auto dereference_move = std::views::transform([](auto&& x) { return std::move(*x); }); 425 | 426 | /* 427 | Transforms a range of chars into its lowercase equivalent. 428 | */ 429 | constexpr auto ascii_lowercase_transform = std::views::transform([](char const c) { 430 | return static_cast(std::tolower(static_cast(c))); 431 | }); 432 | 433 | /* 434 | Returns whether lhs and rhs are equal, regardless of casing, assuming both are encoded in ASCII. 435 | */ 436 | [[nodiscard]] 437 | constexpr bool equal_ascii_case_insensitive(std::string_view const lhs, std::string_view const rhs) noexcept { 438 | return std::ranges::equal(lhs | ascii_lowercase_transform, rhs | ascii_lowercase_transform); 439 | } 440 | 441 | //--------------------------------------------------------- 442 | 443 | /* 444 | Returns the default port corresponding to the specified protocol. 445 | */ 446 | [[nodiscard]] 447 | constexpr Port default_port_for_protocol(Protocol const protocol) noexcept { 448 | return static_cast(protocol); 449 | } 450 | 451 | [[nodiscard]] 452 | constexpr bool is_protocol_tls_encrypted(Protocol const protocol) noexcept { 453 | return protocol == Protocol::Https; 454 | } 455 | 456 | /* 457 | Returns the protocol that corresponds to the specified case-insensitive string. 458 | For example, "http" converts to Protocol::Http. 459 | */ 460 | [[nodiscard]] 461 | constexpr Protocol get_protocol_from_string(std::string_view const protocol_string) noexcept { 462 | if (equal_ascii_case_insensitive(protocol_string, "http")) { 463 | return Protocol::Http; 464 | } 465 | else if (equal_ascii_case_insensitive(protocol_string, "https")) { 466 | return Protocol::Https; 467 | } 468 | return Protocol::Unknown; 469 | } 470 | 471 | /* 472 | The result of the split_url function. 473 | */ 474 | struct UrlComponents { 475 | Protocol protocol{Protocol::Unknown}; 476 | std::string_view host; 477 | Port port{default_port_for_protocol(Protocol::Unknown)}; 478 | std::string_view path; 479 | }; 480 | 481 | 482 | struct HostAndPort { 483 | std::string_view host; 484 | std::optional port; 485 | }; 486 | 487 | /* 488 | Splits a domain name into a name and an optional port. 489 | For example, "localhost:8080" returns "localhost" and 8080, while 490 | "google.com" returns "google.com" and no port (std::nullopt). 491 | */ 492 | [[nodiscard]] 493 | inline HostAndPort split_domain_name(std::string_view const domain_name) 494 | { 495 | if (auto const colon_position = domain_name.rfind(':'); 496 | colon_position != std::string_view::npos) 497 | { 498 | if (auto const port = string_to_integral(domain_name.substr(colon_position + 1))) 499 | { 500 | return HostAndPort{ 501 | .host{domain_name.substr(0, colon_position)}, 502 | .port{port} 503 | }; 504 | } 505 | else 506 | { 507 | return HostAndPort{ 508 | .host{domain_name.substr(0, colon_position)}, 509 | .port = std::nullopt 510 | }; 511 | } 512 | } 513 | return HostAndPort{ 514 | .host{domain_name}, 515 | .port = std::nullopt 516 | }; 517 | } 518 | 519 | /* 520 | Splits an URL into its components. 521 | */ 522 | [[nodiscard]] 523 | inline UrlComponents split_url(std::string_view const url) noexcept { 524 | using namespace std::string_view_literals; 525 | 526 | if (url.empty()) { 527 | return {}; 528 | } 529 | 530 | auto result = UrlComponents{}; 531 | 532 | constexpr auto whitespace_characters = " \t\r\n"sv; 533 | 534 | // Find the start position of the protocol. 535 | auto start_position = url.find_first_not_of(whitespace_characters); 536 | if (start_position == std::string_view::npos) { 537 | return {}; 538 | } 539 | 540 | constexpr auto protocol_suffix = "://"sv; 541 | 542 | // Find the end position of the protocol. 543 | if (auto const position = url.find(protocol_suffix, start_position); 544 | position != std::string_view::npos) 545 | { 546 | result.protocol = get_protocol_from_string(url.substr(start_position, position - start_position)); 547 | result.port = default_port_for_protocol(result.protocol); 548 | 549 | // The start position of the domain name. 550 | start_position = position + protocol_suffix.length(); 551 | } 552 | 553 | // Find the end position of the domain name and start of the path. 554 | if (auto const slash_position = url.find('/', start_position); 555 | slash_position != std::string_view::npos) 556 | { 557 | auto [host, port] = split_domain_name(url.substr(start_position, slash_position - start_position)); 558 | 559 | result.host = host; 560 | 561 | if (port) { 562 | result.port = *port; 563 | } 564 | 565 | start_position = slash_position; 566 | } 567 | else { 568 | // There was nothing after the domain name. 569 | auto [host, port] = split_domain_name(url.substr(start_position)); 570 | 571 | result.host = host; 572 | 573 | if (port) { 574 | result.port = *port; 575 | } 576 | 577 | result.path = "/"sv; 578 | return result; 579 | } 580 | 581 | // Find the end position of the path. 582 | auto const end_position = url.find_last_not_of(whitespace_characters) + 1; 583 | result.path = url.substr(start_position, end_position - start_position); 584 | return result; 585 | } 586 | 587 | /* 588 | Returns the file name part of a URL (or file path with only forward slashes). 589 | */ 590 | [[nodiscard]] 591 | constexpr std::string_view extract_filename(std::string_view const url) 592 | { 593 | if (auto const slash_pos = url.rfind('/'); 594 | slash_pos != std::string_view::npos) 595 | { 596 | if (auto const question_mark_pos = url.find('?', slash_pos + 1); 597 | question_mark_pos != std::string_view::npos) 598 | { 599 | return url.substr(slash_pos + 1, question_mark_pos - slash_pos - 1); 600 | } 601 | 602 | return url.substr(slash_pos + 1); 603 | } 604 | return {}; 605 | } 606 | 607 | /* 608 | Returns whether character is allowed in a URI-encoded string or not. 609 | */ 610 | [[nodiscard]] 611 | constexpr bool get_is_allowed_uri_character(char const character) noexcept { 612 | constexpr auto other_characters = std::string_view{"%-._~:/?#[]@!$&'()*+,;="}; 613 | 614 | return (character >= '0' && character <= '9') || 615 | (character >= 'a' && character <= 'z') || 616 | (character >= 'A' && character <= 'Z') || 617 | other_characters.find(character) != std::string_view::npos; 618 | } 619 | 620 | /* 621 | Returns the URI-encoded equivalent of uri. 622 | */ 623 | [[nodiscard]] 624 | inline std::string uri_encode(std::string_view const uri) { 625 | auto result_string = std::string(); 626 | result_string.reserve(uri.size()); 627 | 628 | for (auto const character : uri) { 629 | if (get_is_allowed_uri_character(character)) { 630 | result_string += character; 631 | } 632 | else { 633 | result_string += "%xx"; 634 | std::to_chars( 635 | &result_string.back() - 1, 636 | &result_string.back() + 1, 637 | static_cast(character), 638 | 16 639 | ); 640 | } 641 | } 642 | return result_string; 643 | } 644 | 645 | } // namespace utils 646 | 647 | //--------------------------------------------------------- 648 | 649 | namespace errors { 650 | 651 | /* 652 | The connection to the server failed in some way. 653 | For example, there is no internet connection or the server name is invalid. 654 | */ 655 | class ConnectionFailed : public std::exception { 656 | public: 657 | [[nodiscard]] 658 | char const* what() const noexcept override { 659 | return reason_.c_str(); 660 | } 661 | 662 | [[nodiscard]] 663 | bool get_is_tls_failure() const noexcept { 664 | return is_tls_failure_; 665 | } 666 | 667 | ConnectionFailed(std::string reason, bool const is_tls_failure = false) noexcept : 668 | reason_(std::move(reason)), 669 | is_tls_failure_{is_tls_failure} 670 | {} 671 | 672 | private: 673 | std::string reason_; 674 | bool is_tls_failure_; 675 | }; 676 | 677 | class ResponseParsingFailed : public std::exception { 678 | public: 679 | [[nodiscard]] 680 | char const* what() const noexcept override { 681 | return reason_.c_str(); 682 | } 683 | 684 | ResponseParsingFailed(std::string reason) : 685 | reason_(std::move(reason)) 686 | {} 687 | 688 | private: 689 | std::string reason_; 690 | }; 691 | 692 | } // namespace errors 693 | 694 | //--------------------------------------------------------- 695 | 696 | /* 697 | This type is used by the Socket class to signify that 698 | the peer closed the connection during a read call. 699 | */ 700 | struct ConnectionClosed {}; 701 | 702 | /* 703 | An abstraction on top of low level socket and TLS encryption APIs. 704 | Marking a Socket as const only means it won't be moved from or move assigned to. 705 | */ 706 | class Socket { 707 | public: 708 | /* 709 | Sends data to the peer through the socket. 710 | */ 711 | void write(std::span data) const; 712 | /* 713 | Sends a string to the peer through the socket. 714 | This function takes a basic_string_view, think about 715 | whether you want it to be null terminated or not. 716 | */ 717 | void write(std::string_view const string_view) const { 718 | write(utils::string_to_data(string_view)); 719 | } 720 | 721 | /* 722 | Receives data from the socket and reads it into a buffer. 723 | This function blocks until there is some data available. 724 | The data that was read may be smaller than the buffer. 725 | The function either returns the number of bytes that were read 726 | or a ConnectionClosed value if the peer closed the connection. 727 | */ 728 | [[nodiscard("The result is important as it contains the size that was actually read.")]] 729 | std::variant read(std::span buffer) const; 730 | /* 731 | Receives data from the socket. 732 | This function blocks until there is some data available. 733 | The function either returns the buffer that was read 734 | or a ConnectionClosed value if the peer closed the connection. 735 | The returned DataVector may be smaller than what was requested. 736 | */ 737 | [[nodiscard]] 738 | auto read(std::size_t const number_of_bytes = 512) const 739 | -> std::variant 740 | { 741 | auto result = utils::DataVector(number_of_bytes); 742 | if (auto const read_result = read(result); std::holds_alternative(read_result)) { 743 | result.resize(std::get(read_result)); 744 | return result; 745 | } 746 | return ConnectionClosed{}; 747 | } 748 | 749 | /* 750 | Reads any available data from the socket into a buffer. 751 | This function is nonblocking, and may return std::size_t{} if 752 | there was no data available. The function either returns the number 753 | of bytes that were read or a ConnectionClosed value if the peer 754 | closed the connection. 755 | */ 756 | [[nodiscard("The result is important as it contains the size that was actually read.")]] 757 | std::variant read_available(std::span buffer) const; 758 | /* 759 | Reads any available data from the socket into a buffer. 760 | This function is nonblocking, and may return an empty vector if 761 | there was no data available. The function either returns a utils::DataVector 762 | of the data that was read or a ConnectionClosed value if the peer 763 | closed the connection. 764 | */ 765 | template 766 | [[nodiscard]] 767 | std::variant read_available() const { 768 | auto buffer = utils::DataVector(read_buffer_size); 769 | auto read_offset = std::size_t{}; 770 | 771 | while (true) { 772 | if (auto const read_result = read_available( 773 | std::span{buffer.data() + read_offset, read_buffer_size} 774 | ); std::holds_alternative(read_result)) 775 | { 776 | if (auto const bytes_read = std::get(read_result)) { 777 | read_offset += bytes_read; 778 | buffer.resize(read_offset + read_buffer_size); 779 | } 780 | else return buffer; 781 | } 782 | else return ConnectionClosed{}; 783 | } 784 | return {}; 785 | } 786 | 787 | Socket() = delete; 788 | ~Socket(); // = default in .cpp 789 | 790 | Socket(Socket&&) noexcept; // = default in .cpp 791 | Socket& operator=(Socket&&) noexcept; // = default in .cpp 792 | 793 | Socket(Socket const&) = delete; 794 | Socket& operator=(Socket const&) = delete; 795 | 796 | private: 797 | class Implementation; 798 | std::unique_ptr implementation_; 799 | 800 | Socket(std::string_view server, Port port, bool is_tls_encrypted); 801 | friend Socket open_socket(std::string_view, Port, bool); 802 | }; 803 | 804 | /* 805 | Opens a socket to a server through a port. 806 | If port is 443 OR is_tls_encrypted is true, TLS encryption is used. 807 | Otherwise it is unencrypted. 808 | */ 809 | [[nodiscard]] 810 | inline Socket open_socket(std::string_view const server, Port const port, bool const is_tls_encrypted = false) { 811 | return Socket{server, port, is_tls_encrypted}; 812 | } 813 | 814 | //--------------------------------------------------------- 815 | 816 | struct Header; 817 | 818 | /* 819 | Represents a HTTP header whose data was copied from somewhere at some point. 820 | It consists of std::string objects instead of std::string_view. 821 | */ 822 | struct HeaderCopy { 823 | std::string name, value; 824 | 825 | [[nodiscard]] 826 | inline explicit operator Header() const; 827 | }; 828 | /* 829 | Represents a HTTP header whose data is not owned by this object. 830 | It consists of std::string_view objects instead of std::string. 831 | */ 832 | struct Header { 833 | std::string_view name, value; 834 | 835 | [[nodiscard]] 836 | explicit operator HeaderCopy() const { 837 | return HeaderCopy{ 838 | .name = std::string{name}, 839 | .value = std::string{value}, 840 | }; 841 | } 842 | }; 843 | HeaderCopy::operator Header() const { 844 | return Header{ 845 | .name = std::string_view{name}, 846 | .value = std::string_view{value}, 847 | }; 848 | } 849 | 850 | template 851 | concept IsHeader = utils::IsAnyOf; 852 | 853 | /* 854 | Compares two headers, taking into account case insensitivity. 855 | */ 856 | [[nodiscard]] 857 | bool operator==(IsHeader auto const& lhs, IsHeader auto const& rhs) { 858 | return lhs.value == rhs.value && utils::equal_ascii_case_insensitive(lhs.name, rhs.name); 859 | } 860 | 861 | enum class StatusCode { 862 | Continue = 100, 863 | SwitchingProtocols = 101, 864 | Processing = 102, 865 | EarlyHints = 103, 866 | 867 | Ok = 200, 868 | Created = 201, 869 | Accepted = 202, 870 | NonAuthoritativeInformation = 203, 871 | NoContent = 204, 872 | ResetContent = 205, 873 | PartialContent = 206, 874 | MultiStatus = 207, 875 | AlreadyReported = 208, 876 | ImUsed = 226, 877 | 878 | MultipleChoices = 300, 879 | MovedPermanently = 301, 880 | Found = 302, 881 | SeeOther = 303, 882 | NotModified = 304, 883 | UseProxy = 305, 884 | SwitchProxy = 306, 885 | TemporaryRedirect = 307, 886 | PermanentRedirect = 308, 887 | 888 | BadRequest = 400, 889 | Unauthorized = 401, 890 | PaymentRequired = 402, 891 | Forbidden = 403, 892 | NotFound = 404, 893 | MethodNotAllowed = 405, 894 | NotAcceptable = 406, 895 | ProxyAuthenticationRequired = 407, 896 | RequestTimeout = 408, 897 | Conflict = 409, 898 | Gone = 410, 899 | LengthRequired = 411, 900 | PreconditionFailed = 412, 901 | PayloadTooLarge = 413, 902 | UriTooLong = 414, 903 | UnsupportedMediaType = 415, 904 | RangeNotSatisfiable = 416, 905 | ExpectationFailed = 417, 906 | ImATeapot = 418, 907 | MisdirectedRequest = 421, 908 | UnprocessableEntity = 422, 909 | Locked = 423, 910 | FailedDependency = 424, 911 | TooEarly = 425, 912 | UpgradeRequired = 426, 913 | PreconditionRequired = 428, 914 | TooManyRequests = 429, 915 | RequestHeaderFieldsTooLarge = 431, 916 | UnavailableForLegalReasons = 451, 917 | 918 | InternalServerError = 500, 919 | NotImplemented = 501, 920 | BadGateway = 502, 921 | ServiceUnavailable = 503, 922 | GatewayTimeout = 504, 923 | HttpVersionNotSupported = 505, 924 | VariantAlsoNegotiates = 506, 925 | InsufficientStorage = 507, 926 | LoopDetected = 508, 927 | NotExtended = 510, 928 | NetworkAuthenticationRequired = 511, 929 | 930 | Unknown = -1 931 | }; 932 | 933 | struct StatusLine { 934 | std::string http_version; 935 | StatusCode status_code = StatusCode::Unknown; 936 | std::string status_message; 937 | 938 | [[nodiscard]] 939 | bool operator==(StatusLine const&) const noexcept = default; 940 | }; 941 | 942 | namespace algorithms { 943 | 944 | [[nodiscard]] 945 | inline StatusLine parse_status_line(std::string_view const line) { 946 | auto status_line = StatusLine{}; 947 | 948 | auto cursor = std::size_t{}; 949 | 950 | if (auto const http_version_end = line.find(' '); http_version_end != std::string_view::npos) 951 | { 952 | status_line.http_version = line.substr(0, http_version_end); 953 | cursor = http_version_end + 1; 954 | } 955 | else return status_line; 956 | 957 | if (auto const status_code_end = line.find(' ', cursor); status_code_end != std::string_view::npos) 958 | { 959 | if (auto const status_code = utils::string_to_integral(line.substr(cursor, status_code_end))) 960 | { 961 | status_line.status_code = static_cast(*status_code); 962 | } 963 | else return status_line; 964 | cursor = status_code_end + 1; 965 | } 966 | else return status_line; 967 | 968 | status_line.status_message = line.substr(cursor, line.find_last_not_of("\r\n ") + 1 - cursor); 969 | return status_line; 970 | } 971 | 972 | [[nodiscard]] 973 | constexpr std::optional
parse_header(std::string_view const line) { 974 | /* 975 | "An HTTP header consists of its case-insensitive name followed by a colon (:), 976 | then by its value. Whitespace before the value is ignored." 977 | (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) 978 | 979 | So we're just ignoring whitespace before the value, and after because there may be 980 | an \r there if the line endings are CRLF. 981 | */ 982 | 983 | auto const colon_pos = line.find(':'); 984 | if (colon_pos == std::string_view::npos) { 985 | return {}; 986 | } 987 | 988 | constexpr auto whitespace_characters = std::string_view{" \t\r"}; 989 | 990 | auto const value_start = line.find_first_not_of(whitespace_characters, colon_pos + 1); 991 | if (value_start == std::string_view::npos) { 992 | return {}; 993 | } 994 | 995 | // This will never be npos, assuming the header 996 | // string isn't mutated by some other thread. 997 | auto const value_end = line.find_last_not_of(whitespace_characters); 998 | 999 | return Header{ 1000 | .name = line.substr(0, colon_pos), 1001 | .value = line.substr(value_start, value_end + 1 - value_start) 1002 | }; 1003 | } 1004 | 1005 | [[nodiscard]] 1006 | inline std::vector
parse_headers_string(std::string_view const headers) 1007 | { 1008 | auto result = std::vector
(); 1009 | 1010 | std::ranges::copy( 1011 | headers 1012 | | std::views::split('\n') | std::views::transform(utils::range_to_string_view) 1013 | | std::views::transform(parse_header) | utils::filter_true | utils::dereference_move, 1014 | std::back_inserter(result) 1015 | ); 1016 | 1017 | return result; 1018 | } 1019 | 1020 | template> 1021 | [[nodiscard]] 1022 | inline Header_ const* find_header_by_name(Range_ const& headers, std::string_view const name) 1023 | { 1024 | auto const lowercase_name_to_search = utils::range_to_string( 1025 | name | utils::ascii_lowercase_transform 1026 | ); 1027 | auto const pos = std::ranges::find_if(headers, [&](Header_ const& header) { 1028 | return std::ranges::equal(lowercase_name_to_search, header.name | utils::ascii_lowercase_transform); 1029 | }); 1030 | if (pos == std::ranges::end(headers)) { 1031 | return nullptr; 1032 | } 1033 | else { 1034 | return &*pos; 1035 | } 1036 | } 1037 | 1038 | struct ParsedResponse { 1039 | StatusLine status_line; 1040 | std::string headers_string; 1041 | std::vector
headers; // Points into headers_string 1042 | utils::DataVector body_data; 1043 | 1044 | [[nodiscard]] 1045 | bool operator==(ParsedResponse const&) const noexcept = default; 1046 | 1047 | ParsedResponse() = default; 1048 | ParsedResponse(StatusLine p_status_line, std::string p_headers_string = {}, std::vector
p_headers = {}, utils::DataVector p_body_data = {}) : 1049 | status_line{std::move(p_status_line)}, 1050 | headers_string(std::move(p_headers_string)), 1051 | headers(std::move(p_headers)), 1052 | body_data(std::move(p_body_data)) 1053 | {} 1054 | 1055 | ParsedResponse(ParsedResponse&&) = default; 1056 | ParsedResponse& operator=(ParsedResponse&&) = default; 1057 | 1058 | ParsedResponse(ParsedResponse const&) = delete; 1059 | ParsedResponse& operator=(ParsedResponse const&) = delete; 1060 | }; 1061 | 1062 | struct ParsedHeadersInterface { 1063 | virtual ~ParsedHeadersInterface() = default; 1064 | 1065 | constexpr virtual ParsedResponse const& get_parsed_response() const noexcept = 0; 1066 | 1067 | /* 1068 | Returns the status code from the response header. 1069 | */ 1070 | [[nodiscard]] 1071 | StatusCode get_status_code() const { 1072 | return get_parsed_response().status_line.status_code; 1073 | } 1074 | /* 1075 | Returns the status code description from the response header. 1076 | */ 1077 | [[nodiscard]] 1078 | std::string_view get_status_message() const { 1079 | return get_parsed_response().status_line.status_message; 1080 | } 1081 | /* 1082 | Returns the HTTP version from the response header. 1083 | */ 1084 | [[nodiscard]] 1085 | std::string_view get_http_version() const { 1086 | return get_parsed_response().status_line.http_version; 1087 | } 1088 | /* 1089 | Returns a const reference to the parsed status line object. 1090 | */ 1091 | [[nodiscard]] 1092 | StatusLine const& get_status_line() const { 1093 | return get_parsed_response().status_line; 1094 | } 1095 | 1096 | /* 1097 | Returns the headers of the response as a string. 1098 | The returned string_view shall not outlive this Response object. 1099 | */ 1100 | [[nodiscard]] 1101 | std::string_view get_headers_string() const { 1102 | return get_parsed_response().headers_string; 1103 | } 1104 | 1105 | /* 1106 | Returns the headers of the response as Header objects. 1107 | The returned span shall not outlive this Response object. 1108 | */ 1109 | [[nodiscard]] 1110 | std::span
get_headers() const { 1111 | return get_parsed_response().headers; 1112 | } 1113 | /* 1114 | Returns a header of the response by its name. 1115 | The returned header shall not outlive this Response object. 1116 | */ 1117 | [[nodiscard]] 1118 | std::optional
get_header(std::string_view const name) const { 1119 | if (auto const header = algorithms::find_header_by_name(get_parsed_response().headers, name)) { 1120 | return *header; 1121 | } 1122 | else return {}; 1123 | } 1124 | /* 1125 | Returns a header value of the response by its name. 1126 | The returned std::string_view shall not outlive this Response object. 1127 | */ 1128 | [[nodiscard]] 1129 | std::optional get_header_value(std::string_view const name) const { 1130 | if (auto const header = algorithms::find_header_by_name(get_parsed_response().headers, name)) { 1131 | return header->value; 1132 | } 1133 | else return {}; 1134 | } 1135 | }; 1136 | 1137 | class ResponseParser; 1138 | 1139 | } // namespace algorithms 1140 | 1141 | class ResponseProgressRaw { 1142 | friend class algorithms::ResponseParser; 1143 | 1144 | public: 1145 | constexpr void stop() noexcept { 1146 | is_stopped_ = true; 1147 | } 1148 | 1149 | std::span data; 1150 | std::size_t new_data_start; 1151 | 1152 | explicit constexpr ResponseProgressRaw(std::span const p_data, std::size_t const p_new_data_start) noexcept : 1153 | data{p_data}, new_data_start{p_new_data_start} 1154 | {} 1155 | 1156 | private: 1157 | bool is_stopped_{}; 1158 | }; 1159 | 1160 | class ResponseProgressHeaders : public algorithms::ParsedHeadersInterface { 1161 | public: 1162 | ResponseProgressRaw raw_progress; 1163 | 1164 | constexpr void stop() noexcept { 1165 | raw_progress.stop(); 1166 | } 1167 | 1168 | [[nodiscard]] 1169 | constexpr algorithms::ParsedResponse const& get_parsed_response() const noexcept override { 1170 | return parsed_response_; 1171 | } 1172 | 1173 | ResponseProgressHeaders(ResponseProgressRaw const p_raw_progress, algorithms::ParsedResponse const& parsed_response) : 1174 | raw_progress{p_raw_progress}, parsed_response_{parsed_response} 1175 | {} 1176 | 1177 | ResponseProgressHeaders() = delete; 1178 | ~ResponseProgressHeaders() = default; 1179 | 1180 | ResponseProgressHeaders(ResponseProgressHeaders const&) = delete; 1181 | ResponseProgressHeaders& operator=(ResponseProgressHeaders const&) = delete; 1182 | 1183 | ResponseProgressHeaders(ResponseProgressHeaders&&) noexcept = delete; 1184 | ResponseProgressHeaders& operator=(ResponseProgressHeaders&&) noexcept = delete; 1185 | 1186 | private: 1187 | algorithms::ParsedResponse const& parsed_response_; 1188 | }; 1189 | 1190 | class ResponseProgressBody : public algorithms::ParsedHeadersInterface { 1191 | public: 1192 | ResponseProgressRaw raw_progress; 1193 | 1194 | std::span body_data_so_far; 1195 | /* 1196 | This may not have a value if the transfer encoding is chunked, in which 1197 | case the full body length is not known ahead of time. 1198 | */ 1199 | std::optional total_expected_body_size; 1200 | 1201 | constexpr void stop() noexcept { 1202 | raw_progress.stop(); 1203 | } 1204 | 1205 | [[nodiscard]] 1206 | constexpr algorithms::ParsedResponse const& get_parsed_response() const noexcept override { 1207 | return parsed_response_; 1208 | } 1209 | 1210 | ResponseProgressBody( 1211 | ResponseProgressRaw const p_raw_progress, 1212 | algorithms::ParsedResponse const& parsed_response, 1213 | std::span const p_body_data_so_far, 1214 | std::optional const p_total_expected_body_size 1215 | ) : 1216 | raw_progress{p_raw_progress}, 1217 | body_data_so_far{p_body_data_so_far}, 1218 | total_expected_body_size{p_total_expected_body_size}, 1219 | parsed_response_{parsed_response} 1220 | {} 1221 | 1222 | ResponseProgressBody() = delete; 1223 | ~ResponseProgressBody() = default; 1224 | 1225 | ResponseProgressBody(ResponseProgressBody const&) = delete; 1226 | ResponseProgressBody& operator=(ResponseProgressBody const&) = delete; 1227 | 1228 | ResponseProgressBody(ResponseProgressBody&&) noexcept = delete; 1229 | ResponseProgressBody& operator=(ResponseProgressBody&&) noexcept = delete; 1230 | 1231 | private: 1232 | algorithms::ParsedResponse const& parsed_response_; 1233 | }; 1234 | 1235 | /* 1236 | Represents the response of a HTTP request. 1237 | */ 1238 | class Response : public algorithms::ParsedHeadersInterface { 1239 | public: 1240 | [[nodiscard]] 1241 | constexpr algorithms::ParsedResponse const& get_parsed_response() const noexcept override { 1242 | return parsed_response_; 1243 | } 1244 | 1245 | /* 1246 | Returns the body of the response. 1247 | The returned std::span shall not outlive this Response object. 1248 | */ 1249 | [[nodiscard]] 1250 | std::span get_body() const { 1251 | return parsed_response_.body_data; 1252 | } 1253 | /* 1254 | Returns the body of the response as a string. 1255 | The returned std::string_view shall not outlive this Response object. 1256 | */ 1257 | [[nodiscard]] 1258 | std::string_view get_body_string() const { 1259 | return utils::data_to_string(get_body()); 1260 | } 1261 | 1262 | [[nodiscard]] 1263 | std::string_view get_url() const { 1264 | return url_; 1265 | } 1266 | 1267 | // std::future requires default constructibility on MSVC... Because of ABI stability. 1268 | Response() = default; 1269 | ~Response() = default; 1270 | 1271 | Response(Response const&) = delete; 1272 | Response& operator=(Response const&) = delete; 1273 | 1274 | Response(Response&&) noexcept = default; 1275 | Response& operator=(Response&&) noexcept = default; 1276 | 1277 | Response(algorithms::ParsedResponse&& parsed_response, std::string&& url) : 1278 | parsed_response_{std::move(parsed_response)}, 1279 | url_{std::move(url)} 1280 | {} 1281 | 1282 | private: 1283 | algorithms::ParsedResponse parsed_response_; 1284 | std::string url_; 1285 | }; 1286 | 1287 | namespace algorithms { 1288 | 1289 | class ChunkyBodyParser { 1290 | public: 1291 | [[nodiscard]] 1292 | std::optional parse_new_data(std::span const new_data) { 1293 | if (has_returned_result_) { 1294 | return {}; 1295 | } 1296 | if (is_finished_) { 1297 | has_returned_result_ = true; 1298 | return std::move(result_); 1299 | } 1300 | 1301 | auto cursor = start_parse_offset_; 1302 | 1303 | while (true) { 1304 | if (cursor >= new_data.size()) { 1305 | start_parse_offset_ = cursor - new_data.size(); 1306 | return {}; 1307 | } 1308 | if (auto const cursor_offset = parse_next_part_(new_data.subspan(cursor))) { 1309 | cursor += cursor_offset; 1310 | } 1311 | else { 1312 | has_returned_result_ = true; 1313 | return std::move(result_); 1314 | } 1315 | } 1316 | } 1317 | [[nodiscard]] 1318 | std::span get_result_so_far() const { 1319 | return result_; 1320 | } 1321 | 1322 | private: 1323 | static constexpr auto newline = std::string_view{"\r\n"}; 1324 | 1325 | /* 1326 | "part" refers to a separately parsed unit of data. 1327 | This partitioning makes the parsing algorithm simpler. 1328 | Returns the position where the part ended. 1329 | It may be past the end of the part. 1330 | */ 1331 | [[nodiscard]] 1332 | std::size_t parse_next_part_(std::span const new_data) { 1333 | if (chunk_size_left_) { 1334 | return parse_chunk_body_part_(new_data); 1335 | } 1336 | else return parse_chunk_separator_part_(new_data); 1337 | } 1338 | 1339 | [[nodiscard]] 1340 | std::size_t parse_chunk_body_part_(std::span const new_data) { 1341 | if (chunk_size_left_ > new_data.size()) 1342 | { 1343 | chunk_size_left_ -= new_data.size(); 1344 | utils::append_to_vector(result_, new_data); 1345 | return new_data.size(); 1346 | } 1347 | else { 1348 | utils::append_to_vector(result_, new_data.first(chunk_size_left_)); 1349 | 1350 | // After each chunk, there is a \r\n and then the size of the next chunk. 1351 | // We skip the \r\n so the next part starts at the size number. 1352 | auto const part_end = chunk_size_left_ + newline.size(); 1353 | chunk_size_left_ = 0; 1354 | return part_end; 1355 | } 1356 | } 1357 | 1358 | [[nodiscard]] 1359 | std::size_t parse_chunk_separator_part_(std::span const new_data) { 1360 | auto const data_string = utils::data_to_string(new_data); 1361 | 1362 | auto const first_newline_character_pos = data_string.find(newline[0]); 1363 | 1364 | if (first_newline_character_pos == std::string_view::npos) { 1365 | chunk_size_string_buffer_ += data_string; 1366 | return new_data.size(); 1367 | } 1368 | else if (chunk_size_string_buffer_.empty()) { 1369 | parse_chunk_size_left_(data_string.substr(0, first_newline_character_pos)); 1370 | } 1371 | else { 1372 | chunk_size_string_buffer_ += data_string.substr(0, first_newline_character_pos); 1373 | parse_chunk_size_left_(chunk_size_string_buffer_); 1374 | chunk_size_string_buffer_.clear(); 1375 | } 1376 | 1377 | if (chunk_size_left_ == 0) { 1378 | is_finished_ = true; 1379 | return 0; 1380 | } 1381 | 1382 | return first_newline_character_pos + newline.size(); 1383 | } 1384 | 1385 | void parse_chunk_size_left_(std::string_view const string) { 1386 | // hexadecimal 1387 | if (auto const result = utils::string_to_integral(string, 16)) { 1388 | chunk_size_left_ = *result; 1389 | } 1390 | else throw errors::ResponseParsingFailed{"Failed parsing http body chunk size."}; 1391 | } 1392 | 1393 | utils::DataVector result_; 1394 | 1395 | bool is_finished_{false}; 1396 | bool has_returned_result_{false}; 1397 | 1398 | std::size_t start_parse_offset_{}; 1399 | 1400 | std::string chunk_size_string_buffer_; 1401 | std::size_t chunk_size_left_{}; 1402 | }; 1403 | 1404 | struct ResponseCallbacks { 1405 | std::function handle_raw_progress; 1406 | std::function handle_headers; 1407 | std::function handle_body_progress; 1408 | std::function handle_finish; 1409 | std::function handle_stop; 1410 | }; 1411 | 1412 | /* 1413 | Separate, testable module that parses a http response. 1414 | It has support for optional response progress callbacks. 1415 | */ 1416 | class ResponseParser { 1417 | public: 1418 | /* 1419 | Parses a new packet of data from the HTTP response. 1420 | If it reached the end of the response, the parsed result is returned. 1421 | */ 1422 | [[nodiscard]] 1423 | std::optional parse_new_data(std::span const data) { 1424 | if (is_done_) { 1425 | return {}; 1426 | } 1427 | 1428 | auto const new_data_start = buffer_.size(); 1429 | 1430 | utils::append_to_vector(buffer_, data); 1431 | 1432 | if (callbacks_ && (*callbacks_)->handle_raw_progress) { 1433 | auto raw_progress = ResponseProgressRaw{buffer_, new_data_start}; 1434 | (*callbacks_)->handle_raw_progress(raw_progress); 1435 | if (raw_progress.is_stopped_) { 1436 | finish_(); 1437 | } 1438 | } 1439 | 1440 | if (!is_done_ && result_.headers_string.empty()) { 1441 | try_parse_headers_(new_data_start); 1442 | } 1443 | 1444 | if (!is_done_ && !result_.headers_string.empty()) { 1445 | if (chunky_body_parser_) { 1446 | parse_new_chunky_body_data_(new_data_start); 1447 | } 1448 | else { 1449 | parse_new_regular_body_data_(new_data_start); 1450 | } 1451 | } 1452 | if (is_done_) { 1453 | return std::move(result_); 1454 | } 1455 | return {}; 1456 | } 1457 | 1458 | ResponseParser() = default; 1459 | ResponseParser(ResponseCallbacks& callbacks) : 1460 | callbacks_{&callbacks} 1461 | {} 1462 | 1463 | private: 1464 | void finish_() { 1465 | is_done_ = true; 1466 | if (callbacks_ && (*callbacks_)->handle_stop) { 1467 | (*callbacks_)->handle_stop(); 1468 | } 1469 | } 1470 | 1471 | void try_parse_headers_(std::size_t const new_data_start) { 1472 | // Only do anything if the headers are completely received. 1473 | if (auto const headers_string = try_extract_headers_string_(new_data_start)) 1474 | { 1475 | result_.headers_string = *headers_string; 1476 | 1477 | auto status_line_end = result_.headers_string.find_first_of("\r\n"); 1478 | if (status_line_end == std::string_view::npos) { 1479 | status_line_end = result_.headers_string.size(); 1480 | } 1481 | 1482 | result_.status_line = algorithms::parse_status_line( 1483 | std::string_view{result_.headers_string}.substr(0, status_line_end) 1484 | ); 1485 | 1486 | if (result_.headers_string.size() > status_line_end) { 1487 | result_.headers = algorithms::parse_headers_string( 1488 | std::string_view{result_.headers_string}.substr(status_line_end) 1489 | ); 1490 | } 1491 | 1492 | if (callbacks_ && (*callbacks_)->handle_headers) { 1493 | auto progress_headers = ResponseProgressHeaders{ResponseProgressRaw{buffer_, new_data_start}, result_}; 1494 | (*callbacks_)->handle_headers(progress_headers); 1495 | if (progress_headers.raw_progress.is_stopped_) { 1496 | finish_(); 1497 | } 1498 | } 1499 | 1500 | if (auto const body_size_try = get_body_size_()) { 1501 | body_size_ = *body_size_try; 1502 | } 1503 | else if (auto const transfer_encoding = algorithms::find_header_by_name(result_.headers, "transfer-encoding"); 1504 | transfer_encoding && transfer_encoding->value == "chunked") 1505 | { 1506 | chunky_body_parser_ = ChunkyBodyParser{}; 1507 | } 1508 | } 1509 | } 1510 | /* 1511 | Checks whether the headers have been completely received from the server and if so returns them as a string. 1512 | Otherwise returns an empty `std::optional`. 1513 | 1514 | `new_data_start` is the byte index of the beginning of the new data chunk inside `buffer_`. 1515 | */ 1516 | [[nodiscard]] 1517 | std::optional try_extract_headers_string_(std::size_t const new_data_start) { 1518 | // An empty line marks the end of the headers part of the response. 1519 | // '\n' line endings are not conformant with the HTTP standard but may appear anyways. 1520 | for (std::string_view const empty_line : {"\r\n\r\n", "\n\n"}) 1521 | { 1522 | // The byte index inside buffer_ to start searching for an empty line at. 1523 | // This could just be set to zero but to minimize the amount of text to search, 1524 | // we start at the latest point where we have not searched yet. 1525 | auto const find_start = new_data_start >= empty_line.length() - 1 1526 | ? new_data_start - (empty_line.length() - 1) 1527 | : std::size_t{}; 1528 | 1529 | // String view of the whole buffer_ which has been received at this point. 1530 | auto const string_view_to_search = utils::data_to_string(std::span{buffer_}); 1531 | 1532 | if (auto const position = string_view_to_search.find(empty_line, find_start); 1533 | position != std::string_view::npos) 1534 | { 1535 | body_start_ = position + empty_line.length(); 1536 | return string_view_to_search.substr(0, position); 1537 | } 1538 | } 1539 | return {}; 1540 | } 1541 | [[nodiscard]] 1542 | std::optional get_body_size_() const { 1543 | if (auto const content_length_string = 1544 | algorithms::find_header_by_name(result_.headers, "content-length")) 1545 | { 1546 | if (auto const parse_result = 1547 | utils::string_to_integral(content_length_string->value)) 1548 | { 1549 | return *parse_result; 1550 | } 1551 | } 1552 | return {}; 1553 | } 1554 | 1555 | void parse_new_chunky_body_data_(std::size_t const new_data_start) { 1556 | // May need to add an offset if this packet is where the headers end and the body starts. 1557 | auto const body_parse_start = std::max(new_data_start, body_start_); 1558 | if (auto body = chunky_body_parser_->parse_new_data(std::span{buffer_}.subspan(body_parse_start))) 1559 | { 1560 | result_.body_data = *std::move(body); 1561 | 1562 | if (callbacks_ && (*callbacks_)->handle_body_progress) { 1563 | auto body_progress = ResponseProgressBody{ 1564 | ResponseProgressRaw{buffer_, new_data_start}, 1565 | result_, 1566 | result_.body_data, {} 1567 | }; 1568 | (*callbacks_)->handle_body_progress(body_progress); 1569 | } 1570 | 1571 | finish_(); 1572 | } 1573 | else if (callbacks_ && (*callbacks_)->handle_body_progress) { 1574 | auto body_progress = ResponseProgressBody{ 1575 | ResponseProgressRaw{buffer_, new_data_start}, 1576 | result_, 1577 | chunky_body_parser_->get_result_so_far(), {} 1578 | }; 1579 | (*callbacks_)->handle_body_progress(body_progress); 1580 | if (body_progress.raw_progress.is_stopped_) { 1581 | finish_(); 1582 | } 1583 | } 1584 | } 1585 | 1586 | void parse_new_regular_body_data_(std::size_t const new_data_start) { 1587 | if (buffer_.size() >= body_start_ + body_size_) { 1588 | auto const body_begin = buffer_.begin() + body_start_; 1589 | result_.body_data = utils::DataVector(body_begin, body_begin + body_size_); 1590 | 1591 | if (callbacks_ && (*callbacks_)->handle_body_progress) { 1592 | auto body_progress = ResponseProgressBody{ 1593 | ResponseProgressRaw{buffer_, new_data_start}, 1594 | result_, 1595 | result_.body_data, 1596 | body_size_ 1597 | }; 1598 | (*callbacks_)->handle_body_progress(body_progress); 1599 | } 1600 | 1601 | finish_(); 1602 | } 1603 | else if (callbacks_ && (*callbacks_)->handle_body_progress) { 1604 | auto body_progress = ResponseProgressBody{ 1605 | ResponseProgressRaw{buffer_, new_data_start}, 1606 | result_, 1607 | std::span{buffer_}.subspan(body_start_), 1608 | body_size_ 1609 | }; 1610 | (*callbacks_)->handle_body_progress(body_progress); 1611 | if (body_progress.raw_progress.is_stopped_) { 1612 | finish_(); 1613 | } 1614 | } 1615 | } 1616 | 1617 | utils::DataVector buffer_; 1618 | 1619 | ParsedResponse result_; 1620 | bool is_done_{false}; 1621 | 1622 | std::size_t body_start_{}; 1623 | std::size_t body_size_{}; 1624 | 1625 | std::optional chunky_body_parser_; 1626 | 1627 | std::optional callbacks_; 1628 | }; 1629 | 1630 | constexpr auto default_receive_buffer_size = std::size_t{1} << 12; 1631 | 1632 | template 1633 | [[nodiscard]] 1634 | inline Response receive_response(Socket const&& socket, std::string&& url, ResponseCallbacks&& callbacks) { 1635 | // Does not need to be atomic because handle_stop will always be called from this thread. 1636 | auto has_stopped = false; 1637 | callbacks.handle_stop = [&has_stopped]{ has_stopped = true; }; 1638 | 1639 | auto response_parser = algorithms::ResponseParser{callbacks}; 1640 | 1641 | auto read_buffer = std::array(); 1642 | 1643 | while (!has_stopped) { 1644 | if (auto const read_result = socket.read(read_buffer); 1645 | std::holds_alternative(read_result)) 1646 | { 1647 | if (auto parse_result = response_parser.parse_new_data( 1648 | std::span{read_buffer}.first(std::get(read_result)) 1649 | )) 1650 | { 1651 | auto response = Response{std::move(*parse_result), std::move(url)}; 1652 | if (callbacks.handle_finish) { 1653 | callbacks.handle_finish(response); 1654 | } 1655 | return response; 1656 | } 1657 | } 1658 | else throw errors::ConnectionFailed{"The peer closed the connection unexpectedly"}; 1659 | } 1660 | 1661 | utils::unreachable(); 1662 | } 1663 | 1664 | 1665 | 1666 | } // namespace algorithms 1667 | 1668 | //--------------------------------------------------------- 1669 | 1670 | /* 1671 | Enumeration of the different HTTP request methods that can be used. 1672 | */ 1673 | enum class RequestMethod { 1674 | Connect, 1675 | Delete, 1676 | Get, 1677 | Head, 1678 | Options, 1679 | Patch, 1680 | Post, 1681 | Put, 1682 | Trace, 1683 | }; 1684 | 1685 | /* 1686 | Converts a RequestMethod to its uppercase string equivalent. 1687 | For example, RequestMethod::Get becomes std::string_view{"GET"}. 1688 | */ 1689 | [[nodiscard]] 1690 | inline std::string_view request_method_to_string(RequestMethod const method) { 1691 | using enum RequestMethod; 1692 | switch (method) { 1693 | case Connect: return "CONNECT"; 1694 | case Delete: return "DELETE"; 1695 | case Get: return "GET"; 1696 | case Head: return "HEAD"; 1697 | case Options: return "OPTIONS"; 1698 | case Patch: return "PATCH"; 1699 | case Post: return "POST"; 1700 | case Put: return "PUT"; 1701 | case Trace: return "TRACE"; 1702 | } 1703 | utils::unreachable(); 1704 | } 1705 | 1706 | //--------------------------------------------------------- 1707 | 1708 | /* 1709 | Represents a HTTP request. 1710 | It is created by calling any of the HTTP verb functions (http_client::get, http_client::post, http_client::put ...) 1711 | */ 1712 | class Request { 1713 | public: 1714 | /* 1715 | Adds headers to the request as a string. 1716 | These are in the format: "NAME: [ignored whitespace] VALUE" 1717 | The string can be multiple lines for multiple headers. 1718 | Non-ASCII bytes are considered opaque data, 1719 | according to the HTTP specification. 1720 | */ 1721 | [[nodiscard]] 1722 | Request&& add_headers(std::string_view const headers_string) && { 1723 | if (headers_string.empty()) { 1724 | return std::move(*this); 1725 | } 1726 | 1727 | headers_ += headers_string; 1728 | if (headers_string.back() != '\n') { 1729 | headers_ += "\r\n"; // CRLF is the correct line ending for the HTTP protocol 1730 | } 1731 | 1732 | return std::move(*this); 1733 | } 1734 | /* 1735 | Adds headers to the request. 1736 | */ 1737 | template 1738 | [[nodiscard]] 1739 | Request&& add_headers(std::span const headers) && { 1740 | auto headers_string = std::string{}; 1741 | headers_string.reserve(headers.size()*128); 1742 | 1743 | for (auto const& header : headers) { 1744 | headers_string += std::format("{}: {}\r\n", header.name, header.value); 1745 | } 1746 | 1747 | return std::move(*this).add_headers(headers_string); 1748 | } 1749 | /* 1750 | Adds headers to the request. 1751 | */ 1752 | [[nodiscard]] 1753 | Request&& add_headers(std::initializer_list
const headers) && { 1754 | return std::move(*this).add_headers(std::span{headers}); 1755 | } 1756 | /* 1757 | Adds headers to the request. 1758 | This is a variadic template that can take any number of headers. 1759 | */ 1760 | template 1761 | [[nodiscard]] 1762 | Request&& add_headers(Header_&& ... p_headers) && { 1763 | auto const headers = std::array{Header{p_headers}...}; 1764 | return std::move(*this).add_headers(std::span{headers}); 1765 | } 1766 | /* 1767 | Adds a single header to the request. 1768 | Equivalent to add_headers with a single Header argument. 1769 | */ 1770 | [[nodiscard]] 1771 | Request&& add_header(Header const& header) && { 1772 | return std::move(*this).add_headers(std::format("{}: {}", header.name, header.value)); 1773 | } 1774 | 1775 | /* 1776 | Sets the content of the request as a sequence of bytes. 1777 | */ 1778 | template 1779 | [[nodiscard]] 1780 | Request&& set_body(std::span const body_data) && { 1781 | body_.resize(body_data.size()); 1782 | if constexpr (std::same_as) { 1783 | std::ranges::copy(body_data, body_.begin()); 1784 | } 1785 | else { 1786 | std::ranges::copy(std::span{reinterpret_cast(body_data.data()), body_data.size()}, body_.begin()); 1787 | } 1788 | return std::move(*this); 1789 | } 1790 | /* 1791 | Sets the content of the request as a string view. 1792 | */ 1793 | [[nodiscard]] 1794 | Request&& set_body(std::string_view const body_data) && { 1795 | return std::move(*this).set_body(utils::string_to_data(body_data)); 1796 | } 1797 | /* 1798 | Sets a callback to be called after every chunk or buffer of response data has been received from the server. 1799 | The callback takes a mutable ResponseProgressRaw reference which is used to retrieve the data 1800 | and possibly stop receiving data from the server. 1801 | */ 1802 | [[nodiscard]] 1803 | Request&& set_raw_progress_callback(std::function callback) && { 1804 | callbacks_.handle_raw_progress = std::move(callback); 1805 | return std::move(*this); 1806 | } 1807 | /* 1808 | Sets a callback to be called as soon as the whole header portion of the response has been received. 1809 | The callback takes a mutable ResponseProgressHeaders reference which is used to retrieve the parsed header data 1810 | and possibly stop receiving data from the server. 1811 | */ 1812 | [[nodiscard]] 1813 | Request&& set_headers_callback(std::function callback) && { 1814 | callbacks_.handle_headers = std::move(callback); 1815 | return std::move(*this); 1816 | } 1817 | [[nodiscard]] 1818 | Request&& set_body_progress_callback(std::function callback) && { 1819 | callbacks_.handle_body_progress = std::move(callback); 1820 | return std::move(*this); 1821 | } 1822 | [[nodiscard]] 1823 | Request&& set_finish_callback(std::function callback) && { 1824 | callbacks_.handle_finish = std::move(callback); 1825 | return std::move(*this); 1826 | } 1827 | 1828 | /* 1829 | Sets a flag such that redirects are followed automatically. 1830 | This happens when either StatusCode::MovedPermanently (301) and StatusCode::Found (302) is received 1831 | and the server supplies a URL to follow via a "location" header. 1832 | */ 1833 | [[nodiscard]] 1834 | Request&& follow_redirects() && { 1835 | follow_redirects_ = true; 1836 | return std::move(*this); 1837 | } 1838 | 1839 | // Note: send and send_async are not [[nodiscard]] because callbacks 1840 | // could potentially be used exclusively to handle the response. 1841 | 1842 | /* 1843 | Sends the request and blocks until the response has been received. 1844 | */ 1845 | Response send() && { 1846 | return algorithms::receive_response<>(send_and_get_receive_socket_(), std::move(url_), std::move(callbacks_)); 1847 | } 1848 | /* 1849 | Sends the request and blocks until the response has been received. 1850 | 1851 | The buffer_size template parameter specifies the size of the buffer that data 1852 | from the server is read into at a time. If it is small, then data will be received 1853 | in many times in smaller pieces, with some time cost. If it is big, then 1854 | data will be read few times but in large pieces, with more memory cost. 1855 | */ 1856 | template 1857 | Response send() && { 1858 | return algorithms::receive_response(send_and_get_receive_socket_(), std::move(url_), std::move(callbacks_)); 1859 | } 1860 | /* 1861 | Sends the request and returns immediately after the data has been sent. 1862 | The returned future receives the response asynchronously. 1863 | */ 1864 | std::future send_async() && { 1865 | return std::async(&algorithms::receive_response<>, send_and_get_receive_socket_(), std::move(url_), std::move(callbacks_)); 1866 | } 1867 | /* 1868 | Sends the request and returns immediately after the data has been sent. 1869 | The returned future receives the response asynchronously. 1870 | 1871 | The buffer_size template parameter specifies the size of the buffer that data 1872 | from the server is read into at a time. If it is small, then data will be received 1873 | many times in smaller pieces, with some time cost. If it is big, then 1874 | data will be read few times but in large pieces, with more memory cost. 1875 | */ 1876 | template 1877 | std::future send_async() && { 1878 | return std::async(&algorithms::receive_response, send_and_get_receive_socket_(), std::move(url_), std::move(callbacks_)); 1879 | } 1880 | 1881 | Request() = delete; 1882 | ~Request() = default; 1883 | 1884 | Request(Request&&) noexcept = default; 1885 | Request& operator=(Request&&) noexcept = default; 1886 | 1887 | Request(Request const&) = delete; 1888 | Request& operator=(Request const&) = delete; 1889 | 1890 | private: 1891 | [[nodiscard]] 1892 | Socket send_and_get_receive_socket_() { 1893 | auto socket = open_socket(url_components_.host, url_components_.port, utils::is_protocol_tls_encrypted(url_components_.protocol)); 1894 | 1895 | using namespace std::string_view_literals; 1896 | 1897 | if (!body_.empty()) { 1898 | headers_ += std::format("Transfer-Encoding: identity\r\nContent-Length: {}\r\n", body_.size()); 1899 | } 1900 | 1901 | auto const request_data = utils::concatenate_byte_data( 1902 | request_method_to_string(method_), 1903 | ' ', 1904 | url_components_.path, 1905 | " HTTP/1.1\r\nHost: "sv, 1906 | url_components_.host, 1907 | headers_, 1908 | "\r\n"sv, 1909 | body_ 1910 | ); 1911 | socket.write(request_data); 1912 | 1913 | return socket; 1914 | } 1915 | 1916 | RequestMethod method_; 1917 | 1918 | bool follow_redirects_{}; 1919 | 1920 | std::string url_; 1921 | utils::UrlComponents url_components_; 1922 | 1923 | std::string headers_{"\r\n"}; 1924 | utils::DataVector body_; 1925 | 1926 | algorithms::ResponseCallbacks callbacks_; 1927 | 1928 | Request(RequestMethod const method, std::string_view const url, Protocol const default_protocol) : 1929 | method_{method}, 1930 | url_{utils::uri_encode(url)}, 1931 | url_components_{utils::split_url(std::string_view{url_})} 1932 | { 1933 | if (url_components_.protocol == Protocol::Unknown) 1934 | { 1935 | url_components_.protocol = default_protocol; 1936 | } 1937 | if (url_components_.port == utils::default_port_for_protocol(Protocol::Unknown)) 1938 | { 1939 | url_components_.port = utils::default_port_for_protocol(url_components_.protocol); 1940 | } 1941 | } 1942 | friend Request get(std::string_view, Protocol); 1943 | friend Request post(std::string_view, Protocol); 1944 | friend Request put(std::string_view, Protocol); 1945 | friend Request make_request(RequestMethod, std::string_view, Protocol); 1946 | }; 1947 | 1948 | /* 1949 | Creates a GET request. 1950 | url is a URL to the server or resource that the request targets. 1951 | If url contains a protocol prefix, it is used. Otherwise, default_protocol is used. 1952 | */ 1953 | [[nodiscard]] 1954 | inline Request get(std::string_view const url, Protocol const default_protocol = Protocol::Http) { 1955 | return Request{RequestMethod::Get, url, default_protocol}; 1956 | } 1957 | 1958 | /* 1959 | Creates a POST request. 1960 | url is a URL to the server or resource that the request targets. 1961 | If url contains a protocol prefix, it is used. Otherwise, default_protocol is used. 1962 | */ 1963 | [[nodiscard]] 1964 | inline Request post(std::string_view const url, Protocol const default_protocol = Protocol::Http) { 1965 | return Request{RequestMethod::Post, url, default_protocol}; 1966 | } 1967 | 1968 | /* 1969 | Creates a PUT request. 1970 | url is a URL to the server or resource that the request targets. 1971 | If url contains a protocol prefix, it is used. Otherwise, default_protocol is used. 1972 | */ 1973 | [[nodiscard]] 1974 | inline Request put(std::string_view const url, Protocol const default_protocol = Protocol::Http) { 1975 | return Request{RequestMethod::Put, url, default_protocol}; 1976 | } 1977 | 1978 | /* 1979 | Creates a http(s) request. 1980 | Can be used to do the same things as get() and post(), but with more method options. 1981 | If url contains a protocol prefix, it is used. Otherwise, default_protocol is used. 1982 | */ 1983 | [[nodiscard]] 1984 | inline Request make_request( 1985 | RequestMethod const method, 1986 | std::string_view const url, 1987 | Protocol const default_protocol = Protocol::Http 1988 | ) { 1989 | return Request{method, url, default_protocol}; 1990 | } 1991 | 1992 | } // namespace http_client 1993 | -------------------------------------------------------------------------------- /source/cpp20_http_client.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021-2023 Björn Sundin 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | #include "cpp20_http_client.hpp" 26 | 27 | //--------------------------------------------------------- 28 | 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | using namespace std::chrono_literals; 35 | 36 | //--------------------------------------------------------- 37 | 38 | #ifdef _WIN32 39 | # ifndef NOMINMAX 40 | # define NOMINMAX 41 | # endif 42 | 43 | // Required by SSPI API headers for some reason. 44 | # ifndef SECURITY_WIN32 45 | # define SECURITY_WIN32 46 | # endif 47 | 48 | // Windows socket API. 49 | # include 50 | # include 51 | 52 | // Windows secure channel API. 53 | # include 54 | # include 55 | 56 | // Mingw does not define this. 57 | # ifndef SECBUFFER_ALERT 58 | constexpr auto SECBUFFER_ALERT = 17; 59 | # endif 60 | #elif __has_include() // This header must exist on platforms that conform to the POSIX specifications. 61 | // The POSIX library is available on this platform. 62 | # define IS_POSIX 63 | 64 | # include 65 | # include 66 | # include 67 | # include 68 | # include 69 | # include 70 | # include 71 | 72 | # include 73 | # include 74 | 75 | // Name clash 76 | # ifdef unix 77 | # undef unix 78 | # endif 79 | #endif // __has_include() 80 | 81 | //--------------------------------------------------------- 82 | 83 | namespace http_client { 84 | 85 | // Platform-specific utilities. 86 | namespace utils { 87 | 88 | void enable_utf8_console() { 89 | #ifdef _WIN32 90 | SetConsoleOutputCP(CP_UTF8); 91 | #endif 92 | // Pretty much everyone else uses utf-8 by default. 93 | } 94 | 95 | #ifdef _WIN32 96 | namespace win { 97 | 98 | [[nodiscard]] 99 | std::wstring utf8_to_wide(std::string_view const input) { 100 | auto result = std::wstring(MultiByteToWideChar( 101 | CP_UTF8, 0, 102 | input.data(), static_cast(input.size()), 103 | 0, 0 104 | ), '\0'); 105 | 106 | MultiByteToWideChar( 107 | CP_UTF8, 0, 108 | input.data(), static_cast(input.size()), 109 | result.data(), static_cast(result.size()) 110 | ); 111 | 112 | return result; 113 | } 114 | 115 | void utf8_to_wide(std::string_view const input, std::span const output) { 116 | auto const length = MultiByteToWideChar( 117 | CP_UTF8, 0, 118 | input.data(), static_cast(input.size()), 119 | output.data(), static_cast(output.size()) 120 | ); 121 | 122 | if (length > 0) { 123 | output[length] = 0; 124 | } 125 | } 126 | 127 | [[nodiscard]] 128 | std::string wide_to_utf8(std::wstring_view const input) { 129 | auto result = std::string(WideCharToMultiByte( 130 | CP_UTF8, 0, 131 | input.data(), static_cast(input.size()), 132 | 0, 0, nullptr, nullptr 133 | ), '\0'); 134 | 135 | WideCharToMultiByte( 136 | CP_UTF8, 0, 137 | input.data(), static_cast(input.size()), 138 | result.data(), static_cast(result.size()), 139 | nullptr, nullptr 140 | ); 141 | 142 | return result; 143 | } 144 | 145 | void wide_to_utf8(std::wstring_view const input, std::span const output) { 146 | auto const length = WideCharToMultiByte( 147 | CP_UTF8, 0, 148 | input.data(), static_cast(input.size()), 149 | output.data(), static_cast(output.size()), 150 | nullptr, nullptr 151 | ); 152 | 153 | if (length > 0) { 154 | output[length] = 0; 155 | } 156 | } 157 | 158 | [[nodiscard]] 159 | std::string get_error_message(DWORD const message_id) { 160 | auto buffer = static_cast(nullptr); 161 | 162 | [[maybe_unused]] 163 | auto const buffer_cleanup = Cleanup{[&]{::LocalFree(buffer);}}; 164 | 165 | auto const size = ::FormatMessageW( 166 | FORMAT_MESSAGE_FROM_SYSTEM | 167 | FORMAT_MESSAGE_IGNORE_INSERTS | 168 | FORMAT_MESSAGE_ALLOCATE_BUFFER, 169 | nullptr, 170 | message_id, 171 | MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), 172 | reinterpret_cast(&buffer), 173 | 1, 174 | nullptr 175 | ); 176 | 177 | return wide_to_utf8(std::wstring_view{buffer, size}); 178 | } 179 | 180 | } // namespace win 181 | 182 | #endif // _WIN32 183 | 184 | #ifdef IS_POSIX 185 | 186 | namespace unix { 187 | 188 | using UniqueBio = std::unique_ptr; 189 | 190 | [[nodiscard]] 191 | std::string get_openssl_error_string() { 192 | auto const memory_file_handle = UniqueBio{::BIO_new(::BIO_s_mem())}; 193 | ::ERR_print_errors(memory_file_handle.get()); 194 | 195 | auto buffer = static_cast(nullptr); 196 | auto const length = ::BIO_get_mem_data(memory_file_handle.get(), &buffer); 197 | 198 | return std::string(static_cast(buffer), length); 199 | } 200 | 201 | } // namespace unix 202 | 203 | #endif // IS_POSIX 204 | 205 | //--------------------------------------------------------- 206 | 207 | #ifdef _WIN32 208 | 209 | [[noreturn]] 210 | void throw_connection_error( 211 | std::string reason, 212 | int const error_code = static_cast(GetLastError()), 213 | bool const is_tls_error = false 214 | ) { 215 | throw errors::ConnectionFailed{ 216 | std::format("{} with code {}: {}", reason, error_code, win::get_error_message(error_code)), 217 | is_tls_error 218 | }; 219 | } 220 | 221 | #endif // _WIN32 222 | 223 | #ifdef IS_POSIX 224 | 225 | [[noreturn]] 226 | void throw_connection_error(std::string reason, int const error_code = errno, bool const is_tls_error = false) { 227 | throw errors::ConnectionFailed{ 228 | std::format("{} with code {}: {}", reason, error_code, std::generic_category().message(error_code)), 229 | is_tls_error 230 | }; 231 | } 232 | 233 | #endif // IS_POSIX 234 | 235 | } // namespace utils 236 | 237 | #ifdef _WIN32 238 | 239 | class WinSockLifetime { 240 | public: 241 | WinSockLifetime() { 242 | auto api_info = ::WSADATA{}; 243 | if (auto const result = ::WSAStartup(MAKEWORD(2, 2), &api_info)) { 244 | utils::throw_connection_error("Failed to initialize Winsock API 2.2", result); 245 | } 246 | } 247 | ~WinSockLifetime() { 248 | if (!is_moved_) { 249 | ::WSACleanup(); 250 | } 251 | } 252 | 253 | WinSockLifetime(WinSockLifetime&& other) noexcept { 254 | other.is_moved_ = true; 255 | } 256 | WinSockLifetime& operator=(WinSockLifetime&& other) noexcept { 257 | other.is_moved_ = true; 258 | is_moved_ = false; 259 | return *this; 260 | } 261 | 262 | WinSockLifetime(WinSockLifetime const&) = delete; 263 | WinSockLifetime& operator=(WinSockLifetime const&) = delete; 264 | 265 | private: 266 | bool is_moved_{false}; 267 | }; 268 | 269 | using SocketHandle = utils::UniqueHandle< 270 | SOCKET, 271 | decltype([](auto const socket) { 272 | if (shutdown(socket, SD_BOTH) == SOCKET_ERROR) { 273 | utils::throw_connection_error("Failed to shut down socket connection", WSAGetLastError()); 274 | } 275 | closesocket(socket); 276 | }), 277 | INVALID_SOCKET 278 | >; 279 | 280 | class RawSocket { 281 | public: 282 | void set_is_nonblocking(bool const p_is_nonblocking) 283 | { 284 | if (is_nonblocking_ != p_is_nonblocking) 285 | { 286 | is_nonblocking_ = p_is_nonblocking; 287 | 288 | auto is_nonblocking = static_cast(p_is_nonblocking); 289 | ioctlsocket(handle_.get(), FIONBIO, &is_nonblocking); 290 | } 291 | } 292 | [[nodiscard]] 293 | SOCKET get_winsock_handle() { 294 | return handle_.get(); 295 | } 296 | 297 | void write(std::span const data) 298 | { 299 | if (is_closed_) { 300 | reconnect_(); 301 | } 302 | 303 | if (::send( 304 | handle_.get(), 305 | reinterpret_cast(data.data()), 306 | static_cast(data.size()), 307 | 0 308 | ) == SOCKET_ERROR) 309 | { 310 | utils::throw_connection_error("Failed to send data through socket", WSAGetLastError()); 311 | } 312 | } 313 | [[nodiscard]] 314 | auto read(std::span const buffer, bool const is_nonblocking = false) 315 | -> std::variant 316 | { 317 | if (is_closed_) { 318 | return std::size_t{}; 319 | } 320 | 321 | set_is_nonblocking(is_nonblocking); 322 | 323 | if (auto const receive_result = recv( 324 | handle_.get(), 325 | reinterpret_cast(buffer.data()), 326 | static_cast(buffer.size()), 327 | 0 328 | ); receive_result >= 0) 329 | { 330 | if (receive_result == 0) { 331 | is_closed_ = true; 332 | return ConnectionClosed{}; 333 | } 334 | return static_cast(receive_result); 335 | } 336 | else if (is_nonblocking && WSAGetLastError() == WSAEWOULDBLOCK) { 337 | return std::size_t{}; 338 | } 339 | else utils::throw_connection_error("Failed to receive data through socket"); 340 | } 341 | [[nodiscard]] 342 | auto read_available(std::span const buffer) 343 | -> std::variant 344 | { 345 | return read(buffer, true); 346 | } 347 | 348 | RawSocket(std::string_view const server, Port const port) : 349 | address_info_{get_address_info_(server, port)}, 350 | handle_{create_handle_()} 351 | {} 352 | 353 | private: 354 | using AddressInfo = std::unique_ptr; 355 | 356 | [[nodiscard]] 357 | static AddressInfo get_address_info_(std::string_view const server, Port const port) { 358 | auto const wide_server_name = utils::win::utf8_to_wide(server); 359 | auto const wide_port_string = std::to_wstring(port); 360 | auto const hints = addrinfoW{ 361 | .ai_family = AF_UNSPEC, 362 | .ai_socktype = SOCK_STREAM, 363 | .ai_protocol = IPPROTO_TCP, 364 | }; 365 | auto address_info = static_cast(nullptr); 366 | 367 | if (auto const result = GetAddrInfoW( 368 | wide_server_name.data(), 369 | wide_port_string.data(), 370 | &hints, 371 | &address_info 372 | )) 373 | { 374 | throw errors::ConnectionFailed{ 375 | std::format("Failed to get address info for socket creation: {}", utils::win::get_error_message(result)) 376 | }; 377 | } 378 | 379 | return AddressInfo{address_info}; 380 | } 381 | 382 | [[nodiscard]] 383 | SocketHandle create_handle_() const { 384 | auto const handle_error = [](auto const error_message) { 385 | if (auto const error_code = WSAGetLastError(); error_code != WSAEINPROGRESS) { 386 | utils::throw_connection_error(error_message, error_code); 387 | } 388 | constexpr auto time_to_wait_between_attempts = 1ms; 389 | std::this_thread::sleep_for(time_to_wait_between_attempts); 390 | }; 391 | 392 | auto socket_handle = SocketHandle{}; 393 | while ((socket_handle = socket( 394 | address_info_->ai_family, 395 | address_info_->ai_socktype, 396 | address_info_->ai_protocol 397 | )).get() == INVALID_SOCKET) 398 | { 399 | handle_error("Failed to create socket"); 400 | } 401 | 402 | while (connect( 403 | socket_handle.get(), 404 | address_info_->ai_addr, 405 | static_cast(address_info_->ai_addrlen) 406 | ) == SOCKET_ERROR) 407 | { 408 | handle_error("Failed to connect socket"); 409 | } 410 | 411 | return socket_handle; 412 | } 413 | 414 | void reconnect_() { 415 | handle_ = create_handle_(); 416 | is_closed_ = false; 417 | } 418 | 419 | WinSockLifetime api_lifetime_; 420 | AddressInfo address_info_; 421 | SocketHandle handle_; 422 | bool is_nonblocking_{false}; 423 | bool is_closed_{false}; 424 | }; 425 | 426 | using DllHandle = utils::UniqueHandle; 427 | 428 | struct SspiLibrary { 429 | DllHandle dll_handle; 430 | 431 | PSecurityFunctionTableW functions; 432 | 433 | SspiLibrary() : 434 | dll_handle{LoadLibraryW(L"secur32.dll")} 435 | { 436 | auto const throw_error = []{ 437 | throw std::system_error{static_cast(GetLastError()), std::system_category(), "Failed to initialize the SSPI library"}; 438 | }; 439 | 440 | if (!dll_handle) { 441 | throw_error(); 442 | } 443 | 444 | // ew :) 445 | auto const init_security_interface = reinterpret_cast( 446 | reinterpret_cast(GetProcAddress(dll_handle.get(), "InitSecurityInterfaceW")) 447 | ); 448 | 449 | functions = init_security_interface(); 450 | if (!functions) { 451 | throw_error(); 452 | } 453 | } 454 | ~SspiLibrary() = default; 455 | 456 | SspiLibrary(SspiLibrary const&) = delete; 457 | SspiLibrary& operator=(SspiLibrary const&) = delete; 458 | SspiLibrary(SspiLibrary&&) = delete; 459 | SspiLibrary& operator=(SspiLibrary&&) = delete; 460 | }; 461 | 462 | auto const sspi_library = SspiLibrary{}; 463 | 464 | [[nodiscard]] 465 | constexpr bool operator==(CredHandle const& first, CredHandle const& second) noexcept { 466 | return first.dwLower == second.dwLower && first.dwUpper == second.dwUpper; 467 | } 468 | [[nodiscard]] 469 | constexpr bool operator!=(CredHandle const& first, CredHandle const& second) noexcept { 470 | return !(first == second); 471 | } 472 | 473 | [[nodiscard]] 474 | constexpr bool operator==(SecBuffer const& first, SecBuffer const& second) noexcept { 475 | return first.pvBuffer == second.pvBuffer; 476 | } 477 | [[nodiscard]] 478 | constexpr bool operator!=(SecBuffer const& first, SecBuffer const& second) noexcept { 479 | return !(first == second); 480 | } 481 | 482 | using SecurityContextHandle = utils::UniqueHandleDeleteSecurityContext(&h); })>; 483 | 484 | SecBufferDesc create_single_schannel_buffer_description(SecBuffer& buffer) { 485 | return { 486 | .ulVersion = SECBUFFER_VERSION, 487 | .cBuffers = 1ul, 488 | .pBuffers = &buffer, 489 | }; 490 | } 491 | SecBufferDesc create_schannel_buffers_description(std::span const buffers) { 492 | return { 493 | .ulVersion = SECBUFFER_VERSION, 494 | .cBuffers = static_cast(buffers.size()), 495 | .pBuffers = buffers.data(), 496 | }; 497 | } 498 | 499 | /* 500 | Holds either received handshake data or TLS message data. 501 | The required size of the TLS handshake message buffer cannot be retrieved 502 | through any API call. See the block comment in the class. 503 | After the handshake is complete, the buffer is resized because then the TLS message 504 | header, trailer and message sizes can be retrieved using QueryContextAttributesW. 505 | */ 506 | class TlsMessageReceiveBuffer { 507 | public: 508 | std::span extra_data; 509 | 510 | using iterator = utils::DataVector::iterator; 511 | 512 | [[nodiscard]] 513 | iterator begin() { 514 | return buffer_.begin(); 515 | } 516 | [[nodiscard]] 517 | iterator end() { 518 | return buffer_.end(); 519 | } 520 | 521 | void grow_to_size(std::size_t const new_size) { 522 | assert(new_size >= buffer_.size()); 523 | 524 | if (!extra_data.empty()) { 525 | auto const extra_data_start = extra_data.data() - buffer_.data(); 526 | assert(extra_data_start > 0 && extra_data_start < static_cast(buffer_.size())); 527 | buffer_.resize(new_size); 528 | extra_data = std::span{buffer_}.subspan(extra_data_start, extra_data.size()); 529 | } 530 | else { 531 | buffer_.resize(new_size); 532 | } 533 | } 534 | [[nodiscard]] 535 | std::span get_full_buffer() { 536 | return buffer_; 537 | } 538 | 539 | [[nodiscard]] 540 | static TlsMessageReceiveBuffer allocate_new() { 541 | return TlsMessageReceiveBuffer{maximum_handshake_message_size}; 542 | } 543 | 544 | TlsMessageReceiveBuffer() = default; 545 | ~TlsMessageReceiveBuffer() = default; 546 | 547 | TlsMessageReceiveBuffer(TlsMessageReceiveBuffer const&) = delete; 548 | TlsMessageReceiveBuffer& operator=(TlsMessageReceiveBuffer const&) = delete; 549 | TlsMessageReceiveBuffer(TlsMessageReceiveBuffer&&) noexcept = default; 550 | TlsMessageReceiveBuffer& operator=(TlsMessageReceiveBuffer&&) noexcept = default; 551 | 552 | private: 553 | /* 554 | When the buffer is too small to fit the whole handshake message received from the peer, the return 555 | code from InitializeSecurityContextW is not SEC_E_INCOMPLETE_MESSAGE, but SEC_E_INVALID_TOKEN. 556 | Trying to grow the buffer after getting that return code does not work. The server closes the 557 | connection when trying to read more data afterwards. It seems that we need a fixed maximum 558 | handshake message/token size. 559 | 560 | It is not clear exactly what this maximum size should be. 561 | The only thing Microsoft's documentation says about this is 562 | "[...] the value of this parameter is a pointer to a 563 | buffer allocated with enough memory to hold the 564 | token returned by the remote computer." 565 | (https://docs.microsoft.com/en-us/windows/win32/api/sspi/nf-sspi-initializesecuritycontextw) 566 | The TLS 1.3 standard specification says: 567 | "The record layer fragments information blocks into TLSPlaintext 568 | records carrying data in chunks of 2^14 bytes or less." 569 | (https://tools.ietf.org/html/rfc8446) 570 | 571 | Looking at a few implementations of TLS sockets using Schannel: 572 | 1. https://github.com/adobe/chromium/blob/master/net/socket/ssl_client_socket_win.cc 573 | Uses 5 + 16*1024 + 64 = 16453 bytes. 574 | 2. https://github.com/curl/curl/blob/master/lib/vtls/schannel.c 575 | Uses 4096 + 1024 = 5120 bytes. 576 | 3. https://github.com/odzhan/shells/tree/master/s6 577 | Uses 32768 bytes. 578 | 4. https://docs.microsoft.com/en-us/windows/win32/secauthn/using-sspi-with-a-windows-sockets-client 579 | Uses 12000 bytes. 580 | 581 | ALL of these implementations use DIFFERENT maximum handshake message sizes. 582 | I decided to follow the TLS specification and use 2^14 bytes for the handshake message buffer, 583 | as this should be the maximum allowed size of any TLSPlaintext record block, which includes handshake messages. 584 | */ 585 | static constexpr auto maximum_handshake_message_size = std::size_t{1 << 14}; 586 | 587 | utils::DataVector buffer_; 588 | 589 | explicit TlsMessageReceiveBuffer(std::size_t const size) : 590 | buffer_(size) 591 | {} 592 | }; 593 | 594 | class SchannelConnectionInitializer { 595 | public: 596 | /* 597 | Returns the resulting security context and a vector of any extra non-handshake data 598 | that should be processed as part of the next message. 599 | */ 600 | std::pair operator()() && { 601 | do_handshake_(); 602 | return {std::move(security_context_), std::move(receive_buffer_)}; 603 | } 604 | 605 | [[nodiscard]] 606 | SchannelConnectionInitializer(RawSocket* const socket, std::string_view const server) : 607 | socket_{socket}, 608 | server_name_{utils::win::utf8_to_wide(server)} 609 | {} 610 | ~SchannelConnectionInitializer() = default; 611 | 612 | SchannelConnectionInitializer(SchannelConnectionInitializer&&) noexcept = delete; 613 | SchannelConnectionInitializer& operator=(SchannelConnectionInitializer&&) noexcept = delete; 614 | SchannelConnectionInitializer(SchannelConnectionInitializer const&) = delete; 615 | SchannelConnectionInitializer& operator=(SchannelConnectionInitializer const&) = delete; 616 | 617 | private: 618 | using CredentialsHandle = utils::UniqueHandleFreeCredentialHandle(&h); })>; 619 | 620 | using HandshakeOutputBuffer = utils::UniqueHandle< 621 | SecBuffer, decltype([](auto const& buffer) { 622 | if (buffer.pvBuffer) { 623 | sspi_library.functions->FreeContextBuffer(buffer.pvBuffer); 624 | } 625 | }) 626 | >; 627 | 628 | void do_handshake_() { 629 | if (auto const [return_code, output_buffer] = process_handshake_data_({}); 630 | return_code != SEC_I_CONTINUE_NEEDED) // First call should always yield this return code. 631 | { 632 | utils::throw_connection_error("Schannel TLS handshake initialization failed", return_code, true); 633 | } 634 | else send_handshake_message_(output_buffer); 635 | 636 | auto offset = std::size_t{}; 637 | while (true) { 638 | auto const read_span = read_response_(offset); 639 | if (auto const [return_code, output_buffer] = process_handshake_data_(read_span); 640 | return_code == SEC_I_CONTINUE_NEEDED) 641 | { 642 | if (output_buffer->cbBuffer) { 643 | send_handshake_message_(output_buffer); 644 | } 645 | offset = 0; 646 | } 647 | else if (return_code == SEC_E_INCOMPLETE_MESSAGE) { 648 | offset = read_span.size(); 649 | } 650 | else if (return_code == SEC_E_OK) { 651 | return; 652 | } 653 | else { 654 | utils::throw_connection_error("Schannel TLS handshake failed", return_code); 655 | } 656 | } 657 | } 658 | 659 | /* 660 | Returns a span over the total read data. 661 | */ 662 | std::span read_response_(std::size_t const offset = {}) { 663 | auto const buffer_span = receive_buffer_.get_full_buffer(); 664 | 665 | if (!receive_buffer_.extra_data.empty()) { 666 | assert(offset == 0); 667 | 668 | auto const extra_data_size = receive_buffer_.extra_data.size(); 669 | std::ranges::copy_backward(receive_buffer_.extra_data, buffer_span.begin() + extra_data_size); 670 | 671 | receive_buffer_.extra_data = {}; 672 | return buffer_span.first(extra_data_size); 673 | } 674 | else if (auto const read_result = socket_->read(buffer_span.subspan(offset)); 675 | std::holds_alternative(read_result)) 676 | { 677 | throw errors::ConnectionFailed{"The connection closed unexpectedly while reading handshake data.", true}; 678 | } 679 | else { 680 | return buffer_span.subspan(0, offset + std::get(read_result)); 681 | } 682 | } 683 | 684 | struct [[nodiscard]] HandshakeProcessResult { 685 | SECURITY_STATUS status_code; 686 | HandshakeOutputBuffer output_buffer; 687 | }; 688 | HandshakeProcessResult process_handshake_data_(std::span const input_buffer) { 689 | constexpr auto request_flags = ISC_REQ_SEQUENCE_DETECT | ISC_REQ_REPLAY_DETECT | 690 | ISC_REQ_CONFIDENTIALITY | ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_STREAM; 691 | 692 | // The second input buffer is used to indicate that extra data 693 | // from the next message was at the end of the input buffer, 694 | // and should be processed in the next call. There's not actually 695 | // any buffer pointer in that SecBuffer. 696 | auto input_buffers = std::array{ 697 | SecBuffer{ 698 | .cbBuffer = static_cast(input_buffer.size()), 699 | .BufferType = SECBUFFER_TOKEN, 700 | .pvBuffer = input_buffer.data(), 701 | }, 702 | SecBuffer{}, 703 | }; 704 | auto input_buffer_description = create_schannel_buffers_description(input_buffers); 705 | 706 | auto output_buffers = std::array{ 707 | SecBuffer{.BufferType = SECBUFFER_TOKEN}, 708 | SecBuffer{.BufferType = SECBUFFER_ALERT}, 709 | SecBuffer{}, 710 | }; 711 | auto output_buffer_description = create_schannel_buffers_description(output_buffers); 712 | 713 | unsigned long returned_flags; 714 | 715 | auto const return_code = sspi_library.functions->InitializeSecurityContextW( 716 | &credentials_.get(), 717 | security_context_ ? &security_context_ : nullptr, // Null on first call, input security context handle 718 | server_name_.data(), 719 | request_flags, 720 | 0, // Reserved 721 | 0, // Not used with Schannel 722 | input_buffer.empty() ? nullptr : &input_buffer_description, // Null on first call 723 | 0, // Reserved 724 | &security_context_, // Output security context handle 725 | &output_buffer_description, 726 | &returned_flags, 727 | nullptr // Don't care about expiration date right now 728 | ); 729 | 730 | if (returned_flags != request_flags) { 731 | utils::throw_connection_error("The schannel security context flags were not supported"); 732 | } 733 | 734 | return HandshakeProcessResult{[&]{ 735 | if (input_buffers[1].BufferType == SECBUFFER_EXTRA) { 736 | receive_buffer_.extra_data = input_buffer.last(input_buffers[1].cbBuffer); 737 | } 738 | 739 | if (return_code == SEC_I_COMPLETE_AND_CONTINUE || return_code == SEC_I_COMPLETE_NEEDED) { 740 | sspi_library.functions->CompleteAuthToken(&security_context_, &output_buffer_description); 741 | 742 | if (return_code == SEC_I_COMPLETE_AND_CONTINUE) { 743 | return SEC_I_CONTINUE_NEEDED; 744 | } 745 | return SEC_E_OK; 746 | } 747 | return return_code; 748 | }(), HandshakeOutputBuffer{output_buffers[0]}}; 749 | } 750 | void send_handshake_message_(HandshakeOutputBuffer const& message_buffer) { 751 | socket_->write(std::span{ 752 | static_cast(message_buffer->pvBuffer), 753 | static_cast(message_buffer->cbBuffer) 754 | }); 755 | } 756 | 757 | [[nodiscard]] 758 | static CredentialsHandle acquire_credentials_handle_() { 759 | auto credentials_data = SCHANNEL_CRED{ 760 | .dwVersion = SCHANNEL_CRED_VERSION, 761 | }; 762 | CredHandle credentials_handle; 763 | TimeStamp credentials_time_limit; 764 | 765 | auto const security_status = sspi_library.functions->AcquireCredentialsHandleW( 766 | nullptr, 767 | const_cast(UNISP_NAME_W), 768 | SECPKG_CRED_OUTBOUND, 769 | nullptr, 770 | &credentials_data, 771 | nullptr, 772 | nullptr, 773 | &credentials_handle, 774 | &credentials_time_limit 775 | ); 776 | if (security_status != SEC_E_OK) { 777 | utils::throw_connection_error("Failed to acquire credentials", security_status, true); 778 | } 779 | 780 | return CredentialsHandle{credentials_handle}; 781 | } 782 | 783 | CredentialsHandle credentials_{acquire_credentials_handle_()}; 784 | RawSocket* socket_; 785 | std::wstring server_name_; 786 | 787 | SecurityContextHandle security_context_; 788 | TlsMessageReceiveBuffer receive_buffer_{TlsMessageReceiveBuffer::allocate_new()}; 789 | }; 790 | 791 | class TlsSocket { 792 | public: 793 | void write(std::span data) { 794 | while (!data.empty()) { 795 | auto const message_length = std::min(data.size(), static_cast(stream_sizes_.cbMaximumMessage)); 796 | 797 | auto const output_buffer = encrypt_message_(data.first(message_length)); 798 | raw_socket_->write(output_buffer); 799 | 800 | data = data.subspan(message_length); 801 | } 802 | } 803 | 804 | [[nodiscard]] 805 | auto read(std::span const buffer, bool const is_nonblocking = false) 806 | -> std::variant 807 | { 808 | if (decrypted_message_left_.empty()) { 809 | auto const receive_buffer_span = receive_buffer_.get_full_buffer(); 810 | auto read_offset = std::size_t{}; 811 | 812 | while (true) { 813 | if (auto const read_result = read_encrypted_data_(read_offset, is_nonblocking); 814 | std::holds_alternative(read_result)) 815 | { 816 | return read_result; 817 | } 818 | else if (decrypt_message_(receive_buffer_span.first(read_offset + std::get(read_result))) 819 | || is_nonblocking) 820 | { 821 | break; 822 | } 823 | else { 824 | read_offset += std::get(read_result); 825 | } 826 | } 827 | } 828 | if (decrypted_message_left_.empty()) { 829 | return std::size_t{}; 830 | } 831 | 832 | auto const size = std::min(decrypted_message_left_.size(), buffer.size()); 833 | std::ranges::copy(decrypted_message_left_.first(size), buffer.begin()); 834 | decrypted_message_left_ = decrypted_message_left_.subspan(size); 835 | 836 | return size; 837 | } 838 | [[nodiscard]] 839 | auto read_available(std::span const buffer) 840 | -> std::variant 841 | { 842 | return read(buffer, true); 843 | } 844 | 845 | TlsSocket(std::string_view const server, Port const port) 846 | { 847 | initialize_connection_(server, port); 848 | } 849 | 850 | private: 851 | void initialize_connection_(std::string_view const server, Port const port) { 852 | if (raw_socket_) { 853 | return; 854 | } 855 | 856 | raw_socket_ = std::make_unique(server, port); 857 | 858 | std::tie(security_context_, receive_buffer_) = SchannelConnectionInitializer{raw_socket_.get(), server}(); 859 | initialize_stream_sizes_(); 860 | } 861 | 862 | void initialize_stream_sizes_() { 863 | if (auto const result = sspi_library.functions->QueryContextAttributesW(&security_context_, SECPKG_ATTR_STREAM_SIZES, &stream_sizes_); 864 | result != SEC_E_OK) 865 | { 866 | utils::throw_connection_error("Failed to query Schannel security context stream sizes", result, true); 867 | } 868 | receive_buffer_.grow_to_size(stream_sizes_.cbHeader + stream_sizes_.cbMaximumMessage + stream_sizes_.cbTrailer); 869 | } 870 | 871 | [[nodiscard]] 872 | utils::DataVector encrypt_message_(std::span const data) { 873 | // https://docs.microsoft.com/en-us/windows/win32/api/sspi/nf-sspi-encryptmessage 874 | 875 | auto full_buffer = utils::DataVector(stream_sizes_.cbHeader + data.size() + stream_sizes_.cbTrailer); 876 | std::ranges::copy(data, full_buffer.begin() + stream_sizes_.cbHeader); 877 | 878 | auto buffers = std::array{ 879 | SecBuffer{ 880 | .cbBuffer = stream_sizes_.cbHeader, 881 | .BufferType = SECBUFFER_STREAM_HEADER, 882 | .pvBuffer = full_buffer.data(), 883 | }, 884 | SecBuffer{ 885 | .cbBuffer = static_cast(data.size()), 886 | .BufferType = SECBUFFER_DATA, 887 | .pvBuffer = full_buffer.data() + stream_sizes_.cbHeader, 888 | }, 889 | SecBuffer{ 890 | .cbBuffer = stream_sizes_.cbTrailer, 891 | .BufferType = SECBUFFER_STREAM_TRAILER, 892 | .pvBuffer = full_buffer.data() + stream_sizes_.cbHeader + data.size(), 893 | }, 894 | // Empty buffer that must be supplied at the end. 895 | SecBuffer{}, 896 | }; 897 | 898 | auto buffers_description = create_schannel_buffers_description(buffers); 899 | 900 | if (auto const result = sspi_library.functions->EncryptMessage(&security_context_, 0, &buffers_description, 0); 901 | result != SEC_E_OK) 902 | { 903 | utils::throw_connection_error("Failed to encrypt TLS message", result, true); 904 | } 905 | 906 | return full_buffer; 907 | } 908 | 909 | [[nodiscard]] 910 | auto read_encrypted_data_(std::size_t const offset, bool const is_nonblocking) 911 | -> std::variant 912 | { 913 | auto const buffer_span = receive_buffer_.get_full_buffer(); 914 | 915 | if (!receive_buffer_.extra_data.empty()) { 916 | assert(offset == 0); 917 | 918 | auto const extra_data_size = receive_buffer_.extra_data.size(); 919 | std::ranges::copy_backward(receive_buffer_.extra_data, buffer_span.begin() + extra_data_size); 920 | 921 | receive_buffer_.extra_data = {}; 922 | return extra_data_size; 923 | } 924 | else { 925 | return raw_socket_->read(buffer_span.subspan(offset), is_nonblocking); 926 | } 927 | } 928 | 929 | // Returns false if the encrypted message was incomplete 930 | [[nodiscard]] 931 | bool decrypt_message_(std::span const message) { 932 | // https://docs.microsoft.com/en-us/windows/win32/secauthn/stream-contexts 933 | auto buffers = std::array{ 934 | SecBuffer{ // This will hold the message header afterwards 935 | .cbBuffer = static_cast(message.size()), 936 | .BufferType = SECBUFFER_DATA, 937 | .pvBuffer = message.data(), 938 | }, 939 | SecBuffer{}, // Will hold the decrypted data 940 | SecBuffer{}, // Will hold the message trailer 941 | SecBuffer{}, // May hold size of extra undecrypted data (from the next message) 942 | }; 943 | auto message_buffer_description = create_schannel_buffers_description(buffers); 944 | 945 | if (auto const status_code = sspi_library.functions->DecryptMessage(&security_context_, &message_buffer_description, 0, nullptr); 946 | status_code == SEC_E_OK) 947 | { 948 | decrypted_message_left_ = {static_cast(buffers[1].pvBuffer), static_cast(buffers[1].cbBuffer)}; 949 | 950 | // https://docs.microsoft.com/en-us/windows/win32/secauthn/extra-buffers-returned-by-schannel 951 | // Data from the next message. Always at the end. 952 | if (buffers[3].BufferType == SECBUFFER_EXTRA) { 953 | receive_buffer_.extra_data = message.last(buffers[3].cbBuffer); 954 | } 955 | return true; 956 | } 957 | else if (status_code == SEC_E_INCOMPLETE_MESSAGE) { 958 | return false; 959 | } 960 | else { 961 | utils::throw_connection_error("Failed to decrypt a received TLS message", status_code, true); 962 | } 963 | } 964 | 965 | std::unique_ptr raw_socket_; 966 | 967 | SecurityContextHandle security_context_; 968 | 969 | SecPkgContext_StreamSizes stream_sizes_; 970 | 971 | TlsMessageReceiveBuffer receive_buffer_; 972 | 973 | // This is the part of the message buffer that contains the 974 | // rest of the decrypted message data that has not been read yet. 975 | std::span decrypted_message_left_; 976 | }; 977 | 978 | #endif // _WIN32 979 | 980 | #ifdef IS_POSIX 981 | 982 | using PosixSocketHandle = int; 983 | 984 | using SocketHandle = utils::UniqueHandle< 985 | PosixSocketHandle, 986 | decltype([](auto const handle) { 987 | if (::shutdown(handle, SHUT_RDWR) == -1) { 988 | utils::throw_connection_error("Failed to shut down socket connection"); 989 | } 990 | ::close(handle); 991 | }), 992 | PosixSocketHandle{-1} 993 | >; 994 | 995 | class RawSocket { 996 | public: 997 | void make_nonblocking() { 998 | if (!is_nonblocking_) { 999 | auto const flags = ::fcntl(handle_.get(), F_GETFL); 1000 | if (-1 == ::fcntl(handle_.get(), F_SETFL, flags | O_NONBLOCK)) { 1001 | utils::throw_connection_error("Failed to turn on nonblocking mode on socket"); 1002 | } 1003 | is_nonblocking_ = true; 1004 | } 1005 | } 1006 | void make_blocking() { 1007 | if (is_nonblocking_) { 1008 | auto const flags = ::fcntl(handle_.get(), F_GETFL); 1009 | if (-1 == ::fcntl(handle_.get(), F_SETFL, flags & ~O_NONBLOCK)) { 1010 | utils::throw_connection_error("Failed to turn off nonblocking mode on socket"); 1011 | } 1012 | is_nonblocking_ = false; 1013 | } 1014 | } 1015 | [[nodiscard]] 1016 | PosixSocketHandle get_posix_handle() const { 1017 | return handle_.get(); 1018 | } 1019 | 1020 | void reconnect_() { 1021 | handle_ = create_handle_(); 1022 | is_closed_ = false; 1023 | } 1024 | 1025 | void write(std::span const data) { 1026 | if (is_closed_) { 1027 | reconnect_(); 1028 | } 1029 | 1030 | if (::send( 1031 | handle_.get(), 1032 | data.data(), 1033 | static_cast(data.size()), 1034 | 0 1035 | ) == -1) 1036 | { 1037 | utils::throw_connection_error("Failed to send data through socket"); 1038 | } 1039 | } 1040 | [[nodiscard]] 1041 | auto read(std::span const buffer, bool is_nonblocking = false) 1042 | -> std::variant 1043 | { 1044 | if (is_closed_) { 1045 | return std::size_t{}; 1046 | } 1047 | 1048 | if (auto const receive_result = ::recv( 1049 | handle_.get(), 1050 | reinterpret_cast(buffer.data()), 1051 | static_cast(buffer.size()), 1052 | is_nonblocking ? MSG_DONTWAIT : 0 1053 | ); receive_result >= 0) 1054 | { 1055 | if (receive_result == 0) { 1056 | is_closed_ = true; 1057 | return ConnectionClosed{}; 1058 | } 1059 | return static_cast(receive_result); 1060 | } 1061 | else if (is_nonblocking && (errno == EWOULDBLOCK || errno == EAGAIN)) { 1062 | return std::size_t{}; 1063 | } 1064 | utils::throw_connection_error("Failed to receive data through socket"); 1065 | } 1066 | [[nodiscard]] 1067 | auto read_available(std::span const buffer) 1068 | -> std::variant 1069 | { 1070 | return read(buffer, true); 1071 | } 1072 | 1073 | RawSocket(std::string_view const server, Port const port) : 1074 | address_info_{get_address_info_(std::string{server}, port)}, 1075 | handle_{create_handle_()} 1076 | {} 1077 | 1078 | private: 1079 | using AddressInfo = std::unique_ptr; 1080 | 1081 | [[nodiscard]] 1082 | static AddressInfo get_address_info_(std::string const server, Port const port) { 1083 | auto const port_string = std::to_string(port); 1084 | auto const hints = addrinfo{ 1085 | .ai_flags{}, 1086 | .ai_family{AF_UNSPEC}, 1087 | .ai_socktype{SOCK_STREAM}, 1088 | .ai_protocol{IPPROTO_TCP}, 1089 | .ai_addrlen{}, 1090 | .ai_addr{}, 1091 | .ai_canonname{}, 1092 | .ai_next{} 1093 | }; 1094 | auto address_info = static_cast(nullptr); 1095 | 1096 | if (auto const result = ::getaddrinfo( 1097 | reinterpret_cast(server.data()), 1098 | port_string.data(), 1099 | &hints, 1100 | &address_info 1101 | )) 1102 | { 1103 | throw errors::ConnectionFailed{ 1104 | std::format("Failed to get address info for socket creation: {}", gai_strerror(result)) 1105 | }; 1106 | } 1107 | 1108 | return AddressInfo{address_info}; 1109 | } 1110 | 1111 | [[nodiscard]] 1112 | SocketHandle create_handle_() const { 1113 | auto socket_handle = SocketHandle{::socket( 1114 | address_info_->ai_family, 1115 | address_info_->ai_socktype, 1116 | address_info_->ai_protocol 1117 | )}; 1118 | if (!socket_handle) { 1119 | utils::throw_connection_error("Failed to create socket"); 1120 | } 1121 | 1122 | while (::connect( 1123 | socket_handle.get(), 1124 | address_info_->ai_addr, 1125 | static_cast(address_info_->ai_addrlen) 1126 | ) == -1) 1127 | { 1128 | if (auto const error_code = errno; error_code != EINPROGRESS) { 1129 | utils::throw_connection_error("Failed to connect socket", error_code); 1130 | } 1131 | constexpr auto time_to_wait_between_attempts = 1ms; 1132 | std::this_thread::sleep_for(time_to_wait_between_attempts); 1133 | } 1134 | 1135 | return socket_handle; 1136 | } 1137 | 1138 | AddressInfo address_info_; 1139 | 1140 | SocketHandle handle_; 1141 | 1142 | bool is_nonblocking_{false}; 1143 | 1144 | bool is_closed_{false}; 1145 | }; 1146 | 1147 | class TlsSocket { 1148 | public: 1149 | void write(std::span const data) { 1150 | ensure_connected_(); 1151 | 1152 | if (::SSL_write( 1153 | tls_connection_.get(), 1154 | data.data(), 1155 | static_cast(data.size()) 1156 | ) == -1) 1157 | { 1158 | utils::throw_connection_error("Failed to send data through socket"); 1159 | } 1160 | } 1161 | [[nodiscard]] 1162 | auto read(std::span const buffer) 1163 | -> std::variant 1164 | { 1165 | if (is_closed_) { 1166 | return std::size_t{}; 1167 | } 1168 | 1169 | raw_socket_->make_blocking(); 1170 | if (auto const read_result = ::SSL_read( 1171 | tls_connection_.get(), 1172 | buffer.data(), 1173 | static_cast(buffer.size()) 1174 | ); read_result >= 0) 1175 | { 1176 | if (read_result == 0) { 1177 | is_closed_ = true; 1178 | return ConnectionClosed{}; 1179 | } 1180 | return static_cast(read_result); 1181 | } 1182 | utils::throw_connection_error("Failed to receive data from socket"); 1183 | } 1184 | [[nodiscard]] 1185 | auto read_available(std::span const buffer) 1186 | -> std::variant 1187 | { 1188 | if (is_closed_) { 1189 | return std::size_t{}; 1190 | } 1191 | 1192 | raw_socket_->make_nonblocking(); 1193 | if (auto const read_result = ::SSL_read( 1194 | tls_connection_.get(), 1195 | buffer.data(), 1196 | static_cast(buffer.size()) 1197 | ); read_result > 0) 1198 | { 1199 | return static_cast(read_result); 1200 | } 1201 | else switch (auto const error_code = ::SSL_get_error(tls_connection_.get(), read_result)) { 1202 | case SSL_ERROR_WANT_READ: 1203 | case SSL_ERROR_WANT_WRITE: 1204 | // No available data to read at the moment. 1205 | return std::size_t{}; 1206 | case SSL_ERROR_ZERO_RETURN: 1207 | case SSL_ERROR_SYSCALL: 1208 | if (errno == 0) { 1209 | is_closed_ = true; 1210 | // Peer shut down the connection. 1211 | return ConnectionClosed{}; 1212 | } 1213 | [[fallthrough]]; 1214 | default: 1215 | utils::throw_connection_error("Failed to read available data from socket", error_code); 1216 | } 1217 | utils::unreachable(); 1218 | } 1219 | 1220 | TlsSocket(std::string_view const server, Port const port) { 1221 | initialize_connection_(server, port); 1222 | } 1223 | 1224 | private: 1225 | using TlsContext = std::unique_ptr; 1226 | using TlsConnection = std::unique_ptr; 1227 | 1228 | static void throw_tls_error_() { 1229 | throw errors::ConnectionFailed{utils::unix::get_openssl_error_string(), true}; 1230 | } 1231 | 1232 | void ensure_connected_() { 1233 | if (is_closed_) { 1234 | raw_socket_->reconnect_(); 1235 | update_tls_socket_handle_(); 1236 | } 1237 | } 1238 | 1239 | void initialize_connection_(std::string_view const server, Port const port) { 1240 | if (raw_socket_) { 1241 | return; 1242 | } 1243 | 1244 | configure_tls_context_(); 1245 | configure_tls_connection_(std::string{server}, port); 1246 | connect_(); 1247 | } 1248 | 1249 | void configure_tls_context_() { 1250 | if (1 != SSL_CTX_set_default_verify_paths(tls_context_.get())) { 1251 | throw_tls_error_(); 1252 | } 1253 | SSL_CTX_set_read_ahead(tls_context_.get(), true); 1254 | } 1255 | 1256 | void configure_tls_connection_(std::string const server, Port const port) { 1257 | auto const host_name_c_string = server.data(); 1258 | 1259 | // For SNI (Server Name Identification) 1260 | // The macro casts the string to a void* for some reason. Ew. 1261 | // The casts are to suppress warnings about it. 1262 | if (1 != SSL_set_tlsext_host_name(tls_connection_.get(), reinterpret_cast(const_cast(host_name_c_string)))) { 1263 | throw_tls_error_(); 1264 | } 1265 | // Configure automatic hostname check 1266 | if (1 != SSL_set1_host(tls_connection_.get(), host_name_c_string)) { 1267 | throw_tls_error_(); 1268 | } 1269 | 1270 | // Set the socket to be used by the tls connection 1271 | raw_socket_ = std::make_unique(server, port); 1272 | update_tls_socket_handle_(); 1273 | } 1274 | 1275 | void update_tls_socket_handle_() { 1276 | if (1 != SSL_set_fd(tls_connection_.get(), raw_socket_->get_posix_handle())) { 1277 | throw_tls_error_(); 1278 | } 1279 | } 1280 | 1281 | void connect_() { 1282 | SSL_connect(tls_connection_.get()); 1283 | 1284 | // Just to check that a certificate was presented by the server 1285 | if (auto const certificate = SSL_get_peer_certificate(tls_connection_.get())) { 1286 | X509_free(certificate); 1287 | } 1288 | else throw_tls_error_(); 1289 | 1290 | // Get result of the certificate verification 1291 | auto const verify_result = SSL_get_verify_result(tls_connection_.get()); 1292 | if (X509_V_OK != verify_result) { 1293 | throw_tls_error_(); 1294 | } 1295 | } 1296 | 1297 | std::unique_ptr raw_socket_; 1298 | bool is_closed_{false}; 1299 | 1300 | TlsContext tls_context_ = []{ 1301 | if (auto const method = TLS_client_method()) { 1302 | if (auto const tls = SSL_CTX_new(method)) { 1303 | return TlsContext{tls}; 1304 | } 1305 | } 1306 | throw_tls_error_(); 1307 | return TlsContext{}; 1308 | }(); 1309 | 1310 | TlsConnection tls_connection_ = [this]{ 1311 | if (auto const tls_connection = SSL_new(tls_context_.get())) { 1312 | return TlsConnection{tls_connection}; 1313 | } 1314 | throw_tls_error_(); 1315 | return TlsConnection{}; 1316 | }(); 1317 | }; 1318 | #endif // IS_POSIX 1319 | 1320 | class Socket::Implementation { 1321 | public: 1322 | void write(std::span const buffer) { 1323 | if (std::holds_alternative(socket_)) { 1324 | std::get(socket_).write(buffer); 1325 | } 1326 | else std::get(socket_).write(buffer); 1327 | } 1328 | [[nodiscard]] 1329 | auto read(std::span const buffer) 1330 | -> std::variant 1331 | { 1332 | if (std::holds_alternative(socket_)) { 1333 | return std::get(socket_).read(buffer); 1334 | } 1335 | return std::get(socket_).read(buffer); 1336 | } 1337 | [[nodiscard]] 1338 | auto read_available(std::span const buffer) 1339 | -> std::variant 1340 | { 1341 | if (std::holds_alternative(socket_)) { 1342 | return std::get(socket_).read_available(buffer); 1343 | } 1344 | return std::get(socket_).read_available(buffer); 1345 | } 1346 | 1347 | Implementation(std::string_view const server, Port const port, bool const is_tls_encrypted) : 1348 | socket_{select_socket_(server, port, is_tls_encrypted)} 1349 | {} 1350 | 1351 | private: 1352 | using SocketVariant = std::variant; 1353 | 1354 | [[nodiscard]] 1355 | static SocketVariant select_socket_(std::string_view const server, Port const port, bool const is_tls_encrypted) 1356 | { 1357 | if (is_tls_encrypted) { 1358 | return TlsSocket{server, port}; 1359 | } 1360 | return RawSocket{server, port}; 1361 | } 1362 | 1363 | SocketVariant socket_; 1364 | }; 1365 | 1366 | void Socket::write(std::span data) const { 1367 | implementation_->write(data); 1368 | } 1369 | 1370 | auto Socket::read(std::span buffer) const 1371 | -> std::variant 1372 | { 1373 | return implementation_->read(buffer); 1374 | } 1375 | 1376 | auto Socket::read_available(std::span buffer) const 1377 | -> std::variant 1378 | { 1379 | return implementation_->read_available(buffer); 1380 | } 1381 | 1382 | Socket::Socket(std::string_view const server, Port const port, bool const is_tls_encrypted) : 1383 | implementation_{std::make_unique(server, port, is_tls_encrypted)} 1384 | {} 1385 | Socket::~Socket() = default; 1386 | 1387 | Socket::Socket(Socket&&) noexcept = default; 1388 | Socket& Socket::operator=(Socket&&) noexcept = default; 1389 | 1390 | } // namespace http_client 1391 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | file(GLOB TEST_SOURCES *.cpp) 3 | 4 | add_executable(cpp20_http_client_test ${TEST_SOURCES}) 5 | 6 | target_link_libraries(cpp20_http_client_test PRIVATE cpp20_http_client) 7 | 8 | find_package(Catch2 CONFIG REQUIRED) 9 | target_link_libraries(cpp20_http_client_test PRIVATE Catch2::Catch2 Catch2::Catch2WithMain) 10 | 11 | add_test(NAME unit_tests COMMAND cpp20_http_client_test) 12 | 13 | add_custom_target(run_tests 14 | COMMAND ${CMAKE_BINARY_DIR}/bin/cpp20_http_client_test --use-colour yes 15 | DEPENDS cpp20_http_client_test 16 | WORKING_DIRECTORY ${CMAKE_BINARY_DIR} 17 | COMMENT "Running tests..." 18 | ) 19 | -------------------------------------------------------------------------------- /tests/chunky_body_parser.cpp: -------------------------------------------------------------------------------- 1 | #include "testing_header.hpp" 2 | 3 | void test_chunky_body_parser(std::string_view const chunky_body, std::string_view const expected_result) 4 | { 5 | auto const chunky_body_data = utils::string_to_data(chunky_body); 6 | for (auto const packet_size : {1, 8, 32, 128, 512, 2048}) 7 | { 8 | auto parser = algorithms::ChunkyBodyParser{}; 9 | 10 | for (auto pos = std::size_t{};; pos += packet_size) 11 | { 12 | auto const new_data_end = chunky_body_data.begin() + std::min(pos + packet_size, chunky_body_data.size()); 13 | if (auto const result = parser.parse_new_data(std::span{chunky_body_data.begin() + pos, new_data_end})) { 14 | CHECK(utils::data_to_string(std::span{*result}) == expected_result); 15 | break; 16 | } 17 | else if (new_data_end == chunky_body_data.end()) { 18 | CHECK(chunky_body_data.empty()); 19 | break; 20 | } 21 | } 22 | } 23 | } 24 | 25 | TEST_CASE("Chunked http body parsing") { 26 | constexpr auto input = 27 | "12\r\n" 28 | "Hello this is the " 29 | "\r\n16\r\n" 30 | "body of some web page " 31 | "\r\n14\r\n" 32 | "and it is using the " 33 | "\r\n1C\r\n" 34 | "chunked transfer encoding." 35 | "\r\n" 36 | "\r\n14\r\n" 37 | "That was a new line!" 38 | "\r\n0\r\n" 39 | "\r\n"sv; 40 | constexpr auto expected_output = 41 | "Hello this is the body of some web page and it is using the chunked transfer encoding.\r\n" 42 | "That was a new line!"sv; 43 | 44 | test_chunky_body_parser(input, expected_output); 45 | } 46 | 47 | TEST_CASE("Chunked http body parser, small chunks") { 48 | constexpr auto input = "1\r\nH\r\n2\r\nel\r\n1\r\nl\r\n1\r\no\r\n0\r\n\r\n"sv; 49 | constexpr auto expected_output = "Hello"sv; 50 | test_chunky_body_parser(input, expected_output); 51 | } 52 | 53 | TEST_CASE("Chunked http body parser, invalid input") { 54 | constexpr auto input = "hello world \r\n\r\n19\r\nåäöasdfjkl"; 55 | REQUIRE_THROWS_AS(test_chunky_body_parser(input, ""), errors::ResponseParsingFailed); 56 | } 57 | 58 | TEST_CASE("Chunked http body parser, empty input") { 59 | test_chunky_body_parser("", ""); 60 | } 61 | -------------------------------------------------------------------------------- /tests/concatenate_byte_data.cpp: -------------------------------------------------------------------------------- 1 | #include "testing_header.hpp" 2 | 3 | TEST_CASE("utils::concatenate_byte_data with different types of byte data.") { 4 | auto const expected_result = utils::string_to_data( 5 | "This is a test of my very own function called \"concatenate_byte_data\". Some numbers: \x5\x9\xA1\xFB."sv 6 | ); 7 | auto const result = utils::concatenate_byte_data( 8 | std::byte{u8'T'}, "his"sv, ' ', "is"sv, ' ', "a test of my very "sv, 9 | std::array{'o', 'w', 'n', ' '}, 10 | "function called "sv, '\"', "concatenate_byte_data"sv, std::byte{'\"'}, std::byte{'.'}, 11 | std::vector{' ', 'S', 'o', 'm', 'e', ' '}, 12 | "numbers: "sv, 13 | std::array{std::byte{0x5}, std::byte{0x9}, std::byte{0xA1}, std::byte{0xFB}, std::byte{'.'}} 14 | ); 15 | CHECK(std::ranges::equal(expected_result, result)); 16 | } 17 | TEST_CASE("utils::concatenate_byte_data const to non-const.") { 18 | auto const byte = std::byte{'A'}; 19 | auto const array = std::array{'h', 'i'}; 20 | auto const expected_result = utils::string_to_data("Aho"); 21 | auto result = utils::concatenate_byte_data(byte, array); 22 | result[2] = std::byte{'o'}; 23 | CHECK(std::ranges::equal(expected_result, result)); 24 | } 25 | 26 | TEST_CASE("utils::concatenate_byte_data with one argument.") { 27 | CHECK(std::ranges::equal(utils::concatenate_byte_data("hello"sv), utils::string_to_data("hello"sv))); 28 | } 29 | TEST_CASE("utils::concatenate_byte_data with empty ranges.") { 30 | CHECK(utils::concatenate_byte_data(""sv, std::vector{}, std::string{}).empty()); 31 | } 32 | -------------------------------------------------------------------------------- /tests/extract_filename.cpp: -------------------------------------------------------------------------------- 1 | #include "testing_header.hpp" 2 | 3 | TEST_CASE("Trying extract_filename with empty string") { 4 | CHECK(utils::extract_filename(""sv).empty()); 5 | } 6 | 7 | TEST_CASE("Trying extract_filename with urls") { 8 | CHECK(utils::extract_filename("https://www.youtube.com/watch?v=lXKDu6cdXLI"sv) == "watch"); 9 | CHECK(utils::extract_filename("http://bjornsundin.com/info/index.html"sv) == "index.html"); 10 | CHECK(utils::extract_filename("https://github.com/avocadoboi/cpp20-http-client"sv) == "cpp20-http-client"); 11 | } 12 | 13 | TEST_CASE("Trying extract_filename with invalid urls") { 14 | CHECK(utils::extract_filename("this is an invalid URL."sv).empty()); 15 | CHECK(utils::extract_filename("öafskjahögworhwr"sv).empty()); 16 | } 17 | -------------------------------------------------------------------------------- /tests/http_response_parser.cpp: -------------------------------------------------------------------------------- 1 | #include "testing_header.hpp" 2 | 3 | void test_response_parser(std::string_view const input, algorithms::ParsedResponse const& expected_result) 4 | { 5 | auto const response_data = utils::string_to_data(input); 6 | 7 | for (std::size_t const packet_size : {1, 8, 32, 128, 512, 2048}) 8 | { 9 | auto parser = algorithms::ResponseParser{}; 10 | 11 | for (auto pos = std::size_t{};; pos += packet_size) 12 | { 13 | if (auto const result = parser.parse_new_data( 14 | response_data.subspan(pos, std::min(response_data.size() - pos, packet_size)) 15 | )) 16 | { 17 | REQUIRE(result == expected_result); 18 | // test_utils::check_http_response(result, expected_result); 19 | break; 20 | } 21 | } 22 | } 23 | } 24 | 25 | utils::DataVector string_to_data_vector(std::string_view const string) { 26 | auto const data = utils::string_to_data(string); 27 | return utils::DataVector(data.begin(), data.end()); 28 | } 29 | 30 | TEST_CASE("Http response parser, conforming line endings") { 31 | auto const expected_headers_string = std::string{ 32 | "HTTP/1.1 200 OK\r\n" 33 | "Content-Length: 40\r\n" 34 | "Content-Type: text/html; charset=UTF-8\r\n" 35 | "Date: Sat, 19 Sep 2020 22:49:51 GMT" 36 | }; 37 | constexpr auto expected_body_string = 38 | "This is a test\n" 39 | "Line two\n" 40 | "\n" 41 | "Another line!!!"; 42 | 43 | auto const input = 44 | expected_headers_string + "\r\n\r\n" + expected_body_string; 45 | 46 | auto const expected_result = algorithms::ParsedResponse{ 47 | test_utils::ok_status_line, 48 | expected_headers_string, 49 | { 50 | Header{.name="content-length", .value="40"}, 51 | Header{.name="content-type", .value="text/html; charset=UTF-8"}, 52 | Header{.name="date", .value="Sat, 19 Sep 2020 22:49:51 GMT"} 53 | }, 54 | string_to_data_vector(expected_body_string), 55 | }; 56 | 57 | test_response_parser(input, expected_result); 58 | } 59 | 60 | TEST_CASE("Http response parser, nonconforming line endings") { 61 | auto const expected_headers_string = std::string{ 62 | "HTTP/1.1 200 OK\n" 63 | "Content-Length: 40\n" 64 | "Content-Type: text/html; charset=UTF-8\n" 65 | "Date: Sat, 19 Sep 2020 22:49:51 GMT" 66 | }; 67 | constexpr auto expected_body_string = 68 | "This is a test\n" 69 | "Line two\n" 70 | "\n" 71 | "Another line!!!"; 72 | 73 | auto const input = 74 | expected_headers_string + "\n\n" + expected_body_string; 75 | 76 | auto const expected_result = algorithms::ParsedResponse{ 77 | test_utils::ok_status_line, 78 | expected_headers_string, 79 | { 80 | Header{.name="content-length", .value="40"}, 81 | Header{.name="content-type", .value="text/html; charset=UTF-8"}, 82 | Header{.name="date", .value="Sat, 19 Sep 2020 22:49:51 GMT"} 83 | }, 84 | string_to_data_vector(expected_body_string), 85 | }; 86 | 87 | test_response_parser(input, expected_result); 88 | } 89 | 90 | TEST_CASE("Http response parser, no body") { 91 | constexpr auto input = "HTTP/1.1 404 Not Found\r\n\r\n"; 92 | 93 | auto const expected_result = algorithms::ParsedResponse{ 94 | StatusLine{ 95 | .http_version = "HTTP/1.1", 96 | .status_code = StatusCode::NotFound, 97 | .status_message = "Not Found" 98 | }, 99 | "HTTP/1.1 404 Not Found" 100 | }; 101 | test_response_parser(input, expected_result); 102 | } 103 | 104 | TEST_CASE("Http response parser, chunked transfer encoding") { 105 | auto const expected_headers_string = std::string{ 106 | "HTTP/1.1 200 OK\r\n" 107 | "Content-Type: text/html; charset=UTF-8\r\n" 108 | "Date: Sat, 19 Sep 2020 22:49:51 GMT\r\n" 109 | "transfer-encoding: chunked" 110 | }; 111 | constexpr auto expected_body_string = "This is a test\nLine two\n\nAnother line!!!"; 112 | 113 | constexpr auto chunked_body_string = 114 | "1\r\nT" 115 | "\r\nE\r\n" 116 | "his is a test\n" 117 | "\r\nA\r\n" 118 | "Line two\n\n" 119 | "\r\nF\r\n" 120 | "Another line!!!" 121 | "\r\n0\r\n\r\n"; 122 | 123 | auto const input = 124 | expected_headers_string + "\r\n\r\n" + chunked_body_string; 125 | 126 | auto const expected_result = algorithms::ParsedResponse{ 127 | test_utils::ok_status_line, 128 | expected_headers_string, 129 | { 130 | Header{.name="content-type", .value="text/html; charset=UTF-8"}, 131 | Header{.name="date", .value="Sat, 19 Sep 2020 22:49:51 GMT"}, 132 | Header{.name="transfer-encoding", .value="chunked"}, 133 | }, 134 | string_to_data_vector(expected_body_string), 135 | }; 136 | 137 | test_response_parser(input, expected_result); 138 | } 139 | -------------------------------------------------------------------------------- /tests/http_response_parser_callbacks.cpp: -------------------------------------------------------------------------------- 1 | #include "testing_header.hpp" 2 | 3 | [[nodiscard]] 4 | auto parse_input_in_chunks(algorithms::ResponseParser&& parser, std::string_view const input_string, std::size_t const chunk_size) 5 | -> algorithms::ParsedResponse 6 | { 7 | for (auto pos = std::size_t{};; pos += chunk_size) { 8 | if (auto result = parser.parse_new_data( 9 | utils::string_to_data(input_string) 10 | .subspan(pos, std::min(input_string.size() - pos, chunk_size)) 11 | )) 12 | { 13 | return *std::move(result); 14 | } 15 | } 16 | } 17 | 18 | // The parser is tested separately without callbacks, so there's 19 | // no need to have lots of different types of input here. 20 | // Only the callback system is tested here. 21 | // We do test chunked and non-chunked transfer separately. 22 | 23 | auto const headers_string_chunked_transfer = std::string{ 24 | "HTTP/1.1 200 OK\r\n" 25 | "Content-Type: text/html; charset=UTF-8\r\n" 26 | "Date: Sat, 19 Sep 2020 22:49:51 GMT\r\n" 27 | "Transfer-Encoding: chunked" 28 | }; 29 | auto const headers_chunked_transfer = std::vector{ 30 | Header{.name="content-type", .value="text/html; charset=UTF-8"}, 31 | Header{.name="date", .value="Sat, 19 Sep 2020 22:49:51 GMT"}, 32 | Header{.name="transfer-encoding", .value="chunked"}, 33 | }; 34 | 35 | auto const headers_string_identity_transfer = std::string{ 36 | "HTTP/1.1 200 OK\r\n" 37 | "Content-Type: text/html; charset=UTF-8\r\n" 38 | "Date: Sat, 19 Sep 2020 22:49:51 GMT\r\n" 39 | "Content-Length: 40" 40 | }; 41 | auto const headers_identity_transfer = std::vector{ 42 | Header{.name="content-type", .value="text/html; charset=UTF-8"}, 43 | Header{.name="date", .value="Sat, 19 Sep 2020 22:49:51 GMT"}, 44 | Header{.name="content-length", .value="40"}, 45 | }; 46 | 47 | constexpr auto identity_body_string = "This is a test\nLine two\n\nAnother line!!!"sv; 48 | 49 | constexpr auto chunked_body_string = 50 | "1\r\nT" 51 | "\r\nE\r\n" 52 | "his is a test\n" 53 | "\r\nA\r\n" 54 | "Line two\n\n" 55 | "\r\nF\r\n" 56 | "Another line!!!" 57 | "\r\n0\r\n\r\n"sv; 58 | constexpr auto header_body_separator = "\r\n\r\n"sv; 59 | 60 | constexpr auto chunk_sizes_to_test = std::array{1, 8, 32, 128, 512}; 61 | 62 | void test_callbacks_full_input( 63 | std::string_view const headers_string, 64 | std::vector
const& headers, 65 | std::string_view const body_string 66 | ) { 67 | auto const input_string = (std::string{headers_string} += header_body_separator) += body_string; 68 | 69 | auto const expected_body_data = utils::string_to_data(identity_body_string); 70 | 71 | auto const expected_result = algorithms::ParsedResponse{ 72 | test_utils::ok_status_line, 73 | std::string{headers_string}, 74 | headers, 75 | std::vector(expected_body_data.begin(), expected_body_data.end()), 76 | }; 77 | 78 | for (auto const chunk_size : chunk_sizes_to_test) { 79 | auto number_of_parsed_packets = 0; 80 | 81 | auto response_callbacks = algorithms::ResponseCallbacks{ 82 | .handle_raw_progress = [&](ResponseProgressRaw& progress) { 83 | CHECK(progress.new_data_start == number_of_parsed_packets*chunk_size); 84 | 85 | auto const input_data = utils::string_to_data(std::string_view{input_string}); 86 | if (progress.new_data_start + chunk_size > input_data.size()) { 87 | CHECK(std::ranges::equal(progress.data, input_data)); 88 | } 89 | else { 90 | CHECK(std::ranges::equal(progress.data, input_data.first(progress.new_data_start + chunk_size))); 91 | } 92 | 93 | ++number_of_parsed_packets; 94 | }, 95 | .handle_headers = [&](ResponseProgressHeaders& progress) { 96 | CHECK(progress.get_status_line() == expected_result.status_line); 97 | CHECK(progress.get_headers_string() == expected_result.headers_string); 98 | CHECK(std::ranges::equal(progress.get_headers(), expected_result.headers)); 99 | }, 100 | .handle_body_progress = [&](ResponseProgressBody& progress) { 101 | CHECK(std::ranges::equal(progress.body_data_so_far, expected_body_data.first(progress.body_data_so_far.size()))); 102 | }, 103 | .handle_finish{}, 104 | .handle_stop{} 105 | }; 106 | auto const result = parse_input_in_chunks(algorithms::ResponseParser{response_callbacks}, input_string, chunk_size); 107 | CHECK(result == expected_result); 108 | CHECK(number_of_parsed_packets <= std::ceil(static_cast(input_string.size()) / static_cast(chunk_size))); 109 | } 110 | } 111 | 112 | TEST_CASE("Response parser with callbacks and chunked transfer, full input") { 113 | test_callbacks_full_input(headers_string_chunked_transfer, headers_chunked_transfer, chunked_body_string); 114 | } 115 | TEST_CASE("Response parser with callbacks and identity transfer, full input") { 116 | test_callbacks_full_input(headers_string_identity_transfer, headers_identity_transfer, identity_body_string); 117 | } 118 | 119 | void test_callbacks_stopping_after_head( 120 | std::string_view const headers_string, 121 | std::vector
const& headers, 122 | std::string_view const body_string 123 | ) { 124 | auto input_string = (std::string{headers_string} += header_body_separator) += body_string; 125 | 126 | auto const expected_result = algorithms::ParsedResponse{ 127 | test_utils::ok_status_line, 128 | std::string{headers_string}, 129 | headers, 130 | }; 131 | 132 | for (auto const chunk_size : chunk_sizes_to_test) { 133 | auto number_of_parsed_packets = 0; 134 | auto got_any_body = false; 135 | auto response_callbacks = algorithms::ResponseCallbacks{ 136 | .handle_raw_progress = [&](ResponseProgressRaw& progress) { 137 | CHECK(progress.new_data_start == number_of_parsed_packets*chunk_size); 138 | 139 | auto const input_data = utils::string_to_data(std::string_view{input_string}); 140 | if (chunk_size > input_data.size() || progress.new_data_start > input_data.size() - chunk_size) { 141 | CHECK(std::ranges::equal(progress.data, input_data)); 142 | } 143 | else { 144 | CHECK(std::ranges::equal(progress.data, input_data.first(progress.new_data_start + chunk_size))); 145 | } 146 | 147 | ++number_of_parsed_packets; 148 | }, 149 | .handle_headers = [&](ResponseProgressHeaders& progress) { 150 | CHECK(progress.get_parsed_response() == expected_result); 151 | progress.stop(); 152 | }, 153 | .handle_body_progress = [&](ResponseProgressBody&) { 154 | got_any_body = true; 155 | }, 156 | .handle_finish{}, 157 | .handle_stop{} 158 | }; 159 | CHECK( 160 | parse_input_in_chunks(algorithms::ResponseParser{response_callbacks}, input_string, chunk_size) == 161 | expected_result 162 | ); 163 | CHECK(!got_any_body); 164 | CHECK(number_of_parsed_packets == std::ceil(static_cast(headers_string.size() + header_body_separator.size()) / 165 | static_cast(chunk_size))); 166 | } 167 | } 168 | 169 | TEST_CASE("Response parser with callbacks and chunked transfer, stopping after head") { 170 | test_callbacks_stopping_after_head(headers_string_chunked_transfer, headers_chunked_transfer, chunked_body_string); 171 | } 172 | TEST_CASE("Response parser with callbacks and identity transfer, stopping after head") { 173 | test_callbacks_stopping_after_head(headers_string_identity_transfer, headers_identity_transfer, identity_body_string); 174 | } 175 | -------------------------------------------------------------------------------- /tests/parse_headers_string.cpp: -------------------------------------------------------------------------------- 1 | #include "testing_header.hpp" 2 | 3 | TEST_CASE("Trying parse_headers_string with single line") { 4 | auto const headers = algorithms::parse_headers_string("Last-Modified: tomorrow at 4 am"); 5 | 6 | REQUIRE(headers.size() == 1); 7 | CHECK(headers[0] == Header{.name="Last-modified", .value="tomorrow at 4 am"}); 8 | } 9 | 10 | TEST_CASE("Trying parse_headers_string with multiple lines") { 11 | auto const headers = algorithms::parse_headers_string( 12 | R"( 13 | 14 | 15 | One: aaa 16 | Two: bbbbbbb 17 | Three: ccccccc 18 | 19 | Last-Modified: tomorrow at 4 am 20 | 21 | )" 22 | ); 23 | constexpr auto expected = std::array{ 24 | Header{.name="oNe", .value="aaa"}, 25 | Header{.name="TwO", .value="bbbbbbb"}, 26 | Header{.name="thRee", .value="ccccccc"}, 27 | Header{.name="last-modified", .value="tomorrow at 4 am"}, 28 | }; 29 | 30 | CHECK(std::ranges::equal(headers, expected)); 31 | } 32 | 33 | TEST_CASE("Trying parse_headers_string with multiple lines without any valid headers") { 34 | auto const headers = algorithms::parse_headers_string( 35 | R"( 36 | One ~ aaa 37 | Two....... bbbbbbb 38 | 39 | Three!! ccccccc 40 | 41 | 42 | Last-Modified - tomorrow at 4 am 43 | )" 44 | ); 45 | CHECK(headers.empty()); 46 | } 47 | 48 | TEST_CASE("Trying parse_headers_string with empty string") { 49 | CHECK(algorithms::parse_headers_string("").empty()); 50 | } 51 | -------------------------------------------------------------------------------- /tests/parse_status_line.cpp: -------------------------------------------------------------------------------- 1 | #include "testing_header.hpp" 2 | 3 | void check_status_line(StatusLine const& status_line) { 4 | CHECK(status_line.http_version == "HTTP/1.1"); 5 | CHECK(status_line.status_code == StatusCode::Forbidden); 6 | CHECK(status_line.status_message == "Forbidden"); 7 | } 8 | 9 | TEST_CASE("parse_status_line without newline") { 10 | auto const status_line = algorithms::parse_status_line("HTTP/1.1 403 Forbidden"); 11 | check_status_line(status_line); 12 | } 13 | TEST_CASE("parse_status_line with newline") { 14 | auto const status_line = algorithms::parse_status_line("HTTP/1.1 403 Forbidden\r\n"); 15 | check_status_line(status_line); 16 | } 17 | TEST_CASE("parse_status_line with nonconforming newline") { 18 | auto const status_line = algorithms::parse_status_line("HTTP/1.1 403 Forbidden\n"); 19 | check_status_line(status_line); 20 | } 21 | TEST_CASE("parse_status_line with empty string") { 22 | auto const [http_version, status_code, status_message] = algorithms::parse_status_line(""); 23 | CHECK(http_version.empty()); 24 | CHECK(status_code == StatusCode::Unknown); 25 | CHECK(status_message.empty()); 26 | } 27 | -------------------------------------------------------------------------------- /tests/split_url.cpp: -------------------------------------------------------------------------------- 1 | #include "testing_header.hpp" 2 | 3 | #include 4 | 5 | TEST_CASE("Trying split_url for a localhost address") { 6 | auto const [protocol, host_name, port, path] = utils::split_url("http://localhost:8082/blablabla"); 7 | 8 | CHECK(protocol == Protocol::Http); 9 | CHECK(host_name == "localhost"); 10 | CHECK(port == Port{8082}); 11 | CHECK(path == "/blablabla"); 12 | } 13 | TEST_CASE("Trying split_url for a localhost address without path") { 14 | auto const [protocol, host_name, port, path] = utils::split_url("https://localhost:8082"); 15 | 16 | CHECK(protocol == Protocol::Https); 17 | CHECK(host_name == "localhost"); 18 | CHECK(port == Port{8082}); 19 | CHECK(path == "/"); 20 | } 21 | TEST_CASE("Trying split_url for a localhost address with invalid port number") { 22 | auto const [protocol, host_name, port, path] = utils::split_url("https://localhost:what/blablabla"); 23 | 24 | CHECK(protocol == Protocol::Https); 25 | CHECK(host_name == "localhost"); 26 | CHECK(port == utils::default_port_for_protocol(protocol)); 27 | CHECK(path == "/blablabla"); 28 | } 29 | 30 | TEST_CASE("Trying split_url for https://google.com/") { 31 | auto const [protocol, host_name, port, path] = utils::split_url("https://google.com/"); 32 | 33 | CHECK(protocol == Protocol::Https); 34 | CHECK(host_name == "google.com"); 35 | CHECK(port == utils::default_port_for_protocol(protocol)); 36 | CHECK(path == "/"); 37 | } 38 | TEST_CASE("Trying split_url for https://google.com/ with extra spacing") { 39 | auto const [protocol, host_name, port, path] = utils::split_url(" \thttps://google.com/ \n "); 40 | 41 | CHECK(protocol == Protocol::Https); 42 | CHECK(host_name == "google.com"); 43 | CHECK(port == utils::default_port_for_protocol(protocol)); 44 | CHECK(path == "/"); 45 | } 46 | 47 | TEST_CASE("Trying split_url for http://bjornsundin.com/projects/index.html") { 48 | auto const [protocol, host_name, port, path] = utils::split_url("http://bjornsundin.com/projects/index.html"); 49 | 50 | CHECK(protocol == Protocol::Http); 51 | CHECK(host_name == "bjornsundin.com"); 52 | CHECK(port == utils::default_port_for_protocol(protocol)); 53 | CHECK(path == "/projects/index.html"); 54 | } 55 | 56 | TEST_CASE("Trying split_url for github.com/avocadoboi") { 57 | auto const [protocol, host_name, port, path] = utils::split_url("github.com/avocadoboi"); 58 | 59 | CHECK(protocol == Protocol::Unknown); 60 | CHECK(host_name == "github.com"); 61 | CHECK(port == utils::default_port_for_protocol(protocol)); 62 | CHECK(path == "/avocadoboi"); 63 | } 64 | 65 | TEST_CASE("Trying split_url for github.com") { 66 | auto const [protocol, host_name, port, path] = utils::split_url("github.com"); 67 | 68 | CHECK(protocol == Protocol::Unknown); 69 | CHECK(host_name == "github.com"); 70 | CHECK(port == utils::default_port_for_protocol(protocol)); 71 | CHECK(path == "/"); 72 | } 73 | 74 | TEST_CASE("Trying split_url for single character.") { 75 | auto const [protocol, host_name, port, path] = utils::split_url("a"); 76 | 77 | CHECK(protocol == Protocol::Unknown); 78 | CHECK(host_name == "a"); 79 | CHECK(port == utils::default_port_for_protocol(protocol)); 80 | CHECK(path == "/"); 81 | } 82 | 83 | TEST_CASE("Trying split_url for empty string.") { 84 | auto const [protocol, host_name, port, path] = utils::split_url(""); 85 | 86 | CHECK(protocol == Protocol::Unknown); 87 | CHECK(host_name == ""); 88 | CHECK(port == utils::default_port_for_protocol(protocol)); 89 | CHECK(path == ""); 90 | } 91 | 92 | -------------------------------------------------------------------------------- /tests/testing_header.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | using namespace http_client; 6 | using namespace std::string_view_literals; 7 | 8 | namespace test_utils { 9 | 10 | auto const ok_status_line = StatusLine{ 11 | .http_version = "HTTP/1.1", 12 | .status_code = StatusCode::Ok, 13 | .status_message = "OK", 14 | }; 15 | 16 | } // namespace test_utils 17 | -------------------------------------------------------------------------------- /tests/uri_encode.cpp: -------------------------------------------------------------------------------- 1 | #include "testing_header.hpp" 2 | 3 | TEST_CASE("uri_encode #1") { 4 | REQUIRE(utils::uri_encode("https://ja.wikipedia.org/wiki/パーセントエンコーディング"sv) == 5 | "https://ja.wikipedia.org/wiki/%e3%83%91%e3%83%bc%e3%82%bb%e3%83%b3%e3%83%88%e3%82%a8%e3%83%b3%e3%82%b3%e3" 6 | "%83%bc%e3%83%87%e3%82%a3%e3%83%b3%e3%82%b0"); 7 | } 8 | 9 | TEST_CASE("uri_encode #2") { 10 | REQUIRE(utils::uri_encode("https://pt.wikipedia.org/wiki/Codificação_por_cento"sv) == 11 | "https://pt.wikipedia.org/wiki/Codifica%c3%a7%c3%a3o_por_cento"); 12 | } 13 | 14 | TEST_CASE("uri_encode with already encoded std::string_view") { 15 | auto const url = "https://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8"sv; 16 | REQUIRE(utils::uri_encode(url) == url); 17 | } 18 | 19 | TEST_CASE("uri_encode with empty string") { 20 | CHECK(utils::uri_encode(""sv) == ""); 21 | } 22 | --------------------------------------------------------------------------------