diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..5f617f4 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,67 @@ +name: Coverage +on: + push: + branches: + - '**' + pull_request: + branches: + - '**' + +jobs: + build-test: + runs-on: ubuntu-24.04 + name: ubuntu-coverage + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y ninja-build cmake clang llvm + + - name: Run CMake configuration and build + run: | + cmake examples -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ \ + -DHTTP_BUILD_FUZZERS=ON + cmake --build build --target request_parser response_parser websocket_parser sha1 base64 --parallel + + - name: Extract corpus + run: | + tar -zxvf examples/fuzz/seeds.tgz + + - name: Run fuzzers + run: | + mkdir -p /tmp/corpus + LLVM_PROFILE_FILE="request.profraw" ./build/request_parser /tmp/corpus/ seeds/request_parser/ -max_total_time=30 + LLVM_PROFILE_FILE="response.profraw" ./build/response_parser /tmp/corpus/ seeds/response_parser/ -max_total_time=30 + LLVM_PROFILE_FILE="websocket.profraw" ./build/websocket_parser /tmp/corpus/ seeds/websocket_parser/ -max_total_time=30 + LLVM_PROFILE_FILE="sha1.profraw" ./build/sha1 -max_total_time=30 + LLVM_PROFILE_FILE="base64.profraw" ./build/base64 -max_total_time=30 + + - name: Generate coverage report + run: | + llvm-profdata merge -o coverage.profdata \ + request.profraw \ + response.profraw \ + websocket.profraw \ + sha1.profraw \ + base64.profraw + llvm-cov export -format=lcov -instr-profile coverage.profdata \ + -object ./build/request_parser \ + -object ./build/response_parser \ + -object ./build/websocket_parser \ + -object ./build/sha1 \ + -object ./build/base64 \ + -sources ./src \ + > coverage_${{github.sha}}.txt + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: pfeatherstone/https + files: coverage_${{github.sha}}.txt \ No newline at end of file diff --git a/.gitignore b/.gitignore index d9caafe..d2e99d4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ **/build -**/.vscode \ No newline at end of file +**/.vscode +**/*.profraw +**/*.profdata +**/seeds/** \ No newline at end of file diff --git a/README.md b/README.md index 768d404..cba1eb3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ ![Ubuntu](https://github.com/pfeatherstone/https/actions/workflows/ubuntu.yml/badge.svg) ![MacOS](https://github.com/pfeatherstone/https/actions/workflows/macos.yml/badge.svg) ![Windows](https://github.com/pfeatherstone/https/actions/workflows/windows.yml/badge.svg) +[![codecov](https://codecov.io/gh/pfeatherstone/https/branch/main/graph/badge.svg)](https://codecov.io/gh/pfeatherstone/https) # https diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 855e9c3..925dcc6 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,14 +1,20 @@ cmake_minimum_required(VERSION 3.13) project(Http) +# Policies +if(POLICY CMP0127) + cmake_policy(SET CMP0127 NEW) +endif() +if(POLICY CMP0135) + cmake_policy(SET CMP0135 OLD) +endif() + # Deps include(FetchContent) +include(CMakeDependentOption) find_package(Threads REQUIRED) find_package(OpenSSL REQUIRED) -if(POLICY CMP0135) - cmake_policy(SET CMP0135 OLD) -endif() set(BOOST_INCLUDE_LIBRARIES compat asio) set(BOOST_ENABLE_CMAKE ON) FetchContent_Declare( @@ -17,30 +23,32 @@ FetchContent_Declare( URL_HASH MD5=3edffaacd2cfe63c240ef1b99497c74f) FetchContent_MakeAvailable(Boost) +# Options +cmake_dependent_option(HTTP_BUILD_FUZZERS "Builds Fuzz tests" OFF "CMAKE_CXX_COMPILER_ID STREQUAL \"Clang\"" OFF) + # Lib add_library(http ${CMAKE_CURRENT_SOURCE_DIR}/../src/http.cpp) target_compile_features(http PUBLIC cxx_std_17) target_include_directories(http PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../src) target_link_libraries(http PUBLIC OpenSSL::SSL OpenSSL::Crypto Boost::asio) +target_compile_options(http PRIVATE $<$:-fprofile-instr-generate -fcoverage-mapping>) # Examples -function(add_executable_20 target_name) - add_executable(${target_name} ${ARGN}) - target_compile_features(${target_name} PUBLIC cxx_std_20) - target_link_options(${target_name} PUBLIC $<$:-s>) - target_link_libraries(${target_name} PUBLIC Threads::Threads http) -endfunction() - -function(add_executable_coro target_name) +function(add_example target_name) add_executable(${target_name} ${ARGN}) target_link_options(${target_name} PRIVATE $<$:-s>) target_link_libraries(${target_name} PRIVATE Threads::Threads Boost::compat http) endfunction() -add_executable_20(server ${CMAKE_CURRENT_SOURCE_DIR}/server.cpp) -add_executable_20(client_http ${CMAKE_CURRENT_SOURCE_DIR}/client_http.cpp ${CMAKE_CURRENT_SOURCE_DIR}/extra/yyjson.c) -add_executable_20(client_ws_awaitable ${CMAKE_CURRENT_SOURCE_DIR}/client_ws_awaitable.cpp) -add_executable_coro(client_ws_coro ${CMAKE_CURRENT_SOURCE_DIR}/client_ws_coro.cpp) +function(add_example_20 target_name) + add_example(${target_name} ${ARGN}) + target_compile_features(${target_name} PUBLIC cxx_std_20) +endfunction() + +add_example_20(server ${CMAKE_CURRENT_SOURCE_DIR}/server.cpp) +add_example_20(client_http ${CMAKE_CURRENT_SOURCE_DIR}/client_http.cpp ${CMAKE_CURRENT_SOURCE_DIR}/extra/yyjson.c) +add_example_20(client_ws_awaitable ${CMAKE_CURRENT_SOURCE_DIR}/client_ws_awaitable.cpp) +add_example(client_ws_coro ${CMAKE_CURRENT_SOURCE_DIR}/client_ws_coro.cpp) # Unit tests add_executable(tests @@ -54,4 +62,20 @@ add_executable(tests target_compile_features(tests PUBLIC cxx_std_20) target_link_options(tests PRIVATE $<$:-s>) target_include_directories(tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/extra) -target_link_libraries(tests PRIVATE http) \ No newline at end of file +target_link_libraries(tests PRIVATE http) + +# Fuzz tests +if (HTTP_BUILD_FUZZERS) + function(add_fuzz target_name) + add_executable(${target_name} ${ARGN}) + target_link_libraries(${target_name} PRIVATE http) + target_compile_options(${target_name} PRIVATE -fprofile-instr-generate -fcoverage-mapping) + target_link_options(${target_name} PRIVATE -fsanitize=fuzzer -fprofile-instr-generate -fcoverage-mapping) + endfunction() + + add_fuzz(request_parser fuzz/request_parser.cpp) + add_fuzz(response_parser fuzz/response_parser.cpp) + add_fuzz(websocket_parser fuzz/websocket_parser.cpp) + add_fuzz(sha1 fuzz/sha1.cpp) + add_fuzz(base64 fuzz/base64.cpp) +endif() \ No newline at end of file diff --git a/examples/fuzz/base64.cpp b/examples/fuzz/base64.cpp new file mode 100644 index 0000000..2ef9c59 --- /dev/null +++ b/examples/fuzz/base64.cpp @@ -0,0 +1,13 @@ +#include +#include +#include +#include + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + auto encoded = http::base64_encode(size, data); + auto decoded = http::base64_decode(encoded); + encoded.erase(std::remove(begin(encoded), end(encoded), '='), end(encoded)); + auto decoded2 = http::base64_decode(encoded); + return 0; +} \ No newline at end of file diff --git a/examples/fuzz/request_parser.cpp b/examples/fuzz/request_parser.cpp new file mode 100644 index 0000000..9aadc99 --- /dev/null +++ b/examples/fuzz/request_parser.cpp @@ -0,0 +1,22 @@ +#include +#include +#include + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + std::string buf(reinterpret_cast(data), size); + http::request req{}; + std::error_code ec{}; + http::parser_request parser; + parser.parse(req, buf, ec); + bool is_keep_alive = req.keep_alive(); + bool is_websocket_req = req.is_websocket_req(); + std::string ec_msg = ec ? ec.message() : ""; + const auto& ec_cat = ec.category(); + buf.clear(); + ec = {}; + http::serialize_header(req, buf, ec); + req.clear(); + ec_msg = ec ? ec.message() : ""; + return 0; +} \ No newline at end of file diff --git a/examples/fuzz/response_parser.cpp b/examples/fuzz/response_parser.cpp new file mode 100644 index 0000000..c794091 --- /dev/null +++ b/examples/fuzz/response_parser.cpp @@ -0,0 +1,22 @@ +#include +#include +#include + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + std::string buf(reinterpret_cast(data), size); + http::response resp{}; + std::error_code ec{}; + http::parser_response parser; + parser.parse(resp, buf, ec); + buf.clear(); + std::string ec_msg = ec ? ec.message() : ""; + const auto& ec_cat = ec.category(); + ec = {}; + http::serialize_header(resp, buf, ec); + resp.keep_alive(true); + bool is_websocket = resp.is_websocket_response(); + resp.clear(); + ec_msg = ec ? ec.message() : ""; + return 0; +} \ No newline at end of file diff --git a/examples/fuzz/seeds.tgz b/examples/fuzz/seeds.tgz new file mode 100644 index 0000000..36fc853 Binary files /dev/null and b/examples/fuzz/seeds.tgz differ diff --git a/examples/fuzz/sha1.cpp b/examples/fuzz/sha1.cpp new file mode 100644 index 0000000..5ee2790 --- /dev/null +++ b/examples/fuzz/sha1.cpp @@ -0,0 +1,9 @@ +#include +#include +#include + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + auto sum = http::sha1{}.push(size, data).finish(); + return 0; +} \ No newline at end of file diff --git a/examples/fuzz/websocket_parser.cpp b/examples/fuzz/websocket_parser.cpp new file mode 100644 index 0000000..841d703 --- /dev/null +++ b/examples/fuzz/websocket_parser.cpp @@ -0,0 +1,40 @@ +#include +#include +#include + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + std::string buf(reinterpret_cast(data), size); + + http::request req{}; + std::error_code ec{}; + http::parser_request parser; + parser.parse(req, buf, ec); + bool is_websocket_req = req.is_websocket_req(); + std::string ec_msg = ec ? ec.message() : ""; + const auto& ec_cat = ec.category(); + + std::vector msg; + http::dynamic_buffer view(msg); + http::websocket_parser ws_parser; + ws_parser.parse(view, buf, ec); + const auto opcode = ws_parser.get_opcode(); + const auto is_server = ws_parser.is_server(); + const auto current_size = view.size(); + const auto current_data = view.data(); + const auto current_buf = view.buffer(); + ec_msg = ec ? ec.message() : ""; + + view.resize(10); + view.clear(); + msg.assign(data, data+size); + buf.clear(); + http::serialize_websocket_message(boost::asio::buffer(msg), http::WS_OPCODE_DATA_BINARY, true, buf); + buf.clear(); + http::serialize_websocket_message(boost::asio::buffer(msg), http::WS_OPCODE_DATA_BINARY, false, buf); + buf.clear(); + http::serialize_websocket_message(boost::asio::buffer(msg), http::WS_OPCODE_DATA_TEXT, true, buf); + buf.clear(); + http::serialize_websocket_message(boost::asio::buffer(msg), http::WS_OPCODE_DATA_TEXT, false, buf); + return 0; +} \ No newline at end of file diff --git a/examples/unit_tests/message.cpp b/examples/unit_tests/message.cpp index feb3b6d..761100c 100644 --- a/examples/unit_tests/message.cpp +++ b/examples/unit_tests/message.cpp @@ -115,7 +115,7 @@ TEST_SUITE("[MESSAGE]") http::request req; std::error_code ec{}; - bool finished = http::parser{}.parse(req, bad_req, ec); + bool finished = http::parser_request{}.parse(req, bad_req, ec); REQUIRE(ec == http::http_read_bad_method); } @@ -130,7 +130,7 @@ TEST_SUITE("[MESSAGE]") http::request req; std::error_code ec{}; - bool finished = http::parser{}.parse(req, bad_req, ec); + bool finished = http::parser_request{}.parse(req, bad_req, ec); REQUIRE(ec == http::http_read_unsupported_http_version); } @@ -145,7 +145,7 @@ TEST_SUITE("[MESSAGE]") http::request req; std::error_code ec{}; - bool finished = http::parser{}.parse(req, bad_req, ec); + bool finished = http::parser_request{}.parse(req, bad_req, ec); REQUIRE(ec == http::http_read_unsupported_http_version); } @@ -160,7 +160,7 @@ TEST_SUITE("[MESSAGE]") http::request req; std::error_code ec{}; - bool finished = http::parser{}.parse(req, bad_req, ec); + bool finished = http::parser_request{}.parse(req, bad_req, ec); REQUIRE(ec == http::http_read_unsupported_http_version); } @@ -175,7 +175,7 @@ TEST_SUITE("[MESSAGE]") http::request req; std::error_code ec{}; - bool finished = http::parser{}.parse(req, bad_req, ec); + bool finished = http::parser_request{}.parse(req, bad_req, ec); REQUIRE(ec == http::http_read_header_kv_delimiter_not_found); } @@ -191,7 +191,7 @@ TEST_SUITE("[MESSAGE]") http::request req; std::error_code ec{}; - bool finished = http::parser{}.parse(req, bad_req, ec); + bool finished = http::parser_request{}.parse(req, bad_req, ec); REQUIRE(ec == http::http_read_header_unsupported_field); // (It should be "Host:" not "Host :") } @@ -206,7 +206,7 @@ TEST_SUITE("[MESSAGE]") http::request req; std::error_code ec{}; - bool finished = http::parser{}.parse(req, bad_req, ec); + bool finished = http::parser_request{}.parse(req, bad_req, ec); REQUIRE(!bool(ec)); REQUIRE(!finished); // Incorrect Content-Length REQUIRE(bad_req.empty()); @@ -222,7 +222,7 @@ TEST_SUITE("[MESSAGE]") http::request req; std::error_code ec{}; - bool finished = http::parser{}.parse(req, bad_req, ec); + bool finished = http::parser_request{}.parse(req, bad_req, ec); REQUIRE(!bool(ec)); REQUIRE(!finished); // Waiting for \r\n to test header } @@ -239,7 +239,7 @@ TEST_SUITE("[MESSAGE]") http::request req; std::error_code ec{}; - bool finished = http::parser{}.parse(req, bad_req, ec); + bool finished = http::parser_request{}.parse(req, bad_req, ec); REQUIRE(!bool(ec)); REQUIRE(finished); // Done REQUIRE(bad_req.empty()); @@ -258,7 +258,7 @@ TEST_SUITE("[MESSAGE]") http::response reply; std::error_code ec{}; - bool finished = http::parser{}.parse(reply, bad_reply, ec); + bool finished = http::parser_response{}.parse(reply, bad_reply, ec); REQUIRE(ec == http::http_read_unsupported_http_version); } @@ -272,7 +272,7 @@ TEST_SUITE("[MESSAGE]") http::response reply; std::error_code ec{}; - bool finished = http::parser{}.parse(reply, bad_reply, ec); + bool finished = http::parser_response{}.parse(reply, bad_reply, ec); REQUIRE(ec == http::http_read_unsupported_http_version); } @@ -286,7 +286,7 @@ TEST_SUITE("[MESSAGE]") http::response reply; std::error_code ec{}; - bool finished = http::parser{}.parse(reply, bad_reply, ec); + bool finished = http::parser_response{}.parse(reply, bad_reply, ec); REQUIRE(ec == http::http_read_unsupported_http_version); } @@ -300,7 +300,7 @@ TEST_SUITE("[MESSAGE]") http::response reply; std::error_code ec{}; - bool finished = http::parser{}.parse(reply, bad_reply, ec); + bool finished = http::parser_response{}.parse(reply, bad_reply, ec); REQUIRE(ec == http::http_read_bad_status); } @@ -314,7 +314,7 @@ TEST_SUITE("[MESSAGE]") http::response reply; std::error_code ec{}; - bool finished = http::parser{}.parse(reply, bad_reply, ec); + bool finished = http::parser_response{}.parse(reply, bad_reply, ec); REQUIRE(ec == http::http_read_header_kv_delimiter_not_found); } @@ -328,7 +328,7 @@ TEST_SUITE("[MESSAGE]") http::response reply; std::error_code ec{}; - bool finished = http::parser{}.parse(reply, bad_reply, ec); + bool finished = http::parser_response{}.parse(reply, bad_reply, ec); REQUIRE(ec == http::http_read_header_unsupported_field); } @@ -342,7 +342,7 @@ TEST_SUITE("[MESSAGE]") http::response reply; std::error_code ec{}; - bool finished = http::parser{}.parse(reply, bad_reply, ec); + bool finished = http::parser_response{}.parse(reply, bad_reply, ec); REQUIRE(ec == http::http_read_header_unsupported_field); } @@ -356,7 +356,7 @@ TEST_SUITE("[MESSAGE]") http::response reply; std::error_code ec{}; - bool finished = http::parser{}.parse(reply, bad_reply, ec); + bool finished = http::parser_response{}.parse(reply, bad_reply, ec); REQUIRE(!bool(ec)); REQUIRE(!finished); // Wrong content length. Waiting for rest of payload REQUIRE(bad_reply.empty()); @@ -372,7 +372,7 @@ TEST_SUITE("[MESSAGE]") http::response reply; std::error_code ec{}; - bool finished = http::parser{}.parse(reply, good_reply, ec); + bool finished = http::parser_response{}.parse(reply, good_reply, ec); REQUIRE(!bool(ec)); REQUIRE(finished); // Good REQUIRE(good_reply.empty()); @@ -449,14 +449,14 @@ TEST_SUITE("[MESSAGE]") SUBCASE("parse entire message") { - const bool finished = http::parser{}.parse(req1, buf, ec); + const bool finished = http::parser_request{}.parse(req1, buf, ec); REQUIRE(!bool(ec)); REQUIRE(finished); } SUBCASE("parse block by block") { - http::parser parser; + http::parser_request parser; size_t blocksize{}; @@ -522,14 +522,14 @@ TEST_SUITE("[MESSAGE]") SUBCASE("parse entire message") { - const bool finished = http::parser{}.parse(resp1, buf, ec); + const bool finished = http::parser_response{}.parse(resp1, buf, ec); REQUIRE(!bool(ec)); REQUIRE(finished); } SUBCASE("parse block by block") { - http::parser parser; + http::parser_response parser; size_t blocksize{}; diff --git a/src/http.cpp b/src/http.cpp index 027da17..f430e59 100644 --- a/src/http.cpp +++ b/src/http.cpp @@ -19,14 +19,6 @@ namespace http return static_cast(((v & 0x00FF) << 8) | ((v & 0xFF00) >> 8)); } - constexpr uint32_t byte_swap32(uint32_t v) - { - return static_cast(((v & 0x000000FF) << 24) | - ((v & 0x0000FF00) << 8) | - ((v & 0x00FF0000) >> 8) | - ((v & 0xFF000000) >> 24)); - } - constexpr uint64_t byte_swap64(uint64_t v) { return static_cast(((v & 0x00000000000000FFULL) << 56) | @@ -40,7 +32,6 @@ namespace http } static_assert(byte_swap16(0x1234) == 0x3412, "bad swap"); - static_assert(byte_swap32(0x12345678) == 0x78563412, "bad swap"); static_assert(byte_swap64(0x123456789abcdef1) == 0xf1debc9a78563412, "bad swap"); //---------------------------------------------------------------------------------------------------------------- @@ -59,11 +50,6 @@ namespace http return is_little_endian() ? byte_swap16(v) : v; } - inline uint32_t host_to_b32(uint32_t v) - { - return is_little_endian() ? byte_swap32(v) : v; - } - inline uint64_t host_to_b64(uint64_t v) { return is_little_endian() ? byte_swap64(v) : v; @@ -482,82 +468,85 @@ namespace http //---------------------------------------------------------------------------------------------------------------- - std::string_view status_label(const status_type v) - { - switch(v) - { + constexpr std::pair STATUS_LABELS[] = { // 1xx - case status_type::continue_: return "Continue"; - case status_type::switching_protocols: return "Switching Protocols"; - case status_type::processing: return "Processing"; - case status_type::early_hints: return "Early Hints"; + {status_type::continue_, "Continue"}, + {status_type::switching_protocols, "Switching Protocols"}, + {status_type::processing, "Processing"}, + {status_type::early_hints, "Early Hints"}, // 2xx - case status_type::ok: return "OK"; - case status_type::created: return "Created"; - case status_type::accepted: return "Accepted"; - case status_type::non_authoritative_information: return "Non-Authoritative Information"; - case status_type::no_content: return "No Content"; - case status_type::reset_content: return "Reset Content"; - case status_type::partial_content: return "Partial Content"; - case status_type::multi_status: return "Multi-Status"; - case status_type::already_reported: return "Already Reported"; - case status_type::im_used: return "IM Used"; + {status_type::ok, "OK"}, + {status_type::created, "Created"}, + {status_type::accepted, "Accepted"}, + {status_type::non_authoritative_information, "Non-Authoritative Information"}, + {status_type::no_content, "No Content"}, + {status_type::reset_content, "Reset Content"}, + {status_type::partial_content, "Partial Content"}, + {status_type::multi_status, "Multi-Status"}, + {status_type::already_reported, "Already Reported"}, + {status_type::im_used, "IM Used"}, // 3xx - case status_type::multiple_choices: return "Multiple Choices"; - case status_type::moved_permanently: return "Moved Permanently"; - case status_type::found: return "Found"; - case status_type::see_other: return "See Other"; - case status_type::not_modified: return "Not Modified"; - case status_type::use_proxy: return "Use Proxy"; - case status_type::temporary_redirect: return "Temporary Redirect"; - case status_type::permanent_redirect: return "Permanent Redirect"; + {status_type::multiple_choices, "Multiple Choices"}, + {status_type::moved_permanently, "Moved Permanently"}, + {status_type::found, "Found"}, + {status_type::see_other, "See Other"}, + {status_type::not_modified, "Not Modified"}, + {status_type::use_proxy, "Use Proxy"}, + {status_type::temporary_redirect, "Temporary Redirect"}, + {status_type::permanent_redirect, "Permanent Redirect"}, // 4xx - case status_type::bad_request: return "Bad Request"; - case status_type::unauthorized: return "Unauthorized"; - case status_type::payment_required: return "Payment Required"; - case status_type::forbidden: return "Forbidden"; - case status_type::not_found: return "Not Found"; - case status_type::method_not_allowed: return "Method Not Allowed"; - case status_type::not_acceptable: return "Not Acceptable"; - case status_type::proxy_authentication_required: return "Proxy Authentication Required"; - case status_type::request_timeout: return "Request Timeout"; - case status_type::conflict: return "Conflict"; - case status_type::gone: return "Gone"; - case status_type::length_required: return "Length Required"; - case status_type::precondition_failed: return "Precondition Failed"; - case status_type::payload_too_large: return "Payload Too Large"; - case status_type::uri_too_long: return "URI Too Long"; - case status_type::unsupported_media_type: return "Unsupported Media Type"; - case status_type::range_not_satisfiable: return "Range Not Satisfiable"; - case status_type::expectation_failed: return "Expectation Failed"; - case status_type::i_am_a_teapot: return "I'm a teapot"; - case status_type::misdirected_request: return "Misdirected Request"; - case status_type::unprocessable_entity: return "Unprocessable Entity"; - case status_type::locked: return "Locked"; - case status_type::failed_dependency: return "Failed Dependency"; - case status_type::too_early: return "Too Early"; - case status_type::upgrade_required: return "Upgrade Required"; - case status_type::precondition_required: return "Precondition Required"; - case status_type::too_many_requests: return "Too Many Requests"; - case status_type::request_header_fields_too_large: return "Request Header Fields Too Large"; - case status_type::unavailable_for_legal_reasons: return "Unavailable For Legal Reasons"; + {status_type::bad_request, "Bad Request"}, + {status_type::unauthorized, "Unauthorized"}, + {status_type::payment_required, "Payment Required"}, + {status_type::forbidden, "Forbidden"}, + {status_type::not_found, "Not Found"}, + {status_type::method_not_allowed, "Method Not Allowed"}, + {status_type::not_acceptable, "Not Acceptable"}, + {status_type::proxy_authentication_required, "Proxy Authentication Required"}, + {status_type::request_timeout, "Request Timeout"}, + {status_type::conflict, "Conflict"}, + {status_type::gone, "Gone"}, + {status_type::length_required, "Length Required"}, + {status_type::precondition_failed, "Precondition Failed"}, + {status_type::payload_too_large, "Payload Too Large"}, + {status_type::uri_too_long, "URI Too Long"}, + {status_type::unsupported_media_type, "Unsupported Media Type"}, + {status_type::range_not_satisfiable, "Range Not Satisfiable"}, + {status_type::expectation_failed, "Expectation Failed"}, + {status_type::i_am_a_teapot, "I'm a teapot"}, + {status_type::misdirected_request, "Misdirected Request"}, + {status_type::unprocessable_entity, "Unprocessable Entity"}, + {status_type::locked, "Locked"}, + {status_type::failed_dependency, "Failed Dependency"}, + {status_type::too_early, "Too Early"}, + {status_type::upgrade_required, "Upgrade Required"}, + {status_type::precondition_required, "Precondition Required"}, + {status_type::too_many_requests, "Too Many Requests"}, + {status_type::request_header_fields_too_large, "Request Header Fields Too Large"}, + {status_type::unavailable_for_legal_reasons, "Unavailable For Legal Reasons"}, + // 5xx - case status_type::internal_server_error: return "Internal Server Error"; - case status_type::not_implemented: return "Not Implemented"; - case status_type::bad_gateway: return "Bad Gateway"; - case status_type::service_unavailable: return "Service Unavailable"; - case status_type::gateway_timeout: return "Gateway Timeout"; - case status_type::http_version_not_supported: return "HTTP Version Not Supported"; - case status_type::variant_also_negotiates: return "Variant Also Negotiates"; - case status_type::insufficient_storage: return "Insufficient Storage"; - case status_type::loop_detected: return "Loop Detected"; - case status_type::not_extended: return "Not Extended"; - case status_type::network_authentication_required: return "Network Authentication Required"; - default: break; - } + {status_type::internal_server_error, "Internal Server Error"}, + {status_type::not_implemented, "Not Implemented"}, + {status_type::bad_gateway, "Bad Gateway"}, + {status_type::service_unavailable, "Service Unavailable"}, + {status_type::gateway_timeout, "Gateway Timeout"}, + {status_type::http_version_not_supported, "HTTP Version Not Supported"}, + {status_type::variant_also_negotiates, "Variant Also Negotiates"}, + {status_type::insufficient_storage, "Insufficient Storage"}, + {status_type::loop_detected, "Loop Detected"}, + {status_type::not_extended, "Not Extended"}, + {status_type::network_authentication_required, "Network Authentication Required"}, + }; + + std::string_view status_label(const status_type s) + { + for (auto [k,l] : STATUS_LABELS) + if (k == s) + return l; return ""; } @@ -1038,6 +1027,8 @@ namespace http return ret; } +//---------------------------------------------------------------------------------------------------------------- + void parse_url(std::string_view url, std::string& target, std::vector& params, std::error_code& ec) { // Find target @@ -1085,241 +1076,295 @@ namespace http //---------------------------------------------------------------------------------------------------------------- - template - bool parser::parse(Message& msg, std::string& buf, std::error_code& ec) + constexpr size_t max_header_size = 8192; + + enum parsing_result { - using namespace details; + parse_incomplete, + parse_ok + }; - while (!buf.empty() && !ec && state != done) - { - // Check buffer size - if (buf.size() > max_header_size) - ec = make_error_code(http_read_header_line_too_big); +//---------------------------------------------------------------------------------------------------------------- - // Start line method (Request only) - else if (state == method) - { - constexpr std::size_t max_method_size{16}; + const auto parse_method = [](request& req, std::string& buf, std::error_code& ec, auto cont) + { + constexpr std::size_t max_method_size{16}; - // Sufficient data - if (buf.size() >= max_method_size) - { - std::string_view method_str(&buf[0], max_method_size); - const auto end = method_str.find(' '); - const verb_type method = verb_enum(method_str.substr(0, end)); - - // Found - if (method != METHOD_UNKNOWN) - { - if constexpr (std::is_same_v) - msg.verb = method; - - state = uri; - buf.erase(begin(buf), begin(buf) + end + 1); - } + parsing_result res{parse_incomplete}; - // Not found - else - ec = make_error_code(http_read_bad_method); - } - - // Insufficient - else - break; + // Sufficient data + if (buf.size() >= max_method_size) + { + std::string_view method_str(&buf[0], max_method_size); + const auto end = method_str.find(' '); + const verb_type method = verb_enum(method_str.substr(0, end)); + + // Valid + if (method != METHOD_UNKNOWN) + { + req.verb = method; + buf.erase(begin(buf), begin(buf) + end + 1); + cont(); + res = parse_ok; } - // URI (Request only) - else if (state == uri) - { - auto* end = strchr(&buf[0], ' '); + // Not valid + else + ec = make_error_code(http_read_bad_method); + } + + return res; + }; - // Found - if (end != nullptr) - { - const size_t len = std::distance(&buf[0], end); - if constexpr (std::is_same_v) - parse_url(std::string_view(&buf[0],len), msg.uri, msg.params, ec); - - state = version; - buf.erase(begin(buf), begin(buf) + len + 1); - } +//---------------------------------------------------------------------------------------------------------------- - // Not found - else - break; + const auto parse_uri = [](request& req, std::string& buf, std::error_code& ec, auto cont) + { + parsing_result res{parse_incomplete}; + + auto* end = strchr(&buf[0], ' '); + + if (end != nullptr) + { + const size_t len = std::distance(&buf[0], end); + parse_url(std::string_view(&buf[0],len), req.uri, req.params, ec); + buf.erase(begin(buf), begin(buf) + len + 1); + cont(); + res = parse_ok; + } + + return res; + }; + +//---------------------------------------------------------------------------------------------------------------- + + const auto parse_version = [](auto& msg, std::string& buf, std::error_code& ec, auto cont) + { + using Message = std::remove_cv_t>; + constexpr std::size_t http_size{8}; + constexpr std::size_t tail_size = std::is_same_v ? 2 : 1; + + parsing_result res{parse_incomplete}; + + // Sufficient data + if (buf.size() > 10) + { + buf[http_size] = '\0'; + int major{-1}; + int minor{-1}; + const int ret = sscanf(&buf[0], "HTTP/%i.%i", &major, &minor); + + // Valid + if (ret == 2 && major == 1 && (minor == 0 || minor == 1)) + { + msg.version = (http_version)minor; + buf.erase(begin(buf), begin(buf) + http_size + tail_size); + cont(); + res = parse_ok; } - // HTTP version - else if (state == version) - { - constexpr std::size_t http_size{8}; + // Not valid + else + ec = make_error_code(http_read_unsupported_http_version); + } - // Sufficient data - if (buf.size() > 10) - { - buf[http_size] = '\0'; - int major{-1}; - int minor{-1}; - const int ret = sscanf(&buf[0], "HTTP/%i.%i", &major, &minor); + return res; + }; - // Found - if (ret == 2 && major == 1 && (minor == 0 || minor == 1)) - { - if constexpr (std::is_same_v) - { - state = header_line; - buf.erase(begin(buf), begin(buf) + http_size + 2); - } - - else - { - state = status_code; - buf.erase(begin(buf), begin(buf) + http_size + 1); - } - - msg.version = (http_version)minor; - } +//---------------------------------------------------------------------------------------------------------------- - // Not found - else - ec = make_error_code(http_read_unsupported_http_version); - } + const auto parse_status_code = [](response& msg, std::string& buf, std::error_code& ec, auto cont) + { + parsing_result res{parse_incomplete}; - // Insufficient - else - break; + auto* end = strchr(&buf[0], ' '); + + // Sufficient + if (end != nullptr) + { + const size_t len = std::distance(&buf[0], end); + *end = '\0'; + int status{-1}; + const int ret = sscanf(&buf[0], "%i", &status); + + // Valid + if (ret == 1 && status >= (int)status_type::continue_ && status <= 1000) + { + msg.status = (status_type)status; + buf.erase(begin(buf), begin(buf) + len + 1); + cont(); + res = parse_ok; } + + // Not valid + else + ec = make_error_code(http_read_bad_status); + } + + return res; + }; + +//---------------------------------------------------------------------------------------------------------------- - // Status code (response only) - else if (state == status_code) + const auto parse_status_msg = [](response& msg, std::string& buf, std::error_code& ec, auto cont) + { + parsing_result res{parse_incomplete}; + + auto* end = strstr(&buf[0], "\r\n"); + + // Sufficient + if (end != nullptr) + { + buf.erase(begin(buf), begin(buf) + std::distance(&buf[0], end) + 2); + cont(); + res = parse_ok; + } + + return res; + }; + +//---------------------------------------------------------------------------------------------------------------- + + const auto parse_header = [](auto& msg, std::string& buf, std::error_code& ec, auto cont) + { + using details::get_content; + + parsing_result res{parse_incomplete}; + + auto* end = strstr(&buf[0], "\r\n"); + + // Sufficient + if (end != nullptr) + { + *end = '\0'; + const size_t line_length = std::distance(&buf[0], end); + + // Found header + if (line_length > 0) { - auto* end = strchr(&buf[0], ' '); + auto* kend = strstr(&buf[0], ": "); - // Sufficient - if (end != nullptr) + if (kend == nullptr) + ec = make_error_code(http_read_header_kv_delimiter_not_found); + + else { - const size_t len = std::distance(&buf[0], end); - *end = '\0'; - int status{-1}; - const int ret = sscanf(&buf[0], "%i", &status); + for (auto* ptr = &buf[0] ; ptr != kend ; ++ptr) + *ptr = fast_ascii_tolower(*ptr); + + auto field = field_enum(std::string_view(&buf[0], std::distance(&buf[0], kend))); + auto value = std::string_view(kend+2, std::distance(kend+2, end)); + + if (field == unknown_field) + ec = make_error_code(http_read_header_unsupported_field); - // Found - if (ret == 1 && status >= (int)status_type::continue_ && status <= 1000) - { - if constexpr (std::is_same_v) - msg.status = (status_type)status; - state = status_msg; - buf.erase(begin(buf), begin(buf) + len + 1); - } - - // Not found else - ec = make_error_code(http_read_bad_status); + msg.headers.add(field, value); + res = parse_ok; } - - // Insufficient - else - break; } - // Status label - else if (state == status_msg) + // End of header - found \r\n\r\n + else { - auto* end = strstr(&buf[0], "\r\n"); + const auto it = msg.headers.find(field::content_length); + size_t content_size{0}; + if (it) std::from_chars(it->data(), it->data() + it->size(), content_size); + get_content(msg).resize(content_size); + cont(); + res = parse_ok; + } - // Sufficient - if (end != nullptr) - { - state = header_line; - buf.erase(begin(buf), begin(buf) + std::distance(&buf[0], end) + 2); - } + buf.erase(begin(buf), begin(buf) + line_length + 2); + } - // Insufficient - else - break; - } + return res; + }; - // Header line - else if (state == header_line) - { - auto* end = strstr(&buf[0], "\r\n"); +//---------------------------------------------------------------------------------------------------------------- - // Sufficient - if (end != nullptr) - { - *end = '\0'; - const size_t line_length = std::distance(&buf[0], end); + const auto parse_body = [](auto& msg, std::string& buf, size_t& body_read, std::error_code& ec, auto cont) + { + using details::get_content; + + const size_t remaining = get_content(msg).size() - body_read; + const size_t available = std::min(remaining, buf.size()); + std::copy(begin(buf), begin(buf) + available, begin(get_content(msg)) + body_read); + buf.erase(begin(buf), begin(buf) + available); + body_read += available; + + if (get_content(msg).size() == body_read) + cont(); + + return parse_ok; + }; - // Found header - if (line_length > 0) - { - auto* kend = strstr(&buf[0], ": "); - - if (kend == nullptr) - ec = make_error_code(http_read_header_kv_delimiter_not_found); - - else - { - for (auto* ptr = &buf[0] ; ptr != kend ; ++ptr) - *ptr = fast_ascii_tolower(*ptr); - - auto field = field_enum(std::string_view(&buf[0], std::distance(&buf[0], kend))); - auto value = std::string_view(kend+2, std::distance(kend+2, end)); - - if (field == unknown_field) - ec = make_error_code(http_read_header_unsupported_field); - else - msg.headers.add(field, value); - } - } +//---------------------------------------------------------------------------------------------------------------- - // End of header - found \r\n\r\n - else - { - const auto it = msg.headers.find(field::content_length); - size_t content_size{0}; - if (it) std::from_chars(it->data(), it->data() + it->size(), content_size); - - // Read body - if (content_size > 0) - { - state = body; - get_content(msg).resize(content_size); - } - - // Empty body - else - state = done; - } + bool parser_request::parse(request& req, std::string& buf, std::error_code& ec) + { + using namespace details; - buf.erase(begin(buf), begin(buf) + line_length + 2); - } + while (!buf.empty() && !ec && state != done) + { + // Check buffer size + if (buf.size() > max_header_size) + ec = make_error_code(http_read_header_line_too_big); - // Insufficient - else + else + { + parsing_result res{parse_incomplete}; + + switch(state) + { + case parser_request::method: res = parse_method(req, buf, ec, [&]{state = parser_request::uri;}); break; + case parser_request::uri: res = parse_uri(req, buf, ec, [&]{state = parser_request::version;}); break; + case parser_request::version: res = parse_version(req, buf, ec, [&]{state = parser_request::header_line;}); break; + case parser_request::header_line: res = parse_header(req, buf, ec, [&]{state = !req.content.empty() ? parser_request::body : parser_request::done;}); break; + case parser_request::body: res = parse_body(req, buf, body_read, ec, [&]{state = parser_request::done;}); break; + case parser_request::done: break; + } + + if (res == parse_incomplete) break; } + } - // Body - else if (state == body) + return state == done; + } + + bool parser_response::parse(response& resp, std::string& buf, std::error_code& ec) + { + using namespace details; + + while (!buf.empty() && !ec && state != done) + { + // Check buffer size + if (buf.size() > max_header_size) + ec = make_error_code(http_read_header_line_too_big); + + else { - const size_t remaining = get_content(msg).size() - body_read; - const size_t available = std::min(remaining, buf.size()); - std::copy(begin(buf), begin(buf) + available, begin(get_content(msg)) + body_read); - buf.erase(begin(buf), begin(buf) + available); - body_read += available; - - if (get_content(msg).size() == body_read) - state = done; + parsing_result res{parse_incomplete}; + + switch(state) + { + case parser_response::version: res = parse_version(resp, buf, ec, [&]{state = parser_response::status_code;}); break; + case parser_response::status_code: res = parse_status_code(resp, buf, ec, [&]{state = parser_response::status_msg;}); break; + case parser_response::status_msg: res = parse_status_msg(resp, buf, ec, [&]{state = parser_response::header_line;}); break; + case parser_response::header_line: res = parse_header(resp, buf, ec, [&]{state = !resp.content_str.empty() ? parser_response::body : parser_response::done;}); break; + case parser_response::body: res = parse_body(resp, buf, body_read, ec, [&]{state = parser_response::done;}); break; + case parser_response::done: break; + } + + if (res == parse_incomplete) + break; } } return state == done; } - template class parser; - template class parser; - //---------------------------------------------------------------------------------------------------------------- const auto handle_empty = [](auto& msg) diff --git a/src/http.h b/src/http.h index 64a2bc2..38e37f0 100644 --- a/src/http.h +++ b/src/http.h @@ -621,16 +621,18 @@ namespace http //---------------------------------------------------------------------------------------------------------------- - template - class parser + struct parser_request { - private: - static constexpr std::size_t max_header_size = 8192; - enum {method, uri, version, status_code, status_msg, header_line, body, done} state{std::is_same_v ? method : version}; + enum {method, uri, version, header_line, body, done} state{method}; size_t body_read{0}; + bool parse(request& msg, std::string& buf, std::error_code& ec); + }; - public: - bool parse(Message& req, std::string& buf, std::error_code& ec); + struct parser_response + { + enum {version, status_code, status_msg, header_line, body, done} state{version}; + size_t body_read{0}; + bool parse(response& msg, std::string& buf, std::error_code& ec); }; //---------------------------------------------------------------------------------------------------------------- diff --git a/src/http_async.h b/src/http_async.h index ee6d18d..a3bf03e 100644 --- a/src/http_async.h +++ b/src/http_async.h @@ -168,12 +168,12 @@ namespace http //---------------------------------------------------------------------------------------------------------------- - template + template struct async_http_read_impl { stream& sock; Message& msg; - parser parser_; + Parser parser_; size_t total_read{0}; size_t buf_off{0}; @@ -232,7 +232,7 @@ namespace http ) { return boost::asio::async_compose ( - details::async_http_read_impl{sock, req}, + details::async_http_read_impl{sock, req}, token, sock ); } @@ -250,7 +250,7 @@ namespace http ) { return boost::asio::async_compose ( - details::async_http_read_impl{sock, resp}, + details::async_http_read_impl{sock, resp}, token, sock ); }